Merge pull request #2612 from woocommerce/PCP-3677-pay-pal-subscriptions-api-renewal-order-not-created-in-woo-commerce

PayPal Subscriptions API renewal order not created in WooCommerce (3677)
This commit is contained in:
Emili Castells 2024-10-22 12:42:04 +02:00 committed by GitHub
commit ff14d98e82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 240 additions and 25 deletions

View file

@ -40,4 +40,7 @@ return array(
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
); );
}, },
'paypal-subscriptions.renewal-handler' => static function ( ContainerInterface $container ): RenewalHandler {
return new RenewalHandler( $container->get( 'woocommerce.logger.woocommerce' ) );
},
); );

View file

@ -0,0 +1,107 @@
<?php
/**
* Subscriptions renewal handler.
*
* @package WooCommerce\PayPalCommerce\WcSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use Psr\Log\LoggerInterface;
use WC_Data_Exception;
use WC_Order;
use WC_Subscription;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
/**
* Class RenewalHandler
*/
class RenewalHandler {
use TransactionIdHandlingTrait;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* RenewalHandler constructor.
*
* @param LoggerInterface $logger The logger.
*/
public function __construct( LoggerInterface $logger ) {
$this->logger = $logger;
}
/**
* Process subscription renewal.
*
* @param WC_Subscription[] $subscriptions WC Subscriptions.
* @param string $transaction_id PayPal transaction ID.
* @return void
* @throws WC_Data_Exception If something goes wrong while setting payment method.
*/
public function process( array $subscriptions, string $transaction_id ): void {
foreach ( $subscriptions as $subscription ) {
if ( $this->is_for_renewal_order( $subscription ) ) {
$renewal_order = wcs_create_renewal_order( $subscription );
if ( is_a( $renewal_order, WC_Order::class ) ) {
$this->logger->info(
sprintf(
'Processing renewal order #%s for subscription #%s',
$renewal_order->get_id(),
$subscription->get_id()
)
);
$renewal_order->set_payment_method( $subscription->get_payment_method() );
$renewal_order->payment_complete();
$this->update_transaction_id( $transaction_id, $renewal_order, $this->logger );
break;
}
}
$parent_order = wc_get_order( $subscription->get_parent() );
if ( is_a( $parent_order, WC_Order::class ) ) {
$this->logger->info(
sprintf(
'Processing parent order #%s for subscription #%s',
$parent_order->get_id(),
$subscription->get_id()
)
);
$subscription->update_meta_data( '_ppcp_is_subscription_renewal', 'true' );
$subscription->save_meta_data();
$this->update_transaction_id( $transaction_id, $parent_order, $this->logger );
}
}
}
/**
* Checks whether subscription order is for renewal or not.
*
* @param WC_Subscription $subscription WC Subscription.
* @return bool
*/
private function is_for_renewal_order( WC_Subscription $subscription ): bool {
$subscription_renewal_meta = $subscription->get_meta( '_ppcp_is_subscription_renewal' ) ?? '';
if ( $subscription_renewal_meta === 'true' ) {
return true;
}
if (
time() >= $subscription->get_time( 'start' )
&& ( time() - $subscription->get_time( 'start' ) ) <= ( 8 * HOUR_IN_SECONDS )
) {
return false;
}
return true;
}
}

View file

