Merge pull request #6 from inpsyde/feature/create-order-intent-authorize

Payment with intent authorize
This commit is contained in:
David Remer 2020-04-23 12:09:08 +03:00 committed by GitHub
commit e13eb0ff82
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1642 additions and 54 deletions

View file

@ -1,13 +1,20 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\WcGateway;
use Dhii\Data\Container\ContainerInterface;
use Inpsyde\PayPalCommerce\WcGateway\Admin\OrderDetail;
use Inpsyde\PayPalCommerce\WcGateway\Admin\OrderTablePaymentStatusColumn;
use Inpsyde\PayPalCommerce\WcGateway\Admin\PaymentStatusOrderDetail;
use Inpsyde\PayPalCommerce\WcGateway\Checkout\DisableGateways;
use Inpsyde\PayPalCommerce\WcGateway\Gateway\WcGateway;
use Inpsyde\PayPalCommerce\WcGateway\Gateway\WcGatewayBase;
use Inpsyde\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice;
use Inpsyde\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice;
use Inpsyde\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use Inpsyde\PayPalCommerce\WcGateway\Processor\Processor;
use Inpsyde\PayPalCommerce\WcGateway\Settings\Settings;
use Inpsyde\PayPalCommerce\WcGateway\Settings\SettingsFields;
@ -18,10 +25,21 @@ return [
'wcgateway.gateway' => function (ContainerInterface $container) : WcGateway {
$sessionHandler = $container->get('session.handler');
$cartRepository = $container->get('api.repository.cart');
$endpoint = $container->get('api.endpoint.order');
// TODO eventuall get rid of the endpoints as the processor is sufficient
$orderEndpoint = $container->get('api.endpoint.order');
$paymentsEndpoint = $container->get('api.endpoint.payments');
$orderFactory = $container->get('api.factory.order');
$settingsFields = $container->get('wcgateway.settings.fields');
return new WcGateway($sessionHandler, $cartRepository, $endpoint, $orderFactory, $settingsFields);
$processor = $container->get('wcgateway.processor');
return new WcGateway(
$sessionHandler,
$cartRepository,
$orderEndpoint,
$paymentsEndpoint,
$orderFactory,
$settingsFields,
$processor
);
},
'wcgateway.disabler' => function (ContainerInterface $container) : DisableGateways {
$sessionHandler = $container->get('session.handler');
@ -36,7 +54,27 @@ return [
$settings = $container->get('wcgateway.settings');
return new ConnectAdminNotice($settings);
},
'wcgateway.settings.fields' => function (ContainerInterface $container) : SettingsFields {
'wcgateway.notice.authorize-order-action' =>
function (ContainerInterface $container): AuthorizeOrderActionNotice {
return new AuthorizeOrderActionNotice();
},
'wcgateway.settings.fields' => function (ContainerInterface $container): SettingsFields {
return new SettingsFields();
},
'wcgateway.processor' => function (ContainerInterface $container): Processor {
$authorizedPaymentsProcessor = $container->get('wcgateway.processor.authorized-payments');
return new Processor($authorizedPaymentsProcessor);
},
'wcgateway.processor.authorized-payments' => function (ContainerInterface $container): AuthorizedPaymentsProcessor {
$orderEndpoint = $container->get('api.endpoint.order');
$paymentsEndpoint = $container->get('api.endpoint.payments');
return new AuthorizedPaymentsProcessor($orderEndpoint, $paymentsEndpoint);
},
'wcgateway.admin.order-payment-status' => function(ContainerInterface $container): PaymentStatusOrderDetail {
return new PaymentStatusOrderDetail();
},
'wcgateway.admin.orders-payment-status-column' => function(ContainerInterface $container): OrderTablePaymentStatusColumn {
$settings = $container->get('wcgateway.settings');
return new OrderTablePaymentStatusColumn($settings);
}
];

View file

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\WcGateway\Admin;
use Inpsyde\PayPalCommerce\WcGateway\Settings\Settings;
class OrderTablePaymentStatusColumn
{
const COLUMN_KEY = 'ppcp_payment_status';
const INTENT = 'authorize';
const AFTER_COLUMN_KEY = 'order_status';
protected $settings;
public function __construct(Settings $settings)
{
$this->settings = $settings;
}
public function register(array $columns): array
{
if ($this->settings->get('intent') !== self::INTENT) {
return $columns;
}
$statusColumnPosition = array_search(self::AFTER_COLUMN_KEY, array_keys($columns), true);
$toInsertPosition = false === $statusColumnPosition ? count($columns) : $statusColumnPosition + 1;
$columns = array_merge(
array_slice($columns, 0, $toInsertPosition),
[
self::COLUMN_KEY => __('Payment Captured', 'woocommerce-paypal-gateway'),
],
array_slice($columns, $toInsertPosition)
);
return $columns;
}
public function render(string $column, int $wcOrderId)
{
if ($this->settings->get('intent') !== self::INTENT) {
return;
}
if (self::COLUMN_KEY !== $column) {
return;
}
if ($this->isCaptured($wcOrderId)) {
$this->renderCompletedStatus();
} else {
$this->renderIncompletedStatus();
}
}
protected function isCaptured(int $wcOrderId): bool
{
$wcOrder = wc_get_order($wcOrderId);
$captured = $wcOrder->get_meta('_ppcp_paypal_captured');
if (!empty($captured) && wc_string_to_bool($captured)) {
return true;
}
return false;
}
protected function renderCompletedStatus()
{
echo '<span class="dashicons dashicons-yes"></span>';
}
protected function renderIncompletedStatus()
{
printf(
'<mark class="onbackorder">%s</mark>',
esc_html__('Not captured', 'woocommerce-paypal-gateway')
);
}
}

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\WcGateway\Admin;
class PaymentStatusOrderDetail
{
public function render(int $wcOrderId)
{
$wcOrder = new \WC_Order($wcOrderId);
$intent = $wcOrder->get_meta('_ppcp_paypal_intent');
$captured = $wcOrder->get_meta('_ppcp_paypal_captured');
if (strcasecmp($intent, 'AUTHORIZE') !== 0) {
return;
}
if (!empty($captured) && wc_string_to_bool($captured)) {
return;
}
printf(
// @phpcs:ignore Inpsyde.CodeQuality.LineLength.TooLong
'<li class="wide"><p><mark class="order-status status-on-hold"><span>%1$s</span></mark></p><p>%2$s</p></li>',
esc_html__('Not captured', 'woocommerce-paypal-gateway'),
esc_html__('To capture the payment select capture action from the list below.', 'woocommerce-paypal-gateway'),
);
}
}

View file

@ -1,14 +1,18 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\WcGateway\Gateway;
use Inpsyde\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use Inpsyde\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use Inpsyde\PayPalCommerce\ApiClient\Entity\Order;
use Inpsyde\PayPalCommerce\ApiClient\Entity\OrderStatus;
use Inpsyde\PayPalCommerce\ApiClient\Factory\OrderFactory;
use Inpsyde\PayPalCommerce\ApiClient\Repository\CartRepository;
use Inpsyde\PayPalCommerce\Session\SessionHandler;
use Inpsyde\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice;
use Inpsyde\PayPalCommerce\WcGateway\Processor\Processor;
use Inpsyde\PayPalCommerce\WcGateway\Settings\SettingsFields;
//phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
@ -17,23 +21,29 @@ class WcGateway extends WcGatewayBase implements WcGatewayInterface
{
private $isSandbox = true;
private $sessionHandler;
private $endpoint;
private $orderEndpoint;
private $cartRepository;
private $orderFactory;
private $settingsFields;
private $paymentsEndpoint;
private $processor;
public function __construct(
SessionHandler $sessionHandler,
CartRepository $cartRepository,
OrderEndpoint $endpoint,
OrderEndpoint $orderEndpoint,
PaymentsEndpoint $paymentsEndpoint,
OrderFactory $orderFactory,
SettingsFields $settingsFields
SettingsFields $settingsFields,
Processor $processor
) {
$this->sessionHandler = $sessionHandler;
$this->cartRepository = $cartRepository;
$this->endpoint = $endpoint;
$this->orderEndpoint = $orderEndpoint;
$this->paymentsEndpoint = $paymentsEndpoint;
$this->orderFactory = $orderFactory;
$this->settingsFields = $settingsFields;
$this->processor = $processor;
$this->method_title = __('PayPal Payments', 'woocommerce-paypal-gateway');
$this->method_description = __(
@ -63,15 +73,19 @@ class WcGateway extends WcGatewayBase implements WcGatewayInterface
$this->form_fields = $this->settingsFields->fields();
}
public function process_payment($orderId) : ?array
public function process_payment($orderId): ?array
{
global $woocommerce;
$wcOrder = new \WC_Order($orderId);
//ToDo: We need to fetch the order from paypal again to get it with the new status.
$order = $this->sessionHandler->order();
$wcOrder->update_meta_data('_ppcp_paypal_order_id', $order->id());
$wcOrder->update_meta_data('_ppcp_paypal_intent', $order->intent());
$errorMessage = null;
if (! $order || ! $order->status()->is(OrderStatus::APPROVED)) {
if (!$order || !$order->status()->is(OrderStatus::APPROVED)) {
$errorMessage = 'not approve yet';
}
$errorMessage = null;
@ -81,10 +95,16 @@ class WcGateway extends WcGatewayBase implements WcGatewayInterface
}
$order = $this->patchOrder($wcOrder, $order);
$order = $this->endpoint->capture($order);
if ($order->intent() === 'CAPTURE') {
$order = $this->orderEndpoint->capture($order);
}
if ($order->intent() === 'AUTHORIZE') {
$order = $this->orderEndpoint->authorize($order);
}
$wcOrder->update_status('on-hold', __('Awaiting payment.', 'woocommerce-paypal-gateway'));
if ($order->status()->is(OrderStatus::COMPLETED)) {
if ($order->status()->is(OrderStatus::COMPLETED) && $order->intent() === 'CAPTURE') {
$wcOrder->update_status('processing', __('Payment received.', 'woocommerce-paypal-gateway'));
}
$woocommerce->cart->empty_cart();
@ -96,10 +116,56 @@ class WcGateway extends WcGatewayBase implements WcGatewayInterface
];
}
private function patchOrder(\WC_Order $wcOrder, Order $order) : Order
public function captureAuthorizedPayment(\WC_Order $wcOrder): void
{
$result = $this->processor->authorizedPayments()->process($wcOrder);
if ($result === 'INACCESSIBLE') {
AuthorizeOrderActionNotice::displayMessage(AuthorizeOrderActionNotice::NO_INFO);
}
if ($result === 'ALREADY_CAPTURED') {
if ($wcOrder->get_status() === 'on-hold') {
$wcOrder->add_order_note(
__(
'Payment successfully captured.',
'woocommerce-paypal-gateway'
)
);
$wcOrder->update_status('processing');
$wcOrder->update_meta_data('_ppcp_paypal_captured', 'true');
// TODO investigate why save has to be called
$wcOrder->save();
}
AuthorizeOrderActionNotice::displayMessage(AuthorizeOrderActionNotice::ALREADY_CAPTURED);
}
if ($result === 'FAILED') {
AuthorizeOrderActionNotice::displayMessage(AuthorizeOrderActionNotice::FAILED);
}
if ($result === 'SUCCESSFUL') {
$wcOrder->add_order_note(
__(
'Payment successfully captured.',
'woocommerce-paypal-gateway'
)
);
$wcOrder->update_status('processing');
$wcOrder->update_meta_data('_ppcp_paypal_captured', 'true');
// TODO investigate why save has to be called
$wcOrder->save();
AuthorizeOrderActionNotice::displayMessage(AuthorizeOrderActionNotice::SUCCESS);
}
}
private function patchOrder(\WC_Order $wcOrder, Order $order): Order
{
$updatedOrder = $this->orderFactory->fromWcOrder($wcOrder, $order);
$order = $this->endpoint->patchOrderWith($order, $updatedOrder);
$order = $this->orderEndpoint->patchOrderWith($order, $updatedOrder);
return $order;
}
}

View file

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\WcGateway\Notice;
class AuthorizeOrderActionNotice
{
const NO_INFO = 81;
const ALREADY_CAPTURED = 82;
const FAILED = 83;
const SUCCESS = 84;
public function registerMessages(array $messages): array
{
$messages['shop_order'][self::NO_INFO] = __(
'Could not retrieve information. Try again later.',
'woocommerce-paypal-gateway'
);
$messages['shop_order'][self::ALREADY_CAPTURED] = __(
'Payment already captured.',
'woocommerce-paypal-gateway'
);
$messages['shop_order'][self::FAILED] = __(
'Failed to capture. Try again later.',
'woocommerce-paypal-gateway'
);
$messages['shop_order'][self::SUCCESS] = __(
'Payment successfully captured.',
'woocommerce-paypal-gateway'
);
return $messages;
}
public static function displayMessage(int $messageCode): void
{
add_filter(
'redirect_post_location',
function ($location) use ($messageCode) {
return add_query_arg(
'message',
$messageCode,
$location
);
}
);
}
}

View file

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\WcGateway\Processor;
use Exception;
use Inpsyde\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use Inpsyde\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use Inpsyde\PayPalCommerce\ApiClient\Entity\Authorization;
use Inpsyde\PayPalCommerce\ApiClient\Entity\AuthorizationStatus;
use Inpsyde\PayPalCommerce\ApiClient\Entity\Order;
class AuthorizedPaymentsProcessor
{
public const SUCCESSFUL = 'SUCCESSFUL';
public const ALREADY_CAPTURED = 'ALREADY_CAPTURED';
public const FAILED = 'FAILED';
public const INACCESSIBLE = 'INACCESSIBLE';
private $orderEndpoint;
private $paymentsEndpoint;
public function __construct(
OrderEndpoint $orderEndpoint,
PaymentsEndpoint $paymentsEndpoint
) {
$this->orderEndpoint = $orderEndpoint;
$this->paymentsEndpoint = $paymentsEndpoint;
}
public function process(\WC_Order $wcOrder): string
{
$orderId = $this->getPayPalOrderId($wcOrder);
try {
$order = $this->getCurrentOrderInfo($orderId);
} catch (Exception $exception) {
return self::INACCESSIBLE;
}
$authorizations = $this->getAllAuthorizations($order);
if (!$this->areAuthorizationToCapture(...$authorizations)) {
return self::ALREADY_CAPTURED;
}
try {
$this->captureAuthorizations(...$authorizations);
} catch (Exception $exception) {
return self::FAILED;
}
return self::SUCCESSFUL;
}
protected function getPayPalOrderId(\WC_Order $wcOrder): string
{
return $wcOrder->get_meta('_ppcp_paypal_order_id');
}
protected function getCurrentOrderInfo(string $orderId): Order
{
return $this->orderEndpoint->order($orderId);
}
protected function getAllAuthorizations(Order $order): array
{
$authorizations = [];
foreach ($order->purchaseUnits() as $purchaseUnit) {
foreach ($purchaseUnit->payments()->authorizations() as $authorization) {
$authorizations[] = $authorization;
}
}
return $authorizations;
}
protected function areAuthorizationToCapture(Authorization ...$authorizations): bool
{
$alreadyCapturedAuthorizations = $this->authorizationsWithCapturedStatus(...$authorizations);
return count($alreadyCapturedAuthorizations) !== count($authorizations);
}
protected function captureAuthorizations(Authorization ...$authorizations)
{
$uncapturedAuthorizations = $this->authorizationsWithCreatedStatus(...$authorizations);
foreach ($uncapturedAuthorizations as $authorization) {
$this->paymentsEndpoint->capture($authorization->id());
}
}
/**
* @return Authorization[]
*/
protected function authorizationsWithCreatedStatus(Authorization ...$authorizations): array
{
return array_filter(
$authorizations,
function (Authorization $authorization) {
return $authorization->status()->is(AuthorizationStatus::CREATED);
}
);
}
/**
* @return Authorization[]
*/
protected function authorizationsWithCapturedStatus(Authorization ...$authorizations): array
{
return array_filter(
$authorizations,
function (Authorization $authorization) {
return $authorization->status()->is(AuthorizationStatus::CAPTURED);
}
);
}
}

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\WcGateway\Processor;
class Processor
{
protected $authorizedPaymentsProcessor;
public function __construct(AuthorizedPaymentsProcessor $authorizedPaymentsProcessor)
{
$this->authorizedPaymentsProcessor = $authorizedPaymentsProcessor;
}
public function authorizedPayments() {
return $this->authorizedPaymentsProcessor;
}
}

View file

@ -47,6 +47,21 @@ class SettingsFields
'woocommerce-paypal-gateway'
),
],
'intent' => [
'title' => __('Intent', 'woocommerce-paypal-gateway'),
'type' => 'select',
'class' => 'wc-enhanced-select',
'default' => 'capture',
'desc_tip' => true,
'description' => __(
'The intent to either capture payment immediately or authorize a payment for an order after order creation.',
'woocommerce-paypal-gateway'
),
'options' => [
'capture' => __('Capture', 'woocommerce-paypal-gateway'),
'authorize' => __('Authorize', 'woocommerce-paypal-gateway'),
],
],
];
}

