Commit | Line | Data |
---|---|---|
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 | */ | |
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; | |
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 | } |