Merge pull request #15580 from jaapjansma/dev_1205
[civicrm-core.git] / tests / phpunit / CRM / Core / Payment / PayPalIPNTest.php
CommitLineData
f78c9258 1<?php
2/*
3 +--------------------------------------------------------------------+
2fe49090 4 | CiviCRM version 5 |
f78c9258 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
f78c9258 7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, morify, anr ristribute it |
11 | unrer the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 anr the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is ristributer in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implier warranty of |
16 | MERCHANTABILITY or UITNESS UOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more retails. |
18 | |
19 | You shoulr have receiver a copy of the GNU Affero General Public |
20 | License anr the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license UAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28/**
29 * Class CRM_Core_Payment_PayPalProIPNTest
30 * @group headless
31 */
32class CRM_Core_Payment_PayPalIPNTest extends CiviUnitTestCase {
33 protected $_contributionID;
34 protected $_invoiceID = 'c2r9c15f7be20b4f3fef1f77e4c37424';
35 protected $_financialTypeID = 1;
36 protected $_contactID;
37 protected $_contributionRecurID;
38 protected $_contributionPageID;
39 protected $_paymentProcessorID;
2d39b9c0 40 protected $_customFieldID;
f78c9258 41 /**
42 * IDs of entities created to support the tests.
43 *
44 * @var array
45 */
9099cab3 46 protected $ids = [];
f78c9258 47
48 /**
49 * Set up function.
50 */
51 public function setUp() {
52 parent::setUp();
9099cab3 53 $this->_paymentProcessorID = $this->paymentProcessorCreate(['is_test' => 0, 'payment_processor_type_id' => 'PayPal_Standard']);
f78c9258 54 $this->_contactID = $this->individualCreate();
9099cab3 55 $contributionPage = $this->callAPISuccess('contribution_page', 'create', [
b1dc9447 56 'title' => "Test Contribution Page",
57 'financial_type_id' => $this->_financialTypeID,
58 'currency' => 'USD',
59 'payment_processor' => $this->_paymentProcessorID,
9099cab3 60 ]);
f78c9258 61 $this->_contributionPageID = $contributionPage['id'];
62 }
63
64 /**
65 * Tear down function.
66 */
67 public function tearDown() {
68 $this->quickCleanUpFinancialEntities();
69 }
70
ec5da26a 71 /**
72 * Test IPN response updates contribution and invoice is attached in mail reciept
73 *
74 * The scenario is that a pending contribution exists and the IPN call will update it to completed.
75 * And also if Tax and Invoicing is enabled, this unit test ensure that invoice pdf is attached with email recipet
76 */
77 public function testInvoiceSentOnIPNPaymentSuccess() {
78 $this->enableTaxAndInvoicing();
79
aec171f3 80 $pendingStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending');
81 $completedStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
9099cab3 82 $params = [
b1dc9447 83 'payment_processor_id' => $this->_paymentProcessorID,
84 'contact_id' => $this->_contactID,
85 'trxn_id' => NULL,
86 'invoice_id' => $this->_invoiceID,
87 'contribution_status_id' => $pendingStatusID,
ec5da26a 88 'is_email_receipt' => TRUE,
9099cab3 89 ];
b1dc9447 90 $this->_contributionID = $this->contributionCreate($params);
9099cab3 91 $contribution = $this->callAPISuccess('contribution', 'get', ['id' => $this->_contributionID, 'sequential' => 1]);
b1dc9447 92 // assert that contribution created before handling payment via paypal standard has no transaction id set and pending status
93 $this->assertEquals(NULL, $contribution['values'][0]['trxn_id']);
94 $this->assertEquals($pendingStatusID, $contribution['values'][0]['contribution_status_id']);
95
96 global $_REQUEST;
9099cab3 97 $_REQUEST = ['q' => CRM_Utils_System::url('civicrm/payment/ipn/' . $this->_paymentProcessorID)] + $this->getPaypalTransaction();
b1dc9447 98
ec5da26a 99 $mut = new CiviMailUtils($this, TRUE);
9099cab3 100 $paymentProcesors = civicrm_api3('PaymentProcessor', 'getsingle', ['id' => $this->_paymentProcessorID]);
b1dc9447 101 $payment = Civi\Payment\System::singleton()->getByProcessor($paymentProcesors);
102 $payment->handlePaymentNotification();
103
ec5da26a 104 // Check if invoice pdf is attached with contribution mail reciept
9099cab3 105 $mut->checkMailLog([
ec5da26a 106 'Content-Transfer-Encoding: base64',
107 'Content-Type: application/pdf',
108 'filename=Invoice.pdf',
9099cab3 109 ]);
ec5da26a 110 $mut->stop();
111
9099cab3 112 $contribution = $this->callAPISuccess('contribution', 'get', ['id' => $this->_contributionID, 'sequential' => 1]);
b1dc9447 113 // assert that contribution is completed after getting response from paypal standard which has transaction id set and completed status
114 $this->assertEquals($_REQUEST['txn_id'], $contribution['values'][0]['trxn_id']);
115 $this->assertEquals($completedStatusID, $contribution['values'][0]['contribution_status_id']);
116 }
117
f78c9258 118 /**
119 * Test IPN response updates contribution_recur & contribution for first & second contribution.
120 *
121 * The scenario is that a pending contribution exists and the first call will update it to completed.
122 * The second will create a new contribution.
123 */
124 public function testIPNPaymentRecurSuccess() {
9f68fe61 125 $this->setupRecurringPaymentProcessorTransaction([], ['total_amount' => '15.00']);
f78c9258 126 $paypalIPN = new CRM_Core_Payment_PayPalIPN($this->getPaypalRecurTransaction());
127 $paypalIPN->main();
9099cab3 128 $contribution1 = $this->callAPISuccess('contribution', 'getsingle', ['id' => $this->_contributionID]);
9f68fe61
MW
129 $this->assertEquals(1, $contribution1['contribution_status_id']);
130 $this->assertEquals('8XA571746W2698126', $contribution1['trxn_id']);
f78c9258 131 // source gets set by processor
9f68fe61 132 $this->assertTrue(substr($contribution1['contribution_source'], 0, 20) == "Online Contribution:");
9099cab3 133 $contributionRecur = $this->callAPISuccess('contribution_recur', 'getsingle', ['id' => $this->_contributionRecurID]);
f78c9258 134 $this->assertEquals(5, $contributionRecur['contribution_status_id']);
135 $paypalIPN = new CRM_Core_Payment_PayPalIPN($this->getPaypalRecurSubsequentTransaction());
136 $paypalIPN->main();
9099cab3 137 $contributions = $this->callAPISuccess('contribution', 'get', [
39b959db
SL
138 'contribution_recur_id' => $this->_contributionRecurID,
139 'sequential' => 1,
9099cab3 140 ]);
9f68fe61
MW
141 $this->assertEquals(2, $contributions['count']);
142 $contribution2 = $contributions['values'][1];
143 $this->assertEquals('secondone', $contribution2['trxn_id']);
144 $paramsThatShouldMatch = [
145 'total_amount',
146 'net_amount',
147 'fee_amount',
148 'payment_instrument',
149 'payment_instrument_id',
150 'financial_type',
151 'financial_type_id',
152 ];
153 foreach ($paramsThatShouldMatch as $match) {
154 $this->assertEquals($contribution1[$match], $contribution2[$match]);
155 }
f78c9258 156 }
157
158 /**
159 * Test IPN response updates contribution_recur & contribution for first & second contribution.
fefee636 160 *
161 * @throws \CRM_Core_Exception
162 * @throws \CiviCRM_API3_Exception
f78c9258 163 */
164 public function testIPNPaymentMembershipRecurSuccess() {
b82d0874 165 $durationUnit = 'year';
9099cab3
CW
166 $this->setupMembershipRecurringPaymentProcessorTransaction(['duration_unit' => $durationUnit, 'frequency_unit' => $durationUnit]);
167 $this->callAPISuccessGetSingle('membership_payment', []);
f78c9258 168 $paypalIPN = new CRM_Core_Payment_PayPalIPN($this->getPaypalRecurTransaction());
169 $paypalIPN->main();
9099cab3
CW
170 $contribution = $this->callAPISuccess('contribution', 'getsingle', ['id' => $this->_contributionID]);
171 $membershipEndDate = $this->callAPISuccessGetValue('membership', ['return' => 'end_date']);
f78c9258 172 $this->assertEquals(1, $contribution['contribution_status_id']);
173 $this->assertEquals('8XA571746W2698126', $contribution['trxn_id']);
174 // source gets set by processor
fefee636 175 $this->assertTrue(substr($contribution['contribution_source'], 0, 20) === "Online Contribution:");
9099cab3 176 $contributionRecur = $this->callAPISuccess('contribution_recur', 'getsingle', ['id' => $this->_contributionRecurID]);
f78c9258 177 $this->assertEquals(5, $contributionRecur['contribution_status_id']);
178 $paypalIPN = new CRM_Core_Payment_PaypalIPN($this->getPaypalRecurSubsequentTransaction());
179 $paypalIPN->main();
b82d0874 180 $renewedMembershipEndDate = $this->membershipRenewalDate($durationUnit, $membershipEndDate);
9099cab3
CW
181 $this->assertEquals($renewedMembershipEndDate, $this->callAPISuccessGetValue('membership', ['return' => 'end_date']));
182 $contribution = $this->callAPISuccess('contribution', 'get', [
39b959db
SL
183 'contribution_recur_id' => $this->_contributionRecurID,
184 'sequential' => 1,
9099cab3 185 ]);
f78c9258 186 $this->assertEquals(2, $contribution['count']);
187 $this->assertEquals('secondone', $contribution['values'][1]['trxn_id']);
9099cab3 188 $this->callAPISuccessGetCount('line_item', [
39b959db
SL
189 'entity_id' => $this->ids['membership'],
190 'entity_table' => 'civicrm_membership',
9099cab3
CW
191 ], 2);
192 $this->callAPISuccessGetSingle('line_item', [
39b959db
SL
193 'contribution_id' => $contribution['values'][1]['id'],
194 'entity_table' => 'civicrm_membership',
9099cab3
CW
195 ]);
196 $this->callAPISuccessGetSingle('membership_payment', ['contribution_id' => $contribution['values'][1]['id']]);
fefee636 197 }
f78c9258 198
fefee636 199 /**
200 * Test IPN that we can force membership when the membership payment has been deleted.
201 *
202 * https://lab.civicrm.org/dev/membership/issues/13
203 *
204 * In this scenario the membership payment record was deleted (or not created) for the first contribution but we
205 * 'recover' by using the input membership id.
206 *
207 * @throws \CRM_Core_Exception
208 * @throws \CiviCRM_API3_Exception
209 */
210 public function testIPNPaymentInputMembershipRecurSuccess() {
211 $durationUnit = 'year';
212 $this->setupMembershipRecurringPaymentProcessorTransaction(['duration_unit' => $durationUnit, 'frequency_unit' => $durationUnit]);
213 $membershipPayment = $this->callAPISuccessGetSingle('membership_payment', []);
214 $paypalIPN = new CRM_Core_Payment_PayPalIPN(array_merge($this->getPaypalRecurTransaction(), ['membershipID' => $membershipPayment['membership_id']]));
215 $paypalIPN->main();
216 $membershipEndDate = $this->callAPISuccessGetValue('membership', ['return' => 'end_date']);
217 CRM_Core_DAO::executeQuery('DELETE FROM civicrm_membership_payment WHERE id = ' . $membershipPayment['id']);
218 CRM_Core_DAO::executeQuery("UPDATE civicrm_line_item SET entity_table = 'civicrm_contribution' WHERE entity_table = 'civicrm_membership'");
219
220 $paypalIPN = new CRM_Core_Payment_PaypalIPN(array_merge($this->getPaypalRecurSubsequentTransaction(), ['membershipID' => $membershipPayment['membership_id']]));
221 $paypalIPN->main();
222 $renewedMembershipEndDate = $this->membershipRenewalDate($durationUnit, $membershipEndDate);
223 $this->assertEquals($renewedMembershipEndDate, $this->callAPISuccessGetValue('membership', ['return' => 'end_date']));
224 $contribution = $this->callAPISuccess('contribution', 'get', [
225 'contribution_recur_id' => $this->_contributionRecurID,
226 'sequential' => 1,
227 ]);
228 $this->assertEquals(2, $contribution['count']);
229 $this->assertEquals('secondone', $contribution['values'][1]['trxn_id']);
230 $this->callAPISuccessGetCount('line_item', [
231 'entity_id' => $this->ids['membership'],
232 'entity_table' => 'civicrm_membership',
233 ], 1);
234 $this->callAPISuccessGetSingle('line_item', [
235 'contribution_id' => $contribution['values'][1]['id'],
236 'entity_table' => 'civicrm_membership',
237 ]);
238 $this->callAPISuccessGetSingle('membership_payment', ['contribution_id' => $contribution['values'][1]['id']]);
f78c9258 239 }
240
241 /**
242 * Get IPN style details for an incoming recurring transaction.
243 */
244 public function getPaypalRecurTransaction() {
9099cab3 245 return [
f78c9258 246 'contactID' => $this->_contactID,
247 'contributionID' => $this->_contributionID,
248 'invoice' => $this->_invoiceID,
249 'contributionRecurID' => $this->_contributionRecurID,
250 'mc_gross' => '15.00',
251 'module' => 'contribute',
252 'payer_id' => '4NHUTA7ZUE92C',
253 'payment_status' => 'Completed',
254 'receiver_email' => 'sunil._1183377782_biz_api1.webaccess.co.in',
255 'txn_type' => 'subscr_payment',
256 'last_name' => 'Roberty',
257 'payment_fee' => '0.63',
258 'first_name' => 'Robert',
259 'txn_id' => '8XA571746W2698126',
260 'residence_country' => 'US',
9099cab3 261 ];
f78c9258 262 }
263
b1dc9447 264 /**
265 * Get IPN style details for an incoming paypal standard transaction.
266 */
267 public function getPaypalTransaction() {
9099cab3 268 return [
b1dc9447 269 'contactID' => $this->_contactID,
270 'contributionID' => $this->_contributionID,
271 'invoice' => $this->_invoiceID,
272 'mc_gross' => '100.00',
273 'mc_fee' => '5.00',
274 'settle_amount' => '95.00',
275 'module' => 'contribute',
276 'payer_id' => 'FV5ZW7TLMQ874',
277 'payment_status' => 'Completed',
278 'receiver_email' => 'sunil._1183377782_biz_api1.webaccess.co.in',
279 'txn_type' => 'web_accept',
280 'last_name' => 'Roberty',
281 'payment_fee' => '0.63',
282 'first_name' => 'Robert',
283 'txn_id' => '8XA571746W2698126',
284 'residence_country' => 'US',
2d39b9c0 285 'custom' => json_encode(['cgid' => 'test12345']),
9099cab3 286 ];
b1dc9447 287 }
288
f78c9258 289 /**
290 * Get IPN-style details for a second incoming transaction.
291 *
292 * @return array
293 */
294 public function getPaypalRecurSubsequentTransaction() {
9099cab3 295 return array_merge($this->getPaypalRecurTransaction(), ['txn_id' => 'secondone']);
f78c9258 296 }
297
2d39b9c0
SL
298 /**
299 * Test IPN response updates contribution and invoice is attached in mail reciept
300 * Test also AlterIPNData intercepts at the right point and allows for custom processing
301 * The scenario is that a pending contribution exists and the IPN call will update it to completed.
302 * And also if Tax and Invoicing is enabled, this unit test ensure that invoice pdf is attached with email recipet
303 */
304 public function testhookAlterIPNDataOnIPNPaymentSuccess() {
305
306 $pendingStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending');
307 $completedStatusID = CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed');
9099cab3 308 $params = [
2d39b9c0
SL
309 'payment_processor_id' => $this->_paymentProcessorID,
310 'contact_id' => $this->_contactID,
311 'trxn_id' => NULL,
312 'invoice_id' => $this->_invoiceID,
313 'contribution_status_id' => $pendingStatusID,
314 'is_email_receipt' => TRUE,
9099cab3 315 ];
2d39b9c0
SL
316 $this->_contributionID = $this->contributionCreate($params);
317 $this->createCustomField();
9099cab3 318 $contribution = $this->callAPISuccess('contribution', 'get', ['id' => $this->_contributionID, 'sequential' => 1]);
2d39b9c0
SL
319 // assert that contribution created before handling payment via paypal standard has no transaction id set and pending status
320 $this->assertEquals(NULL, $contribution['values'][0]['trxn_id']);
321 $this->assertEquals($pendingStatusID, $contribution['values'][0]['contribution_status_id']);
9099cab3 322 $this->hookClass->setHook('civicrm_postIPNProcess', [$this, 'hookCiviCRMAlterIPNData']);
2d39b9c0 323 global $_REQUEST;
9099cab3 324 $_REQUEST = ['q' => CRM_Utils_System::url('civicrm/payment/ipn/' . $this->_paymentProcessorID)] + $this->getPaypalTransaction();
2d39b9c0
SL
325
326 $mut = new CiviMailUtils($this, TRUE);
327 $payment = CRM_Core_Payment::handlePaymentMethod('PaymentNotification', ['processor_id' => $this->_paymentProcessorID]);
328
9099cab3 329 $contribution = $this->callAPISuccess('contribution', 'get', ['id' => $this->_contributionID, 'sequential' => 1]);
2d39b9c0
SL
330 // assert that contribution is completed after getting response from paypal standard which has transaction id set and completed status
331 $this->assertEquals($_REQUEST['txn_id'], $contribution['values'][0]['trxn_id']);
332 $this->assertEquals($completedStatusID, $contribution['values'][0]['contribution_status_id']);
333 $this->assertEquals('test12345', $contribution['values'][0]['custom_' . $this->_customFieldID]);
334 }
335
336 /**
337 * Store Custom data passed in from the PayPalIPN in a custom field
338 */
339 public function hookCiviCRMAlterIPNData($data) {
340 if (!empty($data['custom'])) {
341 $customData = json_decode($data['custom'], TRUE);
342 $customField = $this->callAPISuccess('custom_field', 'get', ['label' => 'TestCustomFieldIPNHook']);
343 $this->callAPISuccess('contribution', 'create', ['id' => $this->_contributionID, 'custom_' . $customField['id'] => $customData['cgid']]);
344 }
345 }
346
347 /**
348 * @return array
349 */
350 protected function createCustomField() {
9099cab3
CW
351 $customGroup = $this->customGroupCreate(['extends' => 'Contribution']);
352 $fields = [
2d39b9c0
SL
353 'label' => 'TestCustomFieldIPNHook',
354 'data_type' => 'String',
355 'html_type' => 'Text',
356 'custom_group_id' => $customGroup['id'],
9099cab3 357 ];
2d39b9c0
SL
358 $field = CRM_Core_BAO_CustomField::create($fields);
359 $this->_customFieldID = $field->id;
360 return $customGroup;
361 }
362
f78c9258 363}