Handle free trial sub for cards

authorize 1$ and void
This commit is contained in:
Alex P 2022-04-05 09:31:57 +03:00
parent ade7107227
commit f5a472673b
16 changed files with 288 additions and 69 deletions

View file

@ -48,6 +48,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CartRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository;
@ -228,6 +229,11 @@ return array(
$prefix = $container->get( 'api.prefix' );
return new CustomerRepository( $prefix );
},
'api.repository.order' => static function( ContainerInterface $container ): OrderRepository {
return new OrderRepository(
$container->get( 'api.endpoint.order' )
);
},
'api.factory.application-context' => static function ( ContainerInterface $container ) : ApplicationContextFactory {
return new ApplicationContextFactory();
},

View file

@ -157,6 +157,15 @@ class PurchaseUnit {
return $this->amount;
}
/**
* Sets the amount.
*
* @param Amount $amount The value to set.
*/
public function set_amount( Amount $amount ): void {
$this->amount = $amount;
}
/**
* Returns the shipping.
*

View file

@ -14,12 +14,15 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\AmountBreakdown;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
/**
* Class AmountFactory
*/
class AmountFactory {
use FreeTrialHandlerTrait;
/**
* The item factory.
@ -117,9 +120,15 @@ class AmountFactory {
* @return Amount
*/
public function from_wc_order( \WC_Order $order ): Amount {
$currency = $order->get_currency();
$items = $this->item_factory->from_wc_order( $order );
$total = new Money( (float) $order->get_total(), $currency );
$currency = $order->get_currency();
$items = $this->item_factory->from_wc_order( $order );
$total_value = (float) $order->get_total();
if ( CreditCardGateway::ID === $order->get_payment_method() && $this->is_free_trial_order( $order ) ) {
$total_value = 1.0;
}
$total = new Money( $total_value, $currency );
$item_total = new Money(
(float) array_reduce(
$items,

View file

@ -0,0 +1,54 @@
<?php
/**
* PayPal order repository.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Repository
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Repository;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class OrderRepository
*/
class OrderRepository {
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
protected $order_endpoint;
/**
* OrderRepository constructor.
*
* @param OrderEndpoint $order_endpoint The order endpoint.
*/
public function __construct( OrderEndpoint $order_endpoint ) {
$this->order_endpoint = $order_endpoint;
}
/**
* Gets a PayPal order for the given WooCommerce order.
*
* @param WC_Order $wc_order The WooCommerce order.
* @return Order The PayPal order.
* @throws RuntimeException When there is a problem getting the PayPal order.
*/
public function for_wc_order( WC_Order $wc_order ): Order {
$paypal_order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
if ( ! $paypal_order_id ) {
throw new RuntimeException( 'PayPal order ID not found in meta.' );
}
return $this->order_endpoint->order( $paypal_order_id );
}
}

View file

@ -1,5 +1,6 @@
import onApprove from '../OnApproveHandler/onApproveForContinue.js';
import {payerData} from "../Helper/PayerData";
import {PaymentMethods} from "../Helper/CheckoutMethodState";
class CartActionHandler {
@ -18,6 +19,7 @@ class CartActionHandler {
body: JSON.stringify({
nonce: this.config.ajax.create_order.nonce,
purchase_units: [],
payment_method: PaymentMethods.PAYPAL,
bn_code:bnCode,
payer,
context:this.config.context

View file

@ -1,5 +1,6 @@
import onApprove from '../OnApproveHandler/onApproveForPayNow.js';
import {payerData} from "../Helper/PayerData";
import {getCurrentPaymentMethod} from "../Helper/CheckoutMethodState";
class CheckoutActionHandler {
@ -31,6 +32,7 @@ class CheckoutActionHandler {
bn_code:bnCode,
context:this.config.context,
order_id:this.config.order_id,
payment_method: getCurrentPaymentMethod(),
form:formValues,
createaccount: createaccount
})

View file

@ -2,6 +2,7 @@ import ButtonsToggleListener from '../Helper/ButtonsToggleListener';
import Product from '../Entity/Product';
import onApprove from '../OnApproveHandler/onApproveForContinue';
import {payerData} from "../Helper/PayerData";
import {PaymentMethods} from "../Helper/CheckoutMethodState";
class SingleProductActionHandler {
@ -84,6 +85,7 @@ class SingleProductActionHandler {
purchase_units,
payer,
bn_code:bnCode,
payment_method: PaymentMethods.PAYPAL,
context:this.config.context
})
}).then(function (res) {

View file

@ -13,6 +13,9 @@ use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Address;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Amount;
use WooCommerce\PayPalCommerce\ApiClient\Entity\AmountBreakdown;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PayerName;
@ -25,7 +28,9 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CartRepository;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
@ -33,6 +38,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
*/
class CreateOrderEndpoint implements EndpointInterface {
use FreeTrialHandlerTrait;
const ENDPOINT = 'ppc-create-order';
/**
@ -177,6 +184,7 @@ class CreateOrderEndpoint implements EndpointInterface {
try {
$data = $this->request_data->read_request( $this->nonce() );
$this->parsed_request_data = $data;
$payment_method = $data['payment_method'] ?? '';
$wc_order = null;
if ( 'pay-now' === $data['context'] ) {
$wc_order = wc_get_order( (int) $data['order_id'] );
@ -193,6 +201,16 @@ class CreateOrderEndpoint implements EndpointInterface {
$this->purchase_units = array( $this->purchase_unit_factory->from_wc_order( $wc_order ) );
} else {
$this->purchase_units = $this->cart_repository->all();
// The cart does not have any info about payment method, so we must handle free trial here.
if ( CreditCardGateway::ID === $payment_method && $this->is_free_trial_cart() ) {
$this->purchase_units[0]->set_amount(
new Amount(
new Money( 1.0, $this->purchase_units[0]->amount()->currency_code() ),
$this->purchase_units[0]->amount()->breakdown()
)
);
}
}
$this->set_bn_code( $data );

View file

@ -0,0 +1,77 @@
<?php
/**
* Helper trait for the subscriptions handling.
*
* @package WooCommerce\PayPalCommerce\Subscription
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
use WC_Order;
use WC_Product;
use WC_Subscription;
use WC_Subscriptions_Product;
/**
* Class FreeTrialHandlerTrait
*/
trait FreeTrialHandlerTrait {
use SubscriptionsHandlerTrait;
/**
* S.
*
* @return bool
*/
protected function is_free_trial_cart(): bool {
if ( ! $this->is_wcs_plugin_active() ) {
return false;
}
$cart = WC()->cart;
if ( ! $cart || $cart->is_empty() || (float) $cart->get_total( 'numeric' ) > 0 ) {
return false;
}
foreach ( $cart->get_cart() as $item ) {
$product = $item['data'] ?? null;
if ( ! $product instanceof WC_Product ) {
continue;
}
if ( WC_Subscriptions_Product::get_trial_length( $product ) > 0 ) {
return true;
}
}
return false;
}
/**
* S.
*
* @param WC_Order $wc_order The WooCommerce order.
* @return bool
*/
protected function is_free_trial_order( WC_Order $wc_order ): bool {
if ( ! $this->is_wcs_plugin_active() ) {
return false;
}
if ( (float) $wc_order->get_total( 'numeric' ) > 0 ) {
return false;
}
$subs = wcs_get_subscriptions_for_order( $wc_order );
return ! empty(
array_filter(
$subs,
function ( WC_Subscription $sub ): bool {
return (float) $sub->get_total_initial_payment() <= 0;
}
)
);
}
}

View file

@ -11,6 +11,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription\Helper;
use WC_Product;
use WC_Subscription;
use WC_Subscriptions_Product;
/**
@ -46,7 +48,7 @@ class SubscriptionHelper {
}
foreach ( $cart->get_cart() as $item ) {
if ( ! isset( $item['data'] ) || ! is_a( $item['data'], \WC_Product::class ) ) {
if ( ! isset( $item['data'] ) || ! is_a( $item['data'], WC_Product::class ) ) {
continue;
}
if ( $item['data']->is_type( 'subscription' ) || $item['data']->is_type( 'subscription_variation' ) ) {

View file

@ -0,0 +1,26 @@
<?php
/**
* Helper trait for the free trial subscriptions handling.
*
* @package WooCommerce\PayPalCommerce\Subscription
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
use WC_Subscriptions;
/**
* Class SubscriptionsHandlerTrait
*/
trait SubscriptionsHandlerTrait {
/**
* Whether the subscription plugin is active or not.
*
* @return bool
*/
protected function is_wcs_plugin_active(): bool {
return class_exists( WC_Subscriptions::class );
}
}

View file

@ -44,9 +44,9 @@ return array(
'vaulting.payment-token-checker' => function( ContainerInterface $container ) : PaymentTokenChecker {
return new PaymentTokenChecker(
$container->get( 'vaulting.repository.payment-token' ),
$container->get( 'api.repository.order' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.processor.authorized-payments' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'api.endpoint.payments' ),
$container->get( 'woocommerce.logger.woocommerce' )
);

View file

@ -15,8 +15,10 @@ use RuntimeException;
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\Order;
use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository;
use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -26,6 +28,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
*/
class PaymentTokenChecker {
use FreeTrialHandlerTrait;
/**
* The payment token repository.
*
@ -33,6 +37,13 @@ class PaymentTokenChecker {
*/
protected $payment_token_repository;
/**
* The order repository.
*
* @var OrderRepository
*/
protected $order_repository;
/**
* The settings.
*
@ -47,13 +58,6 @@ class PaymentTokenChecker {
*/
protected $authorized_payments_processor;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
protected $order_endpoint;
/**
* The payments endpoint.
*
@ -72,24 +76,24 @@ class PaymentTokenChecker {
* PaymentTokenChecker constructor.
*
* @param PaymentTokenRepository $payment_token_repository The payment token repository.
* @param OrderRepository $order_repository The order repository.
* @param Settings $settings The settings.
* @param AuthorizedPaymentsProcessor $authorized_payments_processor The authorized payments processor.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param PaymentsEndpoint $payments_endpoint The payments endpoint.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
PaymentTokenRepository $payment_token_repository,
OrderRepository $order_repository,
Settings $settings,
AuthorizedPaymentsProcessor $authorized_payments_processor,
OrderEndpoint $order_endpoint,
PaymentsEndpoint $payments_endpoint,
LoggerInterface $logger
) {
$this->payment_token_repository = $payment_token_repository;
$this->order_repository = $order_repository;
$this->settings = $settings;
$this->authorized_payments_processor = $authorized_payments_processor;
$this->order_endpoint = $order_endpoint;
$this->payments_endpoint = $payments_endpoint;
$this->logger = $logger;
}
@ -115,6 +119,16 @@ class PaymentTokenChecker {
$tokens = $this->payment_token_repository->all_for_user_id( $customer_id );
if ( $tokens ) {
try {
if ( $this->is_free_trial_order( $wc_order ) ) {
if ( CreditCardGateway::ID === $wc_order->get_payment_method() ) {
$order = $this->order_repository->for_wc_order( $wc_order );
$this->authorized_payments_processor->void_authorizations( $order );
$wc_order->payment_complete();
}
return;
}
$this->capture_authorized_payment( $wc_order );
} catch ( Exception $exception ) {
$this->logger->error( $exception->getMessage() );
@ -126,8 +140,8 @@ class PaymentTokenChecker {
$this->logger->error( "Payment for subscription parent order #{$order_id} was not saved on PayPal." );
try {
$order = $this->get_order( $wc_order );
$this->void_authorizations( $order );
$order = $this->order_repository->for_wc_order( $wc_order );
$this->authorized_payments_processor->void_authorizations( $order );
} catch ( RuntimeException $exception ) {
$this->logger->warning( $exception->getMessage() );
}
@ -149,55 +163,6 @@ class PaymentTokenChecker {
}
}
/**
* Voids authorizations for the given PayPal order.
*
* @param Order $order The PayPal order.
* @return void
* @throws RuntimeException When there is a problem voiding authorizations.
*/
private function void_authorizations( Order $order ): void {
$purchase_units = $order->purchase_units();
if ( ! $purchase_units ) {
throw new RuntimeException( 'No purchase units.' );
}
$payments = $purchase_units[0]->payments();
if ( ! $payments ) {
throw new RuntimeException( 'No payments.' );
}
$voidable_authorizations = array_filter(
$payments->authorizations(),
function ( Authorization $authorization ): bool {
return $authorization->is_voidable();
}
);
if ( ! $voidable_authorizations ) {
throw new RuntimeException( 'No voidable authorizations.' );
}
foreach ( $voidable_authorizations as $authorization ) {
$this->payments_endpoint->void( $authorization );
}
}
/**
* Gets a PayPal order from the given WooCommerce order.
*
* @param WC_Order $wc_order The WooCommerce order.
* @return Order The PayPal order.
* @throws RuntimeException When there is a problem getting the PayPal order.
*/
private function get_order( WC_Order $wc_order ): Order {
$paypal_order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
if ( ! $paypal_order_id ) {
throw new RuntimeException( 'PayPal order ID not found in meta.' );
}
return $this->order_endpoint->order( $paypal_order_id );
}
/**
* Updates WC order and subscription status to failed and canceled respectively.
*

View file

@ -14,6 +14,7 @@ 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\Subscription\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait;
@ -24,7 +25,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
*/
trait ProcessPaymentTrait {
use OrderMetaTrait, PaymentsStatusHandlingTrait, TransactionIdHandlingTrait;
use OrderMetaTrait, PaymentsStatusHandlingTrait, TransactionIdHandlingTrait, FreeTrialHandlerTrait;
/**
* Process a payment for an WooCommerce order.
@ -115,7 +116,10 @@ trait ProcessPaymentTrait {
$this->handle_new_order_status( $order, $wc_order );
if ( $this->config->has( 'intent' ) && strtoupper( (string) $this->config->get( 'intent' ) ) === 'CAPTURE' ) {
if ( $this->is_free_trial_order( $wc_order ) ) {
$this->authorized_payments_processor->void_authorizations( $order );
$wc_order->payment_complete();
} elseif ( $this->config->has( 'intent' ) && strtoupper( (string) $this->config->get( 'intent' ) ) === 'CAPTURE' ) {
$this->authorized_payments_processor->capture_authorized_payment( $wc_order );
}

View file

@ -21,6 +21,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice;
@ -244,6 +245,39 @@ class AuthorizedPaymentsProcessor {
}
}
/**
* Voids authorizations for the given PayPal order.
*
* @param Order $order The PayPal order.
* @return void
* @throws RuntimeException When there is a problem voiding authorizations.
*/
public function void_authorizations( Order $order ): void {
$purchase_units = $order->purchase_units();
if ( ! $purchase_units ) {
throw new RuntimeException( 'No purchase units.' );
}
$payments = $purchase_units[0]->payments();
if ( ! $payments ) {
throw new RuntimeException( 'No payments.' );
}
$voidable_authorizations = array_filter(
$payments->authorizations(),
function ( Authorization $authorization ): bool {
return $authorization->is_voidable();
}
);
if ( ! $voidable_authorizations ) {
throw new RuntimeException( 'No voidable authorizations.' );
}
foreach ( $voidable_authorizations as $authorization ) {
$this->payments_endpoint->void( $authorization );
}
}
/**
* Displays the notice for a status.
*

View file

@ -8,6 +8,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use function Brain\Monkey\Functions\expect;
use function Brain\Monkey\Functions\when;
@ -141,6 +142,10 @@ class AmountFactoryTest extends TestCase
->with($order)
->andReturn([$item]);
$order
->shouldReceive('get_payment_method')
->andReturn(PayPalGateway::ID);
$order
->shouldReceive('get_total')
->andReturn(100);
@ -197,6 +202,10 @@ class AmountFactoryTest extends TestCase
->with($order)
->andReturn([$item]);
$order
->shouldReceive('get_payment_method')
->andReturn(PayPalGateway::ID);
$order
->shouldReceive('get_total')
->andReturn(100);