Skip to content
Draft
18 changes: 18 additions & 0 deletions modules/ppcp-wc-gateway/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Service\FailedOrders\FailedOrdersRestEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Service\FailedOrders\FailedOrderTracker;
use WooCommerce\PayPalCommerce\WcGateway\Service\FailedOrders\WordPressOptionsFailedOrderPersistence;
use WooCommerce\PayPalCommerce\WcGateway\Settings\HeaderRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
Expand Down Expand Up @@ -2418,4 +2421,19 @@ static function ( ContainerInterface $container ): DisplayManager {
getenv( 'PCP_APPSWITCH_ENABLED' ) === '1'
);
},

'wcgateway.service.failed-order-persistence' => static function (): WordPressOptionsFailedOrderPersistence {
return new WordPressOptionsFailedOrderPersistence();
},

'wcgateway.service.failed-order-tracker' => static function ( ContainerInterface $container ): FailedOrderTracker {
return new FailedOrderTracker(
$container->get( 'wcgateway.service.failed-order-persistence' )
);
},
'wcgateway.endpoint.failed-order-tracker' => static function ( ContainerInterface $container ): FailedOrdersRestEndpoint {
return new FailedOrdersRestEndpoint(
$container->get( 'wcgateway.service.failed-order-tracker' ),
);
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/**
* Failed Order Persistence Interface
*
* @package WooCommerce\PayPalCommerce\WcGateway\Service
*/

declare(strict_types=1);

namespace WooCommerce\PayPalCommerce\WcGateway\Service\FailedOrders;

interface FailedOrderPersistenceInterface {

/**
* Store failed order transaction data.
*
* @param array $transaction_data The transaction data to store.
*
* @return void
*/
public function store_failed_order( array $transaction_data ): void;

/**
* Retrieve all stored failed orders.
*
* @return array Array of failed order transaction data.
*/
public function get_failed_orders(): array;

/**
* Clear all stored failed orders.
*
* @return void
*/
public function clear_failed_orders(): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php
/**
* Failed Order Tracker Service
*
* @package WooCommerce\PayPalCommerce\WcGateway\Service
*/

declare(strict_types=1);

namespace WooCommerce\PayPalCommerce\WcGateway\Service\FailedOrders;

use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;

class FailedOrderTracker {

/**
* The persistence layer for failed orders.
*
* @var FailedOrderPersistenceInterface
*/
private FailedOrderPersistenceInterface $persistence;

/**
* FailedOrderTracker constructor.
*
* @param FailedOrderPersistenceInterface $persistence The persistence layer.
*/
public function __construct( FailedOrderPersistenceInterface $persistence ) {
$this->persistence = $persistence;
}

/**
* Track failed card order transactions.
*
* @param WC_Order $wc_order The WooCommerce order.
* @param Order $paypal_order The PayPal order.
*
* @return void
*/
public function track_failed_card_order( WC_Order $wc_order, Order $paypal_order ): void {
if ( $this->is_failed_credit_card_transaction( $wc_order ) ) {
$this->record_failed_transaction( $wc_order, $paypal_order );
}
}

/**
* Get recent failed orders.
*
* @param int $limit Number of orders to retrieve.
*
* @return array
*/
public function get_recent_failed_orders( int $limit = 10 ): array {
$failed_orders = $this->persistence->get_failed_orders();

// Sort by timestamp descending (newest first).
usort(
$failed_orders,
function ( $a, $b ) {
return $b['timestamp'] - $a['timestamp'];
}
);

return array_slice( $failed_orders, 0, $limit );
}

/**
* Get failed orders count for time period.
*
* @param int $minutes Time period in minutes.
*
* @return int
*/
public function get_failed_orders_count( int $minutes = 60 ): int {
$failed_orders = $this->persistence->get_failed_orders();
$cutoff_time = current_time( 'timestamp' ) - ( $minutes * 60 );

$recent_failures = array_filter(
$failed_orders,
function ( $order ) use ( $cutoff_time ) {
return $order['timestamp'] > $cutoff_time;
}
);

return count( $recent_failures );
}

/**
* Clear all stored failed orders (for testing).
*
* @return void
*/
public function clear_failed_orders(): void {
$this->persistence->clear_failed_orders();
}

/**
* Check if this is a failed credit card transaction.
*
* @param WC_Order $order The WooCommerce order.
*
* @return bool
*/
private function is_failed_credit_card_transaction( WC_Order $order ): bool {
return $order->has_status( 'failed' ) &&
$order->get_payment_method() === CreditCardGateway::ID;
}

/**
* Record failed transaction details.
*
* @param WC_Order $wc_order The WooCommerce order.
* @param Order $paypal_order The PayPal order.
*
* @return void
*/
private function record_failed_transaction( WC_Order $wc_order, Order $paypal_order ): void {
$fraud_data = $wc_order->get_meta( PayPalGateway::FRAUD_RESULT_META_KEY );

$failed_transaction_data = array(
'timestamp' => current_time( 'timestamp' ),
'order_id' => $wc_order->get_id(),
'total' => $wc_order->get_total(),
'currency' => $wc_order->get_currency(),
'customer_ip' => $wc_order->get_customer_ip_address(),
'billing_email' => $wc_order->get_billing_email(),
'billing_country' => $wc_order->get_billing_country(),
'fraud_data' => $fraud_data,
'paypal_order_id' => $paypal_order->id(),
'failure_reason' => $this->get_failure_reason( $wc_order ),
);

$this->persistence->store_failed_order( $failed_transaction_data );
}

/**
* Get failure reason from order.
*
* @param WC_Order $order The WooCommerce order.
*
* @return string
*/
private function get_failure_reason( WC_Order $order ): string {
$order_notes = wc_get_order_notes(
array(
'order_id' => $order->get_id(),
'limit' => 1,
)
);

if ( ! empty( $order_notes ) ) {
return $order_notes[0]->content;
}

return 'Payment failed';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);

namespace WooCommerce\PayPalCommerce\WcGateway\Service\FailedOrders;

use WP_REST_Server;
use WP_REST_Response;
use WP_REST_Request;
use WP_REST_Controller;

/**
* REST controller for retrieving failed order data.
*/
class FailedOrdersRestEndpoint extends WP_REST_Controller {
/**
* Endpoint namespace.
*/
protected const NAMESPACE = 'wc/v3/wc_paypal';

/**
* The base path for this REST controller.
*
* @var string
*/
protected $rest_base = 'failed_orders';

/**
* Failed order tracker service.
*
* @var FailedOrderTracker
*/
protected FailedOrderTracker $failed_order_tracker;

/**
* Constructor.
*
* @param FailedOrderTracker $failed_order_tracker Failed order tracker service.
*/
public function __construct( FailedOrderTracker $failed_order_tracker ) {
$this->failed_order_tracker = $failed_order_tracker;
}

/**
* Configure REST API routes.
*/
public function register_routes(): void {
/**
* GET /wp-json/wc/v3/wc_paypal/failed_orders
* Query parameters:
* - limit: Number of orders to retrieve (default: 10)
* - minutes: Time period in minutes for count (default: 60)
*/
register_rest_route(
static::NAMESPACE,
'/' . $this->rest_base,
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_failed_orders' ),
'permission_callback' => current_user_can( 'manage_woocommerce' ),
'args' => array(
'limit' => array(
'default' => 10,
'type' => 'integer',
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
),
'minutes' => array(
'default' => 60,
'type' => 'integer',
'minimum' => 1,
'sanitize_callback' => 'absint',
),
),
)
);
}

/**
* Returns recent failed orders.
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The failed orders data or an error response.
*/
public function get_failed_orders( WP_REST_Request $request ): WP_REST_Response {
$limit = $request->get_param( 'limit' );

try {
$failed_orders = $this->failed_order_tracker->get_recent_failed_orders( $limit );

return rest_ensure_response( $failed_orders );
} catch ( \Exception $e ) {
return rest_ensure_response( new \WP_Error( 'failed_orders_error', $e->getMessage() ) );
}
}

/**
* Returns the count of failed orders for a time period.
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The failed orders count or an error response.
*/
public function get_failed_orders_count( WP_REST_Request $request ): WP_REST_Response {
$minutes = $request->get_param( 'minutes' );

try {
$count = $this->failed_order_tracker->get_failed_orders_count( $minutes );

return rest_ensure_response(
array(
'count' => $count,
'time_period_minutes' => $minutes,
)
);
} catch ( \Exception $e ) {
return rest_ensure_response( new \WP_Error( 'failed_orders_count_error', $e->getMessage() ) );
}
}
}
Loading
Loading