Commit f3bd762a authored by rubofvil's avatar rubofvil
Browse files

Add api to import subscribers and customers

parent b3a4d6d9
......@@ -126,10 +126,12 @@ class CRM_Stripe_Api {
switch ($stripeObject->status) {
case \Stripe\Subscription::STATUS_ACTIVE:
return CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'In Progress');
case \Stripe\Subscription::STATUS_CANCELED:
return CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Cancelled');
case \Stripe\Subscription::STATUS_PAST_DUE:
return CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Overdue');
default:
return NULL;
}
case 'customer_id':
......
<?php
use CRM_Stripe_ExtensionUtil as E;
/**
* Stripe.Importallcustomers
*
* @param array $spec description of fields supported by this API call
*
* @return void
*/
function _civicrm_api3_stripe_importallcustomers_spec(&$spec) {
$spec['ppid']['title'] = ts("Use the given Payment Processor ID");
$spec['ppid']['type'] = CRM_Utils_Type::T_INT;
$spec['ppid']['api.required'] = TRUE;
}
/**
* Stripe.Importallcustomers API
*
* @param array $params
* @return void
*/
function civicrm_api3_stripe_importallcustomers($params) {
$result = civicrm_api3('Stripe', 'importcustomers', [
'ppid' => $params['ppid']
]);
$imported = $result['values'];
while ($imported['continue_after']) {
$starting_after = $imported['continue_after'];
$result = civicrm_api3('Stripe', 'importcustomers', [
'ppid' => $params['ppid'],
'starting_after' => $starting_after,
]);
$additional = $result['values'];
$imported['imported'] = array_merge($imported['imported'], $additional['imported']);
$imported['skipped'] = array_merge($imported['skipped'], $additional['skipped']);
$imported['errors'] = array_merge($imported['errors'], $additional['errors']);
if ($additional['continue_after']) {
$imported['continue_after'] = $additional['continue_after'];
} else {
unset($imported['continue_after']);
}
}
return civicrm_api3_create_success($imported);
}
\ No newline at end of file
<?php
use CRM_Stripe_ExtensionUtil as E;
/**
* Stripe.Importallsubscriptions
*
* @param array $spec description of fields supported by this API call
*
* @return void
*/
function _civicrm_api3_stripe_importallsubscriptions_spec(&$spec) {
$spec['ppid']['title'] = ts("Use the given Payment Processor ID");
$spec['ppid']['type'] = CRM_Utils_Type::T_INT;
$spec['ppid']['api.required'] = TRUE;
}
/**
* Stripe.Importallsubscriptions API
*
* @param array $params
* @return void
*/
function civicrm_api3_stripe_importallsubscriptions($params) {
$result = civicrm_api3('Stripe', 'importsubscriptions', [
'ppid' => $params['ppid']
]);
$imported = $result['values'];
while ($imported['continue_after']) {
$starting_after = $imported['continue_after'];
$result = civicrm_api3('Stripe', 'importsubscriptions', [
'ppid' => $params['ppid'],
'starting_after' => $starting_after,
]);
$additional = $result['values'];
$imported['imported'] = array_merge($imported['imported'], $additional['imported']);
$imported['skipped'] = array_merge($imported['skipped'], $additional['skipped']);
$imported['errors'] = array_merge($imported['errors'], $additional['errors']);
if ($additional['continue_after']) {
$imported['continue_after'] = $additional['continue_after'];
} else {
unset($imported['continue_after']);
}
}
return civicrm_api3_create_success($imported);
}
\ No newline at end of file
<?php
use CRM_Stripe_ExtensionUtil as E;
/**
* Stripe.Importcustomers
*
* @param array $spec description of fields supported by this API call
*
* @return void
*/
function _civicrm_api3_stripe_importcustomers_spec(&$spec) {
$spec['ppid']['title'] = ts("Use the given Payment Processor ID");
$spec['ppid']['type'] = CRM_Utils_Type::T_INT;
$spec['ppid']['api.required'] = TRUE;
$spec['limit']['title'] = ts("Limit number of Customers/Subscriptions to be imported");
$spec['limit']['type'] = CRM_Utils_Type::T_INT;
$spec['limit']['api.required'] = FALSE;
$spec['starting_after']['title'] = ts('Start importing customers after this one');
$spec['starting_after']['type'] = CRM_Utils_Type::T_STRING;
$spec['starting_after']['api.required'] = FALSE;
$spec['customer']['title'] = ts('Import a specific customer');
$spec['customer']['type'] = CRM_Utils_Type::T_STRING;
$spec['customer']['api.required'] = FALSE;
}
/**
* Stripe.Importcustomers API
*
* @param array $params
* @return void
*/
function civicrm_api3_stripe_importcustomers($params) {
$ppid = $params['ppid'];
$limit = isset($params['limit']) ? $params['limit'] : 100;
$starting_after = $params['starting_after'];
// Get the payment processor and activate the Stripe API
$payment_processor = civicrm_api3('PaymentProcessor', 'getsingle', ['id' => $ppid]);
$processor = new CRM_Core_Payment_Stripe('', $payment_processor);
$processor->setAPIParams();
// Prepare an array to collect the results
$results = [
'imported' => [],
'skipped' => [],
'errors' => [],
'continue_after' => NULL
];
// Get customers from Stripe
$args = ["limit" => $limit];
if ($starting_after) {
$args['starting_after'] = $starting_after;
}
if ($params['customer']) {
$customer = \Stripe\Customer::retrieve($params['customer']);
$customers_stripe_clean = [$customer];
$customer_ids = [$customer->id];
}
else {
$customers_stripe = \Stripe\Customer::all($args);
// Exit if there aren't records to process
if (!count($customers_stripe->data)) {
return civicrm_api3_create_success($results);
}
// Search the customers in CiviCRM
$customer_ids = array_map(
function ($customer) { return $customer->id; },
$customers_stripe->data
);
$customers_stripe_clean = $customers_stripe->data;
}
$escaped_customer_ids = CRM_Utils_Type::escapeAll($customer_ids, 'String');
$filter_item = array_map(
function ($customer_id) { return "'$customer_id'"; },
$escaped_customer_ids
);
if (count($filter_item)) {
$select = "SELECT sc.*
FROM civicrm_stripe_customers AS sc
WHERE
sc.id IN (" . join(', ', $filter_item) . ") AND
sc.contact_id IS NOT NULL";
$dao = CRM_Core_DAO::executeQuery($select);
$customers_in_civicrm = $dao->fetchAll();
$customer_ids = array_map(
function ($customer) { return $customer['id']; },
$customers_in_civicrm
);
} else {
$customers_in_civicrm = $customer_ids = [];
}
foreach ($customers_stripe_clean as $customer) {
$results['continue_after'] = $customer->id;
$contact_id = NULL;
// Return if contact was found
if (array_search($customer->id, $customer_ids) !== FALSE) {
$customer_in_civicrm = array_filter($customers_in_civicrm,
function ($record) use($customer) { return $record['id'] == $customer->id; }
);
$results['skipped'][] = [
'contact_id' => end($customer_in_civicrm)['contact_id'],
'email' => $customer->email,
'stripe_id' => $customer->id,
];
continue;
}
// Search contact by email
if ($customer->email || $customer->name) {
$re = '/^([^\s]*)\s(.*)$/m';
preg_match_all($re, $customer->name, $matches, PREG_SET_ORDER, 0);
$first_name = isset($matches[0][1]) ? $matches[0][1] : "-";
$last_name = isset($matches[0][2]) ? $matches[0][2] : "-";
// Case to create customer without email
if(!$customer->email) {
$contact_ids = [];
}
else {
$email_result = civicrm_api3('Email', 'get', [
'sequential' => 1,
'email' => $customer->email,
]);
// List of contact ids using this email address
$contacts_by_email = array_map(
function ($found_email) { return $found_email['contact_id']; },
$email_result['values']
);
if (count($contacts_by_email)) {
// Only consider non deleted records of individuals
$undeleted_contacts = civicrm_api3('Contact', 'get', [
'return' => [ 'id' ],
'id' => [ 'IN' => $contacts_by_email ],
'is_deleted' => FALSE,
'contact_type' => 'Individual',
]);
$contact_ids = array_unique(
array_values(
array_map(
function ($found_contact) { return $found_contact['id']; },
$undeleted_contacts['values']
)
)
);
} else {
$contact_ids = [];
}
$data = [
'email' => $customer->email,
'stripe_id' => $customer->id,
];
if (property_exists($customer, 'name')) {
$data['name'] = $customer->name;
}
}
if (count($contact_ids) == 0) {
// Create the new contact record
$params_create_contact = [
'sequential' => 1,
'contact_type' => 'Individual',
'source' => 'Stripe > ' . $customer->description,
'first_name' => $first_name,
'last_name' => $last_name,
];
if ($customer->email) {
$params_create_contact['email'] = $customer->email;
}
$contact = civicrm_api3('Contact', 'create', $params_create_contact);
$contact_id = $contact['id'];
// Report the contact creation
$tag = 'imported';
$data['contact_id'] = $contact_id;
}
else if (count($contact_ids) == 1) {
$contact_id = end($contact_ids);
// Report the contact as found by email
$tag = 'skipped';
$data['contact_id'] = $contact_id;
}
else {
$contact_id = end($contact_ids);
// Report the contact as duplicated
$tag = 'errors';
$data['warning'] = E::ts("Number of contact records " .
"with this email is greater than 1. Contact id: $contact_id " .
"will be used");
$data['contact_ids'] = $contact_ids;
}
}
$results[$tag][] = $data;
// Try to create the Stripe customer record
if ($contact_id != NULL && $contact_id > 0) {
// Keep running if it already existed
try {
CRM_Stripe_Customer::add(
[
'contact_id' => $contact_id,
'id' => $customer->id,
'processor_id' => $ppid
]
);
} catch(Exception $e) {
}
// Update the record's 'is live' descriptor and its email
$is_live = ($payment_processor["is_test"] == 1) ? 0 : 1;
if ($customer->email) {
$queryParams = [
1 => [$customer->email, 'String'],
2 => [$customer->id, 'String'],
3 => [$is_live, 'Integer'],
4 => [$contact_id, 'Integer'],
];
CRM_Core_DAO::executeQuery("UPDATE civicrm_stripe_customers
SET is_live = %3, email = %1, contact_id = %4
WHERE id = %2", $queryParams);
}
else {
$queryParams = [
1 => [$customer->id, 'String'],
2 => [$is_live, 'Integer'],
3 => [$contact_id, 'Integer'],
];
CRM_Core_DAO::executeQuery("UPDATE civicrm_stripe_customers
SET is_live = %2, contact_id = %3
WHERE id = %1", $queryParams);
}
}
}
return civicrm_api3_create_success($results);
}
<?php
use CRM_Stripe_ExtensionUtil as E;
/**
* Stripe.Importsubscriptions
*
* @param array $spec description of fields supported by this API call
*
* @return void
*/
function _civicrm_api3_stripe_importsubscriptions_spec(&$spec) {
$spec['ppid']['title'] = ts('Use the given Payment Processor ID');
$spec['ppid']['type'] = CRM_Utils_Type::T_INT;
$spec['ppid']['api.required'] = TRUE;
$spec['limit']['title'] = ts('Limit number of Customers/Subscriptions to be imported');
$spec['limit']['type'] = CRM_Utils_Type::T_INT;
$spec['limit']['api.required'] = FALSE;
$spec['starting_after']['title'] = ts('Start importing subscriptions after this one');
$spec['starting_after']['type'] = CRM_Utils_Type::T_STRING;
$spec['starting_after']['api.required'] = FALSE;
}
/**
* Stripe.Importsubscriptions API
*
* @param array $params
* @return void
*/
function civicrm_api3_stripe_importsubscriptions($params) {
$ppid = $params['ppid'];
$limit = isset($params['limit']) ? $params['limit'] : 100;
$starting_after = $params['starting_after'];
// Get the payment processor and activate the Stripe API
$payment_processor = civicrm_api3('PaymentProcessor', 'getsingle', ['id' => $ppid]);
$processor = new CRM_Core_Payment_Stripe('', $payment_processor);
$processor->setAPIParams();
// Get the subscriptions from Stripe
$args = [
'limit' => $limit,
'status' => 'all',
];
if ($starting_after) {
$args['starting_after'] = $starting_after;
}
$stripe_subscriptions = \Stripe\Subscription::all($args);
$stripe_subscription_ids = array_map(
function ($subscription) { return $subscription->id; },
$stripe_subscriptions->data
);
// Prepare to collect the results
$results = [
'imported' => [],
'skipped' => [],
'errors' => [],
'continue_after' => NULL
];
// Exit if there aren't records to process
if (!count($stripe_subscription_ids)) {
return civicrm_api3_create_success($results);
}
// Get subscriptions generated in CiviCRM
$recurring_contributions = civicrm_api3('ContributionRecur', 'get', [
'sequential' => 1,
'options' => [ 'limit' => 0 ],
'payment_processor_id' => $ppid,
'trxn_id' => [ 'IN' => $stripe_subscription_ids ]
]);
$subscritpions_civicrm = [];
if (is_array($recurring_contributions['values'])) {
foreach ($recurring_contributions['values'] as $recurring_contribution) {
$trxn_id = $recurring_contribution['trxn_id'];
$subscritpions_civicrm[$trxn_id] = [
'contact_id' => $recurring_contribution['contact_id'],
'recur_id' => $recurring_contribution['id'],
'stripe_id' => $trxn_id,
];
}
}
foreach ($stripe_subscriptions as $stripe_subscription) {
$results['continue_after'] = $stripe_subscription->id;
$new_subscription = [
'is_test' => $payment_processor['is_test'],
'payment_processor_id' => $ppid,
'subscription_id' => $stripe_subscription->id,
];
// Check if the subscription exists in CiviCRM
if (isset($subscritpions_civicrm[$stripe_subscription->id])) {
$results['skipped'][] = $subscritpions_civicrm[$stripe_subscription->id];
$new_subscription['recur_id'] = $subscritpions_civicrm[$stripe_subscription->id]['recur_id'];
}
// Search the Stripe customer to get the contact id
$customer_civicrm = civicrm_api3('StripeCustomer', 'get', [
'sequential' => 1,
'id' => $stripe_subscription->customer,
]);
if (isset($customer_civicrm['values'][0]['contact_id'])) {
$new_subscription['contact_id'] = $customer_civicrm['values'][0]['contact_id'];
}
// Return the record with error if the contact wasn't found
if (!isset($new_subscription['contact_id']) || (! $new_subscription['contact_id'])) {
$new_subscription['error'] = 'Customer not found';
$new_subscription['stripe_customer'] = $stripe_subscription->customer;
$results['errors'][] = $new_subscription;
continue;
}
// Create the subscription
$created_subscription = civicrm_api3('StripeSubscription', 'import', $new_subscription);
$new_subscription['recur_id'] = $created_subscription['values']['recur_id'];
$results['imported'][] = $new_subscription;
}
return civicrm_api3_create_success($results);
}
......@@ -166,9 +166,6 @@ function civicrm_api3_stripe_subscription_import($params) {
];
$customer = civicrm_api3('StripeCustomer', 'get', $customerParams);
if (empty($customer['count'])) {
civicrm_api3('StripeCustomer', 'create', $customerParams);
}
// Create the recur record in CiviCRM
$contributionRecurParams = [
......@@ -198,7 +195,7 @@ function civicrm_api3_stripe_subscription_import($params) {
// Get the invoices for the subscription
$invoiceParams = [
'customer' => CRM_Stripe_Api::getObjectParam('customer_id', $stripeSubscription),
'limit' => 10,
'limit' => 100,
];
$stripeInvoices = \Stripe\Invoice::all($invoiceParams);
foreach ($stripeInvoices->data as $stripeInvoice) {
......@@ -238,9 +235,8 @@ function civicrm_api3_stripe_subscription_import($params) {
elseif ($params['contribution_id']) {
$contributionParams['id'] = $params['contribution_id'];
}
$contribution = civicrm_api3('Contribution', 'create', $contributionParams);
break;
}
}
......
# API
This extension comes with several APIs to help you troubleshoot problems. These can be run via /civicrm/api or via drush if you are using Drupal (drush cvapi Stripe.XXX).
The api commands are:
......@@ -27,6 +28,7 @@ The api commands are:
* `StripeCustomer.updatestripemetadata` - Used to update stripe customers that were created using an older version of the extension (adds name to description and contact_id as a metadata field).
## StripeSubscription
* `StripeSubscription.updatetransactionids` - Used to migrate civicrm_stripe_subscriptions to use recurring contributions directly.
* `StripeSubscription.copytrxnidtoprocessorid` - Used to copy trxn_id to processor_id in civicrm_contribution_recur table so we can use cancelSubscription. Hopefully this won't be needed in future versions of CiviCRM if we can pass more sensible values to the cancelSubscription function.
* `StripeSubscription.import` - Use to import subscriptions into CiviCRM that are in Stripe but not CiviCRM.
......@@ -46,3 +48,27 @@ It's not advised that you use this API for anything else.
Parameters:
* delete_old: Delete old records from database. Specify 0 to disable. Default is "-3 month"
* cancel_incomplete: Cancel incomplete paymentIntents in your stripe account. Specify 0 to disable. Default is "-1 hour"
## Stripe Import
This API import all customers and later the subcriptions from this method(and the contributions associated).
* `Stripe.importsubscriptions`
* Parameters:
* `limit`: Numer of elements to import, customers/subscriptors.
* `ppid` : Use the given Payment Processor ID.
* `starting_after` : Not required, begin after the last subscriber.
* Example
* `drush cvapi Stripe.importsubscriptions ppid=6 limit=1 starting_after=sub_DDRzfRsxxxx`
* Doc
* [Stripe doc](https://stripe.com/docs/api/subscriptions/list)
* `Stripe.importcustomers`
* Parameters:
* `limit`: Numer of elements to import, customers/subscriptors.
* `ppid` : Use the given Payment Processor ID.
* `starting_after` : Not required, begin after the last customer.
* Example
* `drush cvapi Stripe.importcustomers ppid=6 limit=1 starting_after=cus_GiMd3xxxx`
* Doc
* [Stripe doc](https://stripe.com/docs/api/customers/list)
......@@ -49,6 +49,7 @@ Where:
* Use minifier extension to minify js/css assets (much easier for development as we don't ship minified files anymore).
## Release 6.3.2 - Security Release
If you are using Stripe on public forms (without authentication) it is **strongly** recommended that you upgrade and consider installing the new **firewall** extension.
Increasingly spammers are finding CiviCRM sites and spamming the linked Stripe account with 1000s of attempted payments
......@@ -267,17 +268,21 @@ There are no database changes in this release but you should update your Stripe
* Use the parameter on the recurring contribution to decide whether to send out email receipts.
## Release 5.2
*This release introduces a number of new features, standardises the behaviour of recurring contributions/memberships to match standard CiviCRM functionality and does a major cleanup of the backend code to improve stability and allow for new features.*
### Highlights:
* Support Cancel Subscription from CiviCRM and from Stripe.
### Breaking changes:
* The extension now uses the standard CiviCRM Contribution.completetransaction and Contribution.repeattransaction API to handle creation/update of recurring contributions. This means that automatic membership renewal etc. is handled in the standard CiviCRM way instead of using custom code in the Stripe extension. The behaviour *should* be the same but some edge-cases may be fixed while others may appear. Any bugs in this area will now need to be fixed in CiviCRM core - if you want to help with that see https://github.com/civicrm/civicrm-core/pull/11556.
* When recurring contributions were updated by Stripe, they were marked cancelled and a new one created in CiviCRM. This was non-standard behaviour and causes issues with CiviCRM core functionality for membership renewal etc. This has now been changed so only one recurring contribution per subscription will ever exist, which will be updated as necessary during it's lifecycle.
* Different payment amounts are now supported for each contribution in a recurring contribution. Previously they were explicitly rejected by the extension.
### Changes:
* Add http response codes for webhook (invalid parameters now returns 400 Bad Request).
* Major refactor of webhook / events handling (fixes multiple issues, now tested and working on Joomla / Wordpress / Drupal 7).
* Update to latest version of stripe-php library.
......
Markdown is supported
0% or .