Skip to content

Commit 9b96d2b

Browse files
authored
Ur 4686 - Fix - Double Charge on Upgrade. (#1339)
* Fixed upgrade logic . * Moved sql to repositories.
1 parent 3f01b67 commit 9b96d2b

4 files changed

Lines changed: 70 additions & 28 deletions

File tree

modules/membership/includes/Admin/Interfaces/BaseInterface.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,8 @@ public function delete( $id );
5656
* @return void
5757
*/
5858
public function delete_multiple( $ids );
59+
60+
public function acquire_lock( $name, $timeout = 5 );
61+
62+
public function release_lock( $name );
5963
}

modules/membership/includes/Admin/Repositories/BaseRepository.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,15 @@ public function delete_multiple( $ids ) {
9696
public function delete( $id ) {
9797
return $this->wpdb()->delete( $this->table, array( 'ID' => $id ) );
9898
}
99+
100+
// Returns true if acquired, false on contention/timeout, null if the DB has no GET_LOCK support.
101+
public function acquire_lock( $name, $timeout = 5 ) {
102+
$result = $this->wpdb()->get_var( $this->wpdb()->prepare( 'SELECT GET_LOCK(%s, %d)', $name, $timeout ) );
103+
104+
return null === $result ? null : ( '1' === (string) $result );
105+
}
106+
107+
public function release_lock( $name ) {
108+
return $this->wpdb()->get_var( $this->wpdb()->prepare( 'SELECT RELEASE_LOCK(%s)', $name ) );
109+
}
99110
}

