@@ -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