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