Skip to content
Snippets Groups Projects
Commit d6c8df91 authored by mattwire's avatar mattwire
Browse files

Handle customer subscriptions in different currencies (create a new customer for each currency)

parent 2bad2679
Branches
Tags
No related merge requests found
......@@ -12,6 +12,7 @@
use Brick\Money\Money;
use Civi\Api4\ContributionRecur;
use Civi\Api4\PaymentprocessorWebhook;
use Civi\Api4\StripeCustomer;
use CRM_Stripe_ExtensionUtil as E;
use Civi\Payment\PropertyBag;
use Stripe\Stripe;
......@@ -712,6 +713,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
'contact_id' => $propertyBag->getContactID(),
'processor_id' => $this->getPaymentProcessor()['id'],
'email' => $propertyBag->getEmail(),
'currency' => mb_strtolower($propertyBag->getCurrency()),
// Include this to allow redirect within session on payment failure
'error_url' => $propertyBag->getCustomProperty('error_url'),
];
......@@ -720,27 +722,54 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
// 1. Look for an existing customer.
// 2. If no customer (or a deleted customer found), create a new one.
// 3. If existing customer found, update the metadata that Stripe holds for this customer.
$stripeCustomerID = CRM_Stripe_Customer::find($customerParams);
// Customers can only be billed for subscriptions in a single currency. currency field was added in 6.10
// So we look for a customer with matching currency and if not check for an empty currency (if customer was created before 6.10)
$stripeCustomer = StripeCustomer::get(FALSE)
->addWhere('contact_id', '=', $customerParams['contact_id'])
->addWhere('processor_id', '=', $customerParams['processor_id'])
->addClause('OR', ['currency', 'IS EMPTY'], ['currency', '=', $customerParams['currency']])
->addOrderBy('currency', 'DESC')
->execute()->first();
// Customer not in civicrm database. Create a new Customer in Stripe.
if (!isset($stripeCustomerID)) {
$stripeCustomer = CRM_Stripe_Customer::create($customerParams, $this);
if (empty($stripeCustomer)) {
$stripeCustomerObject = CRM_Stripe_Customer::create($customerParams, $this);
}
else {
$shouldDeleteStripeCustomer = $shouldCreateNewStripeCustomer = FALSE;
// Customer was found in civicrm database, fetch from Stripe.
try {
$stripeCustomer = $this->stripeClient->customers->retrieve($stripeCustomerID);
$shouldDeleteStripeCustomer = $stripeCustomer->isDeleted();
$stripeCustomerObject = $this->stripeClient->customers->retrieve($stripeCustomer['customer_id']);
$shouldDeleteStripeCustomer = $stripeCustomerObject->isDeleted();
} catch (Exception $e) {
$err = $this->parseStripeException('retrieve_customer', $e);
\Civi::log()->error($this->getLogPrefix() . 'Failed to retrieve Stripe Customer: ' . $err['code']);
$shouldDeleteStripeCustomer = TRUE;
}
if (empty($stripeCustomer['currency'])) {
// We have no currency set for the customer in the CiviCRM database
if ($stripeCustomerObject->currency === $customerParams['currency']) {
// We can use this customer but we need to update the currency in the civicrm database
StripeCustomer::update(FALSE)
->addValue('currency', $stripeCustomerObject->currency)
->addWhere('id', '=', $stripeCustomer['id'])
->execute();
}
else {
// We need to create a new customer
$shouldCreateNewStripeCustomer = TRUE;
}
}
if ($shouldDeleteStripeCustomer) {
// Customer doesn't exist or was deleted, create a new one
// Customer was deleted, delete it.
CRM_Stripe_Customer::delete($customerParams);
}
if ($shouldDeleteStripeCustomer || $shouldCreateNewStripeCustomer) {
try {
$stripeCustomer = CRM_Stripe_Customer::create($customerParams, $this);
$stripeCustomerObject = CRM_Stripe_Customer::create($customerParams, $this);
} catch (Exception $e) {
// We still failed to create a customer
$err = $this->parseStripeException('create_customer', $e);
......@@ -748,7 +777,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
}
}
}
return $stripeCustomer;
return $stripeCustomerObject;
}
/**
......
......@@ -41,6 +41,7 @@ class CRM_Stripe_Customer {
$result = StripeCustomer::get(FALSE)
->addWhere('contact_id', '=', $params['contact_id'])
->addWhere('processor_id', '=', $params['processor_id'])
->addClause('OR', ['currency', 'IS EMPTY'], ['currency', '=', $params['currency']])
->addSelect('customer_id')
->execute();
......@@ -80,17 +81,6 @@ class CRM_Stripe_Customer {
] + $options, ['customer_id']);
}
/**
* Add a new Stripe customer to the CiviCRM database
*
* @param array $params
*
* @throws \Civi\Payment\Exception\PaymentProcessorException
*/
public static function add(array $params) {
return civicrm_api4('StripeCustomer', 'create', ['checkPermissions' => FALSE, 'values' => $params]);
}
/**
* @param array $params
* @param \CRM_Core_Payment_Stripe $stripe
......@@ -110,7 +100,7 @@ class CRM_Stripe_Customer {
$stripeCustomerParams = CRM_Stripe_BAO_StripeCustomer::getStripeCustomerMetadata($params['contact_id'], $params['invoice_settings'] ?? []);
try {
$stripeCustomer = $stripe->stripeClient->customers->create($stripeCustomerParams);
$stripeCustomerObject = $stripe->stripeClient->customers->create($stripeCustomerParams);
}
catch (Exception $e) {
$err = $stripe->parseStripeException('create_customer', $e);
......@@ -119,14 +109,14 @@ class CRM_Stripe_Customer {
}
// Store the relationship between CiviCRM's email address for the Contact & Stripe's Customer ID.
$params = [
'contact_id' => $params['contact_id'],
'customer_id' => $stripeCustomer->id,
'processor_id' => $params['processor_id'],
];
self::add($params);
return $stripeCustomer;
StripeCustomer::create(FALSE)
->addValue('contact_id', $params['contact_id'])
->addValue('customer_id', $stripeCustomerObject->id)
->addValue('processor_id', $params['processor_id'])
->addValue('currency', $params['currency'])
->execute();
return $stripeCustomerObject;
}
/**
......
......@@ -6,7 +6,7 @@
*
* Generated from com.drastikbydesign.stripe/xml/schema/CRM/Stripe/StripeCustomer.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
* (GenCodeChecksum:6fd6993b0c77bb447ecfb04a5fc2ef34)
* (GenCodeChecksum:ecd2b24395fdeed772fce5cedf8d416e)
*/
use CRM_Stripe_ExtensionUtil as E;
......@@ -67,6 +67,15 @@ class CRM_Stripe_DAO_StripeCustomer extends CRM_Core_DAO {
*/
public $processor_id;
/**
* 3 character string, value from Stripe customer.
*
* @var string|null
* (SQL type: varchar(3))
* Note that values will be retrieved from the database as a string.
*/
public $currency;
/**
* Class constructor.
*/
......@@ -111,8 +120,15 @@ class CRM_Stripe_DAO_StripeCustomer extends CRM_Core_DAO {
'id' => [
'name' => 'id',
'type' => CRM_Utils_Type::T_INT,
'title' => E::ts('ID'),
'description' => E::ts('Unique ID'),
'required' => TRUE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_customers.id',
'table_name' => 'civicrm_stripe_customers',
'entity' => 'StripeCustomer',
......@@ -128,6 +144,12 @@ class CRM_Stripe_DAO_StripeCustomer extends CRM_Core_DAO {
'description' => E::ts('Stripe Customer ID'),
'maxlength' => 255,
'size' => CRM_Utils_Type::HUGE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_customers.customer_id',
'table_name' => 'civicrm_stripe_customers',
'entity' => 'StripeCustomer',
......@@ -138,7 +160,14 @@ class CRM_Stripe_DAO_StripeCustomer extends CRM_Core_DAO {
'contact_id' => [
'name' => 'contact_id',
'type' => CRM_Utils_Type::T_INT,
'title' => E::ts('Contact ID'),
'description' => E::ts('FK to Contact'),
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_customers.contact_id',
'table_name' => 'civicrm_stripe_customers',
'entity' => 'StripeCustomer',
......@@ -152,6 +181,12 @@ class CRM_Stripe_DAO_StripeCustomer extends CRM_Core_DAO {
'type' => CRM_Utils_Type::T_INT,
'title' => E::ts('Payment Processor ID'),
'description' => E::ts('ID from civicrm_payment_processor'),
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_customers.processor_id',
'table_name' => 'civicrm_stripe_customers',
'entity' => 'StripeCustomer',
......@@ -164,6 +199,29 @@ class CRM_Stripe_DAO_StripeCustomer extends CRM_Core_DAO {
],
'add' => NULL,
],
'currency' => [
'name' => 'currency',
'type' => CRM_Utils_Type::T_STRING,
'title' => E::ts('Currency'),
'description' => E::ts('3 character string, value from Stripe customer.'),
'maxlength' => 3,
'size' => CRM_Utils_Type::FOUR,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_customers.currency',
'headerPattern' => '/cur(rency)?/i',
'dataPattern' => '/^[A-Z]{3}$/i',
'default' => NULL,
'table_name' => 'civicrm_stripe_customers',
'entity' => 'StripeCustomer',
'bao' => 'CRM_Stripe_DAO_StripeCustomer',
'localizable' => 0,
'add' => NULL,
],
];
CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']);
}
......
......@@ -6,7 +6,7 @@
*
* Generated from com.drastikbydesign.stripe/xml/schema/CRM/Stripe/StripePaymentintent.xml
* DO NOT EDIT. Generated by CRM_Core_CodeGen
* (GenCodeChecksum:50c4ef9341699c0242005eede56e04d8)
* (GenCodeChecksum:5aaffbeee4dd172cae4daf0e85a77600)
*/
use CRM_Stripe_ExtensionUtil as E;
......@@ -184,8 +184,15 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
'id' => [
'name' => 'id',
'type' => CRM_Utils_Type::T_INT,
'title' => E::ts('ID'),
'description' => E::ts('Unique ID'),
'required' => TRUE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_paymentintent.id',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
......@@ -201,6 +208,12 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
'description' => E::ts('The Stripe PaymentIntent/SetupIntent/PaymentMethod ID'),
'maxlength' => 255,
'size' => CRM_Utils_Type::HUGE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_paymentintent.stripe_intent_id',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
......@@ -213,6 +226,12 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
'type' => CRM_Utils_Type::T_INT,
'title' => E::ts('Contribution ID'),
'description' => E::ts('FK ID from civicrm_contribution'),
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_paymentintent.contribution_id',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
......@@ -225,6 +244,12 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
'type' => CRM_Utils_Type::T_INT,
'title' => E::ts('Payment Processor'),
'description' => E::ts('Foreign key to civicrm_payment_processor.id'),
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_paymentintent.payment_processor_id',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
......@@ -246,6 +271,12 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
'required' => FALSE,
'maxlength' => 255,
'size' => CRM_Utils_Type::HUGE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_paymentintent.description',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
......@@ -261,6 +292,12 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
'required' => FALSE,
'maxlength' => 25,
'size' => CRM_Utils_Type::MEDIUM,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_paymentintent.status',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
......@@ -276,6 +313,12 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
'required' => FALSE,
'maxlength' => 255,
'size' => CRM_Utils_Type::HUGE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_paymentintent.identifier',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
......@@ -286,7 +329,14 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
'contact_id' => [
'name' => 'contact_id',
'type' => CRM_Utils_Type::T_INT,
'title' => E::ts('Contact ID'),
'description' => E::ts('FK to Contact'),
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_paymentintent.contact_id',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
......@@ -300,6 +350,12 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
'type' => CRM_Utils_Type::T_TIMESTAMP,
'title' => E::ts('Created Date'),
'description' => E::ts('When was paymentIntent created'),
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_paymentintent.created_date',
'default' => 'CURRENT_TIMESTAMP',
'table_name' => 'civicrm_stripe_paymentintent',
......@@ -316,6 +372,12 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
'required' => FALSE,
'maxlength' => 100,
'size' => CRM_Utils_Type::HUGE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_paymentintent.flags',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
......@@ -331,6 +393,12 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
'required' => FALSE,
'maxlength' => 255,
'size' => CRM_Utils_Type::HUGE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_paymentintent.referrer',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
......@@ -346,6 +414,12 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
'required' => FALSE,
'maxlength' => 255,
'size' => CRM_Utils_Type::HUGE,
'usage' => [
'import' => FALSE,
'export' => FALSE,
'duplicate_matching' => FALSE,
'token' => FALSE,
],
'where' => 'civicrm_stripe_paymentintent.extra_data',
'table_name' => 'civicrm_stripe_paymentintent',
'entity' => 'StripePaymentintent',
......
......@@ -423,7 +423,7 @@ class CRM_Stripe_PaymentIntent {
}
}
catch (Exception $e) {
\Civi::log()->debug(get_class($e) . $e->getMessage());
\Civi::log('stripe')->debug($this->paymentProcessor->getLogPrefix() . get_class($e) . $e->getMessage());
}
}
else {
......
......@@ -420,7 +420,7 @@ class CRM_Stripe_Upgrader extends CRM_Extension_Upgrader_Base {
}
public function upgrade_6803() {
$this->ctx->log->info('Applying Stripe update 5028. In civicrm_stripe_customers database table, rename id to customer_id, add new id column');
$this->ctx->log->info('In civicrm_stripe_customers database table, rename id to customer_id, add new id column');
if (!CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_stripe_customers', 'customer_id')) {
// ALTER TABLE ... RENAME COLUMN only in MySQL8+
CRM_Core_DAO::executeQuery("ALTER TABLE civicrm_stripe_customers CHANGE COLUMN id customer_id varchar(255) COMMENT 'Stripe Customer ID'");
......@@ -433,4 +433,12 @@ class CRM_Stripe_Upgrader extends CRM_Extension_Upgrader_Base {
return TRUE;
}
public function upgrade_6900() {
$this->ctx->log->info('Add currency to civicrm_stripe_customers because the customer can only have one currency for subscriptions');
if (!CRM_Core_BAO_SchemaHandler::checkIfFieldExists('civicrm_stripe_customers', 'currency')) {
CRM_Core_DAO::executeQuery("ALTER TABLE civicrm_stripe_customers ADD COLUMN currency varchar(3) DEFAULT NULL COMMENT '3 character string, value from Stripe customer.'");
}
return TRUE;
}
}
......@@ -102,7 +102,7 @@ function _civicrm_api3_stripe_customer_create_spec(&$spec) {
* @throws \Civi\Payment\Exception\PaymentProcessorException
*/
function civicrm_api3_stripe_customer_create($params) {
CRM_Stripe_Customer::add($params);
civicrm_api4('StripeCustomer', 'create', ['checkPermissions' => FALSE, 'values' => $params]);
return civicrm_api3_create_success([]);
}
......
......@@ -16,6 +16,7 @@ CREATE TABLE `civicrm_stripe_customers` (
`customer_id` varchar(255) COMMENT 'Stripe Customer ID',
`contact_id` int unsigned COMMENT 'FK to Contact',
`processor_id` int unsigned COMMENT 'ID from civicrm_payment_processor',
`currency` varchar(3) DEFAULT NULL COMMENT '3 character string, value from Stripe customer.',
PRIMARY KEY (`id`),
UNIQUE INDEX `customer_id`(customer_id),
CONSTRAINT FK_civicrm_stripe_customers_contact_id FOREIGN KEY (`contact_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE
......
......@@ -54,12 +54,15 @@
<labelColumn>name</labelColumn>
</pseudoconstant>
</field>
<!-- <foreignKey>
<name>processor_id</name>
<table>civicrm_payment_processor</table>
<key>id</key>
<onDelete>SET NULL</onDelete>
</foreignKey> -->
<field>
<name>currency</name>
<type>varchar</type>
<length>3</length>
<default>NULL</default>
<headerPattern>/cur(rency)?/i</headerPattern>
<dataPattern>/^[A-Z]{3}$/i</dataPattern>
<comment>3 character string, value from Stripe customer.</comment>
</field>
</table>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment