Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Now allowing checking token orders for completion #3226

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions adminpages/orders.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,27 @@
}
}

// Check if a token order has been completed.
if ( ! empty( $_REQUEST['token_order'] ) ) {
// Check the nonce.
if ( empty( $_REQUEST['pmpro_orders_nonce'] ) || ! check_admin_referer( 'check_token_order', 'pmpro_orders_nonce' ) ) {
// Nonce failed.
$pmpro_msg = __( 'Nonce failed for checking token order.', 'paid-memberships-pro' );
$pmpro_msgt = 'error';
} else {
// Nonce passed. Process the token order.
$completed = pmpro_check_token_order_for_completion( (int) $_REQUEST['token_order'] );
if ( is_string( $completed ) ) {
// An error string was returned.
$pmpro_msg = __( 'Error checking token order: ', 'paid-memberships-pro' ) . $completed;
$pmpro_msgt = 'error';
} else {
$pmpro_msg = __( 'The token order has been completed.', 'paid-memberships-pro' );
$pmpro_msgt = 'success';
}
}
}

$thisyear = date( 'Y', $now );

// this array stores fields that should be read only
Expand Down
24 changes: 24 additions & 0 deletions classes/class-pmpro-orders-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,30 @@ public function column_order_code( $item ) {
);
}

// If the order is in token status and the gateway allows verifying completion, show the action.
// Checking for the status first to avoid loading the gateway object unnecessarily.
if ( 'token' === $item->status && pmpro_can_check_token_order_for_completion( $item->id ) ) {
$actions['check_token_order'] = sprintf(
'<a title="%1$s" href="%2$s">%3$s</a>',
esc_attr__( 'Recheck', 'paid-memberships-pro' ),
esc_url(
wp_nonce_url(
add_query_arg(
[
'page' => 'pmpro-orders',
'action' => 'check_token_order',
'token_order' => $item->id,
],
admin_url( 'admin.php' )
),
'check_token_order',
'pmpro_orders_nonce'
)
),
esc_html__( 'Recheck', 'paid-memberships-pro' )
);
}