modules/membership/includes/Admin/Services/Stripe/StripeService.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2585,6 +2585,10 @@ public function revert_subscription( int $member_id ): void {
25852585
$last_order = $this->members_orders_repository->get_member_orders( $member_id );
25862586
$subscription = $this->members_subscription_repository->retrieve( $subscription_id );
25872587

2588+
if ( 'stripe' === ( $last_order['payment_method'] ?? '' ) && $this->stripe_charge_succeeded( $last_order, $subscription ) ) {
2589+
return;
2590+
}
2591+
25882592
$this->members_subscription_repository->update( $subscription_id, $last_subscription );
25892593
$this->members_orders_repository->delete_member_order( $member_id, false );
25902594
$refund_response = $this->refund( $last_order, $subscription ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
@@ -2598,6 +2602,22 @@ public function revert_subscription( int $member_id ): void {
25982602
}
25992603
}
26002604

2605+
private function stripe_charge_succeeded( $order, $subscription ) {
2606+
try {
2607+
if ( 'subscription' === ( $order['order_type'] ?? '' ) && ! empty( $subscription['subscription_id'] ) ) {
2608+
$stripe_subscription = \Stripe\Subscription::retrieve( $subscription['subscription_id'] );
2609+
return in_array( $stripe_subscription->status, array( 'active', 'trialing' ), true );
2610+
}
2611+
if ( ! empty( $order['transaction_id'] ) ) {
2612+
$intent = \Stripe\PaymentIntent::retrieve( $order['transaction_id'] );
2613+
return 'succeeded' === $intent->status;
2614+
}
2615+
} catch ( \Stripe\Exception\ApiErrorException $e ) {
2616+
PaymentGatewayLogging::log_error( 'stripe', 'Failed to verify Stripe charge before revert: ' . $e->getMessage() );
2617+
}
2618+
return false;
2619+
}
2620+
26012621
/**
26022622
* Refund an order.
26032623
*

modules/membership/includes/Admin/Services/SubscriptionService.php

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -534,32 +534,47 @@ public function upgrade_membership( $data ) {
534534
}
535535

536536
$selected_membership_details['payment_method'] = $payment_method;
537-
$membership_process = urm_get_membership_process( $user->ID );
538537

539-
$is_upgrading = ! empty( $membership_process['upgrade'] ) && isset( $membership_process['upgrade'][ $data['current_membership_id'] ] );
538+
$lock_name = 'urm_upgrade_lock_' . (int) $user->ID;
539+
$lock = $this->subscription_repository->acquire_lock( $lock_name );
540540

541-
if ( $is_upgrading ) {
541+
$membership_process = urm_get_membership_process( $user->ID );
542+
$in_progress = $membership_process['upgrade'][ $data['current_membership_id'] ] ?? array();
543+
// A stale guard means a previous attempt died or was abandoned before completing; let it be retried.
544+
$is_upgrading = ! empty( $in_progress ) && ( time() - (int) ( $in_progress['started_at'] ?? 0 ) ) < 15 * MINUTE_IN_SECONDS;
545+
546+
if ( false === $lock || $is_upgrading ) {
547+
if ( true === $lock ) {
548+
$this->subscription_repository->release_lock( $lock_name );
549+
}
542550
$response['response']['status'] = false;
543551
$response['response']['message'] = __( 'Membership upgrade process already initiated.', 'user-registration' );
544552

545553
return $response;
546554
}
547555

548-
$membership_process = urm_get_membership_process( $subscription['user_id'] );
549-
$is_upgrading = ! empty( $membership_process['upgrade'] ) && isset( $membership_process['upgrade'][ $data['current_membership_id'] ] );
556+
if ( 'free' !== $payment_method ) {
557+
$membership_process['upgrade'][ $data['current_membership_id'] ] = array(
558+
'from' => $data['current_membership_id'],
559+
'to' => $data['selected_membership_id'],
560+
'subscription_id' => $data['current_subscription_id'],
561+
'started_at' => time(),
562+
);
550563

551-
if ( $is_upgrading ) {
552-
$response['response']['status'] = false;
553-
$response['response']['message'] = __( 'Membership upgrade process already initiated.', 'user-registration' );
564+
update_user_meta( $user->ID, 'urm_membership_process', $membership_process );
565+
}
554566

555-
return $response;
567+
if ( true === $lock ) {
568+
$this->subscription_repository->release_lock( $lock_name );
556569
}
557570

558571
$current_membership_details['ID'] = $data['current_membership_id'];
559572
$selected_membership_details['ID'] = $data['selected_membership_id'];
560573
$upgrade_details = $this->calculate_membership_upgrade_cost( $current_membership_details, $selected_membership_details, $subscription );
561574

562575
if ( isset( $upgrade_details['status'] ) && ! $upgrade_details['status'] ) {
576+
$this->release_upgrade_guard( $user->ID, $data['current_membership_id'] );
577+
563578
return array(
564579
'response' => $upgrade_details,
565580
);
@@ -622,6 +637,8 @@ public function upgrade_membership( $data ) {
622637
if ( ! $cancel_subscription['status'] ) {
623638
$response['status'] = false;
624639

640+
$this->release_upgrade_guard( $user->ID, $data['current_membership_id'] );
641+
625642
return $response;
626643
} else {
627644
$this->subscription_repository->cancel_subscription_by_id( $current_subscription_id, false );
@@ -639,19 +656,6 @@ public function upgrade_membership( $data ) {
639656
$ur_authorize_net_data = isset( $data['ur_authorize_net'] ) ? $data['ur_authorize_net'] : array();
640657
$coupon = isset( $data['coupon'] ) ? $data['coupon'] : '';
641658

642-
if ( 'free' !== $payment_method ) {
643-
$membership_process = urm_get_membership_process( $user->ID );
644-
if ( ! isset( $membership_process['upgrade'][ $data['current_membership_id'] ] ) ) {
645-
$membership_process['upgrade'][ $data['current_membership_id'] ] = array(
646-
'from' => $data['current_membership_id'],
647-
'to' => $data['selected_membership_id'],
648-
'subscription_id' => $data['current_subscription_id'],
649-
);
650-
651-
update_user_meta( $user->ID, 'urm_membership_process', $membership_process );
652-
}
653-
}
654-
655659
$data = array(
656660
'membership' => $data['selected_membership_id'],
657661
'subscription_id' => $subscription['ID'],
@@ -682,12 +686,7 @@ public function upgrade_membership( $data ) {
682686
$response['status'] = true;
683687

684688
} else {
685-
$membership_process = urm_get_membership_process( $user->ID );
686-
if ( ! empty( $membership_process['upgrade'][ $data['current_membership_id'] ] ) ) {
687-
unset( $membership_process['upgrade'][ $data['current_membership_id'] ] );
688-
update_user_meta( $user->ID, 'urm_membership_process', $membership_process );
689-
}
690-
689+
$this->release_upgrade_guard( $user->ID, $data['current_membership_id'] );
691690
$this->orders_repository->delete( $order['ID'] );
692691
}
693692

@@ -703,6 +702,14 @@ public function upgrade_membership( $data ) {
703702
);
704703
}
705704

705+
private function release_upgrade_guard( $member_id, $current_membership_id ) {
706+
$membership_process = urm_get_membership_process( $member_id );
707+
if ( ! empty( $membership_process['upgrade'][ $current_membership_id ] ) ) {
708+
unset( $membership_process['upgrade'][ $current_membership_id ] );
709+
update_user_meta( $member_id, 'urm_membership_process', $membership_process );
710+
}
711+
}
712+
706713
/**
707714
* Calculate upgrade cost and details between memberships.
708715
*

0 commit comments

Comments
 (0)