3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
13 * Class CRM_Core_Payment_PayPalProIPNTest
16 class CRM_Core_Payment_PayPalIPNTest
extends CiviUnitTestCase
{
17 protected $_contributionID;
18 protected $_invoiceID = 'c2r9c15f7be20b4f3fef1f77e4c37424';
19 protected $_financialTypeID = 1;
20 protected $_contactID;
21 protected $_contributionRecurID;
22 protected $_contributionPageID;
23 protected $_paymentProcessorID;
24 protected $_customFieldID;
26 * IDs of entities created to support the tests.
35 public function setUp() {
37 $this->_paymentProcessorID
= $this->paymentProcessorCreate(['is_test' => 0, 'payment_processor_type_id' => 'PayPal_Standard']);
38 $this->_contactID
= $this->individualCreate();
39 $contributionPage = $this->callAPISuccess('contribution_page', 'create', [
40 'title' => "Test Contribution Page",
41 'financial_type_id' => $this->_financialTypeID
,
43 'payment_processor' => $this->_paymentProcessorID
,
45 $this->_contributionPageID
= $contributionPage['id'];
51 public function tearDown() {
52 $this->quickCleanUpFinancialEntities();
56 * Test IPN response updates contribution and invoice is attached in mail reciept
58 * The scenario is that a pending contribution exists and the IPN call will update it to completed.
59 * And also if Tax and Invoicing is enabled, this unit test ensure that invoice pdf is attached with email recipet
61 * @throws \CRM_Core_Exception
63 public function testInvoiceSentOnIPNPaymentSuccess() {
64 $this->enableTaxAndInvoicing();
66 $pendingStatusID = CRM_Core_PseudoConstant
::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending');
67 $completedStatusID = CRM_Core_PseudoConstant
::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
69 'payment_processor_id' => $this->_paymentProcessorID
,
70 'contact_id' => $this->_contactID
,
72 'invoice_id' => $this->_invoiceID
,
73 'contribution_status_id' => $pendingStatusID,
74 'is_email_receipt' => TRUE,
76 $this->_contributionID
= $this->contributionCreate($params);
77 $contribution = $this->callAPISuccess('contribution', 'get', ['id' => $this->_contributionID
, 'sequential' => 1]);
78 // assert that contribution created before handling payment via paypal standard has no transaction id set and pending status
79 $this->assertEquals(NULL, $contribution['values'][0]['trxn_id']);
80 $this->assertEquals($pendingStatusID, $contribution['values'][0]['contribution_status_id']);
83 $_REQUEST = ['q' => CRM_Utils_System
::url('civicrm/payment/ipn/' . $this->_paymentProcessorID
)] +
$this->getPaypalTransaction();
85 $mut = new CiviMailUtils($this, TRUE);
86 $paymentProcesors = $this->callAPISuccessGetSingle('PaymentProcessor', ['id' => $this->_paymentProcessorID
]);
87 $payment = Civi\Payment\System
::singleton()->getByProcessor($paymentProcesors);
88 $payment->handlePaymentNotification();
90 // Check if invoice pdf is attached with contribution mail reciept
92 'Content-Transfer-Encoding: base64',
93 'Content-Type: application/pdf',
94 'filename=Invoice.pdf',
98 $contribution = $this->callAPISuccess('contribution', 'get', ['id' => $this->_contributionID
, 'sequential' => 1]);
99 // assert that contribution is completed after getting response from paypal standard which has transaction id set and completed status
100 $this->assertEquals($_REQUEST['txn_id'], $contribution['values'][0]['trxn_id']);
101 $this->assertEquals($completedStatusID, $contribution['values'][0]['contribution_status_id']);
105 * Test IPN response updates contribution_recur & contribution for first & second contribution.
107 * The scenario is that a pending contribution exists and the first call will update it to completed.
108 * The second will create a new contribution.
110 public function testIPNPaymentRecurSuccess() {
111 $this->setupRecurringPaymentProcessorTransaction([], ['total_amount' => '15.00']);
112 $paypalIPN = new CRM_Core_Payment_PayPalIPN($this->getPaypalRecurTransaction());
114 $contribution1 = $this->callAPISuccess('contribution', 'getsingle', ['id' => $this->_contributionID
]);
115 $this->assertEquals(1, $contribution1['contribution_status_id']);
116 $this->assertEquals('8XA571746W2698126', $contribution1['trxn_id']);
117 // source gets set by processor
118 $this->assertTrue(substr($contribution1['contribution_source'], 0, 20) == "Online Contribution:");
119 $contributionRecur = $this->callAPISuccess('contribution_recur', 'getsingle', ['id' => $this->_contributionRecurID
]);
120 $this->assertEquals(5, $contributionRecur['contribution_status_id']);
121 $paypalIPN = new CRM_Core_Payment_PayPalIPN($this->getPaypalRecurSubsequentTransaction());
123 $contributions = $this->callAPISuccess('contribution', 'get', [
124 'contribution_recur_id' => $this->_contributionRecurID
,
127 $this->assertEquals(2, $contributions['count']);
128 $contribution2 = $contributions['values'][1];
129 $this->assertEquals('secondone', $contribution2['trxn_id']);
130 $paramsThatShouldMatch = [
134 'payment_instrument',
135 'payment_instrument_id',
139 foreach ($paramsThatShouldMatch as $match) {
140 $this->assertEquals($contribution1[$match], $contribution2[$match]);
145 * Test IPN response updates contribution_recur & contribution for first & second contribution.
147 * @throws \CRM_Core_Exception
148 * @throws \CiviCRM_API3_Exception
150 public function testIPNPaymentMembershipRecurSuccess() {
151 $durationUnit = 'year';
152 $this->setupMembershipRecurringPaymentProcessorTransaction(['duration_unit' => $durationUnit, 'frequency_unit' => $durationUnit]);
153 $this->callAPISuccessGetSingle('membership_payment', []);
154 $paypalIPN = new CRM_Core_Payment_PayPalIPN($this->getPaypalRecurTransaction());
156 $contribution = $this->callAPISuccess('contribution', 'getsingle', ['id' => $this->_contributionID
]);
157 $membershipEndDate = $this->callAPISuccessGetValue('membership', ['return' => 'end_date']);
158 $this->assertEquals(1, $contribution['contribution_status_id']);
159 $this->assertEquals('8XA571746W2698126', $contribution['trxn_id']);
160 // source gets set by processor
161 $this->assertTrue(substr($contribution['contribution_source'], 0, 20) === "Online Contribution:");
162 $contributionRecur = $this->callAPISuccess('contribution_recur', 'getsingle', ['id' => $this->_contributionRecurID
]);
163 $this->assertEquals(5, $contributionRecur['contribution_status_id']);
164 $paypalIPN = new CRM_Core_Payment_PaypalIPN($this->getPaypalRecurSubsequentTransaction());
166 $renewedMembershipEndDate = $this->membershipRenewalDate($durationUnit, $membershipEndDate);
167 $this->assertEquals($renewedMembershipEndDate, $this->callAPISuccessGetValue('membership', ['return' => 'end_date']));
168 $contribution = $this->callAPISuccess('contribution', 'get', [
169 'contribution_recur_id' => $this->_contributionRecurID
,
172 $this->assertEquals(2, $contribution['count']);
173 $this->assertEquals('secondone', $contribution['values'][1]['trxn_id']);
174 $this->callAPISuccessGetCount('line_item', [
175 'entity_id' => $this->ids
['membership'],
176 'entity_table' => 'civicrm_membership',
178 $this->callAPISuccessGetSingle('line_item', [
179 'contribution_id' => $contribution['values'][1]['id'],
180 'entity_table' => 'civicrm_membership',
182 $this->callAPISuccessGetSingle('membership_payment', ['contribution_id' => $contribution['values'][1]['id']]);
186 * Test IPN that we can force membership when the membership payment has been deleted.
188 * https://lab.civicrm.org/dev/membership/issues/13
190 * In this scenario the membership payment record was deleted (or not created) for the first contribution but we
191 * 'recover' by using the input membership id.
193 * @throws \CRM_Core_Exception
194 * @throws \CiviCRM_API3_Exception
196 public function testIPNPaymentInputMembershipRecurSuccess() {
197 $durationUnit = 'year';
198 $this->setupMembershipRecurringPaymentProcessorTransaction(['duration_unit' => $durationUnit, 'frequency_unit' => $durationUnit]);
199 $membershipPayment = $this->callAPISuccessGetSingle('membership_payment', []);
200 $paypalIPN = new CRM_Core_Payment_PayPalIPN(array_merge($this->getPaypalRecurTransaction(), ['membershipID' => $membershipPayment['membership_id']]));
202 $membershipEndDate = $this->callAPISuccessGetValue('membership', ['return' => 'end_date']);
203 CRM_Core_DAO
::executeQuery('DELETE FROM civicrm_membership_payment WHERE id = ' . $membershipPayment['id']);
204 CRM_Core_DAO
::executeQuery("UPDATE civicrm_line_item SET entity_table = 'civicrm_contribution' WHERE entity_table = 'civicrm_membership'");
206 $paypalIPN = new CRM_Core_Payment_PaypalIPN(array_merge($this->getPaypalRecurSubsequentTransaction(), ['membershipID' => $membershipPayment['membership_id']]));
208 $renewedMembershipEndDate = $this->membershipRenewalDate($durationUnit, $membershipEndDate);
209 $this->assertEquals($renewedMembershipEndDate, $this->callAPISuccessGetValue('membership', ['return' => 'end_date']));
210 $contribution = $this->callAPISuccess('contribution', 'get', [
211 'contribution_recur_id' => $this->_contributionRecurID
,
214 $this->assertEquals(2, $contribution['count']);
215 $this->assertEquals('secondone', $contribution['values'][1]['trxn_id']);
216 $this->callAPISuccessGetCount('line_item', [
217 'entity_id' => $this->ids
['membership'],
218 'entity_table' => 'civicrm_membership',
220 $this->callAPISuccessGetSingle('line_item', [
221 'contribution_id' => $contribution['values'][1]['id'],
222 'entity_table' => 'civicrm_membership',
224 $this->callAPISuccessGetSingle('membership_payment', ['contribution_id' => $contribution['values'][1]['id']]);
228 * Get IPN style details for an incoming recurring transaction.
230 public function getPaypalRecurTransaction() {
232 'contactID' => $this->_contactID
,
233 'contributionID' => $this->_contributionID
,
234 'invoice' => $this->_invoiceID
,
235 'contributionRecurID' => $this->_contributionRecurID
,
236 'mc_gross' => '15.00',
237 'module' => 'contribute',
238 'payer_id' => '4NHUTA7ZUE92C',
239 'payment_status' => 'Completed',
240 'receiver_email' => 'sunil._1183377782_biz_api1.webaccess.co.in',
241 'txn_type' => 'subscr_payment',
242 'last_name' => 'Roberty',
243 'payment_fee' => '0.63',
244 'first_name' => 'Robert',
245 'txn_id' => '8XA571746W2698126',
246 'residence_country' => 'US',
251 * Get IPN style details for an incoming paypal standard transaction.
253 public function getPaypalTransaction() {
255 'contactID' => $this->_contactID
,
256 'contributionID' => $this->_contributionID
,
257 'invoice' => $this->_invoiceID
,
258 'mc_gross' => '100.00',
260 'settle_amount' => '95.00',
261 'module' => 'contribute',
262 'payer_id' => 'FV5ZW7TLMQ874',
263 'payment_status' => 'Completed',
264 'receiver_email' => 'sunil._1183377782_biz_api1.webaccess.co.in',
265 'txn_type' => 'web_accept',
266 'last_name' => 'Roberty',
267 'payment_fee' => '0.63',
268 'first_name' => 'Robert',
269 'txn_id' => '8XA571746W2698126',
270 'residence_country' => 'US',
271 'custom' => json_encode(['cgid' => 'test12345']),
276 * Get IPN-style details for a second incoming transaction.
280 public function getPaypalRecurSubsequentTransaction() {
281 return array_merge($this->getPaypalRecurTransaction(), ['txn_id' => 'secondone']);
285 * Test IPN response updates contribution and invoice is attached in mail reciept
286 * Test also AlterIPNData intercepts at the right point and allows for custom processing
287 * The scenario is that a pending contribution exists and the IPN call will update it to completed.
288 * And also if Tax and Invoicing is enabled, this unit test ensure that invoice pdf is attached with email recipet
290 public function testhookAlterIPNDataOnIPNPaymentSuccess() {
292 $pendingStatusID = CRM_Core_PseudoConstant
::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending');
293 $completedStatusID = CRM_Core_PseudoConstant
::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
295 'payment_processor_id' => $this->_paymentProcessorID
,
296 'contact_id' => $this->_contactID
,
298 'invoice_id' => $this->_invoiceID
,
299 'contribution_status_id' => $pendingStatusID,
300 'is_email_receipt' => TRUE,
302 $this->_contributionID
= $this->contributionCreate($params);
303 $this->createCustomField();
304 $contribution = $this->callAPISuccess('contribution', 'get', ['id' => $this->_contributionID
, 'sequential' => 1]);
305 // assert that contribution created before handling payment via paypal standard has no transaction id set and pending status
306 $this->assertEquals(NULL, $contribution['values'][0]['trxn_id']);
307 $this->assertEquals($pendingStatusID, $contribution['values'][0]['contribution_status_id']);
308 $this->hookClass
->setHook('civicrm_postIPNProcess', [$this, 'hookCiviCRMAlterIPNData']);
310 $_REQUEST = ['q' => CRM_Utils_System
::url('civicrm/payment/ipn/' . $this->_paymentProcessorID
)] +
$this->getPaypalTransaction();
312 $mut = new CiviMailUtils($this, TRUE);
313 $payment = CRM_Core_Payment
::handlePaymentMethod('PaymentNotification', ['processor_id' => $this->_paymentProcessorID
]);
315 $contribution = $this->callAPISuccess('contribution', 'get', ['id' => $this->_contributionID
, 'sequential' => 1]);
316 // assert that contribution is completed after getting response from paypal standard which has transaction id set and completed status
317 $this->assertEquals($_REQUEST['txn_id'], $contribution['values'][0]['trxn_id']);
318 $this->assertEquals($completedStatusID, $contribution['values'][0]['contribution_status_id']);
319 $this->assertEquals('test12345', $contribution['values'][0]['custom_' . $this->_customFieldID
]);
323 * Allow IPNs to validate when the supplied contact_id has been deleted from the database but there is a valid contact id in the contribution recur object or contribution object
325 public function testPayPalIPNSuccessDeletedContact() {
326 $contactTobeDeleted = $this->individualCreate();
327 $pendingStatusID = CRM_Core_PseudoConstant
::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending');
328 $completedStatusID = CRM_Core_PseudoConstant
::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
330 'payment_processor_id' => $this->_paymentProcessorID
,
331 'contact_id' => $this->_contactID
,
333 'invoice_id' => $this->_invoiceID
,
334 'contribution_status_id' => $pendingStatusID,
335 'is_email_receipt' => TRUE,
337 $this->_contributionID
= $this->contributionCreate($params);
338 $contribution = $this->callAPISuccess('contribution', 'get', ['id' => $this->_contributionID
, 'sequential' => 1]);
339 // assert that contribution created before handling payment via paypal standard has no transaction id set and pending status
340 $this->assertEquals(NULL, $contribution['values'][0]['trxn_id']);
341 $this->assertEquals($pendingStatusID, $contribution['values'][0]['contribution_status_id']);
342 $payPalIPNParams = $this->getPaypalTransaction();
343 $payPalIPNParams['contactID'] = $contactTobeDeleted;
344 $this->callAPISuccess('Contact', 'delete', ['id' => $contactTobeDeleted, 'skip_undelete' => 1]);
346 $_REQUEST = ['q' => CRM_Utils_System
::url('civicrm/payment/ipn/' . $this->_paymentProcessorID
)] +
$payPalIPNParams;
347 // Now process the IPN noting that the contact id that was supplied with the IPN has been deleted but there is still a valid one on the contribution id
348 $payment = CRM_Core_Payment
::handlePaymentMethod('PaymentNotification', ['processor_id' => $this->_paymentProcessorID
]);
350 $contribution = $this->callAPISuccess('contribution', 'get', ['id' => $this->_contributionID
, 'sequential' => 1]);
351 // assert that contribution is completed after getting response from paypal standard which has transaction id set and completed status
352 $this->assertEquals($_REQUEST['txn_id'], $contribution['values'][0]['trxn_id']);
353 $this->assertEquals($completedStatusID, $contribution['values'][0]['contribution_status_id']);
357 * Store Custom data passed in from the PayPalIPN in a custom field
359 public function hookCiviCRMAlterIPNData($data) {
360 if (!empty($data['custom'])) {
361 $customData = json_decode($data['custom'], TRUE);
362 $customField = $this->callAPISuccess('custom_field', 'get', ['label' => 'TestCustomFieldIPNHook']);
363 $this->callAPISuccess('contribution', 'create', ['id' => $this->_contributionID
, 'custom_' . $customField['id'] => $customData['cgid']]);
370 protected function createCustomField() {
371 $customGroup = $this->customGroupCreate(['extends' => 'Contribution']);
373 'label' => 'TestCustomFieldIPNHook',
374 'data_type' => 'String',
375 'html_type' => 'Text',
376 'custom_group_id' => $customGroup['id'],
378 $field = CRM_Core_BAO_CustomField
::create($fields);
379 $this->_customFieldID
= $field->id
;