diff --git a/modules.local/ppcp-button/src/Assets/SmartButton.php b/modules.local/ppcp-button/src/Assets/SmartButton.php index a4ca70ac2..bcd787ea6 100644 --- a/modules.local/ppcp-button/src/Assets/SmartButton.php +++ b/modules.local/ppcp-button/src/Assets/SmartButton.php @@ -400,13 +400,13 @@ class SmartButton implements SmartButtonInterface { } private function hasSubscription(): bool { - if ( ! $this->subscriptionHelper->acceptOnlyAutomaticPaymentGateways() ) { + if ( ! $this->subscriptionHelper->accept_only_automatic_payment_gateways() ) { return false; } if ( is_product() ) { - return $this->subscriptionHelper->currentProductIsSubscription(); + return $this->subscriptionHelper->current_product_is_subscription(); } - return $this->subscriptionHelper->cartContainsSubscription(); + return $this->subscriptionHelper->cart_contains_subscription(); } //phpcs:disable Inpsyde.CodeQuality.FunctionLength.TooLong diff --git a/modules.local/ppcp-subscription/extensions.php b/modules.local/ppcp-subscription/extensions.php index b80286864..8c4769563 100644 --- a/modules.local/ppcp-subscription/extensions.php +++ b/modules.local/ppcp-subscription/extensions.php @@ -1,7 +1,10 @@ static function (ContainerInterface $container): SubscriptionHelper { - return new SubscriptionHelper(); - }, - 'subscription.renewal-handler' => static function (ContainerInterface $container): RenewalHandler { - $logger = $container->get('woocommerce.logger.woocommerce'); - $repository = $container->get('subscription.repository.payment-token'); - $endpoint = $container->get('api.endpoint.order'); - $purchaseFactory = $container->get('api.factory.purchase-unit'); - $payerFactory = $container->get('api.factory.payer'); - return new RenewalHandler( - $logger, - $repository, - $endpoint, - $purchaseFactory, - $payerFactory - ); - }, - 'subscription.repository.payment-token' => static function (ContainerInterface $container): PaymentTokenRepository { - $factory = $container->get('api.factory.payment-token'); - $endpoint = $container->get('api.endpoint.payment-token'); - return new PaymentTokenRepository($factory, $endpoint); - }, -]; +return array( + 'subscription.helper' => static function ( ContainerInterface $container ): SubscriptionHelper { + return new SubscriptionHelper(); + }, + 'subscription.renewal-handler' => static function ( ContainerInterface $container ): RenewalHandler { + $logger = $container->get( 'woocommerce.logger.woocommerce' ); + $repository = $container->get( 'subscription.repository.payment-token' ); + $endpoint = $container->get( 'api.endpoint.order' ); + $purchase_unit_factory = $container->get( 'api.factory.purchase-unit' ); + $payer_factory = $container->get( 'api.factory.payer' ); + return new RenewalHandler( + $logger, + $repository, + $endpoint, + $purchase_unit_factory, + $payer_factory + ); + }, + 'subscription.repository.payment-token' => static function ( ContainerInterface $container ): PaymentTokenRepository { + $factory = $container->get( 'api.factory.payment-token' ); + $endpoint = $container->get( 'api.endpoint.payment-token' ); + return new PaymentTokenRepository( $factory, $endpoint ); + }, +); diff --git a/modules.local/ppcp-subscription/src/Helper/SubscriptionHelper.php b/modules.local/ppcp-subscription/src/Helper/SubscriptionHelper.php deleted file mode 100644 index d97527c76..000000000 --- a/modules.local/ppcp-subscription/src/Helper/SubscriptionHelper.php +++ /dev/null @@ -1,57 +0,0 @@ -pluginIsActive() ) { - return false; - } - $product = wc_get_product(); - return is_a( $product, \WC_Product::class ) && $product->is_type( 'subscription' ); - } - - public function cartContainsSubscription(): bool { - if ( ! $this->pluginIsActive() ) { - return false; - } - $cart = WC()->cart; - if ( ! $cart || $cart->is_empty() ) { - return false; - } - - foreach ( $cart->get_cart() as $item ) { - if ( ! isset( $item['data'] ) || ! is_a( $item['data'], \WC_Product::class ) ) { - continue; - } - if ( $item['data']->is_type( 'subscription' ) ) { - return true; - } - } - - return false; - } - - public function acceptOnlyAutomaticPaymentGateways(): bool { - - if ( ! $this->pluginIsActive() ) { - return false; - } - $acceptManualRenewals = ( 'no' !== get_option( - //phpcs:disable Inpsyde.CodeQuality.VariablesName.SnakeCaseVar - \WC_Subscriptions_Admin::$option_prefix . '_accept_manual_renewals', - //phpcs:enable Inpsyde.CodeQuality.VariablesName.SnakeCaseVar - 'no' - ) ) ? true : false; - return ! $acceptManualRenewals; - } - - public function pluginIsActive(): bool { - - return class_exists( \WC_Subscriptions::class ); - } -} diff --git a/modules.local/ppcp-subscription/src/Helper/class-subscriptionhelper.php b/modules.local/ppcp-subscription/src/Helper/class-subscriptionhelper.php new file mode 100644 index 000000000..16c713725 --- /dev/null +++ b/modules.local/ppcp-subscription/src/Helper/class-subscriptionhelper.php @@ -0,0 +1,86 @@ +plugin_is_active() ) { + return false; + } + $product = wc_get_product(); + return is_a( $product, \WC_Product::class ) && $product->is_type( 'subscription' ); + } + + /** + * Whether the current cart contains subscriptions. + * + * @return bool + */ + public function cart_contains_subscription(): bool { + if ( ! $this->plugin_is_active() ) { + return false; + } + $cart = WC()->cart; + if ( ! $cart || $cart->is_empty() ) { + return false; + } + + foreach ( $cart->get_cart() as $item ) { + if ( ! isset( $item['data'] ) || ! is_a( $item['data'], \WC_Product::class ) ) { + continue; + } + if ( $item['data']->is_type( 'subscription' ) ) { + return true; + } + } + + return false; + } + + /** + * Whether only automatic payment gateways are accepted. + * + * @return bool + */ + public function accept_only_automatic_payment_gateways(): bool { + + if ( ! $this->plugin_is_active() ) { + return false; + } + $accept_manual_renewals = ( 'no' !== get_option( + //phpcs:disable Inpsyde.CodeQuality.VariablesName.SnakeCaseVar + \WC_Subscriptions_Admin::$option_prefix . '_accept_manual_renewals', + //phpcs:enable Inpsyde.CodeQuality.VariablesName.SnakeCaseVar + 'no' + ) ) ? true : false; + return ! $accept_manual_renewals; + } + + /** + * Whether the subscription plugin is active or not. + * + * @return bool + */ + public function plugin_is_active(): bool { + + return class_exists( \WC_Subscriptions::class ); + } +} diff --git a/modules.local/ppcp-subscription/src/RenewalHandler.php b/modules.local/ppcp-subscription/src/RenewalHandler.php deleted file mode 100644 index 1ef255547..000000000 --- a/modules.local/ppcp-subscription/src/RenewalHandler.php +++ /dev/null @@ -1,150 +0,0 @@ -logger = $logger; - $this->repository = $repository; - $this->orderEndpoint = $orderEndpoint; - $this->purchaseUnitFactory = $purchaseUnitFactory; - $this->payerFactory = $payerFactory; - } - - public function renew( \WC_Order $wcOrder ) { - - $this->logger->log( - 'info', - sprintf( - // translators: %d is the id of the order - __( 'Start moneytransfer for order %d', 'woocommerce-paypal-commerce-gateway' ), - (int) $wcOrder->get_id() - ), - array( - 'order' => $wcOrder, - ) - ); - - try { - $this->processOrder( $wcOrder ); - } catch ( \Exception $error ) { - $this->logger->log( - 'error', - sprintf( - // translators: %1$d is the order number, %2$s the error message - __( - 'An error occured while trying to renew the subscription for order %1$d: %2$s', - 'woocommerce-paypal-commerce-gateway' - ), - (int) $wcOrder->get_id(), - $error->getMessage() - ), - array( - 'order' => $wcOrder, - ) - ); - \WC_Subscriptions_Manager::process_subscription_payment_failure_on_order( $wcOrder ); - return; - } - $this->logger->log( - 'info', - sprintf( - // translators: %d is the order number - __( - 'Moneytransfer for order %d is completed.', - 'woocommerce-paypal-commerce-gateway' - ), - (int) $wcOrder->get_id() - ), - array( - 'order' => $wcOrder, - ) - ); - } - - private function processOrder( \WC_Order $wcOrder ) { - - $userId = (int) $wcOrder->get_customer_id(); - $customer = new \WC_Customer( $userId ); - $token = $this->getTokenForCustomer( $customer, $wcOrder ); - if ( ! $token ) { - return; - } - $purchaseUnits = $this->purchaseUnitFactory->fromWcOrder( $wcOrder ); - $payer = $this->payerFactory->fromCustomer( $customer ); - $order = $this->orderEndpoint->createForPurchaseUnits( - array( $purchaseUnits ), - $payer, - $token, - (string) $wcOrder->get_id() - ); - $this->captureOrder( $order, $wcOrder ); - } - - private function getTokenForCustomer( \WC_Customer $customer, \WC_Order $wcOrder ): ?PaymentToken { - - $token = $this->repository->forUserId( (int) $customer->get_id() ); - if ( ! $token ) { - $this->logger->log( - 'error', - sprintf( - // translators: %d is the customer id - __( - 'No payment token found for customer %d', - 'woocommerce-paypal-commerce-gateway' - ), - (int) $customer->get_id() - ), - array( - 'customer' => $customer, - 'order' => $wcOrder, - ) - ); - \WC_Subscriptions_Manager::process_subscription_payment_failure_on_order( $wcOrder ); - } - return $token; - } - - private function captureOrder( Order $order, \WC_Order $wcOrder ) { - - if ( $order->intent() === 'CAPTURE' && $order->status()->is( OrderStatus::COMPLETED ) ) { - $wcOrder->update_status( - 'processing', - __( 'Payment received.', 'woocommerce-paypal-commerce-gateway' ) - ); - \WC_Subscriptions_Manager::process_subscription_payments_on_order( $wcOrder ); - } - - if ( $order->intent() === 'AUTHORIZE' ) { - $this->orderEndpoint->authorize( $order ); - $wcOrder->update_meta_data( PayPalGateway::CAPTURED_META_KEY, 'false' ); - \WC_Subscriptions_Manager::process_subscription_payments_on_order( $wcOrder ); - } - } -} diff --git a/modules.local/ppcp-subscription/src/Repository/PaymentTokenRepository.php b/modules.local/ppcp-subscription/src/Repository/PaymentTokenRepository.php deleted file mode 100644 index b817fabc9..000000000 --- a/modules.local/ppcp-subscription/src/Repository/PaymentTokenRepository.php +++ /dev/null @@ -1,54 +0,0 @@ -factory = $factory; - $this->endpoint = $endpoint; - } - - public function forUserId( int $id ): ?PaymentToken { - try { - $token = (array) get_user_meta( $id, self::USER_META, true ); - if ( ! $token || ! isset( $token['id'] ) ) { - return $this->fetchForUserId( $id ); - } - - $token = $this->factory->fromArray( $token ); - return $token; - } catch ( RuntimeException $error ) { - return null; - } - } - - public function deleteToken( int $userId, PaymentToken $token ): bool { - delete_user_meta( $userId, self::USER_META ); - return $this->endpoint->deleteToken( $token ); - } - - private function fetchForUserId( int $id ): PaymentToken { - - $tokens = $this->endpoint->forUser( $id ); - $token = current( $tokens ); - $tokenArray = $token->toArray(); - update_user_meta( $id, self::USER_META, $tokenArray ); - return $token; - } -} diff --git a/modules.local/ppcp-subscription/src/Repository/class-paymenttokenrepository.php b/modules.local/ppcp-subscription/src/Repository/class-paymenttokenrepository.php new file mode 100644 index 000000000..2200fa826 --- /dev/null +++ b/modules.local/ppcp-subscription/src/Repository/class-paymenttokenrepository.php @@ -0,0 +1,102 @@ +factory = $factory; + $this->endpoint = $endpoint; + } + + /** + * Return a token for a user. + * + * @param int $id The user id. + * + * @return PaymentToken|null + */ + public function for_user_id( int $id ): ?PaymentToken { + try { + $token = (array) get_user_meta( $id, self::USER_META, true ); + if ( ! $token || ! isset( $token['id'] ) ) { + return $this->fetch_for_user_id( $id ); + } + + $token = $this->factory->fromArray( $token ); + return $token; + } catch ( RuntimeException $error ) { + return null; + } + } + + /** + * Delete a token for a user. + * + * @param int $user_id The user id. + * @param PaymentToken $token The token. + * + * @return bool + */ + public function delete_token( int $user_id, PaymentToken $token ): bool { + delete_user_meta( $user_id, self::USER_META ); + return $this->endpoint->deleteToken( $token ); + } + + /** + * Fetch PaymentToken from PayPal for a user. + * + * @param int $id The user id. + * @return PaymentToken + */ + private function fetch_for_user_id( int $id ): PaymentToken { + + $tokens = $this->endpoint->forUser( $id ); + $token = current( $tokens ); + $token_array = $token->toArray(); + update_user_meta( $id, self::USER_META, $token_array ); + return $token; + } +} diff --git a/modules.local/ppcp-subscription/src/class-renewalhandler.php b/modules.local/ppcp-subscription/src/class-renewalhandler.php new file mode 100644 index 000000000..728c4292a --- /dev/null +++ b/modules.local/ppcp-subscription/src/class-renewalhandler.php @@ -0,0 +1,222 @@ +logger = $logger; + $this->repository = $repository; + $this->order_endpoint = $order_endpoint; + $this->purchase_unit_factory = $purchase_unit_factory; + $this->payer_factory = $payer_factory; + } + + /** + * Renew an order. + * + * @param \WC_Order $wc_order The Woocommerce order. + */ + public function renew( \WC_Order $wc_order ) { + + $this->logger->log( + 'info', + sprintf( + // translators: %d is the id of the order. + __( 'Start moneytransfer for order %d', 'woocommerce-paypal-commerce-gateway' ), + (int) $wc_order->get_id() + ), + array( + 'order' => $wc_order, + ) + ); + + try { + $this->process_order( $wc_order ); + } catch ( \Exception $error ) { + $this->logger->log( + 'error', + sprintf( + // translators: %1$d is the order number, %2$s the error message. + __( + 'An error occured while trying to renew the subscription for order %1$d: %2$s', + 'woocommerce-paypal-commerce-gateway' + ), + (int) $wc_order->get_id(), + $error->getMessage() + ), + array( + 'order' => $wc_order, + ) + ); + \WC_Subscriptions_Manager::process_subscription_payment_failure_on_order( $wc_order ); + return; + } + $this->logger->log( + 'info', + sprintf( + // translators: %d is the order number. + __( + 'Moneytransfer for order %d is completed.', + 'woocommerce-paypal-commerce-gateway' + ), + (int) $wc_order->get_id() + ), + array( + 'order' => $wc_order, + ) + ); + } + + /** + * Process a Woocommerce order. + * + * @param \WC_Order $wc_order The Woocommerce order. + * + * @throws \Exception If customer cannot be read/found. + */ + private function process_order( \WC_Order $wc_order ) { + + $user_id = (int) $wc_order->get_customer_id(); + $customer = new \WC_Customer( $user_id ); + $token = $this->get_token_for_customer( $customer, $wc_order ); + if ( ! $token ) { + return; + } + $purchase_unit = $this->purchase_unit_factory->fromWcOrder( $wc_order ); + $payer = $this->payer_factory->fromCustomer( $customer ); + $order = $this->order_endpoint->createForPurchaseUnits( + array( $purchase_unit ), + $payer, + $token, + (string) $wc_order->get_id() + ); + $this->capture_order( $order, $wc_order ); + } + + /** + * Returns a payment token for a customer. + * + * @param \WC_Customer $customer The customer. + * @param \WC_Order $wc_order The current Woocommerce order we want to process. + * + * @return PaymentToken|null + */ + private function get_token_for_customer( \WC_Customer $customer, \WC_Order $wc_order ): ?PaymentToken { + + $token = $this->repository->for_user_id( (int) $customer->get_id() ); + if ( ! $token ) { + $this->logger->log( + 'error', + sprintf( + // translators: %d is the customer id. + __( + 'No payment token found for customer %d', + 'woocommerce-paypal-commerce-gateway' + ), + (int) $customer->get_id() + ), + array( + 'customer' => $customer, + 'order' => $wc_order, + ) + ); + \WC_Subscriptions_Manager::process_subscription_payment_failure_on_order( $wc_order ); + } + return $token; + } + + /** + * If the PayPal order is captured/authorized the Woocommerce order gets updated accordingly. + * + * @param Order $order The PayPal order. + * @param \WC_Order $wc_order The related Woocommerce order. + */ + private function capture_order( Order $order, \WC_Order $wc_order ) { + + if ( $order->intent() === 'CAPTURE' && $order->status()->is( OrderStatus::COMPLETED ) ) { + $wc_order->update_status( + 'processing', + __( 'Payment received.', 'woocommerce-paypal-commerce-gateway' ) + ); + \WC_Subscriptions_Manager::process_subscription_payments_on_order( $wc_order ); + } + + if ( $order->intent() === 'AUTHORIZE' ) { + $this->order_endpoint->authorize( $order ); + $wc_order->update_meta_data( PayPalGateway::CAPTURED_META_KEY, 'false' ); + \WC_Subscriptions_Manager::process_subscription_payments_on_order( $wc_order ); + } + } +} diff --git a/modules.local/ppcp-subscription/src/SubscriptionModule.php b/modules.local/ppcp-subscription/src/class-subscriptionmodule.php similarity index 77% rename from modules.local/ppcp-subscription/src/SubscriptionModule.php rename to modules.local/ppcp-subscription/src/class-subscriptionmodule.php index a419e91a5..7fae9719c 100644 --- a/modules.local/ppcp-subscription/src/SubscriptionModule.php +++ b/modules.local/ppcp-subscription/src/class-subscriptionmodule.php @@ -1,4 +1,9 @@