/**
* Filter the extra actions for this user on this order.
*
Expand Down
10 changes: 10 additions & 0 deletions classes/gateways/class.pmprogateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,14 @@ public function update_subscription_info( $subscription ) {
// Update the subscription.
$subscription->set( $update_array );
}

/**
* Check whether the payment for a token order has been completed. If so, process the order.
*
* @param MemberOrder $order The order object to check.
* @return true|string True if the payment has been completed and the order processed. A string if an error occurred.
*/
function check_token_order( $order ) {
return __( 'Checking token orders is not supported for this gateway.', 'paid-memberships-pro' );
}
}
91 changes: 90 additions & 1 deletion classes/gateways/class.pmprogateway_stripe.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ function __construct( $gateway = null ) {
public static function supports( $feature ) {
$supports = array(
'subscription_sync' => true,
'payment_method_updates' => 'individual'
'payment_method_updates' => 'individual',
'check_token_orders' => true,
);

if ( empty( $supports[$feature] ) ) {
Expand Down Expand Up @@ -3886,6 +3887,94 @@ public static function process_refund( $success, $order ) {
return $success;
}

/**
* Check whether the payment for a token order has been completed. If so, process the order.
*
* @param MemberOrder $order The order object to check.
* @return true|string True if the payment has been completed and the order processed. A string if an error occurred.
*/
function check_token_order( $order ) {
// If this is not a token order, bail.
if ( 'token' !== $order->status ) {
return __( 'This is not a token order.', 'paid-memberships-pro' );
}

// Get the checkout session ID for this order.
$checkout_session_id = get_pmpro_membership_order_meta( $order->id, 'stripe_checkout_session_id', true );
if ( empty( $checkout_session_id ) ) {
return __( 'No checkout session ID found.', 'paid-memberships-pro' );
}

// Get the checkout session from Stripe.
try {
$checkout_session = Stripe_Checkout_Session::retrieve( $checkout_session_id );
} catch ( Stripe\Error\Base $e ) {
return __( 'Could not retrieve checkout session: ', 'paid-memberships-pro' ) . $e->getMessage();
} catch ( \Throwable $e ) {
return __( 'Could not retrieve checkout session: ', 'paid-memberships-pro' ) . $e->getMessage();
} catch ( \Exception $e ) {
return __( 'Could not retrieve checkout session: ', 'paid-memberships-pro' ) . $e->getMessage();
}

// If the checkout session is pending, this is a delayed notification payment method. We don't handle this yet. Bail.
if ( 'pending' === $checkout_session->payment_status ) {
return __( 'Payment is still pending.', 'paid-memberships-pro' );
}

// If the checkout session is not paid, bail.
if ( 'paid' !== $checkout_session->payment_status ) {
return __( 'Checkout session has not yet been completed.', 'paid-memberships-pro' );
}

// The order has been paid. Get the payment and subscription IDs.
if ( $checkout_session->mode === 'payment' ) {
// User purchased a one-time payment level. Assign the charge ID to the order.
try {
$payment_intent_args = array(
'id' => $checkout_session->payment_intent,
'expand' => array(
'payment_method',
'latest_charge',
),
);
$payment_intent = \Stripe\PaymentIntent::retrieve( $payment_intent_args );
$order->payment_transaction_id = $payment_intent->latest_charge->id;
} catch ( \Stripe\Error\Base $e ) {
// Could not get payment intent. We just won't set a payment transaction ID.
}
} elseif ( $checkout_session->mode === 'subscription' ) {
// User purchased a subscription. Assign the subscription ID invoice ID to the order.
$order->subscription_transaction_id = $checkout_session->subscription;
try {
$subscription_args = array(
'id' => $checkout_session->subscription,
'expand' => array(
'latest_invoice',
'default_payment_method',
),
);
$subscription = \Stripe\Subscription::retrieve( $subscription_args );
if ( ! empty( $subscription->latest_invoice->id ) ) {
$order->payment_transaction_id = $subscription->latest_invoice->id;
}
} catch ( \Stripe\Error\Base $e ) {
// Could not get invoices. We just won't set a payment transaction ID.
}
}

// Update the amounts paid.
$currency = pmpro_get_currency();
$currency_unit_multiplier = pow( 10, intval( $currency['decimals'] ) );

$order->total = (float) $checkout_session->amount_total / $currency_unit_multiplier;
$order->subtotal = (float) $checkout_session->amount_subtotal / $currency_unit_multiplier;
$order->tax = (float) $checkout_session->total_details->amount_tax / $currency_unit_multiplier;

// Complete the checkout.
pmpro_pull_checkout_data_from_order( $order );
return pmpro_complete_async_checkout( $order );
}

/**
* Get the description to send to Stripe for an order.
*
Expand Down
62 changes: 62 additions & 0 deletions includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -4964,4 +4964,66 @@ function pmpro_method_defined_in_class( $object, $method_name ) {

// Check if the method's declaring class is the same as the object's class.
return $method->getDeclaringClass()->getName() === $reflection_class->getName();
}

/**
* Check if we can check a token order for completion.
*
* @since TBD
*
* @param int $order_id The ID of the order to check.
* @return bool True if we can check the order for completion, false otherwise.
*/
function pmpro_can_check_token_order_for_completion( $order_id ) {
// Get the order object.
$order = new MemberOrder( $order_id );

// If the order does not exist, we can't check it.
if ( empty( $order->id ) ) {
return false;
}

// If the order is not a token order, we can't check it.
if ( 'token' !== $order->status ) {
return false;
}

// If the order does not have a gateway set, we can't check it.
if ( empty( $order->Gateway ) ) {
return false;
}

// Check if the order supports checking for completion.
return $order->Gateway->supports( 'check_token_orders' );
}

/**
* Check a token order for completion.
*
* @since TBD
*
* @param int $order_id The ID of the order to check.
* @return true|string True if the payment has been completed and the order processed. A string if an error occurred.
*/
function pmpro_check_token_order_for_completion( $order_id ) {
// Get the order object.
$order = new MemberOrder( $order_id );

// If the order does not exist, we can't check it.
if ( empty( $order->id ) ) {
return __( 'Order not found.', 'paid-memberships-pro' );
}

// If the order is not a token order, we can't check it.
if ( 'token' !== $order->status ) {
return __( 'Order is not a token order.', 'paid-memberships-pro' );
}

// If the order does not have a gateway set, we can't check it.
if ( empty( $order->Gateway ) ) {
return __( 'Order gateway not found.', 'paid-memberships-pro' );
}

// Check the order for completion.
return $order->Gateway->check_token_order( $order );
}