Version 4.5.0
This commit is contained in:
1 parent
1a0c7fd3c2
commit
f79dcf067a
3791 files changed
+248032
-76372
No files matched your search
@@ -9,360 +9,611 @@
|
||||
* @since 20 Jul 2017
|
||||
*/
|
||||
|
||||
define('REPORT_EXCEPTIONS', TRUE);
|
||||
$body = trim( @file_get_contents('php://input') );
|
||||
$data = json_decode( $body, TRUE );
|
||||
\define('REPORT_EXCEPTIONS', TRUE);
|
||||
require_once '../../../../init.php';
|
||||
\IPS\Session\Front::i();
|
||||
|
||||
function loadTransaction( $returnUrl )
|
||||
/**
|
||||
* Stripe Webhook Handler
|
||||
*/
|
||||
class stripeWebhookHandler
|
||||
{
|
||||
$url = new \IPS\Http\Url( $returnUrl );
|
||||
$transaction = \IPS\nexus\Transaction::load( $url->queryString['nexusTransactionId'] );
|
||||
|
||||
$settings = json_decode( $transaction->method->settings, TRUE );
|
||||
if ( !validate( $settings['webhook_secret'] ) )
|
||||
/**
|
||||
* @brief Raw Webhook Data (as a string)
|
||||
*/
|
||||
private $body;
|
||||
|
||||
/**
|
||||
* @brief Parsed Webhook Data (as an array)
|
||||
*/
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* @brief Transaction
|
||||
*/
|
||||
private $transaction;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $body The raw body posted to this script
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $body )
|
||||
{
|
||||
throw new \Exception('INVALID_SIGNING_SECRET');
|
||||
$this->body = $body;
|
||||
$this->data = json_decode( $body, TRUE );
|
||||
}
|
||||
|
||||
return $transaction;
|
||||
}
|
||||
|
||||
function validate( $correctSigningSecret )
|
||||
{
|
||||
global $body;
|
||||
|
||||
if ( !$correctSigningSecret )
|
||||
/**
|
||||
* A source has become chargeable (for example, 3DSecure was completed) so we're ready to authorize the payment
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function sourceChargeable()
|
||||
{
|
||||
return TRUE; // In case they upgraded and haven't provided one
|
||||
}
|
||||
|
||||
if ( isset( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) )
|
||||
{
|
||||
foreach ( explode( ',', $_SERVER['HTTP_STRIPE_SIGNATURE'] ) as $row )
|
||||
{
|
||||
list( $k, $v ) = explode( '=', trim( $row ) );
|
||||
$sig[ trim( $k ) ][] = trim( $v );
|
||||
}
|
||||
|
||||
$signedPayload = $sig['t'][0] . '.' . $body;
|
||||
$signature = hash_hmac( 'sha256', $signedPayload, $correctSigningSecret );
|
||||
|
||||
return in_array( $signature, $sig['v1'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
/* source.chargeable means we're ready to charge (then we need to wait for charge.succeeded) */
|
||||
if ( isset( $data['type'] ) and $data['type'] === 'source.chargeable' )
|
||||
{
|
||||
/* Is this a validation? */
|
||||
if ( isset( $data['data']['object']['owner']['email'] ) and $data['data']['object']['owner']['email'] === 'webhook-validate@example.com' and isset( $data['data']['object']['metadata']['method'] ) )
|
||||
{
|
||||
$method = \IPS\nexus\Gateway::load( $data['data']['object']['metadata']['method'] );
|
||||
$settings = json_decode( $method->settings, TRUE );
|
||||
if ( !isset( $settings['webhook_verified'] ) )
|
||||
{
|
||||
if ( validate( $settings['webhook_secret'] ) )
|
||||
{
|
||||
$settings['webhook_verified'] = $settings['webhook_secret'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$settings['webhook_verified'] = 'ERR';
|
||||
}
|
||||
$method->settings = json_encode( $settings );
|
||||
$method->save();
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
/* Don't need to do anything for cards, since this is only for asynchronous payments */
|
||||
if ( isset( $data['data']['object']['type'] ) and in_array( $data['data']['object']['type'], array( 'card' ) ) )
|
||||
if ( isset( $this->data['data']['object']['type'] ) and \in_array( $this->data['data']['object']['type'], array( 'card' ) ) )
|
||||
{
|
||||
\IPS\Output::i()->sendOutput( 'NO_PROCESSING_REQUIRED', 200, 'text/plain' );
|
||||
exit;
|
||||
return 'NO_PROCESSING_REQUIRED';
|
||||
}
|
||||
|
||||
/* Load the transaction */
|
||||
if ( isset( $data['data']['object']['metadata']['Transaction ID'] ) )
|
||||
/* Check the status */
|
||||
if ( !\in_array( $this->transaction->status, array( \IPS\nexus\Transaction::STATUS_PENDING, \IPS\nexus\Transaction::STATUS_WAITING ) ) )
|
||||
{
|
||||
$transaction = \IPS\nexus\Transaction::load( $data['data']['object']['metadata']['Transaction ID'] );
|
||||
}
|
||||
elseif ( isset( $data['data']['object']['redirect']['return_url'] ) )
|
||||
{
|
||||
$transaction = loadTransaction( $data['data']['object']['redirect']['return_url'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
if ( \in_array( $this->transaction->status, array( \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING, \IPS\nexus\Transaction::STATUS_PAID ) ) )
|
||||
{
|
||||
$where = array( array( 't_gw_id=?', $data['data']['object']['id'] ) );
|
||||
if ( isset( $data['data']['object']['metadata']['Invoice ID'] ) )
|
||||
{
|
||||
$where[] = array( 't_invoice=?', $data['data']['object']['metadata']['Invoice ID'] );
|
||||
}
|
||||
|
||||
$transaction = \IPS\nexus\Transaction::constructFromData( \IPS\Db::i()->select( '*', 'nexus_transactions', $where )->first() );
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
\IPS\Output::i()->sendOutput( 'COULD_NOT_FIND_TRANSACTION', 403, 'text/plain' );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
if ( $transaction->status !== \IPS\nexus\Transaction::STATUS_PENDING and $transaction->status !== \IPS\nexus\Transaction::STATUS_WAITING )
|
||||
{
|
||||
if ( $transaction->status === \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING )
|
||||
{
|
||||
\IPS\Output::i()->sendOutput( 'ALREADY_PROCESSED', 200, 'text/plain' );
|
||||
return 'ALREADY_PROCESSED';
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Output::i()->sendOutput( 'BAD_STATUS', 403, 'text/plain' );
|
||||
throw new \Exception('BAD_STATUS');
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
/* Load the source */
|
||||
$source = $transaction->method->api( 'sources/' . preg_replace( '/[^A-Z0-9_]/i', '', $data['data']['object']['id'] ) );
|
||||
if ( $source['client_secret'] != $data['data']['object']['client_secret'] )
|
||||
/* Validate the source with Stripe */
|
||||
$source = $this->transaction->method->api( 'sources/' . preg_replace( '/[^A-Z0-9_]/i', '', $this->data['data']['object']['id'] ) );
|
||||
if ( $source['client_secret'] != $this->data['data']['object']['client_secret'] )
|
||||
{
|
||||
\IPS\Output::i()->sendOutput( 'BAD_SECRET', 403, 'text/plain' );
|
||||
exit;
|
||||
throw new \Exception('BAD_SECRET');
|
||||
}
|
||||
|
||||
/* Check we're not just going to refuse this */
|
||||
$maxMind = NULL;
|
||||
if ( \IPS\Settings::i()->maxmind_key and ( !\IPS\Settings::i()->maxmind_gateways or \IPS\Settings::i()->maxmind_gateways == '*' or in_array( $transaction->method->id, explode( ',', \IPS\Settings::i()->maxmind_gateways ) ) ) )
|
||||
if ( \IPS\Settings::i()->maxmind_key and ( !\IPS\Settings::i()->maxmind_gateways or \IPS\Settings::i()->maxmind_gateways == '*' or \in_array( $this->transaction->method->id, explode( ',', \IPS\Settings::i()->maxmind_gateways ) ) ) )
|
||||
{
|
||||
$maxMind = new \IPS\nexus\Fraud\MaxMind\Request( FALSE );
|
||||
$maxMind->setIpAddress( $transaction->ip );
|
||||
$maxMind->setTransaction( $transaction );
|
||||
$maxMind->setIpAddress( $this->transaction->ip );
|
||||
$maxMind->setTransaction( $this->transaction );
|
||||
}
|
||||
$fraudResult = $transaction->runFraudCheck( $maxMind );
|
||||
$fraudResult = $this->transaction->runFraudCheck( $maxMind );
|
||||
if ( $fraudResult === \IPS\nexus\Transaction::STATUS_REFUSED )
|
||||
{
|
||||
$transaction->executeFraudAction( $fraudResult, FALSE );
|
||||
$transaction->sendNotification();
|
||||
$this->transaction->executeFraudAction( $fraudResult, FALSE );
|
||||
$this->transaction->sendNotification();
|
||||
}
|
||||
|
||||
/* Authorize */
|
||||
else
|
||||
{
|
||||
$transaction->auth = $transaction->method->auth( $transaction, array( $transaction->method->id . '_card' => $source['id'] ) );
|
||||
$transaction->status = \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING;
|
||||
$transaction->save();
|
||||
$this->transaction->auth = $this->transaction->method->auth( $this->transaction, array( $this->transaction->method->id . '_card' => $source['id'] ) );
|
||||
$this->transaction->status = \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING;
|
||||
$this->transaction->save();
|
||||
}
|
||||
|
||||
/* OK */
|
||||
\IPS\Output::i()->sendOutput( 'OK', 200, 'text/plain' );
|
||||
/* Return */
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
/* charge.succeeded means we got the payment */
|
||||
elseif ( isset( $data['type'] ) and $data['type'] === 'charge.succeeded' )
|
||||
/**
|
||||
* A charge was successful on a RECURRING payment, so we can approve the transaction
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function chargeSucceeded()
|
||||
{
|
||||
/* Load the transaction */
|
||||
if ( isset( $data['data']['object']['metadata']['Transaction ID'] ) )
|
||||
/* Check the status */
|
||||
if ( $this->transaction->status !== \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING )
|
||||
{
|
||||
$transaction = \IPS\nexus\Transaction::load( $data['data']['object']['metadata']['Transaction ID'] );
|
||||
}
|
||||
elseif ( isset( $data['data']['object']['source']['redirect']['return_url'] ) )
|
||||
{
|
||||
$transaction = loadTransaction( $data['data']['object']['source']['redirect']['return_url'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
if ( $this->transaction->status === \IPS\nexus\Transaction::STATUS_PAID )
|
||||
{
|
||||
$where = array( array( 't_gw_id=?', $data['data']['object']['id'] ) );
|
||||
if ( isset( $data['data']['object']['metadata']['Invoice ID'] ) )
|
||||
{
|
||||
$where[] = array( 't_invoice=?', $data['data']['object']['metadata']['Invoice ID'] );
|
||||
}
|
||||
|
||||
$transaction = \IPS\nexus\Transaction::constructFromData( \IPS\Db::i()->select( '*', 'nexus_transactions', $where )->first() );
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
\IPS\Output::i()->sendOutput( 'COULD_NOT_FIND_TRANSACTION', 403, 'text/plain' );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
if ( $transaction->status !== \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING )
|
||||
{
|
||||
if ( $transaction->status === \IPS\nexus\Transaction::STATUS_PAID )
|
||||
{
|
||||
\IPS\Output::i()->sendOutput( 'ALREADY_PROCESSED', 200, 'text/plain' );
|
||||
return 'ALREADY_PROCESSED';
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Output::i()->sendOutput( 'BAD_STATUS', 403, 'text/plain' );
|
||||
throw new \Exception('BAD_STATUS');
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
/* Validate the charge with Stripe */
|
||||
try
|
||||
{
|
||||
$charge = $this->transaction->method->api( 'charges/' . preg_replace( '/[^A-Z0-9_]/i', '', $this->data['data']['object']['id'] ) );
|
||||
if ( !\in_array( $charge['status'], array( 'succeeded', 'paid' ) ) )
|
||||
{
|
||||
throw new \Exception;
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \Exception('INVALID_CHARGE');
|
||||
}
|
||||
|
||||
/* Create a MaxMind request */
|
||||
$maxMind = NULL;
|
||||
if ( \IPS\Settings::i()->maxmind_key and ( !\IPS\Settings::i()->maxmind_gateways or \IPS\Settings::i()->maxmind_gateways == '*' or in_array( $transaction->method->id, explode( ',', \IPS\Settings::i()->maxmind_gateways ) ) ) )
|
||||
if ( \IPS\Settings::i()->maxmind_key and ( !\IPS\Settings::i()->maxmind_gateways or \IPS\Settings::i()->maxmind_gateways == '*' or \in_array( $this->transaction->method->id, explode( ',', \IPS\Settings::i()->maxmind_gateways ) ) ) )
|
||||
{
|
||||
$maxMind = new \IPS\nexus\Fraud\MaxMind\Request( FALSE );
|
||||
$maxMind->setIpAddress( $transaction->ip );
|
||||
$maxMind->setTransaction( $transaction );
|
||||
$maxMind->setIpAddress( $this->transaction->ip );
|
||||
$maxMind->setTransaction( $this->transaction );
|
||||
}
|
||||
|
||||
/* Check fraud rules */
|
||||
$fraudResult = $transaction->runFraudCheck( $maxMind );
|
||||
$fraudResult = $this->transaction->runFraudCheck( $maxMind );
|
||||
if ( $fraudResult )
|
||||
{
|
||||
$transaction->executeFraudAction( $fraudResult, TRUE );
|
||||
$this->transaction->executeFraudAction( $fraudResult, TRUE );
|
||||
}
|
||||
|
||||
/* If we're not being fraud blocked, we can approve */
|
||||
if ( $fraudResult === \IPS\nexus\Transaction::STATUS_PAID )
|
||||
{
|
||||
$transaction->member->log( 'transaction', array(
|
||||
$this->transaction->member->log( 'transaction', array(
|
||||
'type' => 'paid',
|
||||
'status' => \IPS\nexus\Transaction::STATUS_PAID,
|
||||
'id' => $transaction->id,
|
||||
'invoice_id' => $transaction->invoice->id,
|
||||
'invoice_title' => $transaction->invoice->title,
|
||||
'id' => $this->transaction->id,
|
||||
'invoice_id' => $this->transaction->invoice->id,
|
||||
'invoice_title' => $this->transaction->invoice->title,
|
||||
) );
|
||||
$transaction->approve();
|
||||
$this->transaction->approve();
|
||||
}
|
||||
|
||||
/* Either way, let the user know we got their payment */
|
||||
$transaction->sendNotification();
|
||||
|
||||
/* OK */
|
||||
\IPS\Output::i()->sendOutput( 'OK', 200, 'text/plain' );
|
||||
$this->transaction->sendNotification();
|
||||
|
||||
/* Return */
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
/* charge.failed means it failed */
|
||||
elseif ( isset( $data['type'] ) and $data['type'] === 'charge.failed' )
|
||||
/**
|
||||
* A charge was successful on a NEW payment, so we can create, capture, and approve the transaction
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function chargeSucceededNew()
|
||||
{
|
||||
/* Load the transaction */
|
||||
if ( isset( $data['data']['object']['metadata']['Transaction ID'] ) )
|
||||
/* Check the status */
|
||||
if ( $this->transaction->status !== \IPS\nexus\Transaction::STATUS_PENDING )
|
||||
{
|
||||
$transaction = \IPS\nexus\Transaction::load( $data['data']['object']['metadata']['Transaction ID'] );
|
||||
}
|
||||
elseif ( isset( $data['data']['object']['source']['redirect']['return_url'] ) )
|
||||
{
|
||||
$transaction = loadTransaction( $data['data']['object']['source']['redirect']['return_url'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
if ( $this->transaction->status === \IPS\nexus\Transaction::STATUS_PAID )
|
||||
{
|
||||
$where = array( array( 't_gw_id=?', $data['data']['object']['id'] ) );
|
||||
if ( isset( $data['data']['object']['metadata']['Invoice ID'] ) )
|
||||
{
|
||||
$where[] = array( 't_invoice=?', $data['data']['object']['metadata']['Invoice ID'] );
|
||||
}
|
||||
|
||||
$transaction = \IPS\nexus\Transaction::constructFromData( \IPS\Db::i()->select( '*', 'nexus_transactions', $where )->first() );
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
\IPS\Output::i()->sendOutput( 'COULD_NOT_FIND_TRANSACTION', 403, 'text/plain' );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
if ( $transaction->status !== \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING )
|
||||
{
|
||||
if ( $transaction->status === \IPS\nexus\Transaction::STATUS_REFUSED )
|
||||
{
|
||||
\IPS\Output::i()->sendOutput( 'ALREADY_PROCESSED', 200, 'text/plain' );
|
||||
return 'ALREADY_PROCESSED';
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Output::i()->sendOutput( 'BAD_STATUS', 403, 'text/plain' );
|
||||
throw new \Exception('BAD_STATUS');
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
/* Validate the charge with Stripe */
|
||||
try
|
||||
{
|
||||
$charge = $this->transaction->method->api( 'charges/' . preg_replace( '/[^A-Z0-9_]/i', '', $this->data['data']['object']['id'] ) );
|
||||
if ( !\in_array( $charge['status'], array( 'succeeded', 'paid' ) ) )
|
||||
{
|
||||
throw new \Exception;
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \Exception('INVALID_CHARGE');
|
||||
}
|
||||
|
||||
/* If this was done by an admin, we just capture and approve straight away */
|
||||
if ( isset( $this->data['data']['object']['metadata']['Admin'] ) and $this->data['data']['object']['metadata']['Admin'] )
|
||||
{
|
||||
$this->transaction->auth = NULL;
|
||||
$this->transaction->capture();
|
||||
$this->transaction->member->log( 'transaction', array(
|
||||
'type' => 'paid',
|
||||
'status' => \IPS\nexus\Transaction::STATUS_PAID,
|
||||
'id' => $this->transaction->id,
|
||||
'invoice_id' => $this->transaction->invoice->id,
|
||||
'invoice_title' => $this->transaction->invoice->title,
|
||||
), \IPS\Member::load( $this->data['data']['object']['metadata']['Admin'] ) );
|
||||
$this->transaction->approve();
|
||||
}
|
||||
|
||||
/* Otherwise, check fraud rules and then capture */
|
||||
else
|
||||
{
|
||||
/* Create a MaxMind request */
|
||||
$maxMind = NULL;
|
||||
if ( \IPS\Settings::i()->maxmind_key and ( !\IPS\Settings::i()->maxmind_gateways or \IPS\Settings::i()->maxmind_gateways == '*' or \in_array( $this->transaction->method->id, explode( ',', \IPS\Settings::i()->maxmind_gateways ) ) ) )
|
||||
{
|
||||
$maxMind = new \IPS\nexus\Fraud\MaxMind\Request( FALSE );
|
||||
$maxMind->setIpAddress( $this->transaction->ip );
|
||||
$maxMind->setTransaction( $this->transaction );
|
||||
}
|
||||
|
||||
/* Set the authorize date (it's already actually been authorized) */
|
||||
$this->transaction->auth = \IPS\DateTime::ts( $this->data['data']['object']['created'] )->add( new \DateInterval( 'P7D' ) );
|
||||
|
||||
/* Check Fraud Rules and capture */
|
||||
$this->transaction->checkFraudRulesAndCapture( $maxMind );
|
||||
}
|
||||
|
||||
/* Send email receipt */
|
||||
$this->transaction->sendNotification();
|
||||
|
||||
/* Return */
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
/**
|
||||
* A charge failed so we need to mark the transaction as failed locally
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function chargeFailed()
|
||||
{
|
||||
/* Check the status */
|
||||
if ( !\in_array( $this->transaction->status, [ \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING, IPS\nexus\Transaction::STATUS_PENDING ] ) )
|
||||
{
|
||||
if ( $this->transaction->status === \IPS\nexus\Transaction::STATUS_REFUSED )
|
||||
{
|
||||
return 'ALREADY_PROCESSED';
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \Exception('BAD_STATUS');
|
||||
}
|
||||
}
|
||||
|
||||
/* Validate the charge with Stripe */
|
||||
try
|
||||
{
|
||||
$charge = $this->transaction->method->api( 'charges/' . preg_replace( '/[^A-Z0-9_]/i', '', $this->data['data']['object']['id'] ) );
|
||||
if ( $charge['status'] !== 'failed' )
|
||||
{
|
||||
throw new \Exception;
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \Exception('INVALID_CHARGE');
|
||||
}
|
||||
|
||||
/* Mark it failed */
|
||||
$transaction->status = \IPS\nexus\Transaction::STATUS_REFUSED;
|
||||
$extra = $transaction->extra;
|
||||
$extra['history'][] = array( 's' => \IPS\nexus\Transaction::STATUS_REFUSED, 'noteRaw' => $data['data']['object']['failure_message'] );
|
||||
$transaction->extra = $extra;
|
||||
$transaction->save();
|
||||
$transaction->member->log( 'transaction', array(
|
||||
$this->transaction->status = \IPS\nexus\Transaction::STATUS_REFUSED;
|
||||
$extra = $this->transaction->extra;
|
||||
$extra['history'][] = array( 's' => \IPS\nexus\Transaction::STATUS_REFUSED, 'noteRaw' => $this->data['data']['object']['failure_message'] );
|
||||
$this->transaction->extra = $extra;
|
||||
$this->transaction->save();
|
||||
$this->transaction->member->log( 'transaction', array(
|
||||
'type' => 'paid',
|
||||
'status' => \IPS\nexus\Transaction::STATUS_REFUSED,
|
||||
'id' => $transaction->id,
|
||||
'invoice_id' => $transaction->invoice->id,
|
||||
'invoice_title' => $transaction->title,
|
||||
'id' => $this->transaction->id,
|
||||
'invoice_id' => $this->transaction->invoice->id,
|
||||
'invoice_title' => $this->transaction->title,
|
||||
), FALSE );
|
||||
|
||||
/* Send notification */
|
||||
$transaction->sendNotification();
|
||||
$this->transaction->sendNotification();
|
||||
|
||||
/* Return */
|
||||
\IPS\Output::i()->sendOutput( 'OK', 200, 'text/plain' );
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
/* charge.dispute.created is a chargeback */
|
||||
elseif ( isset( $data['type'] ) and $data['type'] === 'charge.dispute.created' and isset( $data['data']['object']['charge'] ) )
|
||||
/**
|
||||
* A chargeback/dispute has been made against a transaction so we need to mark it as such locally
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function disputeCreated()
|
||||
{
|
||||
$transaction = \IPS\nexus\Transaction::constructFromData( \IPS\Db::i()->select( '*', 'nexus_transactions', array( 't_gw_id=?', $data['data']['object']['charge'] ) )->first() );
|
||||
$settings = json_decode( $transaction->method->settings, TRUE );
|
||||
if ( !validate( $settings['webhook_secret'] ) )
|
||||
/* Validate the dispute with Stripe */
|
||||
try
|
||||
{
|
||||
throw new \Exception('INVALID_SIGNING_SECRET');
|
||||
$dispute = $this->transaction->method->api( 'disputes/' . preg_replace( '/[^A-Z0-9_]/i', '', $this->data['data']['object']['id'] ) );
|
||||
if ( !\in_array( $dispute['status'], array( 'needs_response', 'warning_needs_response' ) ) )
|
||||
{
|
||||
throw new \Exception;
|
||||
}
|
||||
|
||||
if ( \substr( $this->transaction->gw_id, 0, 3 ) === 'pi_' )
|
||||
{
|
||||
$paymentIntent = $this->transaction->method->api( 'payment_intents/' . $this->transaction->gw_id );
|
||||
|
||||
$ok = FALSE;
|
||||
foreach ( $paymentIntent['charges']['data'] as $charge )
|
||||
{
|
||||
if ( $dispute['charge'] === $charge['id'] )
|
||||
{
|
||||
$ok = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( !$ok )
|
||||
{
|
||||
throw new \Exception;
|
||||
}
|
||||
}
|
||||
elseif ( $dispute['charge'] !== $this->transaction->gw_id )
|
||||
{
|
||||
throw new \Exception;
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \Exception('INVALID_DISPUTE');
|
||||
}
|
||||
|
||||
$transaction->status = $transaction::STATUS_DISPUTED;
|
||||
$extra = $transaction->extra;
|
||||
$extra['history'][] = array( 's' => $transaction::STATUS_DISPUTED, 'on' => $data['data']['object']['created'], 'ref' => $data['data']['object']['id'] );
|
||||
$transaction->extra = $extra;
|
||||
$transaction->save();
|
||||
/* Mark the transaction as disputed */
|
||||
$this->transaction->status = \IPS\nexus\Transaction::STATUS_DISPUTED;
|
||||
$extra = $this->transaction->extra;
|
||||
$extra['history'][] = array( 's' => \IPS\nexus\Transaction::STATUS_DISPUTED, 'on' => $this->data['data']['object']['created'], 'ref' => $this->data['data']['object']['id'] );
|
||||
$this->transaction->extra = $extra;
|
||||
$this->transaction->save();
|
||||
|
||||
if ( $transaction->member )
|
||||
/* Log */
|
||||
if ( $this->transaction->member )
|
||||
{
|
||||
$transaction->member->log( 'transaction', array(
|
||||
$this->transaction->member->log( 'transaction', array(
|
||||
'type' => 'status',
|
||||
'status' => $transaction::STATUS_DISPUTED,
|
||||
'id' => $transaction->id
|
||||
'status' => \IPS\nexus\Transaction::STATUS_DISPUTED,
|
||||
'id' => $this->transaction->id
|
||||
) );
|
||||
}
|
||||
|
||||
$transaction->invoice->markUnpaid( \IPS\nexus\Invoice::STATUS_CANCELED );
|
||||
/* Mark the invoice as not paid (revoking benefits) */
|
||||
$this->transaction->invoice->markUnpaid( \IPS\nexus\Invoice::STATUS_CANCELED );
|
||||
|
||||
/* Send admin notification */
|
||||
\IPS\core\AdminNotification::send( 'nexus', 'Transaction', \IPS\nexus\Transaction::STATUS_DISPUTED, TRUE, $this->transaction );
|
||||
|
||||
/* Return */
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
/* charge.dispute.closed is a chargeback being resolved */
|
||||
elseif ( isset( $data['type'] ) and $data['type'] === 'charge.dispute.closed' and isset( $data['data']['object']['charge'] ) and isset( $data['data']['object']['status'] ) )
|
||||
/**
|
||||
* A chargeback/dispute has been resolved (which may be won or lost)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function disputeClosed()
|
||||
{
|
||||
$transaction = \IPS\nexus\Transaction::constructFromData( \IPS\Db::i()->select( '*', 'nexus_transactions', array( 't_gw_id=?', $data['data']['object']['charge'] ) )->first() );
|
||||
$settings = json_decode( $transaction->method->settings, TRUE );
|
||||
if ( !validate( $settings['webhook_secret'] ) )
|
||||
/* Validate the dispute with Stripe */
|
||||
try
|
||||
{
|
||||
throw new \Exception('INVALID_SIGNING_SECRET');
|
||||
}
|
||||
|
||||
if ( $data['data']['object']['status'] === 'won' )
|
||||
{
|
||||
$transaction->status = $transaction::STATUS_PAID;
|
||||
$transaction->save();
|
||||
if ( !$transaction->invoice->amountToPay()->amount->isGreaterThanZero() )
|
||||
{
|
||||
$transaction->invoice->markPaid();
|
||||
$dispute = $this->transaction->method->api( 'disputes/' . preg_replace( '/[^A-Z0-9_]/i', '', $this->data['data']['object']['id'] ) );
|
||||
if ( \substr( $this->transaction->gw_id, 0, 3 ) === 'pi_' )
|
||||
{
|
||||
$paymentIntent = $this->transaction->method->api( 'payment_intents/' . $this->transaction->gw_id );
|
||||
|
||||
$ok = FALSE;
|
||||
foreach ( $paymentIntent['charges']['data'] as $charge )
|
||||
{
|
||||
if ( $dispute['charge'] === $charge['id'] )
|
||||
{
|
||||
$ok = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( !$ok )
|
||||
{
|
||||
throw new \Exception;
|
||||
}
|
||||
}
|
||||
elseif ( $dispute['charge'] !== $this->transaction->gw_id )
|
||||
{
|
||||
throw new \Exception;
|
||||
}
|
||||
}
|
||||
else
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
$transaction->status = $transaction::STATUS_REFUNDED;
|
||||
$transaction->save();
|
||||
throw new \Exception('INVALID_DISPUTE');
|
||||
}
|
||||
|
||||
/* Did we win or lose? */
|
||||
if ( \in_array( $dispute['status'], array( 'won', 'warning_closed' ) ) )
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
elseif ( \in_array( $dispute['status'], array( 'lost' ) ) )
|
||||
{
|
||||
$this->transaction->status = \IPS\nexus\Transaction::STATUS_REFUNDED;
|
||||
$this->transaction->save();
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
/**
|
||||
* Run
|
||||
*
|
||||
* @param string $signature The signature provided by this request
|
||||
* @return void
|
||||
*/
|
||||
public function run( $signature )
|
||||
{
|
||||
/* Ignore anything we don't care about */
|
||||
if ( !isset( $this->data['type'] ) or !\in_array( $this->data['type'], array( 'source.chargeable', 'charge.succeeded', 'charge.failed', 'charge.dispute.created', 'charge.dispute.closed' ) ) )
|
||||
{
|
||||
return 'UNNEEDED_TYPE';
|
||||
}
|
||||
|
||||
/* Try to find the transaction this is about */
|
||||
$new = FALSE;
|
||||
try
|
||||
{
|
||||
if ( isset( $this->data['data']['object']['metadata']['Transaction ID'] ) )
|
||||
{
|
||||
$this->transaction = \IPS\nexus\Transaction::load( $this->data['data']['object']['metadata']['Transaction ID'] );
|
||||
}
|
||||
elseif ( isset( $this->data['data']['object']['redirect']['return_url'] ) )
|
||||
{
|
||||
$url = new \IPS\Http\Url( $this->data['data']['object']['redirect']['return_url'] );
|
||||
$this->transaction = \IPS\nexus\Transaction::load( $url->queryString['nexusTransactionId'] );
|
||||
}
|
||||
elseif ( isset( $this->data['data']['object']['charge'] ) )
|
||||
{
|
||||
$paymentIntentId = NULL;
|
||||
foreach ( \IPS\nexus\Gateway::roots() as $method )
|
||||
{
|
||||
if ( $method instanceof \IPS\nexus\Gateway\Stripe )
|
||||
{
|
||||
try
|
||||
{
|
||||
$charge = $method->api( 'charges/' . preg_replace( '/[^A-Z0-9_]/i', '', $this->data['data']['object']['charge'] ) );
|
||||
if ( isset( $charge['payment_intent'] ) )
|
||||
{
|
||||
$paymentIntentId = $charge['payment_intent'];
|
||||
}
|
||||
break;
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
// Do nothing - try the next one
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $paymentIntentId )
|
||||
{
|
||||
$this->transaction = \IPS\nexus\Transaction::constructFromData( \IPS\Db::i()->select( '*', 'nexus_transactions', array( '( t_gw_id=? OR t_gw_id=? )', $paymentIntentId, $this->data['data']['object']['charge'] ) )->first() );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->transaction = \IPS\nexus\Transaction::constructFromData( \IPS\Db::i()->select( '*', 'nexus_transactions', array( 't_gw_id=?', $this->data['data']['object']['charge'] ) )->first() );
|
||||
}
|
||||
}
|
||||
elseif ( isset( $this->data['data']['object']['id'] ) and mb_substr( $this->data['data']['object']['id'], 0, 4 ) !== 'src_' )
|
||||
{
|
||||
if ( isset( $this->data['data']['object']['payment_intent'] ) )
|
||||
{
|
||||
$where = array( array( '( t_gw_id=? OR t_gw_id=? )', $this->data['data']['object']['id'], $this->data['data']['object']['payment_intent'] ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$where = array( array( 't_gw_id=?', $this->data['data']['object']['id'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $this->data['data']['object']['metadata']['Invoice ID'] ) )
|
||||
{
|
||||
$where[] = array( 't_invoice=?', $this->data['data']['object']['metadata']['Invoice ID'] );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->transaction = \IPS\nexus\Transaction::constructFromData( \IPS\Db::i()->select( '*', 'nexus_transactions', $where )->first() );
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
/* If we can't find a transaction, but there is an "IP Address" in the metadata, this is from the actual checkout process: create a transaction */
|
||||
if ( isset( $this->data['data']['object']['metadata']['Invoice ID'] ) and isset( $this->data['data']['object']['metadata']['IP Address'] ) and isset( $this->data['data']['object']['metadata']['Payment Method ID'] ) )
|
||||
{
|
||||
$invoice = \IPS\nexus\Invoice::load( \intval( $this->data['data']['object']['metadata']['Invoice ID'] ) );
|
||||
|
||||
$this->transaction = new \IPS\nexus\Transaction;
|
||||
$this->transaction->member = isset( $this->data['data']['object']['metadata']['Customer ID'] ) ? \IPS\nexus\Customer::load( \intval( $this->data['data']['object']['metadata']['Customer ID'] ) ) : $invoice->member;
|
||||
$this->transaction->invoice = $invoice;
|
||||
$this->transaction->amount = new \IPS\nexus\Money( new \IPS\Math\Number( mb_substr( $this->data['data']['object']['amount'], 0, -2 ) . '.' . mb_substr( $this->data['data']['object']['amount'], -2 ) ), mb_strtoupper( $this->data['data']['object']['currency'] ) );
|
||||
$this->transaction->ip = $this->data['data']['object']['metadata']['IP Address'];
|
||||
$this->transaction->gw_id = isset( $this->data['data']['object']['payment_intent'] ) ? $this->data['data']['object']['payment_intent'] : $this->data['data']['object']['id'];
|
||||
$this->transaction->status = \IPS\nexus\Transaction::STATUS_PENDING;
|
||||
$this->transaction->method = \IPS\nexus\Gateway::load( \intval( $this->data['data']['object']['metadata']['Payment Method ID'] ) );
|
||||
$this->transaction->save();
|
||||
|
||||
$new = TRUE;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return 'NO_TRANSACTION_INFORMATION';
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
if ( $this->data['type'] === 'charge.failed' )
|
||||
{
|
||||
return 'COULD_NOT_FIND_TRANSACTION'; // Sometimes if a transaction fails we don't create a transaction record
|
||||
}
|
||||
|
||||
throw new \Exception('COULD_NOT_FIND_TRANSACTION');
|
||||
}
|
||||
|
||||
/* Validate the signature */
|
||||
if ( !( $this->transaction->method instanceof \IPS\nexus\Gateway\Stripe ) )
|
||||
{
|
||||
throw new \Exception('INVALID_GATEWAY');
|
||||
}
|
||||
$settings = json_decode( $this->transaction->method->settings, TRUE );
|
||||
if ( isset( $settings['webhook_secret'] ) and $settings['webhook_secret'] ) // In case they upgraded and haven't provided one
|
||||
{
|
||||
$sig = array();
|
||||
foreach ( explode( ',', $signature ) as $row )
|
||||
{
|
||||
if ( \strpos( $row, '=' ) !== FALSE )
|
||||
{
|
||||
list( $k, $v ) = explode( '=', trim( $row ) );
|
||||
$sig[ trim( $k ) ][] = trim( $v );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $sig['t'] ) and isset( $sig['t'][0] ) )
|
||||
{
|
||||
$signedPayload = $sig['t'][0] . '.' . $this->body;
|
||||
$signature = hash_hmac( 'sha256', $signedPayload, $settings['webhook_secret'] );
|
||||
|
||||
if ( !\in_array( $signature, $sig['v1'] ) )
|
||||
{
|
||||
throw new \Exception('INVALID_SIGNING_SECRET');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \Exception('INVALID_SIGNING_SECRET');
|
||||
}
|
||||
}
|
||||
|
||||
/* Do it */
|
||||
if ( isset( $this->data['type'] ) )
|
||||
{
|
||||
switch ( $this->data['type'] )
|
||||
{
|
||||
case 'source.chargeable':
|
||||
return $this->sourceChargeable();
|
||||
case 'charge.succeeded':
|
||||
return $new ? $this->chargeSucceededNew() : $this->chargeSucceeded();
|
||||
case 'charge.failed':
|
||||
return $this->chargeFailed();
|
||||
case 'charge.dispute.created':
|
||||
return $this->disputeCreated();
|
||||
case 'charge.dispute.closed':
|
||||
return $this->disputeClosed();
|
||||
default:
|
||||
return 'UNNEEDED_TYPE';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$class = new stripeWebhookHandler( trim( @file_get_contents('php://input') ) );
|
||||
try
|
||||
{
|
||||
$response = $class->run( isset( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) ? $_SERVER['HTTP_STRIPE_SIGNATURE'] : '' );
|
||||
\IPS\Output::i()->sendOutput( $response, 200, 'text/plain' );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Output::i()->sendOutput( $e->getMessage(), 500, 'text/plain' );
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user