From c1991e91530b74ae49e1036ce6f13375cc08920c Mon Sep 17 00:00:00 2001 From: David Remer Date: Tue, 28 Jul 2020 12:27:42 +0300 Subject: [PATCH] init subscriptions --- composer.json | 3 +- modules.local/ppcp-button/services.php | 4 +- .../src/Assets/DisabledSmartButton.php | 3 - .../ppcp-button/src/Assets/SmartButton.php | 30 ++-- .../src/Assets/SmartButtonInterface.php | 1 - modules.local/ppcp-subscription/composer.json | 13 ++ .../ppcp-subscription/extensions.php | 6 + modules.local/ppcp-subscription/module.php | 11 ++ modules.local/ppcp-subscription/services.php | 34 +++++ .../src/Helper/SubscriptionHelper.php | 51 +++++++ .../ppcp-subscription/src/RenewalHandler.php | 135 ++++++++++++++++++ .../src/Repository/PaymentTokenRepository.php | 56 ++++++++ .../src/SubscriptionModule.php | 57 ++++++++ modules.local/ppcp-wc-gateway/services.php | 37 ++++- .../src/Checkout/DisableGateways.php | 10 +- .../ppcp-wc-gateway/src/Gateway/WcGateway.php | 7 +- 16 files changed, 412 insertions(+), 46 deletions(-) create mode 100644 modules.local/ppcp-subscription/composer.json create mode 100644 modules.local/ppcp-subscription/extensions.php create mode 100644 modules.local/ppcp-subscription/module.php create mode 100644 modules.local/ppcp-subscription/services.php create mode 100644 modules.local/ppcp-subscription/src/Helper/SubscriptionHelper.php create mode 100644 modules.local/ppcp-subscription/src/RenewalHandler.php create mode 100644 modules.local/ppcp-subscription/src/Repository/PaymentTokenRepository.php create mode 100644 modules.local/ppcp-subscription/src/SubscriptionModule.php diff --git a/composer.json b/composer.json index ff896d95d..f15b06f06 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,8 @@ "Inpsyde\\PayPalCommerce\\WcGateway\\": "modules.local/ppcp-wc-gateway/src/", "Inpsyde\\PayPalCommerce\\Onboarding\\": "modules.local/ppcp-onboarding/src/", "Inpsyde\\Woocommerce\\Logging\\": "modules.local/woocommerce-logging/src/", - "Inpsyde\\PayPalCommerce\\Webhooks\\": "modules.local/ppcp-webhooks/src/" + "Inpsyde\\PayPalCommerce\\Webhooks\\": "modules.local/ppcp-webhooks/src/", + "Inpsyde\\PayPalCommerce\\Subscription\\": "modules.local/ppcp-subscription/src/" } }, "autoload-dev": { diff --git a/modules.local/ppcp-button/services.php b/modules.local/ppcp-button/services.php index a2a1076ba..68d20a4f3 100644 --- a/modules.local/ppcp-button/services.php +++ b/modules.local/ppcp-button/services.php @@ -57,6 +57,7 @@ return [ $clientId = $container->get('button.client_id'); $dccApplies = $container->get('api.helpers.dccapplies'); + $subscriptionHelper = $container->get('subscription.helper'); return new SmartButton( $container->get('button.url'), $container->get('session.handler'), @@ -66,7 +67,8 @@ return [ $payerFactory, $clientId, $requestData, - $dccApplies + $dccApplies, + $subscriptionHelper ); }, 'button.url' => static function (ContainerInterface $container): string { diff --git a/modules.local/ppcp-button/src/Assets/DisabledSmartButton.php b/modules.local/ppcp-button/src/Assets/DisabledSmartButton.php index 94d3483e2..097c88c9a 100644 --- a/modules.local/ppcp-button/src/Assets/DisabledSmartButton.php +++ b/modules.local/ppcp-button/src/Assets/DisabledSmartButton.php @@ -20,7 +20,4 @@ class DisabledSmartButton implements SmartButtonInterface public function canSaveVaultToken(): bool { return false; } - public function hasSubscription(): bool { - return false; - } } diff --git a/modules.local/ppcp-button/src/Assets/SmartButton.php b/modules.local/ppcp-button/src/Assets/SmartButton.php index b3b2602a6..0b2b9bb46 100644 --- a/modules.local/ppcp-button/src/Assets/SmartButton.php +++ b/modules.local/ppcp-button/src/Assets/SmartButton.php @@ -14,6 +14,7 @@ use Inpsyde\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint; use Inpsyde\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint; use Inpsyde\PayPalCommerce\Button\Endpoint\RequestData; use Inpsyde\PayPalCommerce\Session\SessionHandler; +use Inpsyde\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use Inpsyde\PayPalCommerce\WcGateway\Settings\Settings; class SmartButton implements SmartButtonInterface @@ -27,6 +28,7 @@ class SmartButton implements SmartButtonInterface private $clientId; private $requestData; private $dccApplies; + private $subscriptionHelper; public function __construct( string $moduleUrl, @@ -37,7 +39,8 @@ class SmartButton implements SmartButtonInterface PayerFactory $payerFactory, string $clientId, RequestData $requestData, - DccApplies $dccApplies + DccApplies $dccApplies, + SubscriptionHelper $subscriptionHelper ) { $this->moduleUrl = $moduleUrl; @@ -49,6 +52,7 @@ class SmartButton implements SmartButtonInterface $this->clientId = $clientId; $this->requestData = $requestData; $this->dccApplies = $dccApplies; + $this->subscriptionHelper = $subscriptionHelper; } // phpcs:disable Inpsyde.CodeQuality.FunctionLength.TooLong @@ -284,29 +288,15 @@ class SmartButton implements SmartButtonInterface return is_user_logged_in(); } - public function hasSubscription(): bool + private function hasSubscription(): bool { - - if (is_product()) { - $product = wc_get_product(); - return is_a($product, \WC_Product::class) && $product->is_type('subscription'); - } - - $cart = WC()->cart; - if (! $cart || $cart->is_empty()) { + if (! $this->subscriptionHelper->acceptOnlyAutomaticPaymentGateways()) { 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; - } + if (is_product()) { + return $this->subscriptionHelper->currentProductIsSubscription(); } - - return false; + return $this->subscriptionHelper->cartContainsSubscription(); } //phpcs:disable Inpsyde.CodeQuality.FunctionLength.TooLong diff --git a/modules.local/ppcp-button/src/Assets/SmartButtonInterface.php b/modules.local/ppcp-button/src/Assets/SmartButtonInterface.php index 53cdf2345..2c5de6a6b 100644 --- a/modules.local/ppcp-button/src/Assets/SmartButtonInterface.php +++ b/modules.local/ppcp-button/src/Assets/SmartButtonInterface.php @@ -12,5 +12,4 @@ interface SmartButtonInterface public function canSaveVaultToken(): bool; - public function hasSubscription(): bool; } diff --git a/modules.local/ppcp-subscription/composer.json b/modules.local/ppcp-subscription/composer.json new file mode 100644 index 000000000..9586fa8fe --- /dev/null +++ b/modules.local/ppcp-subscription/composer.json @@ -0,0 +1,13 @@ +{ + "name": "inpsyde/ppcp-subscription", + "type": "inpsyde-module", + "require": { + "dhii/module-interface": "0.2.x-dev", + "inpsyde/ppcp-api-client": "dev-master" + }, + "autoload": { + "psr-4": { + "Inpsyde\\PayPalCommerce\\Subscription\\": "src/" + } + } +} \ No newline at end of file diff --git a/modules.local/ppcp-subscription/extensions.php b/modules.local/ppcp-subscription/extensions.php new file mode 100644 index 000000000..2ccfbc173 --- /dev/null +++ b/modules.local/ppcp-subscription/extensions.php @@ -0,0 +1,6 @@ + function(ContainerInterface $container) : SubscriptionHelper { + return new SubscriptionHelper(); + }, + 'subscription.renewal-handler' => 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' => 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 new file mode 100644 index 000000000..cb15851c9 --- /dev/null +++ b/modules.local/ppcp-subscription/src/Helper/SubscriptionHelper.php @@ -0,0 +1,51 @@ +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; + } + $accept_manual_renewals = ( 'no' !== get_option( \WC_Subscriptions_Admin::$option_prefix . '_accept_manual_renewals', 'no' ) ) ? true : false; + return ! $accept_manual_renewals; + } + + public function pluginIsActive() { + return class_exists(\WC_Subscriptions::class); + } +} diff --git a/modules.local/ppcp-subscription/src/RenewalHandler.php b/modules.local/ppcp-subscription/src/RenewalHandler.php new file mode 100644 index 000000000..f0ef2d76c --- /dev/null +++ b/modules.local/ppcp-subscription/src/RenewalHandler.php @@ -0,0 +1,135 @@ +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() + ), + [ + 'order' => $wcOrder, + ] + ); + + try { + $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([$purchaseUnits], $payer, $token, (string) $wcOrder->get_id()); + $this->captureOrder($order, $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() + ), + [ + '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() + ), + [ + '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() + ), + [ + '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 = $this->orderEndpoint->capture($order); + if ($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(WcGateway::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 new file mode 100644 index 000000000..33840c06c --- /dev/null +++ b/modules.local/ppcp-subscription/src/Repository/PaymentTokenRepository.php @@ -0,0 +1,56 @@ +factory = $factory; + $this->endpoint = $endpoint; + } + + public function forUserId(int $id) : ?PaymentToken + { + try { + $token = (array) get_user_meta($id, self::USER_META, true); + if (! $token) { + 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; + } +} \ No newline at end of file diff --git a/modules.local/ppcp-subscription/src/SubscriptionModule.php b/modules.local/ppcp-subscription/src/SubscriptionModule.php new file mode 100644 index 000000000..fe9acfa4d --- /dev/null +++ b/modules.local/ppcp-subscription/src/SubscriptionModule.php @@ -0,0 +1,57 @@ +get('subscription.renewal-handler'); + $handler->renew($order); + }, + 10, + 2 + ); + + add_action( + 'init', + function () { + if (! isset($_GET['doit'])) { + return; + } + $order = wc_get_order(202); + do_action( + 'woocommerce_scheduled_subscription_payment_' . WcGateway::ID, + 0, + $order + ); + } + ); + } +} diff --git a/modules.local/ppcp-wc-gateway/services.php b/modules.local/ppcp-wc-gateway/services.php index 0dbe38e53..4521f4371 100644 --- a/modules.local/ppcp-wc-gateway/services.php +++ b/modules.local/ppcp-wc-gateway/services.php @@ -28,23 +28,18 @@ return [ $notice = $container->get('wcgateway.notice.authorize-order-action'); $settings = $container->get('wcgateway.settings'); - $smartButton = $container->get('button.smart-button'); - $supportsSubscription = $smartButton->canSaveVaultToken(); return new WcGateway( $settingsRenderer, $orderProcessor, $authorizedPayments, $notice, - $settings, - $supportsSubscription + $settings ); }, 'wcgateway.disabler' => static function (ContainerInterface $container): DisableGateways { $sessionHandler = $container->get('session.handler'); $settings = $container->get('wcgateway.settings'); - $smartButton = $container->get('button.smart-button'); - $subscriptionDisable = ! $smartButton->canSaveVaultToken() && $smartButton->hasSubscription(); - return new DisableGateways($sessionHandler, $settings, $subscriptionDisable); + return new DisableGateways($sessionHandler, $settings); }, 'wcgateway.settings' => static function (ContainerInterface $container): Settings { return new Settings(); @@ -496,6 +491,34 @@ return [ ], 'requirements' => [], ], + 'client_id' => [ + 'title' => __('Client Id', 'woocommerce-paypal-commerce-gateway'), + 'type' => 'text', + 'desc_tip' => true, + 'label' => __('Enable logging', 'woocommerce-paypal-commerce-gateway'), + 'description' => __('Enable logging of unexpected behavior. This can also log private data and should only be enabled in a development or stage environment.', 'woocommerce-paypal-commerce-gateway'), + 'default' => false, + 'screens' => [ + State::STATE_START, + State::STATE_PROGRESSIVE, + State::STATE_ONBOARDED, + ], + 'requirements' => [], + ], + 'client_secret' => [ + 'title' => __('Secret Id', 'woocommerce-paypal-commerce-gateway'), + 'type' => 'text', + 'desc_tip' => true, + 'label' => __('Enable logging', 'woocommerce-paypal-commerce-gateway'), + 'description' => __('Enable logging of unexpected behavior. This can also log private data and should only be enabled in a development or stage environment.', 'woocommerce-paypal-commerce-gateway'), + 'default' => false, + 'screens' => [ + State::STATE_START, + State::STATE_PROGRESSIVE, + State::STATE_ONBOARDED, + ], + 'requirements' => [], + ], ]; }, ]; diff --git a/modules.local/ppcp-wc-gateway/src/Checkout/DisableGateways.php b/modules.local/ppcp-wc-gateway/src/Checkout/DisableGateways.php index eebe62f00..bd86b9ac3 100644 --- a/modules.local/ppcp-wc-gateway/src/Checkout/DisableGateways.php +++ b/modules.local/ppcp-wc-gateway/src/Checkout/DisableGateways.php @@ -13,16 +13,13 @@ class DisableGateways private $sessionHandler; private $settings; - private $subscriptionDisable; public function __construct( SessionHandler $sessionHandler, - ContainerInterface $settings, - bool $subscriptionDisable + ContainerInterface $settings ) { $this->sessionHandler = $sessionHandler; $this->settings = $settings; - $this->subscriptionDisable = $subscriptionDisable; } public function handler(array $methods): array @@ -38,11 +35,6 @@ class DisableGateways return $methods; } - if ($this->subscriptionDisable) { - unset($methods[WcGateway::ID]); - return $methods; - } - if (! $this->needsToDisableGateways()) { return $methods; } diff --git a/modules.local/ppcp-wc-gateway/src/Gateway/WcGateway.php b/modules.local/ppcp-wc-gateway/src/Gateway/WcGateway.php index 2dfa83b34..0915aa977 100644 --- a/modules.local/ppcp-wc-gateway/src/Gateway/WcGateway.php +++ b/modules.local/ppcp-wc-gateway/src/Gateway/WcGateway.php @@ -41,8 +41,7 @@ class WcGateway extends \WC_Payment_Gateway OrderProcessor $orderProcessor, AuthorizedPaymentsProcessor $authorizedPayments, AuthorizeOrderActionNotice $notice, - ContainerInterface $config, - bool $supportsSubscription + ContainerInterface $config ) { $this->id = self::ID; @@ -51,8 +50,8 @@ class WcGateway extends \WC_Payment_Gateway $this->notice = $notice; $this->settingsRenderer = $settingsRenderer; $this->config = $config; - if ($supportsSubscription) { - $this->supports = array('subscriptions', 'products'); + if ($this->config->has('vault_enabled') && $this->config->get('vault_enabled')) { + $this->supports = array('subscriptions', 'products', 'subscription_date_changes'); } $this->method_title = __('PayPal Payments', 'woocommerce-paypal-commerce-gateway');