diff --git a/CRM/Core/Payment/Stripe.php b/CRM/Core/Payment/Stripe.php index 86bc898a849aa1915213c27ca2c27dd24cd36c1c..27eaba15e5f4c5936ebb985ce65c119d572e5297 100644 --- a/CRM/Core/Payment/Stripe.php +++ b/CRM/Core/Payment/Stripe.php @@ -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. diff --git a/CRM/Stripe/AJAX.php b/CRM/Stripe/AJAX.php index 2e5b701ff1d15904e655c2637e16f432e2f9ff56..eda4e00aab3984951f1b92574d94321783ee7ec0 100644 --- a/CRM/Stripe/AJAX.php +++ b/CRM/Stripe/AJAX.php @@ -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); } diff --git a/CRM/Stripe/BAO/StripePaymentintent.php b/CRM/Stripe/BAO/StripePaymentintent.php new file mode 100644 index 0000000000000000000000000000000000000000..192ab5b9bd54313af3edb4d35f82b7ace521a07b --- /dev/null +++ b/CRM/Stripe/BAO/StripePaymentintent.php @@ -0,0 +1,81 @@ +<?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; + } +} diff --git a/CRM/Stripe/Customer.php b/CRM/Stripe/Customer.php index bf3002506e27ae1fd24c7acf8b4ad91b0ef76759..f9f49b9de50a79c275d8dd49f721d0e6c3f2cb1b 100644 --- a/CRM/Stripe/Customer.php +++ b/CRM/Stripe/Customer.php @@ -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); } diff --git a/CRM/Stripe/DAO/StripePaymentintent.php b/CRM/Stripe/DAO/StripePaymentintent.php new file mode 100644 index 0000000000000000000000000000000000000000..b59cbc42715263ac1854bb950b0b67dab94040ad --- /dev/null +++ b/CRM/Stripe/DAO/StripePaymentintent.php @@ -0,0 +1,345 @@ +<?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; + } + +} diff --git a/CRM/Stripe/PaymentIntent.php b/CRM/Stripe/PaymentIntent.php new file mode 100644 index 0000000000000000000000000000000000000000..9a8f2da4c3f5b41dca302c3b94b224a3486e8106 --- /dev/null +++ b/CRM/Stripe/PaymentIntent.php @@ -0,0 +1,171 @@ +<?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(); + } + + /** + * @param array $params + * @param \CRM_Core_Payment_Stripe $stripe + * + * @throws \Civi\Payment\Exception\PaymentProcessorException + */ + public static function stripeGet($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']); + $paymentIntent = self::get($params); + $params['status'] = $intent->status; + self::add($params); + } + + /** + * Get an existing Stripe paymentIntent from the CiviCRM database + * + * @param $params + * + * @return array + * @throws \Civi\Payment\Exception\PaymentProcessorException + */ + public static function get($params) { + $requiredParams = ['id']; + foreach ($requiredParams as $required) { + if (empty($params[$required])) { + throw new \Civi\Payment\Exception\PaymentProcessorException('Stripe PaymentIntent (get): Missing required parameter: ' . $required); + } + } + if (empty($params['contact_id'])) { + throw new \Civi\Payment\Exception\PaymentProcessorException('Stripe PaymentIntent (get): contact_id is required'); + } + $queryParams = [ + 1 => [$params['id'], 'String'], + ]; + + $dao = CRM_Core_DAO::executeQuery("SELECT * + FROM civicrm_stripe_paymentintent + WHERE id = %1", $queryParams); + + return $dao->toArray(); + } + +} diff --git a/api/v3/Job/ProcessStripe.mgd.php b/api/v3/Job/ProcessStripe.mgd.php new file mode 100644 index 0000000000000000000000000000000000000000..402c79ca3a9267839343da3d85cbc225e955033d --- /dev/null +++ b/api/v3/Job/ProcessStripe.mgd.php @@ -0,0 +1,20 @@ +<?php + +return [ + 0 => + [ + 'name' => 'ProcessStripe', + 'entity' => 'Job', + 'params' => + [ + 'version' => 3, + 'name' => 'ProcessStripe', + 'description' => 'Process Stripe functions', + 'run_frequency' => 'Hourly', + 'api_entity' => 'Job', + 'api_action' => 'process_stripe', + 'parameters' => 'delete_old=-3 month +cancel_incomplete=-1 day', + ], + ], +]; diff --git a/api/v3/Job/ProcessStripe.php b/api/v3/Job/ProcessStripe.php new file mode 100644 index 0000000000000000000000000000000000000000..44214953b4e6a11302a91852f73bc669a655c015 --- /dev/null +++ b/api/v3/Job/ProcessStripe.php @@ -0,0 +1,67 @@ +<?php +/** + * This job performs various housekeeping actions related to the Stripe payment processor + * + * @param array $params + * + * @return array + * API result array. + * @throws CiviCRM_API3_Exception + */ +function civicrm_api3_job_process_stripe($params) { + $results = []; + + if ($params['delete_old'] !== 0 && !empty($params['delete_old'])) { + // Delete all locally recorded paymentIntents that are older than 3 months + $oldPaymentIntents = civicrm_api3('StripePaymentintent', 'get', [ + 'status' => ['IN' => ["succeeded", "cancelled"]], + 'created_date' => ['<' => $params['delete_old']], + ]); + foreach ($oldPaymentIntents['values'] as $id => $detail) { + civicrm_api3('StripePaymentintent', 'delete', ['id' => $id]); + $results['deleted'][$id] = $detail['paymentintent_id']; + } + } + + if ($params['cancel_incomplete'] !== 0 && !empty($params['cancel_incomplete'])) { + // Cancel incomplete paymentIntents after 1 day + $incompletePaymentIntents = civicrm_api3('StripePaymentintent', 'get', [ + 'status' => ['NOT IN' => ["succeeded", "cancelled"]], + 'created_date' => ['<' => $params['cancel_incomplete']], + ]); + foreach ($incompletePaymentIntents['values'] as $id => $detail) { + try { + /** @var \CRM_Core_Payment_Stripe $paymentProcessor */ + $paymentProcessor = Civi\Payment\System::singleton() + ->getById($detail['payment_processor_id']); + $paymentProcessor->setAPIParams(); + $intent = \Stripe\PaymentIntent::retrieve($detail['paymentintent_id']); + $intent->cancel(['cancellation_reason' => 'abandoned']); + } catch (Exception $e) { + } + civicrm_api3('StripePaymentintent', 'create', [ + 'id' => $id, + 'status' => 'cancelled' + ]); + $results['cancelled'][$id] = $detail['paymentintent_id']; + } + } + + return civicrm_api3_create_success($results, $params); +} + +/** + * Action Payment. + * + * @param array $params + * + * @return array + */ +function _civicrm_api3_job_process_stripe_spec(&$params) { + $params['delete_old']['api.default'] = '-3 month'; + $params['delete_old']['title'] = 'Delete old records after (default: -3 month)'; + $params['delete_old']['description'] = 'Delete old records from database. Specify 0 to disable. Default is "-3 month"'; + $params['cancel_incomplete']['api.default'] = '-1 day'; + $params['cancel_incomplete']['title'] = 'Cancel incomplete records after (default: -1 day)'; + $params['cancel_incomplete']['description'] = 'Cancel incomplete paymentIntents in your stripe account. Specify 0 to disable. Default is "-1 day"'; +} diff --git a/api/v3/StripePaymentintent.php b/api/v3/StripePaymentintent.php new file mode 100644 index 0000000000000000000000000000000000000000..18e19aa3bb5a3773e0c06845d0e6e809daadadd4 --- /dev/null +++ b/api/v3/StripePaymentintent.php @@ -0,0 +1,47 @@ +<?php +use CRM_Stripe_ExtensionUtil as E; + +/** + * StripePaymentintent.create API specification (optional) + * This is used for documentation and validation. + * + * @param array $spec description of fields supported by this API call + * @return void + * @see http://wiki.civicrm.org/confluence/display/CRMDOC/API+Architecture+Standards + */ +function _civicrm_api3_stripe_paymentintent_create_spec(&$spec) { + // $spec['some_parameter']['api.required'] = 1; +} + +/** + * StripePaymentintent.create API + * + * @param array $params + * @return array API result descriptor + * @throws API_Exception + */ +function civicrm_api3_stripe_paymentintent_create($params) { + return _civicrm_api3_basic_create(_civicrm_api3_get_BAO(__FUNCTION__), $params); +} + +/** + * StripePaymentintent.delete API + * + * @param array $params + * @return array API result descriptor + * @throws API_Exception + */ +function civicrm_api3_stripe_paymentintent_delete($params) { + return _civicrm_api3_basic_delete(_civicrm_api3_get_BAO(__FUNCTION__), $params); +} + +/** + * StripePaymentintent.get API + * + * @param array $params + * @return array API result descriptor + * @throws API_Exception + */ +function civicrm_api3_stripe_paymentintent_get($params) { + return _civicrm_api3_basic_get(_civicrm_api3_get_BAO(__FUNCTION__), $params); +} diff --git a/js/civicrmStripeConfirm.js b/js/civicrmStripeConfirm.js index ffa09bdbb1a58f2316c43388ee1d6ece6b0f0635..a26b6edb03b8341726a351dcd575a056b120b179 100644 --- a/js/civicrmStripeConfirm.js +++ b/js/civicrmStripeConfirm.js @@ -1,8 +1,17 @@ /** - * JS Integration between CiviCRM & Stripe. + * This handles confirmation actions on the "Thankyou" pages for contribution/event workflows. */ CRM.$(function($) { - debugging("civicrm_stripe loaded, dom-ready function firing."); + debugging("civicrmStripeConfirm loaded"); + + if (typeof CRM.vars.stripe === 'undefined') { + debugging('CRM.vars.stripe not defined! Not a Stripe processor?'); + return; + } + if (CRM.vars.stripe.paymentIntentStatus === 'succeeded') { + debugging('already succeeded'); + return; + } checkAndLoad(); @@ -62,11 +71,6 @@ CRM.$(function($) { } function checkAndLoad() { - if (typeof CRM.vars.stripe === 'undefined') { - debugging('CRM.vars.stripe not defined! Not a Stripe processor?'); - return; - } - if (typeof Stripe === 'undefined') { if (stripeLoading) { return; diff --git a/js/civicrmStripeConfirm.min.js b/js/civicrmStripeConfirm.min.js new file mode 100644 index 0000000000000000000000000000000000000000..708b8a7c4f45dc20fa47023522266d1d9f305b1f --- /dev/null +++ b/js/civicrmStripeConfirm.min.js @@ -0,0 +1 @@ +CRM.$(function(g){f("civicrmStripeConfirm loaded");if(typeof CRM.vars.stripe==="undefined"){f("CRM.vars.stripe not defined! Not a Stripe processor?");return}if(CRM.vars.stripe.paymentIntentStatus==="succeeded"){f("already succeeded");return}a();if(typeof h==="undefined"){h=Stripe(CRM.vars.stripe.publishableKey)}c();var h;var e=false;window.onbeforeunload=null;function d(i){f("handleServerResponse");if(i.error){}else{if(i.requires_action){b(i)}else{f("success - payment captured")}}}function b(i){h.handleCardAction(i.payment_intent_client_secret).then(function(j){if(j.error){c()}else{f("card action success");c()}})}function c(){f("handle card confirm");var i=CRM.url("civicrm/stripe/confirm-payment");g.post(i,{payment_intent_id:CRM.vars.stripe.paymentIntentID,capture:true,id:CRM.vars.stripe.id}).then(function(j){d(j)})}function a(){if(typeof Stripe==="undefined"){if(e){return}e=true;f("Stripe.js is not loaded!");g.getScript("https://js.stripe.com/v3",function(){f("Script loaded and executed.");e=false})}}function f(i){if((typeof(CRM.vars.stripe)==="undefined")||(Boolean(CRM.vars.stripe.jsDebug)===true)){console.log(new Date().toISOString()+" civicrm_stripe.js: "+i)}}}); \ No newline at end of file diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js index 76fcb4d57238a6e6045e576fca6dfce46e8f3fb4..3d1e73919392ed3ec47a137630deafbcc79c6343 100644 --- a/js/civicrm_stripe.js +++ b/js/civicrm_stripe.js @@ -92,6 +92,7 @@ CRM.$(function($) { amount: getTotalAmount(), currency: CRM.vars.stripe.currency, id: CRM.vars.stripe.id, + description: document.title, }).then(function (result) { // Handle server response (see Step 3) handleServerResponse(result); diff --git a/js/civicrm_stripe.min.js b/js/civicrm_stripe.min.js index c53be3e27d89d9adff9fffca8d27e990a8a0d10c..0f94ba55731e2ce718e6c30d946bd43dfedbaccb 100644 --- a/js/civicrm_stripe.min.js +++ b/js/civicrm_stripe.min.js @@ -1 +1 @@ -CRM.$(function(d){f("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){f("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var q;var b;var c;var a;var o=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){f("civicrmStripeHandleReload");var A=document.getElementById("card-element");if((typeof A!=="undefined")&&(A)){if(!A.children.length){f("checkAndLoad from document.ready");m()}}};window.civicrmStripeHandleReload();function v(C,A){f(C+": success - submitting form");var B=document.createElement("input");B.setAttribute("type","hidden");B.setAttribute("name",C);B.setAttribute("value",A.id);c.appendChild(B);c.submit()}function p(){a.setAttribute("disabled",true);return c.submit()}function h(A){f("error: "+A.error.message);var B=document.getElementById("card-errors");B.style.display="block";B.textContent=A.error.message;document.querySelector("#billing-payment-block").scrollIntoView();window.scrollBy(0,-50);c.dataset.submitted=false;a.removeAttribute("disabled")}function y(){f("handle card payment");q.createPaymentMethod("card",b).then(function(A){if(A.error){h(A)}else{if(g()===true){v("paymentMethodID",A.paymentMethod)}else{var B=CRM.url("civicrm/stripe/confirm-payment");d.post(B,{payment_method_id:A.paymentMethod.id,amount:r(),currency:CRM.vars.stripe.currency,id:CRM.vars.stripe.id}).then(function(C){w(C)})}}})}function w(A){f("handleServerResponse");if(A.error){h(A)}else{if(A.requires_action){s(A)}else{v("paymentIntentID",A.paymentIntent)}}}function s(A){q.handleCardAction(A.payment_intent_client_secret).then(function(B){if(B.error){h(B)}else{v("paymentIntentID",B.paymentIntent)}})}d(document).ajaxComplete(function(C,D,B){if((B.url.match("civicrm(/|%2F)payment(/|%2F)form")!==null)||(B.url.match("civicrm(/|%2F)contact(/|%2F)view(/|%2F)participant")!==null)){if(typeof CRM.vars.stripe==="undefined"){return}var A=k();if(A!==null){if(A!==parseInt(CRM.vars.stripe.id)){f("payment processor changed to id: "+A);if(A===0){return l()}CRM.api3("PaymentProcessor","getvalue",{"return":"user_name",id:A,payment_processor_type_id:CRM.vars.stripe.paymentProcessorTypeID}).done(function(E){var F=E.result;if(F){f("Setting new stripe key to: "+F);CRM.vars.stripe.publishableKey=F}else{return l()}f("checkAndLoad from ajaxComplete");m()})}}}});function l(){f("New payment processor is not Stripe, clearing CRM.vars.stripe");if((typeof b!=="undefined")&&(b)){f("destroying card element");b.destroy();b=undefined}delete (CRM.vars.stripe)}function m(){if(typeof CRM.vars.stripe==="undefined"){f("CRM.vars.stripe not defined! Not a Stripe processor?");return}if(typeof Stripe==="undefined"){if(o){return}o=true;f("Stripe.js is not loaded!");d.getScript("https://js.stripe.com/v3",function(){f("Script loaded and executed.");o=false;e()})}else{e()}}function e(){f("loadStripeBillingBlock");if(typeof q==="undefined"){q=Stripe(CRM.vars.stripe.publishableKey)}var G=q.elements();var D={base:{fontSize:"20px"}};b=G.create("card",{style:D});b.mount("#card-element");f("created new card element",b);document.getElementsByClassName("billing_postal_code-"+CRM.vars.stripe.billingAddressID+"-section")[0].setAttribute("hidden",true);b.addEventListener("change",function(H){u(H)});c=j();if(typeof c.length==="undefined"||c.length===0){f("No billing form!");return}a=z();c.dataset.submitdontprocess=false;var A=c.querySelectorAll('[type="submit"][formnovalidate="1"], [type="submit"][formnovalidate="formnovalidate"], [type="submit"].cancel, [type="submit"].webform-previous'),C;for(C=0;C<A.length;++C){A[C].addEventListener("click",F())}function F(){f("adding submitdontprocess");c.dataset.submitdontprocess=true}a.addEventListener("click",B);function B(H){if(c.dataset.submitted===true){return}c.dataset.submitted=true;if(typeof CRM.vars.stripe==="undefined"){return p()}f("clearing submitdontprocess");c.dataset.submitdontprocess=false;return E(H)}a.removeAttribute("onclick");n();if(x()){d("[type=submit]").click(function(){t(this.value)});c.addEventListener("keydown",function(H){if(H.keyCode===13){t(this.value);E(event)}});d("#billingcheckbox:input").hide();d('label[for="billingcheckbox"]').hide()}function E(J){J.preventDefault();f("submit handler");if(d(c).valid()===false){f("Form not valid");return false}if(typeof CRM.vars.stripe==="undefined"){f("Submitting - not a stripe processor");return true}if(c.dataset.submitted===true){f("form already submitted");return false}var L=parseInt(CRM.vars.stripe.id);var I=null;if(x()){if(!d('input[name="submitted[civicrm_1_contribution_1_contribution_payment_processor_id]"]').length){I=L}else{I=parseInt(c.querySelector('input[name="submitted[civicrm_1_contribution_1_contribution_payment_processor_id]"]:checked').value)}}else{if((c.querySelector(".crm-section.payment_processor-section")!==null)||(c.querySelector(".crm-section.credit_card_info-section")!==null)){L=CRM.vars.stripe.id;if(c.querySelector('input[name="payment_processor_id"]:checked')!==null){I=parseInt(c.querySelector('input[name="payment_processor_id"]:checked').value)}}}if((I===0)||(L===null)||((I===null)&&(L===null))){f("Not a Stripe transaction, or pay-later");return p()}else{f("Stripe is the selected payprocessor")}if(typeof CRM.vars.stripe.publishableKey==="undefined"){f("submit missing stripe-pub-key element or value");return true}if(c.dataset.submitdontprocess===true){f("non-payment submit detected - not submitting payment");return true}if(x()){if(d("#billing-payment-block").is(":hidden")){f("no payment processor on webform");return true}var K=d('[name="submitted[civicrm_1_contribution_1_contribution_payment_processor_id]"]');if(K.length){if(K.filter(":checked").val()==="0"||K.filter(":checked").val()===0){f("no payment processor selected");return true}}}var H=r();if(H=="0"){f("Total amount is 0");return true}if(c.dataset.submitted===true){alert("Form already submitted. Please wait.");return false}else{c.dataset.submitted=true}a.setAttribute("disabled",true);y();return true}}function x(){if(c!==null){return c.classList.contains("webform-client-form")||c.classList.contains("webform-submission-form")}return false}function j(){var A=d("div#card-element").closest("form").prop("id");if((typeof A==="undefined")||(!A.length)){A=d("input[name=hidden_processor]").closest("form").prop("id")}return document.getElementById(A)}function z(){var A=null;if(x()){A=c.querySelector('[type="submit"].webform-submit');if(!A){A=c.querySelector('[type="submit"].webform-button--submit')}}else{A=c.querySelector('[type="submit"].validate')}return A}function r(){var A=null;if((document.getElementById("additional_participants")!==null)&&(document.getElementById("additional_participants").value.length!==0)){f("Cannot setup paymentIntent because we don't know the final price");return A}if(typeof calculateTotalFee=="function"){A=calculateTotalFee()}else{if(x()){d(".line-item:visible","#wf-crm-billing-items").each(function(){A+=parseFloat(d(this).data("amount"))})}else{if(document.getElementById("total_amount")){A=document.getElementById("total_amount").value}}}return A}function g(){if(document.getElementById("is_recur")!==null){return Boolean(document.getElementById("is_recur").checked)}return false}function u(A){if(!A.complete){return}document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID).value=A.value.postalCode}function n(){cividiscountElements=c.querySelectorAll("input#discountcode");var A=function(B){if(B.keyCode===13){B.preventDefault();f("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i<cividiscountElements.length;++i){cividiscountElements[i].addEventListener("keydown",A)}}function f(A){if((typeof(CRM.vars.stripe)==="undefined")||(Boolean(CRM.vars.stripe.jsDebug)===true)){console.log(new Date().toISOString()+" civicrm_stripe.js: "+A)}}function t(B){var A=null;if(document.getElementById("action")!==null){A=document.getElementById("action")}else{A=document.createElement("input")}A.setAttribute("type","hidden");A.setAttribute("name","op");A.setAttribute("id","action");A.setAttribute("value",B);c.appendChild(A)}function k(){if((typeof c==="undefined")||(!c)){c=j();if(!c){return null}}var A=c.querySelector('input[name="payment_processor_id"]:checked');if(A!==null){return parseInt(A.value)}return null}}); \ No newline at end of file +CRM.$(function(d){f("civicrm_stripe loaded, dom-ready function firing.");if(window.civicrmStripeHandleReload){f("calling existing civicrmStripeHandleReload.");window.civicrmStripeHandleReload();return}var q;var b;var c;var a;var o=false;window.onbeforeunload=null;window.civicrmStripeHandleReload=function(){f("civicrmStripeHandleReload");var A=document.getElementById("card-element");if((typeof A!=="undefined")&&(A)){if(!A.children.length){f("checkAndLoad from document.ready");m()}}};window.civicrmStripeHandleReload();function v(C,A){f(C+": success - submitting form");var B=document.createElement("input");B.setAttribute("type","hidden");B.setAttribute("name",C);B.setAttribute("value",A.id);c.appendChild(B);c.submit()}function p(){a.setAttribute("disabled",true);return c.submit()}function h(A){f("error: "+A.error.message);var B=document.getElementById("card-errors");B.style.display="block";B.textContent=A.error.message;document.querySelector("#billing-payment-block").scrollIntoView();window.scrollBy(0,-50);c.dataset.submitted=false;a.removeAttribute("disabled")}function y(){f("handle card payment");q.createPaymentMethod("card",b).then(function(A){if(A.error){h(A)}else{if(g()===true){v("paymentMethodID",A.paymentMethod)}else{var B=CRM.url("civicrm/stripe/confirm-payment");d.post(B,{payment_method_id:A.paymentMethod.id,amount:r(),currency:CRM.vars.stripe.currency,id:CRM.vars.stripe.id,description:document.title}).then(function(C){w(C)})}}})}function w(A){f("handleServerResponse");if(A.error){h(A)}else{if(A.requires_action){s(A)}else{v("paymentIntentID",A.paymentIntent)}}}function s(A){q.handleCardAction(A.payment_intent_client_secret).then(function(B){if(B.error){h(B)}else{v("paymentIntentID",B.paymentIntent)}})}d(document).ajaxComplete(function(C,D,B){if((B.url.match("civicrm(/|%2F)payment(/|%2F)form")!==null)||(B.url.match("civicrm(/|%2F)contact(/|%2F)view(/|%2F)participant")!==null)){if(typeof CRM.vars.stripe==="undefined"){return}var A=k();if(A!==null){if(A!==parseInt(CRM.vars.stripe.id)){f("payment processor changed to id: "+A);if(A===0){return l()}CRM.api3("PaymentProcessor","getvalue",{"return":"user_name",id:A,payment_processor_type_id:CRM.vars.stripe.paymentProcessorTypeID}).done(function(E){var F=E.result;if(F){f("Setting new stripe key to: "+F);CRM.vars.stripe.publishableKey=F}else{return l()}f("checkAndLoad from ajaxComplete");m()})}}}});function l(){f("New payment processor is not Stripe, clearing CRM.vars.stripe");if((typeof b!=="undefined")&&(b)){f("destroying card element");b.destroy();b=undefined}delete (CRM.vars.stripe)}function m(){if(typeof CRM.vars.stripe==="undefined"){f("CRM.vars.stripe not defined! Not a Stripe processor?");return}if(typeof Stripe==="undefined"){if(o){return}o=true;f("Stripe.js is not loaded!");d.getScript("https://js.stripe.com/v3",function(){f("Script loaded and executed.");o=false;e()})}else{e()}}function e(){f("loadStripeBillingBlock");if(typeof q==="undefined"){q=Stripe(CRM.vars.stripe.publishableKey)}var G=q.elements();var D={base:{fontSize:"20px"}};b=G.create("card",{style:D});b.mount("#card-element");f("created new card element",b);document.getElementsByClassName("billing_postal_code-"+CRM.vars.stripe.billingAddressID+"-section")[0].setAttribute("hidden",true);b.addEventListener("change",function(H){u(H)});c=j();if(typeof c.length==="undefined"||c.length===0){f("No billing form!");return}a=z();c.dataset.submitdontprocess=false;var A=c.querySelectorAll('[type="submit"][formnovalidate="1"], [type="submit"][formnovalidate="formnovalidate"], [type="submit"].cancel, [type="submit"].webform-previous'),C;for(C=0;C<A.length;++C){A[C].addEventListener("click",F())}function F(){f("adding submitdontprocess");c.dataset.submitdontprocess=true}a.addEventListener("click",B);function B(H){if(c.dataset.submitted===true){return}c.dataset.submitted=true;if(typeof CRM.vars.stripe==="undefined"){return p()}f("clearing submitdontprocess");c.dataset.submitdontprocess=false;return E(H)}a.removeAttribute("onclick");n();if(x()){d("[type=submit]").click(function(){t(this.value)});c.addEventListener("keydown",function(H){if(H.keyCode===13){t(this.value);E(event)}});d("#billingcheckbox:input").hide();d('label[for="billingcheckbox"]').hide()}function E(J){J.preventDefault();f("submit handler");if(d(c).valid()===false){f("Form not valid");return false}if(typeof CRM.vars.stripe==="undefined"){f("Submitting - not a stripe processor");return true}if(c.dataset.submitted===true){f("form already submitted");return false}var L=parseInt(CRM.vars.stripe.id);var I=null;if(x()){if(!d('input[name="submitted[civicrm_1_contribution_1_contribution_payment_processor_id]"]').length){I=L}else{I=parseInt(c.querySelector('input[name="submitted[civicrm_1_contribution_1_contribution_payment_processor_id]"]:checked').value)}}else{if((c.querySelector(".crm-section.payment_processor-section")!==null)||(c.querySelector(".crm-section.credit_card_info-section")!==null)){L=CRM.vars.stripe.id;if(c.querySelector('input[name="payment_processor_id"]:checked')!==null){I=parseInt(c.querySelector('input[name="payment_processor_id"]:checked').value)}}}if((I===0)||(L===null)||((I===null)&&(L===null))){f("Not a Stripe transaction, or pay-later");return p()}else{f("Stripe is the selected payprocessor")}if(typeof CRM.vars.stripe.publishableKey==="undefined"){f("submit missing stripe-pub-key element or value");return true}if(c.dataset.submitdontprocess===true){f("non-payment submit detected - not submitting payment");return true}if(x()){if(d("#billing-payment-block").is(":hidden")){f("no payment processor on webform");return true}var K=d('[name="submitted[civicrm_1_contribution_1_contribution_payment_processor_id]"]');if(K.length){if(K.filter(":checked").val()==="0"||K.filter(":checked").val()===0){f("no payment processor selected");return true}}}var H=r();if(H=="0"){f("Total amount is 0");return true}if(c.dataset.submitted===true){alert("Form already submitted. Please wait.");return false}else{c.dataset.submitted=true}a.setAttribute("disabled",true);y();return true}}function x(){if(c!==null){return c.classList.contains("webform-client-form")||c.classList.contains("webform-submission-form")}return false}function j(){var A=d("div#card-element").closest("form").prop("id");if((typeof A==="undefined")||(!A.length)){A=d("input[name=hidden_processor]").closest("form").prop("id")}return document.getElementById(A)}function z(){var A=null;if(x()){A=c.querySelector('[type="submit"].webform-submit');if(!A){A=c.querySelector('[type="submit"].webform-button--submit')}}else{A=c.querySelector('[type="submit"].validate')}return A}function r(){var A=null;if((document.getElementById("additional_participants")!==null)&&(document.getElementById("additional_participants").value.length!==0)){f("Cannot setup paymentIntent because we don't know the final price");return A}if(typeof calculateTotalFee=="function"){A=calculateTotalFee()}else{if(x()){d(".line-item:visible","#wf-crm-billing-items").each(function(){A+=parseFloat(d(this).data("amount"))})}else{if(document.getElementById("total_amount")){A=document.getElementById("total_amount").value}}}return A}function g(){if(document.getElementById("is_recur")!==null){return Boolean(document.getElementById("is_recur").checked)}return false}function u(A){if(!A.complete){return}document.getElementById("billing_postal_code-"+CRM.vars.stripe.billingAddressID).value=A.value.postalCode}function n(){cividiscountElements=c.querySelectorAll("input#discountcode");var A=function(B){if(B.keyCode===13){B.preventDefault();f("adding submitdontprocess");c.dataset.submitdontprocess=true}};for(i=0;i<cividiscountElements.length;++i){cividiscountElements[i].addEventListener("keydown",A)}}function f(A){if((typeof(CRM.vars.stripe)==="undefined")||(Boolean(CRM.vars.stripe.jsDebug)===true)){console.log(new Date().toISOString()+" civicrm_stripe.js: "+A)}}function t(B){var A=null;if(document.getElementById("action")!==null){A=document.getElementById("action")}else{A=document.createElement("input")}A.setAttribute("type","hidden");A.setAttribute("name","op");A.setAttribute("id","action");A.setAttribute("value",B);c.appendChild(A)}function k(){if((typeof c==="undefined")||(!c)){c=j();if(!c){return null}}var A=c.querySelector('input[name="payment_processor_id"]:checked');if(A!==null){return parseInt(A.value)}return null}}); \ No newline at end of file diff --git a/sql/auto_install.sql b/sql/auto_install.sql index 2ca7777e06349921f6909da6eaa8d357ef357c05..0d5adf63aeca6745610b8c2d3a2a4868e8682317 100644 --- a/sql/auto_install.sql +++ b/sql/auto_install.sql @@ -1,9 +1,104 @@ -/* Create required tables for Stripe */ - CREATE TABLE IF NOT EXISTS `civicrm_stripe_customers` ( - `id` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `contact_id` int(10) UNSIGNED DEFAULT NULL COMMENT 'FK ID from civicrm_contact', - `processor_id` int(10) DEFAULT NULL COMMENT 'ID from civicrm_payment_processor', - UNIQUE KEY `id` (`id`), - CONSTRAINT `FK_civicrm_stripe_customers_contact_id` FOREIGN KEY (`contact_id`) - REFERENCES `civicrm_contact` (`id`) ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +-- +--------------------------------------------------------------------+ +-- | CiviCRM version 5 | +-- +--------------------------------------------------------------------+ +-- | Copyright CiviCRM LLC (c) 2004-2019 | +-- +--------------------------------------------------------------------+ +-- | This file is a part of CiviCRM. | +-- | | +-- | CiviCRM is free software; you can copy, modify, and distribute it | +-- | under the terms of the GNU Affero General Public License | +-- | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | +-- | | +-- | CiviCRM is distributed in the hope that it will be useful, but | +-- | WITHOUT ANY WARRANTY; without even the implied warranty of | +-- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | +-- | See the GNU Affero General Public License for more details. | +-- | | +-- | You should have received a copy of the GNU Affero General Public | +-- | License and the CiviCRM Licensing Exception along | +-- | with this program; if not, contact CiviCRM LLC | +-- | at info[AT]civicrm[DOT]org. If you have questions about the | +-- | GNU Affero General Public License or the licensing of CiviCRM, | +-- | see the CiviCRM license FAQ at http://civicrm.org/licensing | +-- +--------------------------------------------------------------------+ +-- +-- Generated from schema.tpl +-- DO NOT EDIT. Generated by CRM_Core_CodeGen +-- + + +-- +--------------------------------------------------------------------+ +-- | CiviCRM version 5 | +-- +--------------------------------------------------------------------+ +-- | Copyright CiviCRM LLC (c) 2004-2019 | +-- +--------------------------------------------------------------------+ +-- | This file is a part of CiviCRM. | +-- | | +-- | CiviCRM is free software; you can copy, modify, and distribute it | +-- | under the terms of the GNU Affero General Public License | +-- | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | +-- | | +-- | CiviCRM is distributed in the hope that it will be useful, but | +-- | WITHOUT ANY WARRANTY; without even the implied warranty of | +-- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | +-- | See the GNU Affero General Public License for more details. | +-- | | +-- | You should have received a copy of the GNU Affero General Public | +-- | License and the CiviCRM Licensing Exception along | +-- | with this program; if not, contact CiviCRM LLC | +-- | at info[AT]civicrm[DOT]org. If you have questions about the | +-- | GNU Affero General Public License or the licensing of CiviCRM, | +-- | see the CiviCRM license FAQ at http://civicrm.org/licensing | +-- +--------------------------------------------------------------------+ +-- +-- Generated from drop.tpl +-- DO NOT EDIT. Generated by CRM_Core_CodeGen +-- +-- /******************************************************* +-- * +-- * Clean up the exisiting tables +-- * +-- *******************************************************/ + +SET FOREIGN_KEY_CHECKS=0; + +DROP TABLE IF EXISTS `civicrm_stripe_paymentintent`; + +SET FOREIGN_KEY_CHECKS=1; +-- /******************************************************* +-- * +-- * Create new tables +-- * +-- *******************************************************/ + +-- /******************************************************* +-- * +-- * civicrm_stripe_paymentintent +-- * +-- * Stripe PaymentIntents +-- * +-- *******************************************************/ +CREATE TABLE `civicrm_stripe_paymentintent` ( + + + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique ID', + `paymentintent_id` varchar(255) COMMENT 'The PaymentIntent ID', + `contribution_id` int unsigned COMMENT 'FK ID from civicrm_contribution', + `payment_processor_id` int unsigned COMMENT 'Foreign key to civicrm_payment_processor.id', + `description` varchar(255) NULL COMMENT 'Description of this paymentIntent', + `status` varchar(25) NULL COMMENT 'The status of the paymentIntent', + `identifier` varchar(255) NULL COMMENT 'An identifier that we can use in CiviCRM to find the paymentIntent if we do not have the ID (eg. session key)', + `contact_id` int unsigned COMMENT 'FK to Contact', + `created_date` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT 'When was paymentIntent created', + `flags` varchar(100) NULL COMMENT 'Flags associated with this PaymentIntent (NC=no contributionID when doPayment called)' +, + PRIMARY KEY (`id`) + + , UNIQUE INDEX `UI_paymentintent_id`( + paymentintent_id + ) + +, CONSTRAINT FK_civicrm_stripe_paymentintent_payment_processor_id FOREIGN KEY (`payment_processor_id`) REFERENCES `civicrm_payment_processor`(`id`) ON DELETE SET NULL, CONSTRAINT FK_civicrm_stripe_paymentintent_contact_id FOREIGN KEY (`contact_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE +) ; + + diff --git a/sql/auto_uninstall.sql b/sql/auto_uninstall.sql index 11145c7ef0e27b294bfcbf986421972fb614c66c..275f79f4ce72a2cccb513dbb9c7092aa565ad92f 100644 --- a/sql/auto_uninstall.sql +++ b/sql/auto_uninstall.sql @@ -1,4 +1,38 @@ -/* Remove Stripe tables on uninstall. */ -DROP TABLE IF EXISTS civicrm_stripe_customers; -DROP TABLE IF EXISTS civicrm_stripe_plans; -DROP TABLE IF EXISTS civicrm_stripe_subscriptions; \ No newline at end of file +-- +--------------------------------------------------------------------+ +-- | CiviCRM version 5 | +-- +--------------------------------------------------------------------+ +-- | Copyright CiviCRM LLC (c) 2004-2019 | +-- +--------------------------------------------------------------------+ +-- | This file is a part of CiviCRM. | +-- | | +-- | CiviCRM is free software; you can copy, modify, and distribute it | +-- | under the terms of the GNU Affero General Public License | +-- | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | +-- | | +-- | CiviCRM is distributed in the hope that it will be useful, but | +-- | WITHOUT ANY WARRANTY; without even the implied warranty of | +-- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | +-- | See the GNU Affero General Public License for more details. | +-- | | +-- | You should have received a copy of the GNU Affero General Public | +-- | License and the CiviCRM Licensing Exception along | +-- | with this program; if not, contact CiviCRM LLC | +-- | at info[AT]civicrm[DOT]org. If you have questions about the | +-- | GNU Affero General Public License or the licensing of CiviCRM, | +-- | see the CiviCRM license FAQ at http://civicrm.org/licensing | +-- +--------------------------------------------------------------------+ +-- +-- Generated from drop.tpl +-- DO NOT EDIT. Generated by CRM_Core_CodeGen +-- +-- /******************************************************* +-- * +-- * Clean up the exisiting tables +-- * +-- *******************************************************/ + +SET FOREIGN_KEY_CHECKS=0; + +DROP TABLE IF EXISTS `civicrm_stripe_paymentintent`; + +SET FOREIGN_KEY_CHECKS=1; diff --git a/stripe.civix.php b/stripe.civix.php index 51b73c90b884ed8404ea4c409dea4fe540a206d0..6de525c8f17aa6577c5c659e1a7f3ed6e5569ad1 100644 --- a/stripe.civix.php +++ b/stripe.civix.php @@ -457,5 +457,11 @@ function _stripe_civix_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) { function _stripe_civix_civicrm_entityTypes(&$entityTypes) { $entityTypes = array_merge($entityTypes, array ( + 'CRM_Stripe_DAO_StripePaymentintent' => + array ( + 'name' => 'StripePaymentintent', + 'class' => 'CRM_Stripe_DAO_StripePaymentintent', + 'table' => 'civicrm_stripe_paymentintent', + ), )); } diff --git a/stripe.php b/stripe.php index 35c383bbbc61b8a2f851237e0616daba5d8dac93..c729768a4745e298decb1e237ac941e00a88f1c6 100644 --- a/stripe.php +++ b/stripe.php @@ -70,6 +70,14 @@ function stripe_civicrm_managed(&$entities) { _stripe_civix_civicrm_managed($entities); } + +/** + * Implements hook_civicrm_entityTypes(). + */ +function stripe_civicrm_entityTypes(&$entityTypes) { + _stripe_civix_civicrm_entityTypes($entityTypes); +} + /** * Implements hook_civicrm_alterSettingsFolders(). */ @@ -132,10 +140,27 @@ function stripe_civicrm_buildForm($formName, &$form) { case 'CRM_Event_Form_Registration_ThankYou': \Civi::resources()->addScriptFile(E::LONG_NAME, 'js/civicrmStripeConfirm.js'); - // @todo: Not working yet because the paymentIntentID doesn't get passed - let's save/retrieve from db (use contribution and/or session key) + // This is a fairly nasty way of matching and retrieving our paymentIntent as it is no longer available. + $qfKey = CRM_Utils_Request::retrieve('qfKey', 'String'); + if (!empty($qfKey)) { + $paymentIntent = civicrm_api3('StripePaymentintent', 'getsingle', ['return' => ['paymentintent_id', 'status', 'contribution_id'], 'identifier' => $qfKey]); + } + + if (empty($paymentIntent['contribution_id'])) { + // If we now have a contribution ID try and update it so we can cross-reference the paymentIntent + $contributionId = $form->getVar('_values')['contributionId']; + if (!empty($contributionId)) { + civicrm_api3('StripePaymentintent', 'create', [ + 'id' => $paymentIntent['id'], + 'contribution_id' => $contributionId + ]); + } + } + $jsVars = [ 'id' => $form->_paymentProcessor['id'], - 'paymentIntentID' => \Civi::$statics['paymentIntentID'], + 'paymentIntentID' => $paymentIntent['paymentintent_id'], + 'paymentIntentStatus' => $paymentIntent['status'], 'publishableKey' => CRM_Core_Payment_Stripe::getPublicKeyById($form->_paymentProcessor['id']), 'jsDebug' => (boolean) \Civi::settings()->get('stripe_jsdebug'), ]; diff --git a/xml/schema/CRM/Stripe/StripePaymentintent.entityType.php b/xml/schema/CRM/Stripe/StripePaymentintent.entityType.php new file mode 100644 index 0000000000000000000000000000000000000000..999e2947592e5ff12da38733b10576bace0ffd65 --- /dev/null +++ b/xml/schema/CRM/Stripe/StripePaymentintent.entityType.php @@ -0,0 +1,11 @@ +<?php +// This file declares a new entity type. For more details, see "hook_civicrm_entityTypes" at: +// http://wiki.civicrm.org/confluence/display/CRMDOC/Hook+Reference +return array ( + 0 => + array ( + 'name' => 'StripePaymentintent', + 'class' => 'CRM_Stripe_DAO_StripePaymentintent', + 'table' => 'civicrm_stripe_paymentintent', + ), +); diff --git a/xml/schema/CRM/Stripe/StripePaymentintent.xml b/xml/schema/CRM/Stripe/StripePaymentintent.xml new file mode 100644 index 0000000000000000000000000000000000000000..cbfdf4842991abd766debb843cf832c876204797 --- /dev/null +++ b/xml/schema/CRM/Stripe/StripePaymentintent.xml @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="iso-8859-1" ?> + +<table> + <base>CRM/Stripe</base> + <class>StripePaymentintent</class> + <name>civicrm_stripe_paymentintent</name> + <comment>Stripe PaymentIntents</comment> + <log>true</log> + + <field> + <name>id</name> + <type>int unsigned</type> + <required>true</required> + <comment>Unique ID</comment> + </field> + <primaryKey> + <name>id</name> + <autoincrement>true</autoincrement> + </primaryKey> + + <field> + <name>paymentintent_id</name> + <title>PaymentIntent ID</title> + <type>varchar</type> + <length>255</length> + <comment>The PaymentIntent ID</comment> + </field> + <index> + <name>UI_paymentintent_id</name> + <fieldName>paymentintent_id</fieldName> + <unique>true</unique> + </index> + + <field> + <name>contribution_id</name> + <title>Contribution ID</title> + <type>int unsigned</type> + <comment>FK ID from civicrm_contribution</comment> + </field> + + <field> + <name>payment_processor_id</name> + <title>Payment Processor</title> + <type>int unsigned</type> + <comment>Foreign key to civicrm_payment_processor.id</comment> + <pseudoconstant> + <table>civicrm_payment_processor</table> + <keyColumn>id</keyColumn> + <labelColumn>name</labelColumn> + </pseudoconstant> + </field> + <foreignKey> + <name>payment_processor_id</name> + <table>civicrm_payment_processor</table> + <key>id</key> + <onDelete>SET NULL</onDelete> + </foreignKey> + + <field> + <name>description</name> + <title>Description</title> + <type>varchar</type> + <required>false</required> + <length>255</length> + <comment>Description of this paymentIntent</comment> + </field> + + <field> + <name>status</name> + <type>varchar</type> + <length>25</length> + <required>false</required> + <comment>The status of the paymentIntent</comment> + </field> + + <field> + <name>identifier</name> + <type>varchar</type> + <length>255</length> + <required>false</required> + <comment>An identifier that we can use in CiviCRM to find the paymentIntent if we do not have the ID (eg. session key)</comment> + </field> + + <field> + <name>contact_id</name> + <type>int unsigned</type> + <comment>FK to Contact</comment> + </field> + <foreignKey> + <name>contact_id</name> + <table>civicrm_contact</table> + <key>id</key> + <onDelete>CASCADE</onDelete> + </foreignKey> + + <field> + <name>created_date</name> + <title>Created Date</title> + <type>timestamp</type> + <default>CURRENT_TIMESTAMP</default> + <comment>When was paymentIntent created</comment> + </field> + + <field> + <name>flags</name> + <type>varchar</type> + <length>100</length> + <required>false</required> + <comment>Flags associated with this PaymentIntent (NC=no contributionID when doPayment called)</comment> + </field> + +</table>