@ -99,7 +99,7 @@ return array(
new VaultPaymentTokenCreated( $logger, $prefix, $authorized_payments_processor, $payment_token_factory, $payment_token_helper ), new VaultPaymentTokenCreated( $logger, $prefix, $authorized_payments_processor, $payment_token_factory, $payment_token_helper ),
new VaultPaymentTokenDeleted( $logger ), new VaultPaymentTokenDeleted( $logger ),
new PaymentCapturePending( $logger ), new PaymentCapturePending( $logger ),
new PaymentSaleCompleted( $logger ), new PaymentSaleCompleted( $logger, $container->get( 'paypal-subscriptions.renewal-handler' ) ),
new PaymentSaleRefunded( $logger, $refund_fees_updater ), new PaymentSaleRefunded( $logger, $refund_fees_updater ),
new BillingSubscriptionCancelled( $logger ), new BillingSubscriptionCancelled( $logger ),
new BillingPlanPricingChangeActivated( $logger ), new BillingPlanPricingChangeActivated( $logger ),

View file

@ -10,8 +10,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Webhooks\Handler; namespace WooCommerce\PayPalCommerce\Webhooks\Handler;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WC_Order; use WC_Data_Exception;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; use WooCommerce\PayPalCommerce\PayPalSubscriptions\RenewalHandler;
use WP_REST_Request; use WP_REST_Request;
use WP_REST_Response; use WP_REST_Response;
@ -20,7 +20,14 @@ use WP_REST_Response;
*/ */
class PaymentSaleCompleted implements RequestHandler { class PaymentSaleCompleted implements RequestHandler {
use TransactionIdHandlingTrait, RequestHandlerTrait; use RequestHandlerTrait;
/**
* Renewal handler.
*
* @var RenewalHandler
*/
private $renewal_handler;
/** /**
* The logger. * The logger.
@ -33,9 +40,11 @@ class PaymentSaleCompleted implements RequestHandler {
* PaymentSaleCompleted constructor. * PaymentSaleCompleted constructor.
* *
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
* @param RenewalHandler $renewal_handler Renewal handler.
*/ */
public function __construct( LoggerInterface $logger ) { public function __construct( LoggerInterface $logger, RenewalHandler $renewal_handler ) {
$this->logger = $logger; $this->logger = $logger;
$this->renewal_handler = $renewal_handler;
} }
/** /**
@ -68,7 +77,7 @@ class PaymentSaleCompleted implements RequestHandler {
*/ */
public function handle_request( WP_REST_Request $request ): WP_REST_Response { public function handle_request( WP_REST_Request $request ): WP_REST_Response {
if ( is_null( $request['resource'] ) ) { if ( is_null( $request['resource'] ) ) {
return $this->failure_response(); return $this->failure_response( 'Could not retrieve resource.' );
} }
if ( ! function_exists( 'wcs_get_subscriptions' ) ) { if ( ! function_exists( 'wcs_get_subscriptions' ) ) {
@ -85,7 +94,7 @@ class PaymentSaleCompleted implements RequestHandler {
return $this->failure_response( 'Could not retrieve transaction id for subscription.' ); return $this->failure_response( 'Could not retrieve transaction id for subscription.' );
} }
$args = array( $args = array(
// phpcs:ignore WordPress.DB.SlowDBQuery // phpcs:ignore WordPress.DB.SlowDBQuery
'meta_query' => array( 'meta_query' => array(
array( array(
@ -95,24 +104,13 @@ class PaymentSaleCompleted implements RequestHandler {
), ),
), ),
); );
$subscriptions = wcs_get_subscriptions( $args );
foreach ( $subscriptions as $subscription ) {
$is_renewal = $subscription->get_meta( '_ppcp_is_subscription_renewal' ) ?? '';
if ( $is_renewal ) {
$renewal_order = wcs_create_renewal_order( $subscription );
if ( is_a( $renewal_order, WC_Order::class ) ) {
$renewal_order->set_payment_method( $subscription->get_payment_method() );
$renewal_order->payment_complete();
$this->update_transaction_id( $transaction_id, $renewal_order, $this->logger );
break;
}
}
$parent_order = wc_get_order( $subscription->get_parent() ); $subscriptions = wcs_get_subscriptions( $args );
if ( is_a( $parent_order, WC_Order::class ) ) { if ( $subscriptions ) {
$subscription->update_meta_data( '_ppcp_is_subscription_renewal', 'true' ); try {
$subscription->save_meta_data(); $this->renewal_handler->process( $subscriptions, $transaction_id );
$this->update_transaction_id( $transaction_id, $parent_order, $this->logger ); } catch ( WC_Data_Exception $exception ) {
return $this->failure_response( 'Could not update payment method.' );
} }
} }

View file

@ -0,0 +1,107 @@
<?php
namespace WooCommerce\PayPalCommerce\Tests\E2e;
use WooCommerce\PayPalCommerce\PayPalSubscriptions\RenewalHandler;
class PayPalSubscriptionsRenewalTest extends TestCase
{
public function test_parent_order()
{
$c = $this->getContainer();
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
// Simulates receiving webhook 1 minute after subscription start.
$subscription = $this->createSubscription('-1 minute');
$handler->process([$subscription], 'TRANSACTION-ID');
$renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) );
$this->assertEquals(count($renewal), 0);
}
public function test_renewal_order()
{
$c = $this->getContainer();
$handler = new RenewalHandler($c->get('woocommerce.logger.woocommerce'));
// Simulates receiving webhook 9 hours after subscription start.
$subscription = $this->createSubscription('-9 hour');
$handler->process([$subscription], 'TRANSACTION-ID');
$renewal = $subscription->get_related_orders( 'ids', array( 'renewal' ) );
$this->assertEquals(count($renewal), 1);
}
private function createSubscription(string $startDate)
{
$args = [
'method' => 'POST',
'headers' => [
'Authorization' => 'Basic ' . base64_encode( 'admin:admin' ),
'Content-Type' => 'application/json',
],
'body' => wp_json_encode([
'customer_id' => 1,
'set_paid' => true,
'payment_method' => 'ppcp-gateway',
'billing' => [
'first_name' => 'John',
'last_name' => 'Doe',
'address_1' => '969 Market',
'address_2' => '',
'city' => 'San Francisco',
'state' => 'CA',
'postcode' => '94103',
'country' => 'US',
'email' => 'john.doe@example.com',
'phone' => '(555) 555-5555'
],
'line_items' => [
[
'product_id' => 156,
'quantity' => 1
]
],
]),
];
$response = wp_remote_request(
'https://woocommerce-paypal-payments.ddev.site/wp-json/wc/v3/orders',
$args
);
$body = json_decode( $response['body'] );
$args = [
'method' => 'POST',
'headers' => [
'Authorization' => 'Basic ' . base64_encode( 'admin:admin' ),
'Content-Type' => 'application/json',
],
'body' => wp_json_encode([
'start_date' => gmdate( 'Y-m-d H:i:s', strtotime($startDate) ),
'parent_id' => $body->id,
'customer_id' => 1,
'status' => 'active',
'billing_period' => 'day',
'billing_interval' => 1,
'payment_method' => 'ppcp-gateway',
'line_items' => [
[
'product_id' => $_ENV['PAYPAL_SUBSCRIPTIONS_PRODUCT_ID'],
'quantity' => 1
]
],
]),
];
$response = wp_remote_request(
'https://woocommerce-paypal-payments.ddev.site/wp-json/wc/v3/subscriptions?per_page=1',
$args
);
$body = json_decode( $response['body'] );
return wcs_get_subscription($body->id);
}
}