Merge branch 'trunk' into bug/fix-project-configuration

This commit is contained in:
Alex P 2021-10-07 18:04:36 +03:00
commit c65df28d54
30 changed files with 1151 additions and 521 deletions

2
.gitattributes vendored
View file

@ -19,4 +19,4 @@ phpunit.xml export-ignore
psalm.xml export-ignore
yarn.lock export-ignore
* text eol=lf
* text=auto

View file

@ -142,12 +142,14 @@ return array(
},
'api.endpoint.payments' => static function ( ContainerInterface $container ): PaymentsEndpoint {
$authorizations_factory = $container->get( 'api.factory.authorization' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$capture_factory = $container->get( 'api.factory.capture' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new PaymentsEndpoint(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$authorizations_factory,
$capture_factory,
$logger
);
},

View file

@ -11,6 +11,8 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
@ -338,8 +340,8 @@ class OrderEndpoint {
$order = $this->order_factory->from_paypal_response( $json );
$purchase_units_payments_captures_status = $order->purchase_units()[0]->payments()->captures()[0]->status() ?? '';
if ( $purchase_units_payments_captures_status && 'DECLINED' === $purchase_units_payments_captures_status ) {
$capture_status = $order->purchase_units()[0]->payments()->captures()[0]->status() ?? null;
if ( $capture_status && $capture_status->is( CaptureStatus::DECLINED ) ) {
throw new RuntimeException( __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' ) );
}
@ -412,6 +414,12 @@ class OrderEndpoint {
throw $error;
}
$order = $this->order_factory->from_paypal_response( $json );
$authorization_status = $order->purchase_units()[0]->payments()->authorizations()[0]->status() ?? null;
if ( $authorization_status && $authorization_status->is( AuthorizationStatus::DENIED ) ) {
throw new RuntimeException( __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' ) );
}
return $order;
}

View file

@ -11,11 +11,13 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory;
/**
* Class PaymentsEndpoint
@ -45,6 +47,13 @@ class PaymentsEndpoint {
*/
private $authorizations_factory;
/**
* The capture factory.
*
* @var CaptureFactory
*/
private $capture_factory;
/**
* The logger.
*
@ -58,18 +67,21 @@ class PaymentsEndpoint {
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param AuthorizationFactory $authorization_factory The authorization factory.
* @param CaptureFactory $capture_factory The capture factory.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $host,
Bearer $bearer,
AuthorizationFactory $authorization_factory,
CaptureFactory $capture_factory,
LoggerInterface $logger
) {
$this->host = $host;
$this->bearer = $bearer;
$this->authorizations_factory = $authorization_factory;
$this->capture_factory = $capture_factory;
$this->logger = $logger;
}
@ -136,10 +148,11 @@ class PaymentsEndpoint {
*
* @param string $authorization_id The id.
*
* @return Authorization
* @return Capture
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function capture( string $authorization_id ): Authorization {
public function capture( string $authorization_id ): Capture {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/payments/authorizations/' . $authorization_id . '/capture';
$args = array(
@ -155,39 +168,18 @@ class PaymentsEndpoint {
$json = json_decode( $response['body'] );
if ( is_wp_error( $response ) ) {
$error = new RuntimeException(
__( 'Could not capture authorized payment.', 'woocommerce-paypal-payments' )
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
throw new RuntimeException( 'Could not capture authorized payment.' );
}
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 201 !== $status_code ) {
$error = new PayPalApiException(
throw new PayPalApiException(
$json,
$status_code
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
}
$authorization = $this->authorizations_factory->from_paypal_response( $json );
return $authorization;
return $this->capture_factory->from_paypal_response( $json );
}
/**
@ -195,10 +187,11 @@ class PaymentsEndpoint {
*
* @param Refund $refund The refund to be processed.
*
* @return bool
* @return void
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function refund( Refund $refund ) : bool {
public function refund( Refund $refund ) : void {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/payments/captures/' . $refund->for_capture()->id() . '/refund';
$args = array(
@ -215,37 +208,50 @@ class PaymentsEndpoint {
$json = json_decode( $response['body'] );
if ( is_wp_error( $response ) ) {
$error = new RuntimeException(
__( 'Could not refund payment.', 'woocommerce-paypal-payments' )
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
throw new RuntimeException( 'Could not refund payment.' );
}
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 201 !== $status_code ) {
$error = new PayPalApiException(
throw new PayPalApiException(
$json,
$status_code
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
}
}
/**
* Voids a transaction.
*
* @param Authorization $authorization The PayPal payment authorization to void.
*
* @return void
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function void( Authorization $authorization ) : void {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/payments/authorizations/' . $authorization->id() . '/void';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
throw new RuntimeException( 'Could not void transaction.' );
}
return true;
$status_code = (int) wp_remote_retrieve_response_code( $response );
// Currently it can return body with 200 status, despite the docs saying that it should be 204 No content.
// We don't care much about body, so just checking that it was successful.
if ( $status_code < 200 || $status_code > 299 ) {
throw new PayPalApiException( null, $status_code );
}
}
}

View file

@ -44,13 +44,21 @@ class AuthorizationStatus {
*/
private $status;
/**
* The details.
*
* @var AuthorizationStatusDetails|null
*/
private $details;
/**
* AuthorizationStatus constructor.
*
* @param string $status The status.
* @param string $status The status.
* @param AuthorizationStatusDetails|null $details The details.
* @throws RuntimeException When the status is not valid.
*/
public function __construct( string $status ) {
public function __construct( string $status, ?AuthorizationStatusDetails $details = null ) {
if ( ! in_array( $status, self::VALID_STATUS, true ) ) {
throw new RuntimeException(
sprintf(
@ -60,7 +68,8 @@ class AuthorizationStatus {
)
);
}
$this->status = $status;
$this->status = $status;
$this->details = $details;
}
/**
@ -91,4 +100,13 @@ class AuthorizationStatus {
public function name(): string {
return $this->status;
}
/**
* Returns the details.
*
* @return AuthorizationStatusDetails|null
*/
public function details(): ?AuthorizationStatusDetails {
return $this->details;
}
}

View file

@ -26,17 +26,10 @@ class Capture {
/**
* The status.
*
* @var string
* @var CaptureStatus
*/
private $status;
/**
* The status details.
*
* @var string
*/
private $status_details;
/**
* The amount.
*
@ -75,19 +68,17 @@ class Capture {
/**
* Capture constructor.
*
* @param string $id The ID.
* @param string $status The status.
* @param string $status_details The status details.
* @param Amount $amount The amount.
* @param bool $final_capture The final capture.
* @param string $seller_protection The seller protection.
* @param string $invoice_id The invoice id.
* @param string $custom_id The custom id.
* @param string $id The ID.
* @param CaptureStatus $status The status.
* @param Amount $amount The amount.
* @param bool $final_capture The final capture.
* @param string $seller_protection The seller protection.
* @param string $invoice_id The invoice id.
* @param string $custom_id The custom id.
*/
public function __construct(
string $id,
string $status,
string $status_details,
CaptureStatus $status,
Amount $amount,
bool $final_capture,
string $seller_protection,
@ -97,7 +88,6 @@ class Capture {
$this->id = $id;
$this->status = $status;
$this->status_details = $status_details;
$this->amount = $amount;
$this->final_capture = $final_capture;
$this->seller_protection = $seller_protection;
@ -117,21 +107,12 @@ class Capture {
/**
* Returns the status.
*
* @return string
* @return CaptureStatus
*/
public function status() : string {
public function status() : CaptureStatus {
return $this->status;
}
/**
* Returns the status details object.
*
* @return \stdClass
*/
public function status_details() : \stdClass {
return (object) array( 'reason' => $this->status_details );
}
/**
* Returns the amount.
*
@ -183,15 +164,18 @@ class Capture {
* @return array
*/
public function to_array() : array {
return array(
$data = array(
'id' => $this->id(),
'status' => $this->status(),
'status_details' => (array) $this->status_details(),
'status' => $this->status()->name(),
'amount' => $this->amount()->to_array(),
'final_capture' => $this->final_capture(),
'seller_protection' => (array) $this->seller_protection(),
'invoice_id' => $this->invoice_id(),
'custom_id' => $this->custom_id(),
);
if ( $this->status()->details() ) {
$data['status_details'] = array( 'reason' => $this->status()->details()->reason() );
}
return $data;
}
}

View file

@ -0,0 +1,56 @@
<?php
/**
* The AuthorizationStatusDetails object.
*
* @see https://developer.paypal.com/docs/api/payments/v2/#definition-authorization_status_details
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class AuthorizationStatusDetails
*/
class AuthorizationStatusDetails {
const PENDING_REVIEW = 'PENDING_REVIEW';
/**
* The reason.
*
* @var string
*/
private $reason;
/**
* AuthorizationStatusDetails constructor.
*
* @param string $reason The reason explaining authorization status.
*/
public function __construct( string $reason ) {
$this->reason = $reason;
}
/**
* Compares the current reason with a given one.
*
* @param string $reason The reason to compare with.
*
* @return bool
*/
public function is( string $reason ): bool {
return $this->reason === $reason;
}
/**
* Returns the reason explaining authorization status.
*
* @return string
*/
public function reason(): string {
return $this->reason;
}
}

View file

@ -0,0 +1,79 @@
<?php
/**
* The CaptureStatus object.
*
* @see https://developer.paypal.com/docs/api/orders/v2/#definition-capture_status
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class CaptureStatus
*/
class CaptureStatus {
const COMPLETED = 'COMPLETED';
const DECLINED = 'DECLINED';
const PARTIALLY_REFUNDED = 'PARTIALLY_REFUNDED';
const REFUNDED = 'REFUNDED';
const FAILED = 'FAILED';
const PENDING = 'PENDING';
/**
* The status.
*
* @var string
*/
private $status;
/**
* The details.
*
* @var CaptureStatusDetails|null
*/
private $details;
/**
* CaptureStatus constructor.
*
* @param string $status The status.
* @param CaptureStatusDetails|null $details The details.
*/
public function __construct( string $status, ?CaptureStatusDetails $details = null ) {
$this->status = $status;
$this->details = $details;
}
/**
* Compares the current status with a given one.
*
* @param string $status The status to compare with.
*
* @return bool
*/
public function is( string $status ): bool {
return $this->status === $status;
}
/**
* Returns the status.
*
* @return string
*/
public function name(): string {
return $this->status;
}
/**
* Returns the details.
*
* @return CaptureStatusDetails|null
*/
public function details(): ?CaptureStatusDetails {
return $this->details;
}
}

View file

@ -0,0 +1,66 @@
<?php
/**
* The CaptureStatusDetails object.
*
* @see https://developer.paypal.com/docs/api/payments/v2/#definition-capture_status_details
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class CaptureStatusDetails
*/
class CaptureStatusDetails {
const BUYER_COMPLAINT = 'BUYER_COMPLAINT';
const CHARGEBACK = 'CHARGEBACK';
const ECHECK = 'ECHECK';
const INTERNATIONAL_WITHDRAWAL = 'INTERNATIONAL_WITHDRAWAL';
const OTHER = 'OTHER';
const PENDING_REVIEW = 'PENDING_REVIEW';
const RECEIVING_PREFERENCE_MANDATES_MANUAL_ACTION = 'RECEIVING_PREFERENCE_MANDATES_MANUAL_ACTION';
const REFUNDED = 'REFUNDED';
const TRANSACTION_APPROVED_AWAITING_FUNDING = 'TRANSACTION_APPROVED_AWAITING_FUNDING';
const UNILATERAL = 'REFUNDED';
const VERIFICATION_REQUIRED = 'VERIFICATION_REQUIRED';
/**
* The reason.
*
* @var string
*/
private $reason;
/**
* CaptureStatusDetails constructor.
*
* @param string $reason The reason explaining capture status.
*/
public function __construct( string $reason ) {
$this->reason = $reason;
}
/**
* Compares the current reason with a given one.
*
* @param string $reason The reason to compare with.
*
* @return bool
*/
public function is( string $reason ): bool {
return $this->reason === $reason;
}
/**
* Returns the reason explaining capture status.
*
* @return string
*/
public function reason(): string {
return $this->reason;
}
}

View file

@ -39,9 +39,13 @@ class PayPalApiException extends RuntimeException {
$response = new \stdClass();
}
if ( ! isset( $response->message ) ) {
$response->message = __(
'Unknown error while connecting to PayPal.',
'woocommerce-paypal-payments'
$response->message = sprintf(
/* translators: %1$d - HTTP status code number (404, 500, ...) */
__(
'Unknown error while connecting to PayPal. Status code: %1$d.',
'woocommerce-paypal-payments'
),
$this->status_code
);
}
if ( ! isset( $response->name ) ) {

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatusDetails;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
@ -39,9 +40,14 @@ class AuthorizationFactory {
);
}
$reason = $data->status_details->reason ?? null;
return new Authorization(
$data->id,
new AuthorizationStatus( $data->status )
new AuthorizationStatus(
$data->status,
$reason ? new AuthorizationStatusDetails( $reason ) : null
)
);
}
}

View file

@ -10,6 +10,8 @@ declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatusDetails;
/**
* Class CaptureFactory
@ -42,11 +44,14 @@ class CaptureFactory {
*/
public function from_paypal_response( \stdClass $data ) : Capture {
$reason = isset( $data->status_details->reason ) ? (string) $data->status_details->reason : '';
$reason = $data->status_details->reason ?? null;
return new Capture(
(string) $data->id,
(string) $data->status,
$reason,
new CaptureStatus(
(string) $data->status,
$reason ? new CaptureStatusDetails( $reason ) : null
),
$this->amount_factory->from_paypal_response( $data->amount ),
(bool) $data->final_capture,
(string) $data->seller_protection->status,

View file

@ -608,7 +608,10 @@ class SmartButton implements SmartButtonInterface {
*/
private function get_3ds_contingency(): string {
if ( $this->settings->has( '3d_secure_contingency' ) ) {
return $this->settings->get( '3d_secure_contingency' );
$value = $this->settings->get( '3d_secure_contingency' );
if ( $value ) {
return $value;
}
}
return 'SCA_WHEN_REQUIRED';

View file

@ -54,6 +54,7 @@ return array(
$transaction_url_provider = $container->get( 'wcgateway.transaction-url-provider' );
$subscription_helper = $container->get( 'subscription.helper' );
$page_id = $container->get( 'wcgateway.current-ppcp-settings-page-id' );
$environment = $container->get( 'onboarding.environment' );
return new PayPalGateway(
$settings_renderer,
$order_processor,
@ -65,7 +66,8 @@ return array(
$state,
$transaction_url_provider,
$subscription_helper,
$page_id
$page_id,
$environment
);
},
'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway {
@ -84,7 +86,8 @@ return array(
$payer_factory = $container->get( 'api.factory.payer' );
$order_endpoint = $container->get( 'api.endpoint.order' );
$subscription_helper = $container->get( 'subscription.helper' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$environment = $container->get( 'onboarding.environment' );
return new CreditCardGateway(
$settings_renderer,
$order_processor,
@ -101,7 +104,8 @@ return array(
$payer_factory,
$order_endpoint,
$subscription_helper,
$logger
$logger,
$environment
);
},
'wcgateway.disabler' => static function ( ContainerInterface $container ): DisableGateways {
@ -209,25 +213,28 @@ return array(
$authorized_payments_processor,
$settings,
$logger,
$environment->current_environment_is( Environment::SANDBOX )
$environment
);
},
'wcgateway.processor.refunds' => static function ( ContainerInterface $container ): RefundProcessor {
$order_endpoint = $container->get( 'api.endpoint.order' );
$payments_endpoint = $container->get( 'api.endpoint.payments' );
return new RefundProcessor( $order_endpoint, $payments_endpoint );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new RefundProcessor( $order_endpoint, $payments_endpoint, $logger );
},
'wcgateway.processor.authorized-payments' => static function ( ContainerInterface $container ): AuthorizedPaymentsProcessor {
$order_endpoint = $container->get( 'api.endpoint.order' );
$payments_endpoint = $container->get( 'api.endpoint.payments' );
return new AuthorizedPaymentsProcessor( $order_endpoint, $payments_endpoint );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new AuthorizedPaymentsProcessor( $order_endpoint, $payments_endpoint, $logger );
},
'wcgateway.admin.render-authorize-action' => static function ( ContainerInterface $container ): RenderAuthorizeAction {
return new RenderAuthorizeAction();
},
'wcgateway.admin.order-payment-status' => static function ( ContainerInterface $container ): PaymentStatusOrderDetail {
return new PaymentStatusOrderDetail();
$column = $container->get( 'wcgateway.admin.orders-payment-status-column' );
return new PaymentStatusOrderDetail( $column );
},
'wcgateway.admin.orders-payment-status-column' => static function ( ContainerInterface $container ): OrderTablePaymentStatusColumn {
$settings = $container->get( 'wcgateway.settings' );

View file

@ -81,7 +81,7 @@ class OrderTablePaymentStatusColumn {
$wc_order = wc_get_order( $wc_order_id );
if ( ! is_a( $wc_order, \WC_Order::class ) || ! $this->render_for_order( $wc_order ) ) {
if ( ! is_a( $wc_order, \WC_Order::class ) || ! $this->should_render_for_order( $wc_order ) ) {
return;
}
@ -100,8 +100,14 @@ class OrderTablePaymentStatusColumn {
*
* @return bool
*/
private function render_for_order( \WC_Order $order ): bool {
return ! empty( $order->get_meta( PayPalGateway::CAPTURED_META_KEY ) );
public function should_render_for_order( \WC_Order $order ): bool {
$intent = $order->get_meta( PayPalGateway::INTENT_META_KEY );
$captured = $order->get_meta( PayPalGateway::CAPTURED_META_KEY );
$status = $order->get_status();
$not_allowed_statuses = array( 'refunded' );
return ! empty( $intent ) && strtoupper( self::INTENT ) === strtoupper( $intent ) &&
! empty( $captured ) &&
! in_array( $status, $not_allowed_statuses, true );
}
/**
@ -111,7 +117,7 @@ class OrderTablePaymentStatusColumn {
*
* @return bool
*/
private function is_captured( \WC_Order $wc_order ): bool {
public function is_captured( \WC_Order $wc_order ): bool {
$captured = $wc_order->get_meta( PayPalGateway::CAPTURED_META_KEY );
return wc_string_to_bool( $captured );
}

View file

@ -16,6 +16,22 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
*/
class PaymentStatusOrderDetail {
/**
* The capture info column.
*
* @var OrderTablePaymentStatusColumn
*/
private $column;
/**
* PaymentStatusOrderDetail constructor.
*
* @param OrderTablePaymentStatusColumn $column The capture info column.
*/
public function __construct( OrderTablePaymentStatusColumn $column ) {
$this->column = $column;
}
/**
* Renders the not captured information.
*
@ -23,14 +39,8 @@ class PaymentStatusOrderDetail {
*/
public function render( int $wc_order_id ) {
$wc_order = new \WC_Order( $wc_order_id );
$intent = $wc_order->get_meta( PayPalGateway::INTENT_META_KEY );
$captured = $wc_order->get_meta( PayPalGateway::CAPTURED_META_KEY );
if ( strcasecmp( $intent, 'AUTHORIZE' ) !== 0 ) {
return;
}
if ( ! empty( $captured ) && wc_string_to_bool( $captured ) ) {
if ( ! $this->column->should_render_for_order( $wc_order ) || $this->column->is_captured( $wc_order ) ) {
return;
}

View file

@ -13,6 +13,7 @@ use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
@ -96,6 +97,13 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
*/
private $order_endpoint;
/**
* The environment.
*
* @var Environment
*/
protected $environment;
/**
* CreditCardGateway constructor.
*
@ -115,6 +123,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @param LoggerInterface $logger The logger.
* @param Environment $environment The environment.
*/
public function __construct(
SettingsRenderer $settings_renderer,
@ -132,7 +141,8 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
PayerFactory $payer_factory,
OrderEndpoint $order_endpoint,
SubscriptionHelper $subscription_helper,
LoggerInterface $logger
LoggerInterface $logger,
Environment $environment
) {
$this->id = self::ID;
@ -143,6 +153,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
$this->config = $config;
$this->session_handler = $session_handler;
$this->refund_processor = $refund_processor;
$this->environment = $environment;
if ( $state->current_state() === State::STATE_ONBOARDED ) {
$this->supports = array( 'refunds' );
@ -424,4 +435,13 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
private function is_enabled(): bool {
return $this->config->has( 'dcc_enabled' ) && $this->config->get( 'dcc_enabled' );
}
/**
* Returns the environment.
*
* @return Environment
*/
protected function environment(): Environment {
return $this->environment;
}
}

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Gateway;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
@ -111,6 +112,13 @@ class PayPalGateway extends \WC_Payment_Gateway {
*/
protected $page_id;
/**
* The environment.
*
* @var Environment
*/
protected $environment;
/**
* PayPalGateway constructor.
*
@ -125,6 +133,7 @@ class PayPalGateway extends \WC_Payment_Gateway {
* @param TransactionUrlProvider $transaction_url_provider Service providing transaction view URL based on order.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page.
* @param Environment $environment The environment.
*/
public function __construct(
SettingsRenderer $settings_renderer,
@ -137,7 +146,8 @@ class PayPalGateway extends \WC_Payment_Gateway {
State $state,
TransactionUrlProvider $transaction_url_provider,
SubscriptionHelper $subscription_helper,
string $page_id
string $page_id,
Environment $environment
) {
$this->id = self::ID;
@ -150,6 +160,7 @@ class PayPalGateway extends \WC_Payment_Gateway {
$this->refund_processor = $refund_processor;
$this->transaction_url_provider = $transaction_url_provider;
$this->page_id = $page_id;
$this->environment = $environment;
$this->onboarded = $state->current_state() === State::STATE_ONBOARDED;
if ( $this->onboarded ) {
@ -239,20 +250,10 @@ class PayPalGateway extends \WC_Payment_Gateway {
* @return bool
*/
public function capture_authorized_payment( \WC_Order $wc_order ): bool {
$is_processed = $this->authorized_payments->process( $wc_order );
$this->render_authorization_message_for_status( $this->authorized_payments->last_status() );
$result_status = $this->authorized_payments->process( $wc_order );
$this->render_authorization_message_for_status( $result_status );
if ( $is_processed ) {
$wc_order->add_order_note(
__( 'Payment successfully captured.', 'woocommerce-paypal-payments' )
);
$wc_order->update_meta_data( self::CAPTURED_META_KEY, 'true' );
$wc_order->save();
$wc_order->payment_complete();
return true;
}
if ( $this->authorized_payments->last_status() === AuthorizedPaymentsProcessor::ALREADY_CAPTURED ) {
if ( AuthorizedPaymentsProcessor::ALREADY_CAPTURED === $result_status ) {
if ( $wc_order->get_status() === 'on-hold' ) {
$wc_order->add_order_note(
__( 'Payment successfully captured.', 'woocommerce-paypal-payments' )
@ -264,6 +265,23 @@ class PayPalGateway extends \WC_Payment_Gateway {
$wc_order->payment_complete();
return true;
}
$captures = $this->authorized_payments->captures();
if ( empty( $captures ) ) {
return false;
}
$this->handle_capture_status( end( $captures ), $wc_order );
if ( AuthorizedPaymentsProcessor::SUCCESSFUL === $result_status ) {
$wc_order->add_order_note(
__( 'Payment successfully captured.', 'woocommerce-paypal-payments' )
);
$wc_order->update_meta_data( self::CAPTURED_META_KEY, 'true' );
$wc_order->save();
return true;
}
return false;
}
@ -275,10 +293,11 @@ class PayPalGateway extends \WC_Payment_Gateway {
private function render_authorization_message_for_status( string $status ) {
$message_mapping = array(
AuthorizedPaymentsProcessor::SUCCESSFUL => AuthorizeOrderActionNotice::SUCCESS,
AuthorizedPaymentsProcessor::ALREADY_CAPTURED => AuthorizeOrderActionNotice::ALREADY_CAPTURED,
AuthorizedPaymentsProcessor::INACCESSIBLE => AuthorizeOrderActionNotice::NO_INFO,
AuthorizedPaymentsProcessor::NOT_FOUND => AuthorizeOrderActionNotice::NOT_FOUND,
AuthorizedPaymentsProcessor::SUCCESSFUL => AuthorizeOrderActionNotice::SUCCESS,
AuthorizedPaymentsProcessor::ALREADY_CAPTURED => AuthorizeOrderActionNotice::ALREADY_CAPTURED,
AuthorizedPaymentsProcessor::INACCESSIBLE => AuthorizeOrderActionNotice::NO_INFO,
AuthorizedPaymentsProcessor::NOT_FOUND => AuthorizeOrderActionNotice::NOT_FOUND,
AuthorizedPaymentsProcessor::BAD_AUTHORIZATION => AuthorizeOrderActionNotice::BAD_AUTHORIZATION,
);
$display_message = ( isset( $message_mapping[ $status ] ) ) ?
$message_mapping[ $status ]
@ -429,4 +448,13 @@ class PayPalGateway extends \WC_Payment_Gateway {
return $ret;
}
/**
* Returns the environment.
*
* @return Environment
*/
protected function environment(): Environment {
return $this->environment;
}
}

View file

@ -9,14 +9,21 @@ declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\WcGateway\Gateway;
use Exception;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait;
/**
* Trait ProcessPaymentTrait
*/
trait ProcessPaymentTrait {
use OrderMetaTrait, PaymentsStatusHandlingTrait;
/**
* Process a payment for an WooCommerce order.
*
@ -73,41 +80,37 @@ trait ProcessPaymentTrait {
$selected_token
);
if ( $order->status()->is( OrderStatus::COMPLETED ) && $order->intent() === 'CAPTURE' ) {
$wc_order->update_status(
'processing',
__( 'Payment received.', 'woocommerce-paypal-payments' )
);
$this->add_paypal_meta( $wc_order, $order, $this->environment() );
$this->session_handler->destroy_session_data();
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $wc_order ),
);
if ( ! $order->status()->is( OrderStatus::COMPLETED ) ) {
$this->logger->warning( "Unexpected status for order {$order->id()} using a saved credit card: " . $order->status()->name() );
return null;
}
if ( $order->status()->is( OrderStatus::COMPLETED ) && $order->intent() === 'AUTHORIZE' ) {
$this->order_endpoint->authorize( $order );
if ( ! in_array(
$order->intent(),
array( 'CAPTURE', 'AUTHORIZE' ),
true
) ) {
$this->logger->warning( "Could neither capture nor authorize order {$order->id()} using a saved credit card:" . 'Status: ' . $order->status()->name() . ' Intent: ' . $order->intent() );
return null;
}
if ( $order->intent() === 'AUTHORIZE' ) {
$order = $this->order_endpoint->authorize( $order );
$wc_order->update_meta_data( PayPalGateway::CAPTURED_META_KEY, 'false' );
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() );
$wc_order->update_status(
'on-hold',
__( 'Awaiting payment.', 'woocommerce-paypal-payments' )
);
$this->session_handler->destroy_session_data();
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $wc_order ),
);
}
$this->logger->warning( "Could neither capture nor authorize order {$order->id()} using a saved credit card:" . 'Status: ' . $order->status()->name() . ' Intent: ' . $order->intent() );
$this->handle_new_order_status( $order, $wc_order );
} catch ( RuntimeException $error ) {
$this->logger->error( $error->getMessage() );
$this->session_handler->destroy_session_data();
wc_add_notice( $error->getMessage(), 'error' );
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $wc_order ),
);
} catch ( RuntimeException $error ) {
$this->handle_failure( $wc_order, $error );
return null;
}
}
@ -186,12 +189,7 @@ trait ProcessPaymentTrait {
$this->session_handler->destroy_session_data();
} catch ( RuntimeException $error ) {
$wc_order->update_status(
'failed',
__( 'Could not process order.', 'woocommerce-paypal-payments' )
);
$this->session_handler->destroy_session_data();
wc_add_notice( $error->getMessage(), 'error' );
$this->handle_failure( $wc_order, $error );
return $failure_data;
}
@ -234,4 +232,30 @@ trait ProcessPaymentTrait {
}
return false;
}
/**
* Handles the payment failure.
*
* @param \WC_Order $wc_order The order.
* @param Exception $error The error causing the failure.
*/
protected function handle_failure( \WC_Order $wc_order, Exception $error ): void {
$this->logger->error( 'Payment failed: ' . $error->getMessage() );
$wc_order->update_status(
'failed',
__( 'Could not process order.', 'woocommerce-paypal-payments' )
);
$this->session_handler->destroy_session_data();
wc_add_notice( $error->getMessage(), 'error' );
}
/**
* Returns the environment.
*
* @return Environment
*/
abstract protected function environment(): Environment;
}

View file

@ -18,11 +18,12 @@ class AuthorizeOrderActionNotice {
const QUERY_PARAM = 'ppcp-authorized-message';
const NO_INFO = 81;
const ALREADY_CAPTURED = 82;
const FAILED = 83;
const SUCCESS = 84;
const NOT_FOUND = 85;
const NO_INFO = 81;
const ALREADY_CAPTURED = 82;
const FAILED = 83;
const SUCCESS = 84;
const NOT_FOUND = 85;
const BAD_AUTHORIZATION = 86;
/**
* Returns the current message if there is one.
@ -45,35 +46,42 @@ class AuthorizeOrderActionNotice {
* @return array
*/
private function current_message(): array {
$messages[ self::NO_INFO ] = array(
$messages[ self::NO_INFO ] = array(
'message' => __(
'Could not retrieve information. Try again later.',
'woocommerce-paypal-payments'
),
'type' => 'error',
);
$messages[ self::ALREADY_CAPTURED ] = array(
$messages[ self::ALREADY_CAPTURED ] = array(
'message' => __(
'Payment already captured.',
'woocommerce-paypal-payments'
),
'type' => 'error',
);
$messages[ self::FAILED ] = array(
$messages[ self::FAILED ] = array(
'message' => __(
'Failed to capture. Try again later.',
'Failed to capture. Try again later or checks the logs.',
'woocommerce-paypal-payments'
),
'type' => 'error',
);
$messages[ self::NOT_FOUND ] = array(
$messages[ self::BAD_AUTHORIZATION ] = array(
'message' => __(
'Cannot capture, no valid payment authorization.',
'woocommerce-paypal-payments'
),
'type' => 'error',
);
$messages[ self::NOT_FOUND ] = array(
'message' => __(
'Could not find payment to process.',
'woocommerce-paypal-payments'
),
'type' => 'error',
);
$messages[ self::SUCCESS ] = array(
$messages[ self::SUCCESS ] = array(
'message' => __(
'Payment successfully captured.',
'woocommerce-paypal-payments'

View file

@ -10,10 +10,12 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus;
use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
@ -22,11 +24,14 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
*/
class AuthorizedPaymentsProcessor {
const SUCCESSFUL = 'SUCCESSFUL';
const ALREADY_CAPTURED = 'ALREADY_CAPTURED';
const FAILED = 'FAILED';
const INACCESSIBLE = 'INACCESSIBLE';
const NOT_FOUND = 'NOT_FOUND';
use PaymentsStatusHandlingTrait;
const SUCCESSFUL = 'SUCCESSFUL';
const ALREADY_CAPTURED = 'ALREADY_CAPTURED';
const FAILED = 'FAILED';
const INACCESSIBLE = 'INACCESSIBLE';
const NOT_FOUND = 'NOT_FOUND';
const BAD_AUTHORIZATION = 'BAD_AUTHORIZATION';
/**
* The Order endpoint.
@ -43,25 +48,35 @@ class AuthorizedPaymentsProcessor {
private $payments_endpoint;
/**
* The last status.
* The logger.
*
* @var string
* @var LoggerInterface
*/
private $last_status = '';
private $logger;
/**
* The capture results.
*
* @var Capture[]
*/
private $captures;
/**
* AuthorizedPaymentsProcessor constructor.
*
* @param OrderEndpoint $order_endpoint The Order endpoint.
* @param PaymentsEndpoint $payments_endpoint The Payments endpoint.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
OrderEndpoint $order_endpoint,
PaymentsEndpoint $payments_endpoint
PaymentsEndpoint $payments_endpoint,
LoggerInterface $logger
) {
$this->order_endpoint = $order_endpoint;
$this->payments_endpoint = $payments_endpoint;
$this->logger = $logger;
}
/**
@ -69,46 +84,47 @@ class AuthorizedPaymentsProcessor {
*
* @param \WC_Order $wc_order The WooCommerce order.
*
* @return bool
* @return string One of the AuthorizedPaymentsProcessor status constants.
*/
public function process( \WC_Order $wc_order ): bool {
public function process( \WC_Order $wc_order ): string {
$this->captures = array();
try {
$order = $this->paypal_order_from_wc_order( $wc_order );
} catch ( Exception $exception ) {
if ( $exception->getCode() === 404 ) {
$this->last_status = self::NOT_FOUND;
return false;
return self::NOT_FOUND;
}
$this->last_status = self::INACCESSIBLE;
return false;
return self::INACCESSIBLE;
}
$authorizations = $this->all_authorizations( $order );
if ( ! $this->are_authorzations_to_capture( ...$authorizations ) ) {
$this->last_status = self::ALREADY_CAPTURED;
return false;
if ( ! $this->authorizations_to_capture( ...$authorizations ) ) {
if ( $this->captured_authorizations( ...$authorizations ) ) {
return self::ALREADY_CAPTURED;
}
return self::BAD_AUTHORIZATION;
}
try {
$this->capture_authorizations( ...$authorizations );
} catch ( Exception $exception ) {
$this->last_status = self::FAILED;
return false;
$this->logger->error( 'Failed to capture authorization: ' . $exception->getMessage() );
return self::FAILED;
}
$this->last_status = self::SUCCESSFUL;
return true;
return self::SUCCESSFUL;
}
/**
* Returns the last status.
* Returns the capture results.
*
* @return string
* @return Capture[]
*/
public function last_status(): string {
return $this->last_status;
public function captures(): array {
return $this->captures;
}
/**
@ -141,17 +157,6 @@ class AuthorizedPaymentsProcessor {
return $authorizations;
}
/**
* Whether Authorizations need to be captured.
*
* @param Authorization ...$authorizations All Authorizations.
*
* @return bool
*/
private function are_authorzations_to_capture( Authorization ...$authorizations ): bool {
return (bool) count( $this->authorizations_to_capture( ...$authorizations ) );
}
/**
* Captures the authorizations.
*
@ -160,7 +165,7 @@ class AuthorizedPaymentsProcessor {
private function capture_authorizations( Authorization ...$authorizations ) {
$uncaptured_authorizations = $this->authorizations_to_capture( ...$authorizations );
foreach ( $uncaptured_authorizations as $authorization ) {
$this->payments_endpoint->capture( $authorization->id() );
$this->captures[] = $this->payments_endpoint->capture( $authorization->id() );
}
}
@ -171,11 +176,38 @@ class AuthorizedPaymentsProcessor {
* @return Authorization[]
*/
private function authorizations_to_capture( Authorization ...$authorizations ): array {
return $this->filter_authorizations(
$authorizations,
array( AuthorizationStatus::CREATED, AuthorizationStatus::PENDING )
);
}
/**
* The authorizations which were captured.
*
* @param Authorization ...$authorizations All Authorizations.
* @return Authorization[]
*/
private function captured_authorizations( Authorization ...$authorizations ): array {
return $this->filter_authorizations(
$authorizations,
array( AuthorizationStatus::CAPTURED )
);
}
/**
* The authorizations which need to be filtered.
*
* @param Authorization[] $authorizations All Authorizations.
* @param string[] $statuses Allowed statuses, the constants from AuthorizationStatus.
* @return Authorization[]
*/
private function filter_authorizations( array $authorizations, array $statuses ): array {
return array_filter(
$authorizations,
static function ( Authorization $authorization ): bool {
return $authorization->status()->is( AuthorizationStatus::CREATED )
|| $authorization->status()->is( AuthorizationStatus::PENDING );
static function ( Authorization $authorization ) use ( $statuses ): bool {
$status = $authorization->status();
return in_array( $status->name(), $statuses, true );
}
);
}

View file

@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
@ -25,12 +26,14 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
*/
class OrderProcessor {
use OrderMetaTrait, PaymentsStatusHandlingTrait;
/**
* Whether current payment mode is sandbox.
* The environment.
*
* @var bool
* @var Environment
*/
protected $sandbox_mode;
protected $environment;
/**
* The payment token repository.
@ -105,7 +108,7 @@ class OrderProcessor {
* @param AuthorizedPaymentsProcessor $authorized_payments_processor The Authorized Payments Processor.
* @param Settings $settings The Settings.
* @param LoggerInterface $logger A logger service.
* @param bool $sandbox_mode Whether sandbox mode enabled.
* @param Environment $environment The environment.
*/
public function __construct(
SessionHandler $session_handler,
@ -115,7 +118,7 @@ class OrderProcessor {
AuthorizedPaymentsProcessor $authorized_payments_processor,
Settings $settings,
LoggerInterface $logger,
bool $sandbox_mode
Environment $environment
) {
$this->session_handler = $session_handler;
@ -124,7 +127,7 @@ class OrderProcessor {
$this->threed_secure = $three_d_secure;
$this->authorized_payments_processor = $authorized_payments_processor;
$this->settings = $settings;
$this->sandbox_mode = $sandbox_mode;
$this->environment = $environment;
$this->logger = $logger;
}
@ -140,12 +143,8 @@ class OrderProcessor {
if ( ! $order ) {
return false;
}
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() );
$wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() );
$wc_order->update_meta_data(
PayPalGateway::ORDER_PAYMENT_MODE_META_KEY,
$this->sandbox_mode ? 'sandbox' : 'live'
);
$this->add_paypal_meta( $wc_order, $order, $this->environment );
$error_message = null;
if ( ! $this->order_is_approved( $order ) ) {
@ -164,12 +163,14 @@ class OrderProcessor {
}
$order = $this->patch_order( $wc_order, $order );
if ( $order->intent() === 'CAPTURE' ) {
$order = $this->order_endpoint->capture( $order );
}
if ( $order->intent() === 'AUTHORIZE' ) {
$order = $this->order_endpoint->authorize( $order );
$wc_order->update_meta_data( PayPalGateway::CAPTURED_META_KEY, 'false' );
}
@ -179,16 +180,9 @@ class OrderProcessor {
$this->set_order_transaction_id( $transaction_id, $wc_order );
}
$wc_order->update_status(
'on-hold',
__( 'Awaiting payment.', 'woocommerce-paypal-payments' )
);
if ( $order->status()->is( OrderStatus::COMPLETED ) && $order->intent() === 'CAPTURE' ) {
$this->handle_new_order_status( $order, $wc_order );
$wc_order->payment_complete();
}
if ( $this->capture_authorized_downloads( $order ) && $this->authorized_payments_processor->process( $wc_order ) ) {
if ( $this->capture_authorized_downloads( $order ) && AuthorizedPaymentsProcessor::SUCCESSFUL === $this->authorized_payments_processor->process( $wc_order ) ) {
$wc_order->add_order_note(
__( 'Payment successfully captured.', 'woocommerce-paypal-payments' )
);

View file

@ -9,10 +9,15 @@ declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Amount;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
@ -22,6 +27,10 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
*/
class RefundProcessor {
private const REFUND_MODE_REFUND = 'refund';
private const REFUND_MODE_VOID = 'void';
private const REFUND_MODE_UNKNOWN = 'unknown';
/**
* The order endpoint.
*
@ -36,16 +45,25 @@ class RefundProcessor {
*/
private $payments_endpoint;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* RefundProcessor constructor.
*
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param PaymentsEndpoint $payments_endpoint The payments endpoint.
* @param LoggerInterface $logger The logger.
*/
public function __construct( OrderEndpoint $order_endpoint, PaymentsEndpoint $payments_endpoint ) {
public function __construct( OrderEndpoint $order_endpoint, PaymentsEndpoint $payments_endpoint, LoggerInterface $logger ) {
$this->order_endpoint = $order_endpoint;
$this->payments_endpoint = $payments_endpoint;
$this->logger = $logger;
}
/**
@ -56,44 +74,116 @@ class RefundProcessor {
* @param string $reason The reason for the refund.
*
* @return bool
*
* @phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.Missing
*/
public function process( \WC_Order $wc_order, float $amount = null, string $reason = '' ) : bool {
$order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
if ( ! $order_id ) {
return false;
}
try {
$order = $this->order_endpoint->order( $order_id );
if ( ! $order ) {
return false;
$order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
if ( ! $order_id ) {
throw new RuntimeException( 'PayPal order ID not found in meta.' );
}
$order = $this->order_endpoint->order( $order_id );
$purchase_units = $order->purchase_units();
if ( ! $purchase_units ) {
return false;
throw new RuntimeException( 'No purchase units.' );
}
$payments = $purchase_units[0]->payments();
if ( ! $payments ) {
return false;
}
$captures = $payments->captures();
if ( ! $captures ) {
return false;
throw new RuntimeException( 'No payments.' );
}
$capture = $captures[0];
$refund = new Refund(
$capture,
$capture->invoice_id(),
$reason,
new Amount(
new Money( $amount, $wc_order->get_currency() )
$this->logger->debug(
sprintf(
'Trying to refund/void order %1$s, payments: %2$s.',
$order->id(),
wp_json_encode( $payments->to_array() )
)
);
return $this->payments_endpoint->refund( $refund );
} catch ( RuntimeException $error ) {
$mode = $this->determine_refund_mode( $payments );
switch ( $mode ) {
case self::REFUND_MODE_REFUND:
$captures = $payments->captures();
if ( ! $captures ) {
throw new RuntimeException( 'No capture.' );
}
$capture = $captures[0];
$refund = new Refund(
$capture,
$capture->invoice_id(),
$reason,
new Amount(
new Money( $amount, $wc_order->get_currency() )
)
);
$this->payments_endpoint->refund( $refund );
break;
case self::REFUND_MODE_VOID:
$voidable_authorizations = array_filter(
$payments->authorizations(),
array( $this, 'is_voidable_authorization' )
);
if ( ! $voidable_authorizations ) {
throw new RuntimeException( 'No voidable authorizations.' );
}
foreach ( $voidable_authorizations as $authorization ) {
$this->payments_endpoint->void( $authorization );
}
$wc_order->set_status( 'refunded' );
$wc_order->save();
break;
default:
throw new RuntimeException( 'Nothing to refund/void.' );
}
return true;
} catch ( Exception $error ) {
$this->logger->error( 'Refund failed: ' . $error->getMessage() );
return false;
}
}
/**
* Determines the refunding mode.
*
* @param Payments $payments The order payments state.
*
* @return string One of the REFUND_MODE_ constants.
*/
private function determine_refund_mode( Payments $payments ): string {
$authorizations = $payments->authorizations();
if ( $authorizations ) {
foreach ( $authorizations as $authorization ) {
if ( $this->is_voidable_authorization( $authorization ) ) {
return self::REFUND_MODE_VOID;
}
}
}
if ( $payments->captures() ) {
return self::REFUND_MODE_REFUND;
}
return self::REFUND_MODE_UNKNOWN;
}
/**
* Checks whether the authorization can be voided.
*
* @param Authorization $authorization The authorization to check.
* @return bool
*/
private function is_voidable_authorization( Authorization $authorization ): bool {
return $authorization->status()->is( AuthorizationStatus::CREATED ) ||
$authorization->status()->is( AuthorizationStatus::PENDING );
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* Adds common metadata to the order.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Processor
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Trait OrderMetaTrait.
*/
trait OrderMetaTrait {
/**
* Adds common metadata to the order.
*
* @param WC_Order $wc_order The WC order to which metadata will be added.
* @param Order $order The PayPal order.
* @param Environment $environment The environment.
*/
protected function add_paypal_meta(
WC_Order $wc_order,
Order $order,
Environment $environment
): void {
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() );
$wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() );
$wc_order->update_meta_data(
PayPalGateway::ORDER_PAYMENT_MODE_META_KEY,
$environment->current_environment_is( Environment::SANDBOX ) ? 'sandbox' : 'live'
);
}
}

View file

@ -0,0 +1,141 @@
<?php
/**
* Common operations performed after payment authorization/capture.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Processor
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus;
use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Trait PaymentsStatusHandlingTrait.
*/
trait PaymentsStatusHandlingTrait {
/**
* Changes status of a newly created order, based on the capture/authorization.
*
* @param Order $order The PayPal order.
* @param WC_Order $wc_order The WC order.
*
* @throws RuntimeException If payment denied.
*/
protected function handle_new_order_status(
Order $order,
WC_Order $wc_order
): void {
if ( $order->intent() === 'CAPTURE' ) {
$this->handle_capture_status( $order->purchase_units()[0]->payments()->captures()[0], $wc_order );
} elseif ( $order->intent() === 'AUTHORIZE' ) {
$this->handle_authorization_status( $order->purchase_units()[0]->payments()->authorizations()[0], $wc_order );
}
}
/**
* Changes the order status, based on the capture.
*
* @param Capture $capture The capture.
* @param WC_Order $wc_order The WC order.
*
* @throws RuntimeException If payment denied.
*/
protected function handle_capture_status(
Capture $capture,
WC_Order $wc_order
): void {
$status = $capture->status();
if ( $status->details() ) {
$this->add_status_details_note( $wc_order, $status->name(), $status->details()->reason() );
}
switch ( $status->name() ) {
case CaptureStatus::COMPLETED:
$wc_order->payment_complete();
break;
// It is checked in the capture endpoint already, but there are other ways to capture,
// such as when paid via saved card.
case CaptureStatus::DECLINED:
$wc_order->update_status(
'failed',
__( 'Could not capture the payment.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' ) );
case CaptureStatus::PENDING:
case CaptureStatus::FAILED:
$wc_order->update_status(
'on-hold',
__( 'Awaiting payment.', 'woocommerce-paypal-payments' )
);
break;
}
}
/**
* Changes the order status, based on the authorization.
*
* @param Authorization $authorization The authorization.
* @param WC_Order $wc_order The WC order.
*
* @throws RuntimeException If payment denied.
*/
protected function handle_authorization_status(
Authorization $authorization,
WC_Order $wc_order
): void {
$status = $authorization->status();
if ( $status->details() ) {
$this->add_status_details_note( $wc_order, $status->name(), $status->details()->reason() );
}
switch ( $status->name() ) {
case AuthorizationStatus::CREATED:
case AuthorizationStatus::PENDING:
$wc_order->update_status(
'on-hold',
__( 'Awaiting payment.', 'woocommerce-paypal-payments' )
);
break;
case AuthorizationStatus::DENIED:
$wc_order->update_status(
'failed',
__( 'Could not get the payment authorization.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( __( 'Payment provider declined the payment, please use a different payment method.', 'woocommerce-paypal-payments' ) );
}
}
/**
* Adds the order note with status details.
*
* @param WC_Order $wc_order The WC order to which the note will be added.
* @param string $status The status name.
* @param string $reason The status reason.
*/
protected function add_status_details_note(
WC_Order $wc_order,
string $status,
string $reason
): void {
$wc_order->add_order_note(
sprintf(
/* translators: %1$s - PENDING, DENIED, ... %2$s - PENDING_REVIEW, ... */
__( 'PayPal order payment is set to %1$s status, details: %2$s.', 'woocommerce-paypal-payments' ),
$status,
$reason
)
);
$wc_order->save();
}
}

View file

@ -8,6 +8,7 @@ use Requests_Utility_CaseInsensitiveDictionary;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PatchCollection;
@ -278,7 +279,7 @@ class OrderEndpointTest extends TestCase
$expectedOrder->shouldReceive('purchase_units')->once()->andReturn(['0'=>$purchaseUnit]);
$purchaseUnit->shouldReceive('payments')->once()->andReturn($payment);
$payment->shouldReceive('captures')->once()->andReturn(['0'=>$capture]);
$capture->shouldReceive('status')->once()->andReturn('');
$capture->shouldReceive('status')->once()->andReturn(new CaptureStatus(CaptureStatus::COMPLETED));
$result = $testee->capture($orderToCapture);
$this->assertEquals($expectedOrder, $result);

View file

@ -4,13 +4,16 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Psr\Log\NullLogger;
use Requests_Utility_CaseInsensitiveDictionary;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ErrorResponseCollection;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Token;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ErrorResponseCollectionFactory;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use Mockery;
@ -20,7 +23,22 @@ use function Brain\Monkey\Functions\expect;
class PaymentsEndpointTest extends TestCase
{
public function testAuthorizationDefault()
private $authorizationFactory;
private $captureFactory;
private $logger;
public function setUp(): void
{
parent::setUp();
$this->authorizationFactory = Mockery::mock(AuthorizationFactory::class);
$this->captureFactory = Mockery::mock(CaptureFactory::class);
$this->logger = new NullLogger();
}
public function testAuthorizationDefault()
{
expect('wp_json_encode')->andReturnUsing('json_encode');
$host = 'https://example.com/';
@ -34,15 +52,10 @@ class PaymentsEndpointTest extends TestCase
->expects('bearer')->andReturn($token);
$authorization = Mockery::mock(Authorization::class);
$authorizationFactory = Mockery::mock(AuthorizationFactory::class);
$authorizationFactory
$this->authorizationFactory
->expects('from_paypal_response')
->andReturn($authorization);
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldNotReceive('log');
$logger->shouldReceive('debug');
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
@ -53,8 +66,9 @@ class PaymentsEndpointTest extends TestCase
$testee = new PaymentsEndpoint(
$host,
$bearer,
$authorizationFactory,
$logger
$this->authorizationFactory,
$this->captureFactory,
$this->logger
);
expect('wp_remote_get')->andReturnUsing(
@ -91,12 +105,6 @@ class PaymentsEndpointTest extends TestCase
$bearer = Mockery::mock(Bearer::class);
$bearer->expects('bearer')->andReturn($token);
$authorizationFactory = Mockery::mock(AuthorizationFactory::class);
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log');
$logger->shouldReceive('debug');
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
@ -107,8 +115,9 @@ class PaymentsEndpointTest extends TestCase
$testee = new PaymentsEndpoint(
$host,
$bearer,
$authorizationFactory,
$logger
$this->authorizationFactory,
$this->captureFactory,
$this->logger
);
expect('wp_remote_get')->andReturn($rawResponse);
@ -130,8 +139,6 @@ class PaymentsEndpointTest extends TestCase
$bearer = Mockery::mock(Bearer::class);
$bearer->expects('bearer')->andReturn($token);
$authorizationFactory = Mockery::mock(AuthorizationFactory::class);
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
@ -139,15 +146,12 @@ class PaymentsEndpointTest extends TestCase
'headers' => $headers,
];
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log');
$logger->shouldReceive('debug');
$testee = new PaymentsEndpoint(
$host,
$bearer,
$authorizationFactory,
$logger
$this->authorizationFactory,
$this->captureFactory,
$this->logger
);
expect('wp_remote_get')->andReturn($rawResponse);
@ -171,16 +175,10 @@ class PaymentsEndpointTest extends TestCase
$bearer
->expects('bearer')->andReturn($token);
$authorization = Mockery::mock(Authorization::class);
$authorizationFactory = Mockery::mock(AuthorizationFactory::class);
$authorizationFactory
$capture = Mockery::mock(Capture::class);
$this->captureFactory
->expects('from_paypal_response')
->andReturn($authorization);
$logger = Mockery::mock(LoggerInterface::class);
$logger->shouldNotReceive('log');
$logger->shouldReceive('debug');
->andReturn($capture);
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
@ -192,8 +190,9 @@ class PaymentsEndpointTest extends TestCase
$testee = new PaymentsEndpoint(
$host,
$bearer,
$authorizationFactory,
$logger
$this->authorizationFactory,
$this->captureFactory,
$this->logger
);
expect('wp_remote_get')->andReturnUsing(
@ -218,7 +217,7 @@ class PaymentsEndpointTest extends TestCase
expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(201);
$result = $testee->capture($authorizationId);
$this->assertEquals($authorization, $result);
$this->assertEquals($capture, $result);
}
public function testCaptureIsWpError()
@ -233,12 +232,6 @@ class PaymentsEndpointTest extends TestCase
$bearer = Mockery::mock(Bearer::class);
$bearer->expects('bearer')->andReturn($token);
$authorizationFactory = Mockery::mock(AuthorizationFactory::class);
$logger = Mockery::mock(LoggerInterface::class);
$logger->expects('log');
$logger->expects('debug');
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
@ -249,8 +242,9 @@ class PaymentsEndpointTest extends TestCase
$testee = new PaymentsEndpoint(
$host,
$bearer,
$authorizationFactory,
$logger
$this->authorizationFactory,
$this->captureFactory,
$this->logger
);
expect('wp_remote_get')->andReturn($rawResponse);
@ -272,8 +266,6 @@ class PaymentsEndpointTest extends TestCase
$bearer = Mockery::mock(Bearer::class);
$bearer->expects('bearer')->andReturn($token);
$authorizationFactory = Mockery::mock(AuthorizationFactory::class);
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
@ -281,15 +273,12 @@ class PaymentsEndpointTest extends TestCase
'headers' => $headers,
];
$logger = Mockery::mock(LoggerInterface::class);
$logger->expects('log');
$logger->expects('debug');
$testee = new PaymentsEndpoint(
$host,
$bearer,
$authorizationFactory,
$logger
$this->authorizationFactory,
$this->captureFactory,
$this->logger
);
expect('wp_remote_get')->andReturn($rawResponse);

View file

@ -5,6 +5,9 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Gateway;
use Psr\Container\ContainerInterface;
use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
@ -21,8 +24,15 @@ use function Brain\Monkey\Functions\when;
class WcGatewayTest extends TestCase
{
private $environment;
public function testProcessPaymentSuccess() {
public function setUp(): void {
parent::setUp();
$this->environment = Mockery::mock(Environment::class);
}
public function testProcessPaymentSuccess() {
expect('is_admin')->andReturn(false);
$orderId = 1;
@ -69,7 +79,8 @@ class WcGatewayTest extends TestCase
$state,
$transactionUrlProvider,
$subscriptionHelper,
PayPalGateway::ID
PayPalGateway::ID,
$this->environment
);
expect('wc_get_order')
@ -118,7 +129,8 @@ class WcGatewayTest extends TestCase
$state,
$transactionUrlProvider,
$subscriptionHelper,
PayPalGateway::ID
PayPalGateway::ID,
$this->environment
);
expect('wc_get_order')
@ -184,7 +196,8 @@ class WcGatewayTest extends TestCase
$state,
$transactionUrlProvider,
$subscriptionHelper,
PayPalGateway::ID
PayPalGateway::ID,
$this->environment
);
expect('wc_get_order')
@ -223,14 +236,18 @@ class WcGatewayTest extends TestCase
->expects('save');
$settingsRenderer = Mockery::mock(SettingsRenderer::class);
$orderProcessor = Mockery::mock(OrderProcessor::class);
$capture = Mockery::mock(Capture::class);
$capture
->shouldReceive('status')
->andReturn(new CaptureStatus(CaptureStatus::COMPLETED));
$authorizedPaymentsProcessor = Mockery::mock(AuthorizedPaymentsProcessor::class);
$authorizedPaymentsProcessor
->expects('process')
->with($wcOrder)
->andReturnTrue();
->andReturn(AuthorizedPaymentsProcessor::SUCCESSFUL);
$authorizedPaymentsProcessor
->expects('last_status')
->andReturn(AuthorizedPaymentsProcessor::SUCCESSFUL);
->expects('captures')
->andReturn([$capture]);
$authorizedOrderActionNotice = Mockery::mock(AuthorizeOrderActionNotice::class);
$authorizedOrderActionNotice
->expects('display_message')
@ -258,7 +275,8 @@ class WcGatewayTest extends TestCase
$state,
$transactionUrlProvider,
$subscriptionHelper,
PayPalGateway::ID
PayPalGateway::ID,
$this->environment
);
$this->assertTrue($testee->capture_authorized_payment($wcOrder));
@ -286,10 +304,7 @@ class WcGatewayTest extends TestCase
$authorizedPaymentsProcessor
->expects('process')
->with($wcOrder)
->andReturnFalse();
$authorizedPaymentsProcessor
->shouldReceive('last_status')
->andReturn(AuthorizedPaymentsProcessor::ALREADY_CAPTURED);
->andReturn(AuthorizedPaymentsProcessor::ALREADY_CAPTURED);
$authorizedOrderActionNotice = Mockery::mock(AuthorizeOrderActionNotice::class);
$authorizedOrderActionNotice
->expects('display_message')
@ -316,7 +331,8 @@ class WcGatewayTest extends TestCase
$state,
$transactionUrlProvider,
$subscriptionHelper,
PayPalGateway::ID
PayPalGateway::ID,
$this->environment
);
$this->assertTrue($testee->capture_authorized_payment($wcOrder));
@ -338,10 +354,10 @@ class WcGatewayTest extends TestCase
$authorizedPaymentsProcessor
->expects('process')
->with($wcOrder)
->andReturnFalse();
$authorizedPaymentsProcessor
->shouldReceive('last_status')
->andReturn($lastStatus);
->andReturn($lastStatus);
$authorizedPaymentsProcessor
->expects('captures')
->andReturn([]);
$authorizedOrderActionNotice = Mockery::mock(AuthorizeOrderActionNotice::class);
$authorizedOrderActionNotice
->expects('display_message')
@ -368,7 +384,8 @@ class WcGatewayTest extends TestCase
$state,
$transactionUrlProvider,
$subscriptionHelper,
PayPalGateway::ID
PayPalGateway::ID,
$this->environment
);
$this->assertFalse($testee->capture_authorized_payment($wcOrder));
@ -408,7 +425,8 @@ class WcGatewayTest extends TestCase
$onboardingState,
$transactionUrlProvider,
$subscriptionHelper,
PayPalGateway::ID
PayPalGateway::ID,
$this->environment
);
$this->assertSame($needSetup, $testee->needs_setup());

View file

@ -4,10 +4,14 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use Psr\Log\NullLogger;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus;
use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
@ -17,186 +21,153 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use Mockery;
class AuthorizedPaymentsProcessorTest extends TestCase
{
private $wcOrder;
private $paypalOrderId = 'abc';
public function testDefault() {
$orderId = 'abc';
$authorizationId = 'def';
$authorizationStatus = Mockery::mock(AuthorizationStatus::class);
$authorizationStatus
->shouldReceive('is')
->with(AuthorizationStatus::CREATED)
->andReturn(true);
$authorization = Mockery::mock(Authorization::class);
$authorization
->shouldReceive('id')
->andReturn($authorizationId);
$authorization
->shouldReceive('status')
->andReturn($authorizationStatus);
$payments = Mockery::mock(Payments::class);
$payments
->expects('authorizations')
->andReturn([$authorization]);
$purchaseUnit = Mockery::mock(PurchaseUnit::class);
$purchaseUnit
->expects('payments')
->andReturn($payments);
$order = Mockery::mock(Order::class);
$order
->expects('purchase_units')
->andReturn([$purchaseUnit]);
$orderEndpoint = Mockery::mock(OrderEndpoint::class);
$orderEndpoint
->expects('order')
->with($orderId)
->andReturn($order);
$paymentsEndpoint = Mockery::mock(PaymentsEndpoint::class);
$paymentsEndpoint
->expects('capture')
->with($authorizationId)
->andReturn($authorization);
$testee = new AuthorizedPaymentsProcessor($orderEndpoint, $paymentsEndpoint);
private $authorizationId = 'qwe';
$wcOrder = Mockery::mock(\WC_Order::class);
$wcOrder
->expects('get_meta')
->with(PayPalGateway::ORDER_ID_META_KEY)
->andReturn($orderId);
$this->assertTrue($testee->process($wcOrder));
$this->assertEquals(AuthorizedPaymentsProcessor::SUCCESSFUL, $testee->last_status());
private $paypalOrder;
private $orderEndpoint;
private $paymentsEndpoint;
private $testee;
public function setUp(): void {
parent::setUp();
$this->wcOrder = $this->createWcOrder($this->paypalOrderId);
$this->paypalOrder = $this->createPaypalOrder([$this->createAuthorization($this->authorizationId, AuthorizationStatus::CREATED)]);
$this->orderEndpoint = Mockery::mock(OrderEndpoint::class);
$this->orderEndpoint
->shouldReceive('order')
->with($this->paypalOrderId)
->andReturnUsing(function () {
return $this->paypalOrder;
})
->byDefault();
$this->paymentsEndpoint = Mockery::mock(PaymentsEndpoint::class);
$this->testee = new AuthorizedPaymentsProcessor($this->orderEndpoint, $this->paymentsEndpoint, new NullLogger());
}
public function testSuccess() {
$this->paymentsEndpoint
->expects('capture')
->with($this->authorizationId)
->andReturn($this->createCapture(CaptureStatus::COMPLETED));
$this->assertEquals(AuthorizedPaymentsProcessor::SUCCESSFUL, $this->testee->process($this->wcOrder));
}
public function testCapturesAllCaptureable() {
$authorizations = [
$this->createAuthorization('id1', AuthorizationStatus::CREATED),
$this->createAuthorization('id2', AuthorizationStatus::VOIDED),
$this->createAuthorization('id3', AuthorizationStatus::PENDING),
$this->createAuthorization('id4', AuthorizationStatus::CAPTURED),
$this->createAuthorization('id5', AuthorizationStatus::DENIED),
$this->createAuthorization('id6', AuthorizationStatus::EXPIRED),
$this->createAuthorization('id7', AuthorizationStatus::COMPLETED),
];
$this->paypalOrder = $this->createPaypalOrder($authorizations);
foreach ([$authorizations[0], $authorizations[2]] as $authorization) {
$this->paymentsEndpoint
->expects('capture')
->with($authorization->id())
->andReturn($this->createCapture(CaptureStatus::COMPLETED));
}
$this->assertEquals(AuthorizedPaymentsProcessor::SUCCESSFUL, $this->testee->process($this->wcOrder));
}
public function testInaccessible() {
$orderId = 'abc';
$orderEndpoint = Mockery::mock(OrderEndpoint::class);
$orderEndpoint
$this->orderEndpoint
->expects('order')
->with($orderId)
->with($this->paypalOrderId)
->andThrow(RuntimeException::class);
$paymentsEndpoint = Mockery::mock(PaymentsEndpoint::class);
$testee = new AuthorizedPaymentsProcessor($orderEndpoint, $paymentsEndpoint);
$wcOrder = Mockery::mock(\WC_Order::class);
$wcOrder
->expects('get_meta')
->with(PayPalGateway::ORDER_ID_META_KEY)
->andReturn($orderId);
$this->assertFalse($testee->process($wcOrder));
$this->assertEquals(AuthorizedPaymentsProcessor::INACCESSIBLE, $testee->last_status());
$this->assertEquals(AuthorizedPaymentsProcessor::INACCESSIBLE, $this->testee->process($this->wcOrder));
}
public function testNotFound() {
$orderId = 'abc';
$orderEndpoint = Mockery::mock(OrderEndpoint::class);
$orderEndpoint
$this->orderEndpoint
->expects('order')
->with($orderId)
->andThrow(new RuntimeException("text", 404));
$paymentsEndpoint = Mockery::mock(PaymentsEndpoint::class);
$testee = new AuthorizedPaymentsProcessor($orderEndpoint, $paymentsEndpoint);
->with($this->paypalOrderId)
->andThrow(new RuntimeException('text', 404));
$wcOrder = Mockery::mock(\WC_Order::class);
$wcOrder
->expects('get_meta')
->with(PayPalGateway::ORDER_ID_META_KEY)
->andReturn($orderId);
$this->assertFalse($testee->process($wcOrder));
$this->assertEquals(AuthorizedPaymentsProcessor::NOT_FOUND, $testee->last_status());
$this->assertEquals(AuthorizedPaymentsProcessor::NOT_FOUND, $this->testee->process($this->wcOrder));
}
public function testCaptureFails() {
$orderId = 'abc';
$authorizationId = 'def';
$authorizationStatus = Mockery::mock(AuthorizationStatus::class);
$authorizationStatus
->shouldReceive('is')
->with(AuthorizationStatus::CREATED)
->andReturn(true);
$authorization = Mockery::mock(Authorization::class);
$authorization
->shouldReceive('id')
->andReturn($authorizationId);
$authorization
->shouldReceive('status')
->andReturn($authorizationStatus);
$payments = Mockery::mock(Payments::class);
$payments
->expects('authorizations')
->andReturn([$authorization]);
$purchaseUnit = Mockery::mock(PurchaseUnit::class);
$purchaseUnit
->expects('payments')
->andReturn($payments);
$order = Mockery::mock(Order::class);
$order
->expects('purchase_units')
->andReturn([$purchaseUnit]);
$orderEndpoint = Mockery::mock(OrderEndpoint::class);
$orderEndpoint
->expects('order')
->with($orderId)
->andReturn($order);
$paymentsEndpoint = Mockery::mock(PaymentsEndpoint::class);
$paymentsEndpoint
$this->paymentsEndpoint
->expects('capture')
->with($authorizationId)
->with($this->authorizationId)
->andThrow(RuntimeException::class);
$testee = new AuthorizedPaymentsProcessor($orderEndpoint, $paymentsEndpoint);
$wcOrder = Mockery::mock(\WC_Order::class);
$wcOrder
->expects('get_meta')
->with(PayPalGateway::ORDER_ID_META_KEY)
->andReturn($orderId);
$this->assertFalse($testee->process($wcOrder));
$this->assertEquals(AuthorizedPaymentsProcessor::FAILED, $testee->last_status());
$this->assertEquals(AuthorizedPaymentsProcessor::FAILED, $this->testee->process($this->wcOrder));
}
public function testAllAreCaptured() {
$orderId = 'abc';
$authorizationId = 'def';
$authorizationStatus = Mockery::mock(AuthorizationStatus::class);
$authorizationStatus
->shouldReceive('is')
->with(AuthorizationStatus::CREATED)
->andReturn(false);
$authorizationStatus
->shouldReceive('is')
->with(AuthorizationStatus::PENDING)
->andReturn(false);
$authorization = Mockery::mock(Authorization::class);
$authorization
->shouldReceive('id')
->andReturn($authorizationId);
$authorization
->shouldReceive('status')
->andReturn($authorizationStatus);
$payments = Mockery::mock(Payments::class);
$payments
->expects('authorizations')
->andReturn([$authorization]);
$purchaseUnit = Mockery::mock(PurchaseUnit::class);
$purchaseUnit
->expects('payments')
->andReturn($payments);
$order = Mockery::mock(Order::class);
$order
->expects('purchase_units')
->andReturn([$purchaseUnit]);
$orderEndpoint = Mockery::mock(OrderEndpoint::class);
$orderEndpoint
->expects('order')
->with($orderId)
->andReturn($order);
$paymentsEndpoint = Mockery::mock(PaymentsEndpoint::class);
$testee = new AuthorizedPaymentsProcessor($orderEndpoint, $paymentsEndpoint);
public function testAlreadyCaptured() {
$this->paypalOrder = $this->createPaypalOrder([$this->createAuthorization($this->authorizationId, AuthorizationStatus::CAPTURED)]);
$wcOrder = Mockery::mock(\WC_Order::class);
$wcOrder
->expects('get_meta')
->with(PayPalGateway::ORDER_ID_META_KEY)
->andReturn($orderId);
$this->assertFalse($testee->process($wcOrder));
$this->assertEquals(AuthorizedPaymentsProcessor::ALREADY_CAPTURED, $testee->last_status());
$this->assertEquals(AuthorizedPaymentsProcessor::ALREADY_CAPTURED, $this->testee->process($this->wcOrder));
}
}
public function testBadAuthorization() {
$this->paypalOrder = $this->createPaypalOrder([$this->createAuthorization($this->authorizationId, AuthorizationStatus::DENIED)]);
$this->assertEquals(AuthorizedPaymentsProcessor::BAD_AUTHORIZATION, $this->testee->process($this->wcOrder));
}
private function createWcOrder(string $paypalOrderId): WC_Order {
$wcOrder = Mockery::mock(WC_Order::class);
$wcOrder
->shouldReceive('get_meta')
->with(PayPalGateway::ORDER_ID_META_KEY)
->andReturn($paypalOrderId);
return $wcOrder;
}
private function createAuthorization(string $id, string $status): Authorization {
$authorization = Mockery::mock(Authorization::class);
$authorization
->shouldReceive('id')
->andReturn($id);
$authorization
->shouldReceive('status')
->andReturn(new AuthorizationStatus($status));
return $authorization;
}
private function createCapture(string $status): Capture {
$capture = Mockery::mock(Capture::class);
$capture
->shouldReceive('status')
->andReturn(new CaptureStatus($status));
return $capture;
}
private function createPaypalOrder(array $authorizations): Order {
$payments = Mockery::mock(Payments::class);
$payments
->shouldReceive('authorizations')
->andReturn($authorizations);
$purchaseUnit = Mockery::mock(PurchaseUnit::class);
$purchaseUnit
->shouldReceive('payments')
->andReturn($payments);
$order = Mockery::mock(Order::class);
$order
->shouldReceive('purchase_units')
->andReturn([$purchaseUnit]);
return $order;
}
}

View file

@ -4,15 +4,20 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use Dhii\Container\Dictionary;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus;
use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
@ -22,35 +27,44 @@ use function Brain\Monkey\Functions\when;
class OrderProcessorTest extends TestCase
{
private $environment;
public function setUp(): void {
parent::setUp();
$this->environment = new Environment(new Dictionary([]));
}
public function testAuthorize() {
$transactionId = 'ABC123';
$capture = Mockery::mock(Capture::class);
$capture->expects('id')
$authorization = Mockery::mock(Authorization::class);
$authorization->shouldReceive('id')
->andReturn($transactionId);
$authorization->shouldReceive('status')
->andReturn(new AuthorizationStatus(AuthorizationStatus::CREATED));
$payments = Mockery::mock(Payments::class);
$payments->expects('captures')
->andReturn([$capture]);
$payments->shouldReceive('authorizations')
->andReturn([$authorization]);
$payments->shouldReceive('captures')
->andReturn([]);
$purchaseUnit = Mockery::mock(PurchaseUnit::class);
$purchaseUnit->expects('payments')
$purchaseUnit->shouldReceive('payments')
->andReturn($payments);
$wcOrder = Mockery::mock(\WC_Order::class);
$wcOrder->expects('update_meta_data')
->with(PayPalGateway::ORDER_PAYMENT_MODE_META_KEY, 'live');
$wcOrder->expects('set_transaction_id')
->with($transactionId);
$orderStatus = Mockery::mock(OrderStatus::class);
$orderStatus
->expects('is')
->shouldReceive('is')
->with(OrderStatus::APPROVED)
->andReturn(true);
$orderStatus
->expects('is')
->shouldReceive('is')
->with(OrderStatus::COMPLETED)
->andReturn(true);
@ -67,7 +81,7 @@ class OrderProcessorTest extends TestCase
$currentOrder
->shouldReceive('status')
->andReturn($orderStatus);
$currentOrder->expects('purchase_units')
$currentOrder->shouldReceive('purchase_units')
->andReturn([$purchaseUnit]);
$sessionHandler = Mockery::mock(SessionHandler::class);
@ -112,7 +126,7 @@ class OrderProcessorTest extends TestCase
$authorizedPaymentProcessor,
$settings,
$logger,
false
$this->environment
);
$cart = Mockery::mock(\WC_Cart::class);
@ -156,23 +170,25 @@ class OrderProcessorTest extends TestCase
$capture = Mockery::mock(Capture::class);
$capture->expects('id')
->andReturn($transactionId);
$capture->expects('status')
->andReturn(new CaptureStatus(CaptureStatus::COMPLETED));
$payments = Mockery::mock(Payments::class);
$payments->expects('captures')
$payments->shouldReceive('captures')
->andReturn([$capture]);
$purchaseUnit = Mockery::mock(PurchaseUnit::class);
$purchaseUnit->expects('payments')
$purchaseUnit->shouldReceive('payments')
->andReturn($payments);
$wcOrder = Mockery::mock(\WC_Order::class);
$orderStatus = Mockery::mock(OrderStatus::class);
$orderStatus
->expects('is')
->shouldReceive('is')
->with(OrderStatus::APPROVED)
->andReturn(true);
$orderStatus
->expects('is')
->shouldReceive('is')
->with(OrderStatus::COMPLETED)
->andReturn(true);
$orderId = 'abc';
@ -188,7 +204,7 @@ class OrderProcessorTest extends TestCase
->shouldReceive('status')
->andReturn($orderStatus);
$currentOrder
->expects('purchase_units')
->shouldReceive('purchase_units')
->andReturn([$purchaseUnit]);
$sessionHandler = Mockery::mock(SessionHandler::class);
$sessionHandler
@ -240,7 +256,7 @@ class OrderProcessorTest extends TestCase
$authorizedPaymentProcessor,
$settings,
$logger,
false
$this->environment
);
$cart = Mockery::mock(\WC_Cart::class);
@ -264,9 +280,6 @@ class OrderProcessorTest extends TestCase
PayPalGateway::INTENT_META_KEY,
$orderIntent
);
$wcOrder
->expects('update_status')
->with('on-hold', 'Awaiting payment.');
$wcOrder->expects('update_meta_data')
->with(PayPalGateway::ORDER_PAYMENT_MODE_META_KEY, 'live');
$wcOrder->expects('set_transaction_id')
@ -340,7 +353,7 @@ class OrderProcessorTest extends TestCase
$authorizedPaymentProcessor,
$settings,
$logger,
false
$this->environment
);
$wcOrder
@ -355,7 +368,7 @@ class OrderProcessorTest extends TestCase
PayPalGateway::INTENT_META_KEY,
$orderIntent
);
$this->assertFalse($testee->process($wcOrder));
$this->assertNotEmpty($testee->last_error());
}