Version 4.7.23

This commit is contained in:
Neo committed 2025-12-19 16:21:27 -08:00
1 parent 7124a02564
commit 25ddeb65d6
1791 files changed
+76990 -44452

No files matched your search

@@ -122,6 +122,7 @@ class _BillingAgreement extends \IPS\nexus\Customer\BillingAgreement
/* PayPal Subscriptions */
if ( isset( $data['plan_id'] ) )
{
$url = '';
$transactions = $this->method->api( "billing/subscriptions/{$this->gw_id}/transactions?start_time=" . \IPS\DateTime::create()->sub( new \DateInterval( 'P31D' ) )->rfc3339() . '&end_time=' . \IPS\DateTime::create()->rfc3339(), NULL, 'get' );
foreach ( array_reverse( $transactions['transactions'] ?? [] ) as $t )
{
@@ -129,7 +130,8 @@ class _BillingAgreement extends \IPS\nexus\Customer\BillingAgreement
{
try
{
\IPS\Db::i()->select( 't_id', 'nexus_transactions', array( 't_method=? AND t_gw_id=?', $this->method->id, $t['id'] ) )->first();
/* @note SELECT_FROM_WRITE_SERVER transaction race condition */
\IPS\Db::i()->select( 't_id', 'nexus_transactions', [ 't_gw_id=? AND t_method=?', $t['id'], $this->method->id ], flags: \IPS\Db::SELECT_FROM_WRITE_SERVER )->first();
}
catch ( \UnderflowException $e )
{
@@ -157,7 +159,8 @@ class _BillingAgreement extends \IPS\nexus\Customer\BillingAgreement
{
try
{
\IPS\Db::i()->select( 't_id', 'nexus_transactions', array( 't_method=? AND t_gw_id=?', $this->method->id, $t['transaction_id'] ) )->first();
/* @note SELECT_FROM_WRITE_SERVER transaction race condition */
\IPS\Db::i()->select( 't_id', 'nexus_transactions', [ 't_gw_id=? AND t_method=?', $t['transaction_id'], $this->method->id ], flags: \IPS\Db::SELECT_FROM_WRITE_SERVER )->first();
}
catch ( \UnderflowException $e )
{
@@ -94,7 +94,7 @@ class _Exception extends \DomainException
break;
default:
if( isset( $details ) AND $details[0]['issue'] === 'INSTRUMENT_DECLINED' )
if( isset( $details[0] ) AND $details[0]['issue'] === 'INSTRUMENT_DECLINED' )
{
$message = \IPS\Member::loggedIn()->language()->get( 'payment_refused' );
}
@@ -623,6 +623,19 @@ class _PayPal extends \IPS\nexus\Gateway
'return_url' => \IPS\Settings::i()->base_url . 'applications/nexus/interface/gateways/paypal.php?subscription=1&nexusTransactionId=' . $transaction->id,
)
) );
/* PayPal sometimes does not include the subscription ID in the webhook. This causes a race condition
and we can't properly link the transaction. So let's create the Billing Agreement record now. */
$billingAgreement = new \IPS\nexus\Gateway\PayPal\BillingAgreement;
$billingAgreement->gw_id = $subscription['id'];
$billingAgreement->method = $transaction->method;
$billingAgreement->member = $transaction->member;
$billingAgreement->started = \IPS\DateTime::create();
$billingAgreement->next_cycle = \IPS\DateTime::create()->add( $term->interval );
$billingAgreement->save();
$transaction->billing_agreement = $billingAgreement;
$transaction->save();
/* Redirect */
foreach ( $subscription['links'] as $link )
@@ -12,6 +12,9 @@
namespace IPS\nexus\Gateway;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\nexus\Transaction;
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
@@ -41,6 +44,20 @@ class _Stripe extends \IPS\nexus\Gateway
public function checkValidity( \IPS\nexus\Money $amount, \IPS\GeoLocation $billingAddress = NULL, \IPS\nexus\Customer $customer = NULL, $recurrings = array() )
{
$settings = json_decode( $this->settings, TRUE );
/* If we have nothing to pay now, but we have something to pay later,
then check against the renewal amount */
if( $amount->amount->isZero() and \count( $recurrings ) )
{
foreach( $recurrings as $format => $terms )
{
if( $terms['term'] instanceof \IPS\nexus\Purchase\RenewalTerm and $terms['term']->cost->amount->isGreaterThanZero() )
{
$amount = $terms['term']->cost;
break;
}
}
}
/* Stripe has a minimum transaction fee. This is based on the businesses currency, but as we don't know what the transaction rate is
we'll do this check only in the transactions we know - anything else will be rejected when the user tries to pay */
@@ -301,7 +318,7 @@ class _Stripe extends \IPS\nexus\Gateway
'data-zip' => $invoice->billaddress ? $invoice->billaddress->postalCode : NULL,
'data-country' => $invoice->billaddress ? $invoice->billaddress->country : NULL,
'data-email' => $member ? $member->email : $invoice->member->email,
'data-phone' => isset( $member->cm_phone ) ? $member->cm_phone : NULL,
'data-phone' => isset( $member->cm_phone ) ? $member->cm_phone : ( $invoice->member->cm_phone ?? null ),
'data-amount' => static::_amountAsCents( $amount ),
'data-currency' => $amount->currency,
'data-setupIntent' => $setupIntent ? $setupIntent['id'] : NULL,
@@ -314,16 +331,6 @@ class _Stripe extends \IPS\nexus\Gateway
'member' => $member,
) ) );
}
elseif ( $settings['type'] === 'sofort' and \IPS\NEXUS_TEST_GATEWAYS )
{
return array(
new \IPS\Helpers\Form\Radio( 'stripe_debug_action', 'succeeding_charge', FALSE, array( 'options' => array(
'succeeding_charge' => 'stripe_debug_succeeding_charge',
'pending_charge' => 'stripe_debug_pending_charge',
'failing_charge' => 'stripe_debug_failing_charge',
) ) )
);
}
elseif ( \in_array( $settings['type'], array( 'alipay', 'bancontact', 'giropay', 'ideal', 'sofort' ) ) )
{
return array();
@@ -351,7 +358,7 @@ class _Stripe extends \IPS\nexus\Gateway
* @throws
*/
protected function _createPaymentIntent( \IPS\nexus\Invoice $invoice, \IPS\nexus\Money $amount, \IPS\nexus\Customer $member, $paymentMethod, $savePaymentMethod = FALSE, $type = 'checkout' )
{
{
$data = array(
'amount' => static::_amountAsCents( $amount ),
'currency' => $amount->currency,
@@ -362,12 +369,13 @@ class _Stripe extends \IPS\nexus\Gateway
'metadata' => array(
"Invoice ID" => $invoice->id,
"Customer ID" => $member->member_id,
"Customer Email" => $member->email,
"Customer Email" => $invoice->member->email,
"IP Address" => \IPS\Request::i()->ipAddress(),
"Payment Method ID" => $this->id
),
'payment_method' => $paymentMethod,
'payment_method' => $paymentMethod
);
if ( $type === 'admin' )
{
$data['metadata']["Admin"] = \IPS\Member::loggedIn()->member_id;
@@ -425,47 +433,47 @@ class _Stripe extends \IPS\nexus\Gateway
}
}
if ( !isset( $data['customer'] ) )
{
if ( $member->member_id )
{
$response = $this->api( 'customers', array(
'name' => $member->cm_name,
'email' => $member->email,
'metadata' => array(
"Customer ID" => $member->member_id
)
) );
$profiles[ $this->id ] = $response['id'];
$member->cm_profiles = $profiles;
$member->save();
}
else
{
$response = $this->api( 'customers', array(
'name' => $invoice->member->cm_name,
'email' => $invoice->member->email,
) );
$profiles[ $this->id ] = $response['id'];
$guestData = $invoice->guest_data;
$guestData['member']['cm_profiles'] = $profiles;
$guestData['cards'][] = array(
'card_method' => $this->id,
'card_data' => $paymentMethod
);
$invoice->guest_data = $guestData;
$invoice->save();
}
$data['customer'] = $profiles[ $this->id ];
}
if ( $savePaymentMethod )
{
$data['save_payment_method'] = 'true';
$data['setup_future_usage'] = 'off_session';
if ( !isset( $data['customer'] ) )
{
if ( $member->member_id )
{
$response = $this->api( 'customers', array(
'name' => $member->cm_name,
'email' => $member->email,
'metadata' => array(
"Customer ID" => $member->member_id
)
) );
$profiles[ $this->id ] = $response['id'];
$member->cm_profiles = $profiles;
$member->save();
}
else
{
$response = $this->api( 'customers', array(
'name' => $invoice->member->cm_name,
'email' => $invoice->member->email,
) );
$profiles[ $this->id ] = $response['id'];
$guestData = $invoice->guest_data;
$guestData['member']['cm_profiles'] = $profiles;
$guestData['cards'][] = array(
'card_method' => $this->id,
'card_data' => $paymentMethod
);
$invoice->guest_data = $guestData;
$invoice->save();
}
$data['customer'] = $profiles[ $this->id ];
}
}
$response = $this->api( 'payment_intents', $data );
@@ -481,6 +489,91 @@ class _Stripe extends \IPS\nexus\Gateway
return $response;
}
/**
* Create a payment intent for non-card payments
*
* @param Transaction $transaction
* @param string|null $paymentMethod Stripe payment method ID ("pm_....") - can be for a new card or a card already on file
* @param string $type 'checkout' means the cusotmer is doing this on the normal checkout screen, 'admin' means the admin is doing this in the ACP, 'card' means the user is just adding a card
* @return array
*/
protected function _createNonCardPaymentIntent( \IPS\nexus\Transaction $transaction, ?string $paymentMethod, string $type = 'checkout' ) : array
{
$invoice = $transaction->invoice;
$data = array(
'amount' => static::_amountAsCents( $transaction->amount ),
'currency' => $transaction->amount->currency,
'capture_method' => 'automatic',
'confirm' => 'true',
'confirmation_method' => 'automatic',
'description' => $invoice->title,
'metadata' => array(
"Invoice ID" => $invoice->id,
"Customer ID" => $transaction->member->member_id,
"Customer Email" => $transaction->invoice->member->email,
"IP Address" => \IPS\Request::i()->ipAddress(),
"Payment Method ID" => $this->id,
"Transaction ID" => $transaction->id
),
'payment_method' => $paymentMethod,
'payment_method_types' => [ $paymentMethod ],
'return_url' => \IPS\Settings::i()->base_url . 'applications/nexus/interface/gateways/stripe-redirector.php?nexusTransactionId=' . $transaction->id
);
if ( $type === 'admin' )
{
$data['metadata']["Admin"] = \IPS\Member::loggedIn()->member_id;
}
if ( $invoice->shipaddress )
{
$data['shipping'] = array(
'address' => array(
'city' => $invoice->shipaddress->city,
'country' => $invoice->shipaddress->country,
'line1' => $invoice->shipaddress->addressLines[0] ?? NULL,
'line2' => $invoice->shipaddress->addressLines[1] ?? NULL,
'postal_code' => $invoice->shipaddress->postalCode,
'state' => $invoice->shipaddress->region,
),
'name' => $invoice->member->cm_name,
'phone' => $invoice->member->cm_phone ?? NULL
);
}
/* Create a payment method */
$paymentMethodData = [
'type' => $paymentMethod,
'billing_details' => [
'address' => [
'city' => $invoice->billaddress->city,
'country' => $invoice->billaddress->country,
'line1' => $invoice->billaddress->addressLines[0] ?? NULL,
'line2' => $invoice->billaddress->addressLines[1] ?? NULL,
'postal_code' => $invoice->billaddress->postalCode,
'state' => $invoice->billaddress->region
],
'email' => ( $transaction->member->member_id ? $transaction->member->email : $invoice->guest_data['member']['email'] ),
'name' => ( $transaction->member->member_id ? $transaction->member->cm_name : $invoice->guest_data['member']['cm_first_name'] . ' '. $invoice->guest_data['member']['cm_last_name'] ),
'phone' => ( $transaction->member->member_id ? $transaction->member->cm_phone : $invoice->guest_data['member']['cm_phone'] )
]
];
if( $paymentMethod == 'sofort' )
{
$paymentMethodData['sofort'] = array( 'country' => $transaction->invoice->billaddress->country );
}
$response = $this->api( 'payment_methods', $paymentMethodData );
if( isset( $response['id'] ) )
{
$data['payment_method'] = $response['id'];
$data['customer'] = $response['customer'];
}
return $this->api( 'payment_intents', $data );
}
/**
* Authorize
@@ -498,50 +591,27 @@ class _Stripe extends \IPS\nexus\Gateway
$settings = json_decode( $this->settings, TRUE );
/* Do we need to redirect? */
if ( \in_array( $settings['type'], array( 'alipay', 'bancontact', 'giropay', 'ideal', 'sofort' ) ) and !isset( $values[ $this->id . '_card' ] ) )
{
if( \in_array( $settings['type'], [ 'alipay', 'bancontact', 'giropay', 'ideal', 'sofort' ] ) and !isset( $values[ $this->id . '_card' ] ) )
{
/* We need a transaction ID */
$transaction->save();
/* Create the source */
$data = array(
'type' => $settings['type'],
'amount' => static::_amountAsCents( $transaction->amount ),
'currency' => $transaction->amount->currency,
'redirect' => array(
'return_url' => \IPS\Settings::i()->base_url . 'applications/nexus/interface/gateways/stripe-redirector.php?nexusTransactionId=' . $transaction->id
),
'owner' => array(
'email' => $transaction->member->email,
'name' => ( \IPS\NEXUS_TEST_GATEWAYS and isset( $values['stripe_debug_action'] ) ) ? $values['stripe_debug_action'] : $transaction->member->cm_name,
)
);
if ( isset( $transaction->member->cm_phone ) and $transaction->member->cm_phone )
/* Create a payment intent */
$response = $this->_createNonCardPaymentIntent( $transaction, $settings['type'], 'checkout' );
if( $response['status'] == 'requires_action' )
{
$data['owner']['phone'] = $transaction->member->cm_phone;
}
if ( $settings['type'] == 'sofort' )
{
$data['sofort'] = array( 'country' => $transaction->invoice->billaddress->country );
}
if ( $transaction->invoice->billaddress )
{
$data['owner']['address'] = array(
'city' => $transaction->invoice->billaddress->city,
'country' => $transaction->invoice->billaddress->country,
'line1' => isset( $transaction->invoice->billaddress->addressLines[0] ) ? $transaction->invoice->billaddress->addressLines[0] : NULL,
'line2' => isset( $transaction->invoice->billaddress->addressLines[1] ) ? $transaction->invoice->billaddress->addressLines[1] : NULL,
'postal_code' => $transaction->invoice->billaddress->postalCode,
'state' => $transaction->invoice->billaddress->region,
);
}
$response = $this->api( 'sources', $data );
/* Redirect the user */
if ( isset( $response['redirect']['url'] ) )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::external( $response['redirect']['url'] ) );
$transaction->gw_id = $response['id'];
$transaction->status = \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING;
$transaction->save();
$actionType = $response['next_action']['type'];
if ( isset( $response['next_action'][$actionType]['url'] ) )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::createFromString( $response['next_action'][$actionType]['url'] ) );
}
}
/* still here? There was an error */
throw new \RuntimeException;
}
@@ -582,7 +652,7 @@ class _Stripe extends \IPS\nexus\Gateway
"Transaction ID" => $transaction->id,
"Invoice ID" => $transaction->invoice->id,
"Customer ID" => $transaction->member->member_id,
"Customer Email" => $transaction->member->email,
"Customer Email" => $transaction->invoice->member->email,
),
);
if ( $transaction->invoice->shipaddress )
@@ -640,6 +710,7 @@ class _Stripe extends \IPS\nexus\Gateway
}
elseif ( mb_substr( $values[ $this->id . '_card' ]->data, 0, 4 ) === 'src_' )
{
$data['capture'] = 'false'; // Don't auto-capture Apple/Google Pay.
$data['source'] = $values[ $this->id . '_card' ]->data;
}
else
@@ -838,9 +909,12 @@ class _Stripe extends \IPS\nexus\Gateway
{
$response = $charge;
$response['source'] = $response['payment_method_details'];
foreach ( array( 'cvc_check', 'address_line1_check', 'address_postal_code_check' ) as $k )
if( isset( $response['payment_method_details']['card'] ) )
{
$response['source']['card'][ $k ] = isset( $response['payment_method_details']['card']['checks'][ $k ] ) ? $response['payment_method_details']['card']['checks'][ $k ] : 'unavailable';
foreach ( array( 'cvc_check', 'address_line1_check', 'address_postal_code_check' ) as $k )
{
$response['source']['card'][ $k ] = isset( $response['payment_method_details']['card']['checks'][ $k ] ) ? $response['payment_method_details']['card']['checks'][ $k ] : 'unavailable';
}
}
break;
}
@@ -1010,14 +1084,16 @@ class _Stripe extends \IPS\nexus\Gateway
$form->addHeader('stripe_webhook');
$form->addMessage('stripe_webhook_blurb');
\IPS\Member::loggedIn()->language()->words["stripe_webhook_blurb"] = sprintf( \IPS\Member::loggedIn()->language()->get('stripe_webhook_blurb'), (string) \IPS\Http\Url::internal( 'applications/nexus/interface/gateways/stripe.php', 'interface' ) );
\IPS\Member::loggedIn()->language()->words["stripe_webhook_blurb"] = sprintf( \IPS\Member::loggedIn()->language()->get('stripe_webhook_blurb'),
(string) \IPS\Http\Url::internal( 'applications/nexus/interface/gateways/stripe.php', 'interface' ),
\IPS\Member::loggedIn()->language()->formatList( $this->webhookEvents ) );
$form->add( new \IPS\Helpers\Form\Text( 'stripe_webhook_secret', isset( $settings['webhook_secret'] ) ? $settings['webhook_secret'] : NULL, TRUE ) );
}
/**
* @brief Webhook events we need
*/
protected $webhookEvents = array( 'source.chargeable', 'charge.succeeded', 'charge.failed', 'charge.dispute.created', 'charge.dispute.closed' );
protected $webhookEvents = array( 'source.chargeable', 'charge.succeeded', 'charge.failed', 'charge.dispute.created', 'charge.dispute.closed'/*, 'payment_intent.payment_failed'*/ );
/**
* Test Settings