From 78030824bd601f0b59f69f550bd93a6d12302bfe Mon Sep 17 00:00:00 2001 From: David Parker Date: Fri, 9 Feb 2024 10:33:31 -0500 Subject: [PATCH 1/2] Pulling missing subscription orders from Stripe/PPE --- .../class.pmprogateway_paypalexpress.php | 102 ++++++++--- .../gateways/class.pmprogateway_stripe.php | 168 ++++++++++++++++-- 2 files changed, 228 insertions(+), 42 deletions(-) diff --git a/classes/gateways/class.pmprogateway_paypalexpress.php b/classes/gateways/class.pmprogateway_paypalexpress.php index b9cb46d226..db4af1d948 100644 --- a/classes/gateways/class.pmprogateway_paypalexpress.php +++ b/classes/gateways/class.pmprogateway_paypalexpress.php @@ -934,45 +934,99 @@ function update_subscription_info( $subscription ) { return 'Subscription transaction ID is empty.'; } - //paypal profile stuff + // Get subscription information from PayPal. $nvpStr = ""; $nvpStr .= "&PROFILEID=" . urlencode( $subscription_transaction_id ); $response = $this->PPHttpPost('GetRecurringPaymentsProfileDetails', $nvpStr); - if("SUCCESS" == strtoupper($response["ACK"]) || "SUCCESSWITHWARNING" == strtoupper($response["ACK"])) { - // Found subscription. - $update_array = array(); + // If the request failed, return the error message. + if ("SUCCESS" != strtoupper($response["ACK"]) && "SUCCESSWITHWARNING" != strtoupper($response["ACK"])) { + return __( 'Subscription could not be found.', 'paid-memberships-pro' ); + } + + // Found subscription. + $update_array = array(); - // PayPal doesn't send the subscription start date, so let's take a guess based on the user's order history. + // PayPal may or may not return the start date of the subscription. + if ( ! empty( $response['PROFILESTARTDATE'] ) ) { + $update_array['startdate'] = date_i18n( 'Y-m-d H:i:s', strtotime( $response['PROFILESTARTDATE'] ) ); + } else { $oldest_orders = $subscription->get_orders( [ 'limit' => 1, 'orderby' => '`timestamp` ASC, `id` ASC', ] ); - if ( ! empty( $oldest_orders ) ) { $oldest_order = current( $oldest_orders ); - $update_array['startdate'] = date_i18n( 'Y-m-d H:i:s', $oldest_order->getTimestamp( true ) ); } + } - if ( in_array( $response['STATUS'], array( 'Pending', 'Active' ), true ) ) { - // Subscription is active. - $update_array['status'] = 'active'; - $update_array['next_payment_date'] = date( 'Y-m-d H:i:s', strtotime( $response['NEXTBILLINGDATE'] ) ); - $update_array['billing_amount'] = floatval( $response['REGULARAMT'] ); - $update_array['cycle_number'] = (int) $response['REGULARBILLINGFREQUENCY']; - $update_array['cycle_period'] = $response['REGULARBILLINGPERIOD']; - $update_array['trial_amount'] = empty( $response['TRIALAMT'] ) ? 0 : floatval( $response['TRIALAMT'] ); - $update_array['trial_limit'] = empty( $response['TRIALTOTALBILLINGCYCLES'] ) ? 0 : (int) $response['TRIALTOTALBILLINGCYCLES']; - $update_array['billing_limit'] = empty( $response['REGULARTOTALBILLINGCYCLES'] ) ? 0 : (int) $response['REGULARTOTALBILLINGCYCLES']; - } else { - // Subscription is no longer active. - // Can't fill subscription end date, $request only has the date of the last payment. - $update_array['status'] = 'cancelled'; - } - $subscription->set( $update_array ); + if ( in_array( $response['STATUS'], array( 'Pending', 'Active' ), true ) ) { + // Subscription is active. + $update_array['status'] = 'active'; + $update_array['next_payment_date'] = date( 'Y-m-d H:i:s', strtotime( $response['NEXTBILLINGDATE'] ) ); + $update_array['billing_amount'] = floatval( $response['REGULARAMT'] ); + $update_array['cycle_number'] = (int) $response['REGULARBILLINGFREQUENCY']; + $update_array['cycle_period'] = $response['REGULARBILLINGPERIOD']; + $update_array['trial_amount'] = empty( $response['TRIALAMT'] ) ? 0 : floatval( $response['TRIALAMT'] ); + $update_array['trial_limit'] = empty( $response['TRIALTOTALBILLINGCYCLES'] ) ? 0 : (int) $response['TRIALTOTALBILLINGCYCLES']; + $update_array['billing_limit'] = empty( $response['REGULARTOTALBILLINGCYCLES'] ) ? 0 : (int) $response['REGULARTOTALBILLINGCYCLES']; } else { - return __( 'Subscription could not be found.', 'paid-memberships-pro' ); + // Subscription is no longer active. + // Can't fill subscription end date, $request only has the date of the last payment. + $update_array['status'] = 'cancelled'; + } + + // Update the subscription. + $subscription->set( $update_array ); + + // Also use this opportunity to pull any missing subscription payments from PayPal. + // Start searching 5 years ago. + $nvpStr = ""; + $nvpStr .= "&STARTDATE=" . urlencode( date( 'Y-m-d\TH:i:s\Z', strtotime( '-5 years' ) ) ); + $nvpStr .= "&PROFILEID=" . urlencode( $subscription_transaction_id ); + $nvpStr .= "&STATUS=Success"; + $response = $this->PPHttpPost('TransactionSearch', $nvpStr); + + // If the request failed, bail. + if ("SUCCESS" != strtoupper($response["ACK"]) && "SUCCESSWITHWARNING" != strtoupper($response["ACK"])) { + return; + } + + // Loop through the transactions and add any payments that are missing. + $transaction_loop_index = 0; + while ( isset( $response[ "L_TRANSACTIONID{$transaction_loop_index}" ] ) ) { + $transaction_id = $response[ "L_TRANSACTIONID{$transaction_loop_index}" ]; + $transaction_date = $response[ "L_TIMESTAMP{$transaction_loop_index}" ]; + $transaction_amount = $response[ "L_AMT{$transaction_loop_index}" ]; + + // Check if we already have this transaction. + $existing_order = MemberOrder::get_order( + array( + 'payment_transaction_id' => $transaction_id, + ) + ); + if ( empty( $existing_order ) ) { + // We don't have this invoice yet. Add it. + $new_order = new MemberOrder(); + $new_order->user_id = $subscription->get_user_id(); + $new_order->membership_id = $subscription->get_membership_level_id(); + $new_order->timestamp = strtotime( $transaction_date ); + $new_order->status = 'success'; + $new_order->payment_transaction_id = $transaction_id; + $new_order->subscription_transaction_id = $subscription->get_subscription_transaction_id(); + $new_order->payment_type = 'PayPal Express'; + + $new_order->total = $transaction_amount; + $new_order->subtotal = $transaction_amount; + + $new_order->find_billing_address(); + + // Save the order. + $new_order->saveOrder(); + } + + $transaction_loop_index++; } } diff --git a/classes/gateways/class.pmprogateway_stripe.php b/classes/gateways/class.pmprogateway_stripe.php index f18d472c00..6eb3efb81e 100644 --- a/classes/gateways/class.pmprogateway_stripe.php +++ b/classes/gateways/class.pmprogateway_stripe.php @@ -2168,26 +2168,158 @@ public function update_subscription_info( $subscription ) { return $e->getMessage(); } - if ( ! empty( $stripe_subscription ) ) { - $update_array = array( - 'startdate' => date( 'Y-m-d H:i:s', intval( $stripe_subscription->created ) ), - ); - if ( in_array( $stripe_subscription->status, array( 'trialing', 'active' ) ) ) { - // Subscription is active. - $update_array['status'] = 'active'; - $update_array['next_payment_date'] = date( 'Y-m-d H:i:s', intval( $stripe_subscription->current_period_end ) ); - if ( ! empty( $stripe_subscription->items->data[0]->price ) ) { - $stripe_subscription_price = $stripe_subscription->items->data[0]->price; - $update_array['billing_amount'] = $this->convert_unit_amount_to_price( $stripe_subscription_price->unit_amount ); - $update_array['cycle_number'] = $stripe_subscription_price->recurring->interval_count; - $update_array['cycle_period'] = ucfirst( $stripe_subscription_price->recurring->interval ); + // Make sure we have a subscription. + if ( empty( $stripe_subscription ) ) { + // No subscription found. + return __( 'No subscription found.', 'paid-memberships-pro' ); + } + + // Update the subscription. + $update_array = array( + 'startdate' => date( 'Y-m-d H:i:s', intval( $stripe_subscription->created ) ), + ); + if ( in_array( $stripe_subscription->status, array( 'trialing', 'active' ) ) ) { + // Subscription is active. + $update_array['status'] = 'active'; + $update_array['next_payment_date'] = date( 'Y-m-d H:i:s', intval( $stripe_subscription->current_period_end ) ); + if ( ! empty( $stripe_subscription->items->data[0]->price ) ) { + $stripe_subscription_price = $stripe_subscription->items->data[0]->price; + $update_array['billing_amount'] = $this->convert_unit_amount_to_price( $stripe_subscription_price->unit_amount ); + $update_array['cycle_number'] = $stripe_subscription_price->recurring->interval_count; + $update_array['cycle_period'] = ucfirst( $stripe_subscription_price->recurring->interval ); + } + } else { + // Subscription is no longer active. + $update_array['status'] = 'cancelled'; + $update_array['enddate'] = date( 'Y-m-d H:i:s', intval( $stripe_subscription->ended_at ) ); + } + $subscription->set( $update_array ); + + // Also use this opportunity to pull any missing subscription payments from Stripe. + $subscription_invoices_args = array( + 'subscription' => $subscription->get_subscription_transaction_id(), + 'status' => 'paid', + 'limit' => 100, + 'expand' => array( + 'data.default_payment_method', + ) + ); + try { + $stripe_subscription_invoices = Stripe_Invoice::all( $subscription_invoices_args ); + } catch ( \Throwable $e ) { + // Assume no invoices found. + } catch ( \Exception $e ) { + // Assume no invoices found. + } + + if ( ! empty( $stripe_subscription_invoices ) ) { + foreach ( $stripe_subscription_invoices->data as $stripe_invoice ) { + // Ignore free invoices. + if ( empty( $stripe_invoice->total ) || 0 === $stripe_invoice->total ) { + continue; + } + + // Check if we have this invoice already. + $existing_order = MemberOrder::get_order( + array( + 'payment_transaction_id' => $stripe_invoice->id + ) + ); + if ( empty( $existing_order ) ) { + // We don't have this invoice yet. Add it. + $new_order = new MemberOrder(); + $new_order->user_id = $subscription->get_user_id(); + $new_order->membership_id = $subscription->get_membership_level_id(); + $new_order->timestamp = $stripe_invoice->created; + $new_order->status = 'success'; + $new_order->payment_transaction_id = $stripe_invoice->id; + $new_order->subscription_transaction_id = $subscription->get_subscription_transaction_id(); + + // Set the subtotal, tax, and total. + global $pmpro_currency, $pmpro_currencies; + $currency_unit_multiplier = 100; // 100 cents / USD + //account for zero-decimal currencies like the Japanese Yen + if ( is_array( $pmpro_currencies[ $pmpro_currency ] ) && isset( $pmpro_currencies[ $pmpro_currency ]['decimals'] ) && $pmpro_currencies[ $pmpro_currency ]['decimals'] == 0 ) { + $currency_unit_multiplier = 1; + } + if ( isset( $stripe_invoice->amount ) ) { + $new_order->subtotal = $stripe_invoice->amount / $currency_unit_multiplier; + $new_order->tax = 0; + } elseif ( isset( $stripe_invoice->subtotal ) ) { + $new_order->subtotal = ( ! empty( $stripe_invoice->subtotal ) ? $stripe_invoice->subtotal / $currency_unit_multiplier : 0 ); + $new_order->tax = ( ! empty( $stripe_invoice->tax ) ? $stripe_invoice->tax / $currency_unit_multiplier : 0 ); + $new_order->total = ( ! empty( $stripe_invoice->total ) ? $stripe_invoice->total / $currency_unit_multiplier : 0 ); + } + + // Fill the "Payment Type" and credit card fields. + // Find the payment intent. + $payment_intent_args = array( + 'id' => $stripe_invoice->payment_intent, + 'expand' => array( + 'payment_method', + 'latest_charge', + ), + ); + $payment_intent = \Stripe\PaymentIntent::retrieve( $payment_intent_args ); + if ( ! empty( $payment_intent->payment_method ) ) { + $payment_method = $payment_intent->payment_method; + } elseif( ! empty( $payment_intent->latest_charge ) ) { + // If we didn't get a payment method, check the charge. + $payment_method = $payment_intent->latest_charge->payment_method_details; + } + if ( ! empty( $payment_method ) && ! empty( $payment_method->type ) ) { + $new_order->payment_type = 'Stripe - ' . $payment_method->type; + if ( ! empty( $payment_method->card ) ) { + // Paid with a card, let's update order and user meta with the card info. + $new_order->cardtype = $payment_method->card->brand; + $new_order->accountnumber = hideCardNumber( $payment_method->card->last4 ); + $new_order->expirationmonth = $payment_method->card->exp_month; + $new_order->expirationyear = $payment_method->card->exp_year; + $new_order->ExpirationDate = $new_order->expirationmonth . $new_order->expirationyear; + $new_order->ExpirationDate_YdashM = $new_order->expirationyear . "-" . $new_order->expirationmonth; + } else { + $new_order->cardtype = ''; + $new_order->accountnumber = ''; + $new_order->expirationmonth = ''; + $new_order->expirationyear = ''; + $new_order->ExpirationDate = ''; + $new_order->ExpirationDate_YdashM = ''; + } + } else { + // Some defaults. + $new_order->payment_type = 'Stripe'; + $new_order->cardtype = ''; + $new_order->accountnumber = ''; + $new_order->expirationmonth = ''; + $new_order->expirationyear = ''; + $new_order->ExpirationDate = ''; + $new_order->ExpirationDate_YdashM = ''; + } + + // Update billing information. + $new_order->billing = new stdClass(); + if ( ! empty( $payment_method ) && ! empty( $payment_method->billing_details ) && ! empty( $payment_method->billing_details->address ) && ! empty( $payment_method->billing_details->address->line1 ) ) { + $new_order->billing->name = empty( $payment_method->billing_details->name ) ? '' : $payment_method->billing_details->name; + $new_order->billing->street = empty( $payment_method->billing_details->address->line1 ) ? '' : $payment_method->billing_details->address->line1; + $new_order->billing->city = empty( $payment_method->billing_details->address->city ) ? '' : $payment_method->billing_details->address->city; + $new_order->billing->state = empty( $payment_method->billing_details->address->state ) ? '' : $payment_method->billing_details->address->state; + $new_order->billing->zip = empty( $payment_method->billing_details->address->postal_code ) ? '' : $payment_method->billing_details->address->postal_code; + $new_order->billing->country = empty( $payment_method->billing_details->address->country ) ? '' : $payment_method->billing_details->address->country; + $new_order->billing->phone = empty( $payment_method->billing_details->phone ) ? '' : $payment_method->billing_details->phone; + } else { + $new_order->billing->name = empty( $stripe_invoice->customer_name ) ? '' : $stripe_invoice->customer_name; + $new_order->billing->street = empty( $stripe_invoice->customer_address->line1 ) ? '' : $stripe_invoice->customer_address->line1; + $new_order->billing->city = empty( $stripe_invoice->customer_address->city ) ? '' : $stripe_invoice->customer_address->city; + $new_order->billing->state = empty( $stripe_invoice->customer_address->state ) ? '' : $stripe_invoice->customer_address->state; + $new_order->billing->zip = empty( $stripe_invoice->customer_address->postal_code ) ? '' : $stripe_invoice->customer_address->postal_code; + $new_order->billing->country = empty( $stripe_invoice->customer_address->country ) ? '' : $stripe_invoice->customer_address->country; + $new_order->billing->phone = empty( $stripe_invoice->customer_phone ) ? '' : $stripe_invoice->customer_phone; + } + + // Save the order. + $new_order->saveOrder(); } - } else { - // Subscription is no longer active. - $update_array['status'] = 'cancelled'; - $update_array['enddate'] = date( 'Y-m-d H:i:s', intval( $stripe_subscription->ended_at ) ); } - $subscription->set( $update_array ); } } From 776b1a12a085e3622864cd98f8fc26d32684c6fa Mon Sep 17 00:00:00 2001 From: David Parker Date: Fri, 9 Feb 2024 10:35:37 -0500 Subject: [PATCH 2/2] Skip free transactions --- classes/gateways/class.pmprogateway_paypalexpress.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/classes/gateways/class.pmprogateway_paypalexpress.php b/classes/gateways/class.pmprogateway_paypalexpress.php index db4af1d948..ecaff6dab9 100644 --- a/classes/gateways/class.pmprogateway_paypalexpress.php +++ b/classes/gateways/class.pmprogateway_paypalexpress.php @@ -1000,6 +1000,12 @@ function update_subscription_info( $subscription ) { $transaction_date = $response[ "L_TIMESTAMP{$transaction_loop_index}" ]; $transaction_amount = $response[ "L_AMT{$transaction_loop_index}" ]; + // If the payment is free, skip it. + if ( 0 === (float) $transaction_amount ) { + $transaction_loop_index++; + continue; + } + // Check if we already have this transaction. $existing_order = MemberOrder::get_order( array(