View file

@ -1,23 +1,28 @@
<?php
declare(strict_types=1);
namespace Inpsyde\PayPalCommerce\WcGateway;
use Dhii\Container\ServiceProvider;
use Dhii\Modular\Module\ModuleInterface;
use Inpsyde\PayPalCommerce\WcGateway\Admin\OrderDetail;
use Inpsyde\PayPalCommerce\WcGateway\Admin\OrderTablePaymentStatusColumn;
use Inpsyde\PayPalCommerce\WcGateway\Admin\PaymentStatusOrderDetail;
use Inpsyde\PayPalCommerce\WcGateway\Checkout\DisableGateways;
use Inpsyde\PayPalCommerce\WcGateway\Gateway\WcGateway;
use Inpsyde\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice;
use Inpsyde\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice;
use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
class WcGatewayModule implements ModuleInterface
{
public function setup(): ServiceProviderInterface
{
return new ServiceProvider(
require __DIR__.'/../services.php',
require __DIR__.'/../extensions.php'
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
}
@ -26,9 +31,8 @@ class WcGatewayModule implements ModuleInterface
add_filter(
'woocommerce_payment_gateways',
function ($methods) use ($container) : array {
$methods[] = $container->get('wcgateway.gateway');
return (array) $methods;
return (array)$methods;
}
);
@ -39,7 +43,7 @@ class WcGatewayModule implements ModuleInterface
/**
* @var DisableGateways $disabler
*/
return $disabler->handler((array) $methods);
return $disabler->handler((array)$methods);
}
);
@ -53,5 +57,74 @@ class WcGatewayModule implements ModuleInterface
$notice->display();
}
);
add_filter(
'woocommerce_order_actions',
function ($orderActions): array {
$orderActions['ppcp_authorize_order'] = __(
'Capture authorized PayPal payment',
'woocommerce-paypal-gateway'
);
return $orderActions;
}
);
add_action(
'woocommerce_order_action_ppcp_authorize_order',
function (\WC_Order $wcOrder) use ($container) {
/**
* @var WcGateway $gateway
*/
$gateway = $container->get('wcgateway.gateway');
$gateway->captureAuthorizedPayment($wcOrder);
}
);
add_filter(
'post_updated_messages',
function ($messages) use ($container) {
/**
* @var AuthorizeOrderActionNotice $authorizeOrderAction
*/
$authorizeOrderAction = $container->get('wcgateway.notice.authorize-order-action');
return $authorizeOrderAction->registerMessages($messages);
},
20
);
add_action(
'woocommerce_order_actions_start',
function ($wcOrderId) use ($container) {
/**
* @var PaymentStatusOrderDetail $class
*/
$class = $container->get('wcgateway.admin.order-payment-status');
$class->render(intval($wcOrderId));
}
);
add_filter(
'manage_edit-shop_order_columns',
function ($columns) use ($container) {
/**
* @var OrderTablePaymentStatusColumn $paymentStatusColumn
*/
$paymentStatusColumn = $container->get('wcgateway.admin.orders-payment-status-column');
return $paymentStatusColumn->register($columns);
}
);
add_action(
'manage_shop_order_posts_custom_column',
function ($column, $wcOrderId) use ($container) {
/**
* @var OrderTablePaymentStatusColumn $paymentStatusColumn
*/
$paymentStatusColumn = $container->get('wcgateway.admin.orders-payment-status-column');
$paymentStatusColumn->render($column, intval($wcOrderId));
},
10,
2
);
}
}