Skip to content

Commit d0845c3

Browse files
authored
Fix - Retrieve actual PayPal transaction ID instead of subscription ID in payment processing (#1286)
1 parent 23c1127 commit d0845c3

1 file changed

Lines changed: 197 additions & 3 deletions

File tree

modules/membership/includes/Admin/Services/Paypal/NewPaypalService.php

Lines changed: 197 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,11 +1339,48 @@ public function handle_paypal_redirect_response( $params, $payer_id ) {
13391339

13401340
$new_status = 'ACTIVE' === strtoupper( isset( $subscription_details['status'] ) ? $subscription_details['status'] : '' ) ? 'active' : 'pending';
13411341

1342+
// Try to get the real transaction ID from PayPal's subscription transactions API.
1343+
$transaction_id = $paypal_subscription_id;
1344+
$transaction_source = 'subscription_id_placeholder';
1345+
1346+
$txn_response = $this->get_paypal_subscription_transactions(
1347+
$paypal_subscription_id,
1348+
$this->get_paypal_rest_credentials()
1349+
);
1350+
1351+
if ( ! is_wp_error( $txn_response ) && ! empty( $txn_response['transactions'] ) ) {
1352+
foreach ( $txn_response['transactions'] as $txn ) {
1353+
if ( ! empty( $txn['id'] ) && 'Completed' === ( isset( $txn['status'] ) ? $txn['status'] : '' ) ) {
1354+
$transaction_id = sanitize_text_field( $txn['id'] );
1355+
$transaction_source = 'subscription_transactions_api';
1356+
break;
1357+
}
1358+
}
1359+
}
1360+
1361+
PaymentGatewayLogging::log_general(
1362+
'paypal',
1363+
sprintf(
1364+
'[Member ID #%s] Subscription transaction ID resolution on redirect.',
1365+
$member_id
1366+
) . "\n" . wp_json_encode(
1367+
array(
1368+
'paypal_subscription_id' => $paypal_subscription_id,
1369+
'transaction_id' => $transaction_id,
1370+
'source' => $transaction_source,
1371+
'api_error' => is_wp_error( $txn_response ) ? $txn_response->get_error_message() : null,
1372+
'transactions_found' => ! is_wp_error( $txn_response ) ? count( isset( $txn_response['transactions'] ) ? $txn_response['transactions'] : array() ) : 0,
1373+
),
1374+
JSON_PRETTY_PRINT
1375+
),
1376+
'subscription_id_placeholder' === $transaction_source ? 'notice' : 'info'
1377+
);
1378+
13421379
$this->members_orders_repository->update(
13431380
$member_order['ID'],
13441381
array(
13451382
'status' => 'completed',
1346-
'transaction_id' => sanitize_text_field( $paypal_subscription_id ),
1383+
'transaction_id' => $transaction_id,
13471384
)
13481385
);
13491386

@@ -1368,7 +1405,8 @@ public function handle_paypal_redirect_response( $params, $payer_id ) {
13681405
) . "\n" . wp_json_encode(
13691406
array(
13701407
'paypal_subscription_id' => $paypal_subscription_id,
1371-
'transaction_id' => $paypal_subscription_id,
1408+
'transaction_id' => $transaction_id,
1409+
'transaction_source' => $transaction_source,
13721410
'paypal_status' => isset( $subscription_details['status'] ) ? $subscription_details['status'] : '',
13731411
'subscription_status' => $resolved_status,
13741412
'member_id' => $member_id,
@@ -1673,7 +1711,7 @@ public function handle_webhook_event( $event ) {
16731711
break;
16741712

16751713
case 'PAYMENT.SALE.COMPLETED':
1676-
$result = $this->handle_subscription_payment_completed( $resource );
1714+
$result = $this->handle_subscription_sale_webhook_event( $resource );
16771715
break;
16781716

16791717
default:
@@ -2147,6 +2185,142 @@ private function handle_subscription_webhook_event( $event_type, $resource ) {
21472185
return true;
21482186
}
21492187

2188+
/**
2189+
* Handle PAYMENT.SALE.COMPLETED webhook for subscription payments.
2190+
* Stores the real PayPal sale/transaction ID, replacing any subscription-ID placeholder.
2191+
*
2192+
* @param array $resource Webhook resource payload.
2193+
*
2194+
* @return bool
2195+
*/
2196+
private function handle_subscription_sale_webhook_event( $resource ) {
2197+
$transaction_id = sanitize_text_field( isset( $resource['id'] ) ? $resource['id'] : '' );
2198+
$paypal_subscription_id = sanitize_text_field( isset( $resource['billing_agreement_id'] ) ? $resource['billing_agreement_id'] : '' );
2199+
2200+
if ( empty( $transaction_id ) || empty( $paypal_subscription_id ) ) {
2201+
PaymentGatewayLogging::log_general(
2202+
'paypal',
2203+
'[PAYMENT.SALE.COMPLETED] Skipped — missing transaction_id or billing_agreement_id.' . "\n" . wp_json_encode(
2204+
array(
2205+
'transaction_id' => $transaction_id ?: null,
2206+
'paypal_subscription_id' => $paypal_subscription_id ?: null,
2207+
),
2208+
JSON_PRETTY_PRINT
2209+
),
2210+
'info'
2211+
);
2212+
return false;
2213+
}
2214+
2215+
// Already have an order for this exact sale — nothing to update.
2216+
$existing = $this->orders_repository->get_order_by_transaction_id( $transaction_id );
2217+
if ( ! empty( $existing ) ) {
2218+
PaymentGatewayLogging::log_general(
2219+
'paypal',
2220+
'[PAYMENT.SALE.COMPLETED] Order already exists for this transaction — skipped.' . "\n" . wp_json_encode(
2221+
array(
2222+
'transaction_id' => $transaction_id,
2223+
'order_id' => $existing['ID'],
2224+
),
2225+
JSON_PRETTY_PRINT
2226+
),
2227+
'info'
2228+
);
2229+
return true;
2230+
}
2231+
2232+
// Find local subscription record by PayPal subscription ID.
2233+
$membership_subscription = $this->members_subscription_repository->get_subscription_by_subscription_id_meta( $paypal_subscription_id );
2234+
if ( empty( $membership_subscription ) ) {
2235+
PaymentGatewayLogging::log_general(
2236+
'paypal',
2237+
'[PAYMENT.SALE.COMPLETED] No local subscription found for PayPal subscription ID.' . "\n" . wp_json_encode(
2238+
array(
2239+
'paypal_subscription_id' => $paypal_subscription_id,
2240+
'transaction_id' => $transaction_id,
2241+
),
2242+
JSON_PRETTY_PRINT
2243+
),
2244+
'info'
2245+
);
2246+
return false;
2247+
}
2248+
2249+
$local_sub_id = $membership_subscription['ID'];
2250+
$user_id = $membership_subscription['user_id'];
2251+
2252+
// Replace placeholder order whose transaction_id is the subscription ID (not a real sale ID).
2253+
$placeholder = $this->orders_repository->get_order_by_transaction_id( $paypal_subscription_id );
2254+
if ( ! empty( $placeholder ) && ! empty( $placeholder['ID'] ) ) {
2255+
$this->orders_repository->update(
2256+
$placeholder['ID'],
2257+
array(
2258+
'status' => 'completed',
2259+
'transaction_id' => $transaction_id,
2260+
)
2261+
);
2262+
PaymentGatewayLogging::log_general(
2263+
'paypal',
2264+
'[PAYMENT.SALE.COMPLETED] Placeholder order updated with real transaction ID.' . "\n" . wp_json_encode(
2265+
array(
2266+
'order_id' => $placeholder['ID'],
2267+
'member_id' => $user_id,
2268+
'paypal_subscription_id' => $paypal_subscription_id,
2269+
'transaction_id' => $transaction_id,
2270+
),
2271+
JSON_PRETTY_PRINT
2272+
),
2273+
'success'
2274+
);
2275+
return true;
2276+
}
2277+
2278+
// Update a pending order that has no transaction_id yet.
2279+
$pending_order = $this->orders_repository->get_order_by_subscription( $local_sub_id );
2280+
if (
2281+
! empty( $pending_order['ID'] ) &&
2282+
'pending' === ( $pending_order['status'] ?? '' ) &&
2283+
'' === (string) ( $pending_order['transaction_id'] ?? '' )
2284+
) {
2285+
$this->orders_repository->update(
2286+
$pending_order['ID'],
2287+
array(
2288+
'status' => 'completed',
2289+
'transaction_id' => $transaction_id,
2290+
)
2291+
);
2292+
PaymentGatewayLogging::log_general(
2293+
'paypal',
2294+
'[PAYMENT.SALE.COMPLETED] Pending order completed with real transaction ID.' . "\n" . wp_json_encode(
2295+
array(
2296+
'order_id' => $pending_order['ID'],
2297+
'member_id' => $user_id,
2298+
'paypal_subscription_id' => $paypal_subscription_id,
2299+
'transaction_id' => $transaction_id,
2300+
),
2301+
JSON_PRETTY_PRINT
2302+
),
2303+
'success'
2304+
);
2305+
return true;
2306+
}
2307+
2308+
// No matching order found — likely a renewal payment handled by the backfill job.
2309+
PaymentGatewayLogging::log_general(
2310+
'paypal',
2311+
'[PAYMENT.SALE.COMPLETED] No matching order found for transaction — skipped (backfill will handle renewals).' . "\n" . wp_json_encode(
2312+
array(
2313+
'paypal_subscription_id' => $paypal_subscription_id,
2314+
'transaction_id' => $transaction_id,
2315+
),
2316+
JSON_PRETTY_PRINT
2317+
),
2318+
'info'
2319+
);
2320+
2321+
return true;
2322+
}
2323+
21502324
/**
21512325
* Parse custom id format: membership-member_id-current_membership_id-subscription_id
21522326
*
@@ -2659,6 +2833,26 @@ private function get_paypal_subscription( $subscription_id, $paypal_options ) {
26592833
);
26602834
}
26612835

2836+
/**
2837+
* Fetch transaction list for a PayPal subscription.
2838+
*
2839+
* @param string $subscription_id
2840+
* @param array $paypal_options
2841+
*
2842+
* @return array|WP_Error
2843+
*/
2844+
private function get_paypal_subscription_transactions( $subscription_id, $paypal_options ) {
2845+
$start_time = gmdate( 'Y-m-d\TH:i:s\Z', strtotime( '-1 hour' ) );
2846+
$end_time = gmdate( 'Y-m-d\TH:i:s\Z' );
2847+
2848+
return $this->paypal_rest_request(
2849+
'GET',
2850+
'/v1/billing/subscriptions/' . rawurlencode( $subscription_id ) . '/transactions?start_time=' . $start_time . '&end_time=' . $end_time,
2851+
null,
2852+
$paypal_options
2853+
);
2854+
}
2855+
26622856
/**
26632857
* Revise subscription.
26642858
*

0 commit comments

Comments
 (0)