init subscriptions

This commit is contained in:
David Remer 2020-07-28 12:27:42 +03:00
parent b5b29b8e58
commit c1991e9153
16 changed files with 412 additions and 46 deletions

View file

@ -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 {

View file

@ -20,7 +20,4 @@ class DisabledSmartButton implements SmartButtonInterface
public function canSaveVaultToken(): bool {
return false;
}
public function hasSubscription(): bool {
return false;
}
}

View file

@ -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

View file

@ -12,5 +12,4 @@ interface SmartButtonInterface
public function canSaveVaultToken(): bool;
public function hasSubscription(): bool;
}

View file

@ -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/"
}
}
}

View file

@ -0,0 +1,6 @@
<?php
declare(strict_types=1);
return [
];

View file

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Subscription;
use Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return new SubscriptionModule();
};

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Subscription;
use Inpsyde\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use Inpsyde\PayPalCommerce\Subscription\Repository\PaymentTokenRepository;
use Psr\Container\ContainerInterface;
return [
'subscription.helper' => 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);
}
];

View file

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Subscription\Helper;
class SubscriptionHelper
{
public function currentProductIsSubscription() : bool
{
if (! $this->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);
}
}

View file

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Subscription;
use Inpsyde\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use Inpsyde\PayPalCommerce\ApiClient\Entity\Order;
use Inpsyde\PayPalCommerce\ApiClient\Entity\OrderStatus;
use Inpsyde\PayPalCommerce\ApiClient\Entity\PaymentToken;
use Inpsyde\PayPalCommerce\ApiClient\Factory\PayerFactory;
use Inpsyde\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use Inpsyde\PayPalCommerce\Subscription\Repository\PaymentTokenRepository;
use Inpsyde\PayPalCommerce\WcGateway\Gateway\WcGateway;
use Psr\Log\LoggerInterface;
class RenewalHandler
{
private $logger;
private $repository;
private $orderEndpoint;
private $purchaseUnitFactory;
private $payerFactory;
public function __construct(
LoggerInterface $logger,
PaymentTokenRepository $repository,
OrderEndpoint $orderEndpoint,
PurchaseUnitFactory $purchaseUnitFactory,
PayerFactory $payerFactory
) {
$this->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 );
}
}
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Subscription\Repository;
use Inpsyde\PayPalCommerce\ApiClient\Endpoint\PaymentTokenEndpoint;
use Inpsyde\PayPalCommerce\ApiClient\Entity\PaymentToken;
use Inpsyde\PayPalCommerce\ApiClient\Exception\RuntimeException;
use Inpsyde\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory;
class PaymentTokenRepository
{
public const USER_META = 'ppcp-vault-token';
private $factory;
private $endpoint;
public function __construct(
PaymentTokenFactory $factory,
PaymentTokenEndpoint $endpoint
)
{
$this->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;
}
}

View file

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\Subscription;
use Dhii\Container\ServiceProvider;
use Dhii\Modular\Module\ModuleInterface;
use Inpsyde\PayPalCommerce\WcGateway\Gateway\WcGateway;
use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
class SubscriptionModule implements ModuleInterface
{
public function setup(): ServiceProviderInterface
{
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
}
/**
* @inheritDoc
*/
public function run(ContainerInterface $container)
{
add_action(
'woocommerce_scheduled_subscription_payment_' . WcGateway::ID,
function($amount, $order) use ($container) {
if (! is_a($order, \WC_Order::class)) {
return;
}
$handler = $container->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
);
}
);
}
}

View file

@ -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' => [],
],
];
},
];

View file

@ -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;
}

View file

@ -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');