Commit 155328f1 authored by mattwire's avatar mattwire
Browse files

Track paymentIntents and cancel uncaptured ones after 24 hours

parent 72c1c91e
......@@ -560,6 +560,21 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
$this->handleError($e->getCode(), $e->getMessage(), $params['stripe_error_url']);
}
// Update the paymentIntent in the CiviCRM database for later tracking
$intentParams = [
'paymentintent_id' => $intent->id,
'payment_processor_id' => $this->_paymentProcessor['id'],
'status' => $intent->status,
'contribution_id' => $this->getContributionId($params),
'description' => $intentParams['description'],
'identifier' => $params['qfKey'],
'contact_id' => $this->getContactId($params),
];
if (empty($intentParams['contribution_id'])) {
$intentParams['flags'][] = 'NC';
}
CRM_Stripe_BAO_StripePaymentintent::create($intentParams);
// For contribution workflow we have a contributionId so we can set parameters directly.
// For events/membership workflow we have to return the parameters and they might get set...
// For a single charge there is no stripe invoice.
......
......@@ -43,6 +43,7 @@ class CRM_Stripe_AJAX {
$paymentIntentID = CRM_Utils_Request::retrieveValue('payment_intent_id', 'String');
$amount = CRM_Utils_Request::retrieveValue('amount', 'Money');
$capture = CRM_Utils_Request::retrieveValue('capture', 'Boolean', FALSE);
$title = CRM_Utils_Request::retrieveValue('description', 'String');
$confirm = TRUE;
if (empty($amount)) {
$amount = 1;
......@@ -59,10 +60,7 @@ class CRM_Stripe_AJAX {
if ($intent->status === 'requires_confirmation') {
$intent->confirm();
}
if ($intent->status === 'requires_action') {
self::generatePaymentResponse($intent);
}
if ($capture) {
if ($capture && $intent->status === 'requires_capture') {
$intent->capture();
}
}
......@@ -86,6 +84,15 @@ class CRM_Stripe_AJAX {
}
}
// Save the generated paymentIntent in the CiviCRM database for later tracking
$intentParams = [
'paymentintent_id' => $intent->id,
'payment_processor_id' => $processorID,
'status' => $intent->status,
'description' => $title,
];
CRM_Stripe_BAO_StripePaymentintent::create($intentParams);
self::generatePaymentResponse($intent);
}
......
<?php
use CRM_Stripe_ExtensionUtil as E;
class CRM_Stripe_BAO_StripePaymentintent extends CRM_Stripe_DAO_StripePaymentintent {
public static function getEntityName() {
return 'StripePaymentintent';
}
/**
* Create a new StripePaymentintent based on array-data
*
* @param array $params key-value pairs
* @return CRM_Stripe_DAO_StripePaymentintent|NULL
*
public static function create($params) {
$className = 'CRM_Stripe_DAO_StripePaymentintent';
$entityName = 'StripePaymentintent';
$hook = empty($params['id']) ? 'create' : 'edit';
CRM_Utils_Hook::pre($hook, $entityName, CRM_Utils_Array::value('id', $params), $params);
$instance = new $className();
$instance->copyValues($params);
$instance->save();
CRM_Utils_Hook::post($hook, $entityName, $instance->id, $instance);
return $instance;
} */
public static function test() {
}
/**
* Create a new StripePaymentintent based on array-data
*
* @param array $params key-value pairs
*
* @return \CRM_Stripe_BAO_StripePaymentintent
*/
public static function create($params) {
$instance = new self;
try {
if ($params['id']) {
$instance->id = $params['id'];
}
elseif ($params['paymentintent_id']) {
$instance->id = civicrm_api3('StripePaymentintent', 'getvalue', [
'return' => "id",
'paymentintent_id' => $params['paymentintent_id'],
]);
}
if ($instance->id) {
if ($instance->find()) {
$instance->fetch();
}
}
}
catch (Exception $e) {
// do nothing, we're creating a new one
}
$flags = empty($instance->flags) ? [] : unserialize($instance->flags);
if (!empty($params['flags']) && is_array($params['flags'])) {
foreach ($params['flags'] as $flag) {
if (!in_array($flag, $flags)) {
$flags[] = 'NC';
}
}
unset($params['flags']);
}
$instance->flags = serialize($flags);
$hook = empty($instance->id) ? 'create' : 'edit';
CRM_Utils_Hook::pre($hook, self::getEntityName(), CRM_Utils_Array::value('id', $params), $params);
$instance->copyValues($params);
$instance->save();
CRM_Utils_Hook::post($hook, self::getEntityName(), $instance->id, $instance);
return $instance;
}
}
......@@ -109,6 +109,7 @@ class CRM_Stripe_Customer {
2 => [$params['customer_id'], 'String'],
3 => [$params['processor_id'], 'Integer'],
];
CRM_Core_DAO::executeQuery("INSERT INTO civicrm_stripe_customers
(contact_id, id, processor_id) VALUES (%1, %2, %3)", $queryParams);
}
......
<?php
/**
* @package CRM
* @copyright CiviCRM LLC (c) 2004-2019
*
* Generated from /home/dev/civicrm/civicrm-buildkit/build/dmaster/sites/default/files/civicrm/ext/civicrm-stripe/xml/schema/CRM/Stripe/StripePaymentintent.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
* (GenCodeChecksum:9eb72d282f516624404a0f9cf806f534)
*/
/**
* Database access object for the StripePaymentintent entity.
*/
class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
/**
* Static instance to hold the table name.
*
* @var string
*/
public static $_tableName = 'civicrm_stripe_paymentintent';
/**
* Should CiviCRM log any modifications to this table in the civicrm_log table.
*
* @var bool
*/
public static $_log = TRUE;
/**
* Unique ID
*
* @var int
*/
public $id;
/**
* The PaymentIntent ID
*
* @var string
*/
public $paymentintent_id;
/**
* FK ID from civicrm_contribution
*
* @var int
*/
public $contribution_id;
/**
* Foreign key to civicrm_payment_processor.id
*
* @var int
*/
public $payment_processor_id;
/**
* Description of this paymentIntent
*
* @var string
*/
public $description;
/**
* The status of the paymentIntent
*
* @var string
*/
public $status;
/**
* An identifier that we can use in CiviCRM to find the paymentIntent if we do not have the ID (eg. session key)
*
* @var string
*/
public $identifier;
/**
* FK to Contact
*
* @var int
*/
public $contact_id;
/**
* When was paymentIntent created
*
* @var timestamp
*/
public $created_date;
/**
* Flags associated with this PaymentIntent (NC=no contributionID when doPayment called)
*
* @var string
*/
public $flags;
/**
* Class constructor.
*/
public function __construct() {
$this->__table = 'civicrm_stripe_paymentintent';
parent::__construct();
}
/**
* Returns foreign keys and entity references.
*
* @return array
* [CRM_Core_Reference_Interface]
*/
public static function getReferenceColumns() {
if (!isset(Civi::$statics[__CLASS__]['links'])) {
Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__);
Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'payment_processor_id', 'civicrm_payment_processor', 'id');
Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'contact_id', 'civicrm_contact', 'id');
CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']);
}
return Civi::$statics[__CLASS__]['links'];
}
/**
* Returns all the column names of this table
*
* @return array
*/
public static function &fields() {
if (!isset(Civi::$statics[__CLASS__]['fields'])) {
Civi::$statics[__CLASS__]['fields'] = [
'id' => [
'name' => 'id',
'type' => CRM_Utils_Type::T_INT,
'description' => CRM_Stripe_ExtensionUtil::ts('Unique ID'),
'required' => TRUE,
'where' => 'civicrm_stripe_paymentintent.id',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
'bao' => 'CRM_Stripe_DAO_StripePaymentintent',
'localizable' => 0,
],
'paymentintent_id' => [
'name' => 'paymentintent_id',
'type' => CRM_Utils_Type::T_STRING,
'title' => CRM_Stripe_ExtensionUtil::ts('PaymentIntent ID'),
'description' => CRM_Stripe_ExtensionUtil::ts('The PaymentIntent ID'),
'maxlength' => 255,
'size' => CRM_Utils_Type::HUGE,
'where' => 'civicrm_stripe_paymentintent.paymentintent_id',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
'bao' => 'CRM_Stripe_DAO_StripePaymentintent',
'localizable' => 0,
],
'contribution_id' => [
'name' => 'contribution_id',
'type' => CRM_Utils_Type::T_INT,
'title' => CRM_Stripe_ExtensionUtil::ts('Contribution ID'),
'description' => CRM_Stripe_ExtensionUtil::ts('FK ID from civicrm_contribution'),
'where' => 'civicrm_stripe_paymentintent.contribution_id',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
'bao' => 'CRM_Stripe_DAO_StripePaymentintent',
'localizable' => 0,
],
'payment_processor_id' => [
'name' => 'payment_processor_id',
'type' => CRM_Utils_Type::T_INT,
'title' => CRM_Stripe_ExtensionUtil::ts('Payment Processor'),
'description' => CRM_Stripe_ExtensionUtil::ts('Foreign key to civicrm_payment_processor.id'),
'where' => 'civicrm_stripe_paymentintent.payment_processor_id',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
'bao' => 'CRM_Stripe_DAO_StripePaymentintent',
'localizable' => 0,
'pseudoconstant' => [
'table' => 'civicrm_payment_processor',
'keyColumn' => 'id',
'labelColumn' => 'name',
],
],
'description' => [
'name' => 'description',
'type' => CRM_Utils_Type::T_STRING,
'title' => CRM_Stripe_ExtensionUtil::ts('Description'),
'description' => CRM_Stripe_ExtensionUtil::ts('Description of this paymentIntent'),
'required' => FALSE,
'maxlength' => 255,
'size' => CRM_Utils_Type::HUGE,
'where' => 'civicrm_stripe_paymentintent.description',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
'bao' => 'CRM_Stripe_DAO_StripePaymentintent',
'localizable' => 0,
],
'status' => [
'name' => 'status',
'type' => CRM_Utils_Type::T_STRING,
'title' => CRM_Stripe_ExtensionUtil::ts('Status'),
'description' => CRM_Stripe_ExtensionUtil::ts('The status of the paymentIntent'),
'required' => FALSE,
'maxlength' => 25,
'size' => CRM_Utils_Type::MEDIUM,
'where' => 'civicrm_stripe_paymentintent.status',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
'bao' => 'CRM_Stripe_DAO_StripePaymentintent',
'localizable' => 0,
],
'identifier' => [
'name' => 'identifier',
'type' => CRM_Utils_Type::T_STRING,
'title' => CRM_Stripe_ExtensionUtil::ts('Identifier'),
'description' => CRM_Stripe_ExtensionUtil::ts('An identifier that we can use in CiviCRM to find the paymentIntent if we do not have the ID (eg. session key)'),
'required' => FALSE,
'maxlength' => 255,
'size' => CRM_Utils_Type::HUGE,
'where' => 'civicrm_stripe_paymentintent.identifier',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
'bao' => 'CRM_Stripe_DAO_StripePaymentintent',
'localizable' => 0,
],
'contact_id' => [
'name' => 'contact_id',
'type' => CRM_Utils_Type::T_INT,
'description' => CRM_Stripe_ExtensionUtil::ts('FK to Contact'),
'where' => 'civicrm_stripe_paymentintent.contact_id',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
'bao' => 'CRM_Stripe_DAO_StripePaymentintent',
'localizable' => 0,
],
'created_date' => [
'name' => 'created_date',
'type' => CRM_Utils_Type::T_TIMESTAMP,
'title' => CRM_Stripe_ExtensionUtil::ts('Created Date'),
'description' => CRM_Stripe_ExtensionUtil::ts('When was paymentIntent created'),
'where' => 'civicrm_stripe_paymentintent.created_date',
'default' => 'CURRENT_TIMESTAMP',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
'bao' => 'CRM_Stripe_DAO_StripePaymentintent',
'localizable' => 0,
],
'flags' => [
'name' => 'flags',
'type' => CRM_Utils_Type::T_STRING,
'title' => CRM_Stripe_ExtensionUtil::ts('Flags'),
'description' => CRM_Stripe_ExtensionUtil::ts('Flags associated with this PaymentIntent (NC=no contributionID when doPayment called)'),
'required' => FALSE,
'maxlength' => 100,
'size' => CRM_Utils_Type::HUGE,
'where' => 'civicrm_stripe_paymentintent.flags',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
'bao' => 'CRM_Stripe_DAO_StripePaymentintent',
'localizable' => 0,
],
];
CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']);
}
return Civi::$statics[__CLASS__]['fields'];
}
/**
* Return a mapping from field-name to the corresponding key (as used in fields()).
*
* @return array
* Array(string $name => string $uniqueName).
*/
public static function &fieldKeys() {
if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) {
Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields()));
}
return Civi::$statics[__CLASS__]['fieldKeys'];
}
/**
* Returns the names of this table
*
* @return string
*/
public static function getTableName() {
return self::$_tableName;
}
/**
* Returns if this table needs to be logged
*
* @return bool
*/
public function getLog() {
return self::$_log;
}
/**
* Returns the list of fields that can be imported
*
* @param bool $prefix
*
* @return array
*/
public static function &import($prefix = FALSE) {
$r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'stripe_paymentintent', $prefix, []);
return $r;
}
/**
* Returns the list of fields that can be exported
*
* @param bool $prefix
*
* @return array
*/
public static function &export($prefix = FALSE) {
$r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'stripe_paymentintent', $prefix, []);
return $r;
}
/**
* Returns the list of indices
*
* @param bool $localize
*
* @return array
*/
public static function indices($localize = TRUE) {
$indices = [
'UI_paymentintent_id' => [
'name' => 'UI_paymentintent_id',
'field' => [
0 => 'paymentintent_id',
],
'localizable' => FALSE,
'unique' => TRUE,
'sig' => 'civicrm_stripe_paymentintent::1::paymentintent_id',
],
];
return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices;
}
}
<?php
/**
* https://civicrm.org/licensing
*/
/**
* Manage the civicrm_stripe_paymentintent database table which records all created paymentintents
* Class CRM_Stripe_PaymentIntent
*/
class CRM_Stripe_PaymentIntent {
/**
* Add a paymentIntent to the database
*
* @param $params
*
* @throws \Civi\Payment\Exception\PaymentProcessorException
*/
public static function add($params) {
$requiredParams = ['id', 'payment_processor_id'];
foreach ($requiredParams as $required) {
if (empty($params[$required])) {
throw new \Civi\Payment\Exception\PaymentProcessorException('Stripe PaymentIntent (add): Missing required parameter: ' . $required);
}
}
$count = 0;
foreach ($params as $key => $value) {
switch ($key) {
case 'id':
$queryParams[] = [$value, 'String'];
break;
case 'payment_processor_id':
$queryParams[] = [$value, 'Integer'];
break;
case 'contribution_id':
if (empty($value)) {
continue 2;
}
$queryParams[] = [$value, 'Integer'];
break;
case 'description':
$queryParams[] = [$value, 'String'];
break;
case 'status':
$queryParams[] = [$value, 'String'];
break;
case 'identifier':
$queryParams[] = [$value, 'String'];
break;
}
$keys[] = $key;
$update[] = "{$key} = '{$value}'";
$values[] = "%{$count}";
$count++;
}
$query = "INSERT INTO civicrm_stripe_paymentintent
(" . implode(',', $keys) . ") VALUES (" . implode(',', $values) . ")";
$query .= " ON DUPLICATE KEY UPDATE " . implode(',', $update);
CRM_Core_DAO::executeQuery($query, $queryParams);
}
/**
* @param array $params
*
* @throws \Civi\Payment\Exception\PaymentProcessorException
*/
public static function create($params) {
self::add($params);
}
/**
* Delete a Stripe paymentintent from the CiviCRM database
*
* @param array $params
*
* @throws \Civi\Payment\Exception\PaymentProcessorException
*/
public static function delete($params) {
$requiredParams = ['id'];
foreach ($requiredParams as $required) {
if (empty($params[$required])) {
throw new \Civi\Payment\Exception\PaymentProcessorException('Stripe PaymentIntent (delete): Missing required parameter: ' . $required);
}
}
$queryParams = [
1 => [$params['id'], 'String'],
];
$sql = "DELETE FROM civicrm_stripe_paymentintent WHERE id = %1";
CRM_Core_DAO::executeQuery($sql, $queryParams);
}
/**
* @param array $params
* @param \CRM_Core_Payment_Stripe $stripe
*
* @throws \Civi\Payment\Exception\PaymentProcessorException
*/
public static function stripeCancel($params, $stripe) {
$requiredParams = ['id'];
foreach ($requiredParams as $required) {
if (empty($params[$required])) {
throw new \Civi\Payment\Exception\PaymentProcessorException('Stripe PaymentIntent (getFromStripe): Missing required parameter: ' . $required);
}
}
$stripe->setAPIParams();
$intent = \Stripe\PaymentIntent::retrieve($params['id']);
$intent->cancel();
}