Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue223 - Add Testing framework #242

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
319c265
updating to latest generated code from civix.
jmcclelland Jun 26, 2017
1aaee16
adding basic api and first test.
jmcclelland Jun 26, 2017
10583e1
adding tests for Ipn/recurring payments.
jmcclelland Jun 28, 2017
b4a6ae7
removing cache file accidentally committed.
jmcclelland Jun 28, 2017
2598090
include the subscription_id so we can test to ensure it was successful.
jmcclelland Jun 28, 2017
d5d9460
avoid undefined index error.
jmcclelland Jun 28, 2017
50ec128
fix nesting problem with run() and getRecurInfo()
jmcclelland Jun 28, 2017
ec7bf8d
test to ensure cancelling works.
jmcclelland Jun 28, 2017
b510875
Don't run the parent: we don't want a page displayed.
jmcclelland Jun 28, 2017
979b343
update test keys.
jmcclelland Jun 28, 2017
2cfe23b
new api command to populate the system log.
jmcclelland Jun 28, 2017
28cefc5
minor refactoring to make testing and api easier
jmcclelland Jun 30, 2017
8cd513a
documenting API
jmcclelland Jun 30, 2017
4f44cd4
adding documentation on tests.
jmcclelland Jun 30, 2017
0a55de1
add contact_id
jmcclelland Jun 30, 2017
8e2455e
Use new IPN class if it is available.
jmcclelland Jul 6, 2017
2e590d5
test for failed recurring payments.
jmcclelland Jul 7, 2017
e6dba97
refactoring - reuse code when possible.
jmcclelland Jul 8, 2017
c1b0efd
adding test for updates to recurring contribution.
jmcclelland Jul 8, 2017
035cc4a
begin work on testing memberships based on recurring contributions.
jmcclelland Jul 12, 2017
0d0dff8
test transfer of memberhip record to new recur cont id.
jmcclelland Jul 14, 2017
b9f76d0
properly work with refactored IPN code
jmcclelland Jul 14, 2017
b06814a
use refactored IPN if available.
jmcclelland Jul 17, 2017
b5b1ae1
turn off receipts if specified.
jmcclelland Jul 18, 2017
09bdede
update README to let people know about new receipt option.
jmcclelland Jul 18, 2017
b49be0e
add subscription info to brief output.
jmcclelland Jul 18, 2017
a35969b
friendlier response if the Ipn has already been processed.
jmcclelland Jul 18, 2017
f249625
don't overwrite params.
jmcclelland Jul 18, 2017
30c6c6f
Replacing DAO with api call
jmcclelland Oct 24, 2017
a46ae29
default must be preceded with api.
jmcclelland Oct 24, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CRM/Core/Payment/Stripe.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ public function stripeCatchErrors($op = 'create_customer', $stripe_params, $para
$body = $e->getJsonBody();
$err = $body['error'];

if (!array_key_exists('code', $err)) {
$err['code'] = null;
}
//$error_message .= 'Status is: ' . $e->getHttpStatus() . "<br />";
////$error_message .= 'Param is: ' . $err['param'] . "<br />";
$error_message .= 'Type: ' . $err['type'] . "<br />";
Expand Down Expand Up @@ -820,6 +823,7 @@ public function doRecurPayment(&$params, $amount, $stripe_customer) {
// Don't return a $params['trxn_id'] here or else recurring membership contribs will be set
// "Completed" prematurely. Webhook.php does that.

$params['subscription_id'] = $subscription_id;
return $params;

}
Expand Down
63 changes: 41 additions & 22 deletions CRM/Stripe/Page/Webhook.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
require_once 'CRM/Core/Page.php';

class CRM_Stripe_Page_Webhook extends CRM_Core_Page {
function run() {
function getRecurInfo($subscription_id,$test_mode) {

$query_params = array(
Expand Down Expand Up @@ -75,9 +74,19 @@ function getRecurInfo($subscription_id,$test_mode) {

return $recurring_info;
}

function run($data = null) {
// Get the data from Stripe.
$data_raw = file_get_contents("php://input");
$data = json_decode($data_raw);
$is_email_receipt = 1;
// Don't send emails while running php unit tests.
if (defined('STRIPE_PHPUNIT_TEST')) {
$is_email_receipt = 0;
}

if (is_null($data)) {
$data_raw = file_get_contents("php://input");
$data = json_decode($data_raw);
}
if (!$data) {
header('HTTP/1.1 406 Not acceptable');
CRM_Core_Error::Fatal("Stripe Callback: cannot json_decode data, exiting. <br /> $data");
Expand All @@ -90,13 +99,15 @@ function getRecurInfo($subscription_id,$test_mode) {
$processorId = CRM_Utils_Request::retrieve('ppid', 'Integer');
try {
if (empty($processorId)) {
$stripe_key = civicrm_api3('PaymentProcessor', 'getvalue', array(
'return' => 'user_name',
$processor_result = civicrm_api3('PaymentProcessor', 'get', array(
'return' => array('user_name', 'id'),
'payment_processor_type_id' => 'Stripe',
'is_test' => $test_mode,
'is_active' => 1,
'options' => array('limit' => 1),
));
$processorId = $processor_result['id'];
$stripe_key = $processor_result['values'][$processorId]['user_name'];
}
else {
$stripe_key = civicrm_api3('PaymentProcessor', 'getvalue', array(
Expand All @@ -115,9 +126,18 @@ function getRecurInfo($subscription_id,$test_mode) {
\Stripe\Stripe::setAppInfo('CiviCRM', CRM_Utils_System::version(), CRM_Utils_System::baseURL());
\Stripe\Stripe::setApiKey($stripe_key);

// Retrieve Event from Stripe using ID even though we already have the values now.
// This is for extra security precautions mentioned here: https://stripe.com/docs/webhooks
$stripe_event_data = \Stripe\Event::retrieve($data->id);
if (defined('STRIPE_PHPUNIT_TEST') && $data->type == 'invoice.payment_failed') {
// It's impossible to fake a failed payment on a recurring
// contribution in an automated way, so we are faking it in
// unit test.
$stripe_event_data = $data;
}
else {
// Retrieve Event from Stripe using ID even though we already have the values now.
// This is for extra security precautions mentioned here: https://stripe.com/docs/webhooks
$stripe_event_data = \Stripe\Event::retrieve($data->id);
}

$customer_id = $stripe_event_data->data->object->customer;

switch($stripe_event_data->type) {
Expand Down Expand Up @@ -149,7 +169,7 @@ function getRecurInfo($subscription_id,$test_mode) {
}

// First, get the recurring contribution id and previous contribution id.
$recurring_info = getRecurInfo($subscription_id,$test_mode);
$recurring_info = self::getRecurInfo($subscription_id,$test_mode);

// Fetch the previous contribution's status.
$previous_contribution = civicrm_api3('Contribution', 'get', array(
Expand Down Expand Up @@ -192,8 +212,9 @@ function getRecurInfo($subscription_id,$test_mode) {
'trxn_id' => $charge_id,
'total_amount' => $amount,
'fee_amount' => $fee,
'payment_processor_id' => $processorId,
'is_email_receipt' => $is_email_receipt,
));

return;

} else {
Expand All @@ -215,9 +236,8 @@ function getRecurInfo($subscription_id,$test_mode) {
'total_amount' => $amount,
'fee_amount' => $fee,
//'invoice_id' => $new_invoice_id - contribution.repeattransaction doesn't support it currently
'is_email_receipt' => 1,
));

'is_email_receipt' => $is_email_receipt,
));
// Update invoice_id manually. repeattransaction doesn't return the new contrib id either, so we update the db.
$query_params = array(
1 => array($new_invoice_id, 'String'),
Expand Down Expand Up @@ -264,9 +284,9 @@ function getRecurInfo($subscription_id,$test_mode) {
$transaction_id = $charge->id;

// First, get the recurring contribution id and previous contribution id.
$recurring_info = getRecurInfo($subscription_id,$test_mode);

// Fetch the previous contribution's status.
$recurring_info = self::getRecurInfo($subscription_id,$test_mode);
// Fetch the previous contribution's status.
$previous_contribution_status = civicrm_api3('Contribution', 'getvalue', array(
'sequential' => 1,
'return' => "contribution_status_id",
Expand All @@ -284,7 +304,7 @@ function getRecurInfo($subscription_id,$test_mode) {
'financial_type_id' => $recurring_info->financial_type_id,
'receive_date' => $fail_date,
'total_amount' => $amount,
'is_email_receipt' => 1,
'is_email_receipt' => $is_email_receipt,
'is_test' => $test_mode,
));

Expand All @@ -299,7 +319,7 @@ function getRecurInfo($subscription_id,$test_mode) {
'financial_type_id' => $recurring_info->financial_type_id,
'receive_date' => $fail_date,
'total_amount' => $amount,
'is_email_receipt' => 1,
'is_email_receipt' => $is_email_receipt,
'is_test' => $test_mode,
));
}
Expand Down Expand Up @@ -329,7 +349,7 @@ function getRecurInfo($subscription_id,$test_mode) {
$subscription_id = $stripe_event_data->data->object->id;

// First, get the recurring contribution id and previous contribution id.
$recurring_info = getRecurInfo($subscription_id,$test_mode);
$recurring_info = self::getRecurInfo($subscription_id,$test_mode);

//Cancel the recurring contribution
$result = civicrm_api3('ContributionRecur', 'cancel', array(
Expand Down Expand Up @@ -372,8 +392,8 @@ function getRecurInfo($subscription_id,$test_mode) {
$new_civi_invoice = md5(uniqid(rand(), TRUE));

// First, get the recurring contribution id and previous contribution id.
$recurring_info = getRecurInfo($subscription_id,$test_mode);

$recurring_info = self::getRecurInfo($subscription_id,$test_mode);
// Is there a pending charge due to a subcription change? Make up your mind!!
$previous_contribution = civicrm_api3('Contribution', 'get', array(
'sequential' => 1,
Expand Down Expand Up @@ -486,7 +506,6 @@ function getRecurInfo($subscription_id,$test_mode) {

}

parent::run();
}

}
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,37 @@ Inside the customer you will see a Subscriptions section. Click Cancel on the su
Stripe.com will cancel the subscription and will send a webhook to your site (if you have set the webhook options correctly).
Then the stripe_civicrm extension will process the webhook and cancel the Civi recurring contribution.

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:

* Listevents: Events are the notifications that Stripe sends to the Webhook. Listevents will list all notifications that have been sent. You can further restrict them with the following parameters:
* ppid - Use the given Payment Processor ID. By default, uses the saved, live Stripe payment processor and throws an error if there is more than one.
* type - Limit to the given Stripe events type. By default, show all. Optinally limit to, for example, invoice.payment_succeeded.
* limit - Limit number of results returned (100 is max, 10 is default).
* starting_after - Only return results after this event id. This can be used for paging purposes - if you want to retreive more than 100 results.
* Populatelog: If you are running a version of CiviCRM that supports the SystemLog - then this API call will populate your SystemLog with all of your past Stripe Events. You can safely re-run and not create duplicates. With a populated SystemLog - you can selectively replay events that may have caused errors the first time or otherwise not been properly recorded. Parameters:
* ppid - Use the given Payment Processor ID. By default, uses the saved, live Stripe payment processor and throws an error if there is more than one.
* Ipn: Replay a given Stripe Event. Parameters. This will always fetch the chosen Event from Stripe before replaying.
* id - The id from the SystemLog of the event to replay.
* evtid - The Event ID as provided by Stripe.
* ppid - Use the given Payment Processor ID. By default, uses the saved, live Stripe payment processor and throws an error if there is more than one.
* noreceipt - Set to 1 if you want to suppress the generation of receipts or set to 0 or leave out to send receipts normally.

TESTS
------------
This extension comes with two PHP Unit tests:

* Ipn - This unit test ensures that a recurring contribution is properly updated after the event is received from Stripe and that it is properly canceled when cancelled via Stripe.
* Direct - This unit test ensures that a direct payment to Stripe is properly recorded in the database.

Tests can be run most easily via an installation made through CiviCRM Buildkit (https://github.com/civicrm/civicrm-buildkit) by running:

phpunit4 tests/phpunit/CRM/Stripe/IpnTest.php
phpunit4 tests/phpunit/CRM/Stripe/DirectTest.php

GOOD TO KNOW
------------
* The stripe-php package has been added to this project & no longer needs to be
Expand Down
95 changes: 95 additions & 0 deletions api/v3/Stripe/Ipn.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

/**
* This api allows you to replay Stripe events.
*
* You can either pass the id of an entry in the System Log (which can
* be populated with the Stripe.PopulateLog call) or you can pass a
* event id from Stripe directly.
*
* When processing an event, the event will always be re-fetched from the
* Stripe server first, so this will not work while offline or with
* events that were not generated by the Stripe server.
*/

/**
* Stripe.Ipn API specification
*
* @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_Ipn_spec(&$spec) {
$spec['id']['title'] = ts("CiviCRM System Log id to replay from system log.");
$spec['evtid']['title'] = ts("An event id as generated by Stripe.");
$spec['ppid']['title'] = ts("The payment processor to use (required if using evtid)");
$spec['noreceipt']['title'] = ts("Set to 1 to override contribution page settings and do not send a receipt (default is off or 0). )");
$spec['noreceipt']['api.default'] = 0;
}

/**
* Stripe.Ipn API
*
* @param array $params
* @return array API result descriptor
* @see civicrm_api3_create_success
* @see civicrm_api3_create_error
* @throws API_Exception
*/
function civicrm_api3_stripe_Ipn($params) {
$object = NULL;
$ppid = NULL;
if (array_key_exists('id', $params)) {
$data = civicrm_api3('SystemLog', 'getsingle', array('id' => $params['id'], 'return' => array('message', 'context')));
if (empty($data)) {
throw new API_Exception('Failed to find that entry in the system log', 3234);
}
$object = json_decode($data['context']);
if (preg_match('/processor_id=([0-9]+)$/', $object['message'], $matches)) {
$ppid = $matches[1];
}
else {
throw new API_Exception('Failed to find payment processor id in system log', 3235);
}
}
elseif (array_key_exists('evtid', $params)) {
if (!array_key_exists('ppid', $params)) {
throw new API_Exception('Please pass the payment processor id (ppid) if using evtid.', 3236);
}
$ppid = $params['ppid'];
$results = civicrm_api3('PaymentProcessor', 'getsingle', array('id' => $ppid));
// YES! I know, password and user are backwards. wtf??
$sk = $results['user_name'];

require_once ("packages/stripe-php/init.php");
\Stripe\Stripe::setApiKey($sk);
$object = \Stripe\Event::retrieve($params['evtid']);
}
// Avoid a SQL error if this one has been processed already.
$sql = "SELECT COUNT(*) AS count FROM civicrm_contribution WHERE trxn_id = %0";
$count_params = array(
'trxn_id' => $object->data->object->charge,
);
$count_result = civicrm_api3('Contribution', 'get', $count_params);
if ($count_resulst['count'] > 0) {
return civicrm_api3_create_error("Ipn already processed.");
}
if (class_exists('CRM_Core_Payment_StripeIPN')) {
// The $_GET['processor_id'] value is normally set by
// CRM_Core_Payment::handlePaymentMethod
$_GET['processor_id'] = $ppid;
$ipnClass = new CRM_Core_Payment_StripeIPN($object);
if ($params['noreceipt'] == 1) {
$ipnClass->is_email_receipt = 0;
}
$ipnClass->main();
}
else {
// Deprecated method.
$_REQUEST['ppid'] = $ppid;
$stripe = new CRM_Stripe_Page_Webhook();
$stripe->run($object);
}
return civicrm_api3_create_success(array());

}
Loading