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

add testing framework #245

Open
wants to merge 1 commit into
base: 4.7-dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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