Commit | Line | Data |
---|---|---|
bbf58b03 EM |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
7d61e75f | 4 | | Copyright CiviCRM LLC. All rights reserved. | |
bbf58b03 | 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 | | |
bbf58b03 | 9 | +--------------------------------------------------------------------+ |
d25dd0ee | 10 | */ |
bbf58b03 | 11 | |
7fe37828 EM |
12 | /** |
13 | * Class CRM_Contribute_BAO_ContributionRecurTest | |
acb109b7 | 14 | * @group headless |
7fe37828 | 15 | */ |
bbf58b03 | 16 | class CRM_Contribute_BAO_ContributionRecurTest extends CiviUnitTestCase { |
bbf58b03 | 17 | |
d486b01e PN |
18 | use CRMTraits_Financial_OrderTrait; |
19 | ||
20 | /** | |
21 | * Set up for test. | |
22 | * | |
23 | * @throws \CRM_Core_Exception | |
24 | */ | |
00be9182 | 25 | public function setUp() { |
bbf58b03 EM |
26 | parent::setUp(); |
27 | $this->_ids['payment_processor'] = $this->paymentProcessorCreate(); | |
9099cab3 | 28 | $this->_params = [ |
bbf58b03 EM |
29 | 'contact_id' => $this->individualCreate(), |
30 | 'amount' => 3.00, | |
31 | 'frequency_unit' => 'week', | |
32 | 'frequency_interval' => 1, | |
33 | 'installments' => 2, | |
34 | 'start_date' => 'yesterday', | |
35 | 'create_date' => 'yesterday', | |
36 | 'modified_date' => 'yesterday', | |
37 | 'cancel_date' => NULL, | |
38 | 'end_date' => '+ 2 weeks', | |
39 | 'processor_id' => '643411460836', | |
40 | 'trxn_id' => 'e0d0808e26f3e661c6c18eb7c039d363', | |
41 | 'invoice_id' => 'e0d0808e26f3e661c6c18eb7c039d363', | |
42 | 'contribution_status_id' => 1, | |
43 | 'is_test' => 0, | |
44 | 'cycle_day' => 1, | |
45 | 'next_sched_contribution_date' => '+ 1 week', | |
46 | 'failure_count' => 0, | |
47 | 'failure_retry_date' => NULL, | |
48 | 'auto_renew' => 0, | |
49 | 'currency' => 'USD', | |
50 | 'payment_processor_id' => $this->_ids['payment_processor'], | |
51 | 'is_email_receipt' => 1, | |
52 | 'financial_type_id' => 1, | |
53 | 'payment_instrument_id' => 1, | |
54 | 'campaign_id' => NULL, | |
9099cab3 | 55 | ]; |
bbf58b03 EM |
56 | } |
57 | ||
d486b01e PN |
58 | /** |
59 | * Cleanup after test. | |
60 | * | |
61 | * @throws \CRM_Core_Exception | |
62 | */ | |
00be9182 | 63 | public function teardown() { |
d486b01e | 64 | $this->quickCleanUpFinancialEntities(); |
bbf58b03 EM |
65 | } |
66 | ||
67 | /** | |
fe482240 EM |
68 | * Test that an object can be retrieved & saved (per CRM-14986). |
69 | * | |
70 | * This has been causing a DB error so we are checking for absence of error | |
d486b01e PN |
71 | * |
72 | * @throws \CRM_Core_Exception | |
bbf58b03 | 73 | */ |
00be9182 | 74 | public function testFindSave() { |
bbf58b03 EM |
75 | $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params); |
76 | $dao = new CRM_Contribute_BAO_ContributionRecur(); | |
77 | $dao->id = $contributionRecur['id']; | |
78 | $dao->find(TRUE); | |
79 | $dao->is_email_receipt = 0; | |
80 | $dao->save(); | |
81 | } | |
82 | ||
83 | /** | |
fe482240 EM |
84 | * Test cancellation works per CRM-14986. |
85 | * | |
86 | * We are checking for absence of error. | |
d486b01e PN |
87 | * |
88 | * @throws \CRM_Core_Exception | |
bbf58b03 | 89 | */ |
00be9182 | 90 | public function testCancelRecur() { |
bbf58b03 | 91 | $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params); |
9cfc631e | 92 | CRM_Contribute_BAO_ContributionRecur::cancelRecurContribution(['id' => $contributionRecur['id']]); |
bbf58b03 EM |
93 | } |
94 | ||
748bcfb2 | 95 | /** |
0b2f1f29 | 96 | * Test checking if contribution recur object can allow for changes to financial types. |
748bcfb2 | 97 | * |
d486b01e | 98 | * @throws \CRM_Core_Exception |
748bcfb2 SL |
99 | */ |
100 | public function testSupportFinancialTypeChange() { | |
101 | $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params); | |
9099cab3 | 102 | $this->callAPISuccess('Contribution', 'create', [ |
748bcfb2 SL |
103 | 'contribution_recur_id' => $contributionRecur['id'], |
104 | 'total_amount' => '3.00', | |
105 | 'financial_type_id' => 1, | |
106 | 'payment_instrument_id' => 1, | |
107 | 'currency' => 'USD', | |
108 | 'contact_id' => $this->individualCreate(), | |
109 | 'contribution_status_id' => 1, | |
46f459f2 | 110 | 'receive_date' => 'yesterday', |
9099cab3 | 111 | ]); |
748bcfb2 SL |
112 | $this->assertTrue(CRM_Contribute_BAO_ContributionRecur::supportsFinancialTypeChange($contributionRecur['id'])); |
113 | } | |
114 | ||
3d6bf1a7 EE |
115 | /** |
116 | * Test we don't change unintended fields on API edit | |
d486b01e PN |
117 | * |
118 | * @throws \CRM_Core_Exception | |
3d6bf1a7 EE |
119 | */ |
120 | public function testUpdateRecur() { | |
121 | $createParams = $this->_params; | |
122 | $createParams['currency'] = 'XAU'; | |
123 | $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $createParams); | |
9099cab3 | 124 | $editParams = [ |
3d6bf1a7 EE |
125 | 'id' => $contributionRecur['id'], |
126 | 'end_date' => '+ 4 weeks', | |
9099cab3 | 127 | ]; |
3d6bf1a7 EE |
128 | $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $editParams); |
129 | $dao = new CRM_Contribute_BAO_ContributionRecur(); | |
130 | $dao->id = $contributionRecur['id']; | |
131 | $dao->find(TRUE); | |
132 | $this->assertEquals('XAU', $dao->currency, 'Edit clobbered recur currency'); | |
133 | } | |
134 | ||
0b2f1f29 AS |
135 | /** |
136 | * Check test contributions aren't picked up as template for non-test recurs | |
137 | * | |
d486b01e PN |
138 | * @throws \API_Exception |
139 | * @throws \CRM_Core_Exception | |
140 | * @throws \CiviCRM_API3_Exception | |
141 | * @throws \Civi\API\Exception\UnauthorizedException | |
0b2f1f29 AS |
142 | */ |
143 | public function testGetTemplateContributionMatchTest1() { | |
144 | $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params); | |
145 | // Create a first contrib | |
146 | $firstContrib = $this->callAPISuccess('Contribution', 'create', [ | |
147 | 'contribution_recur_id' => $contributionRecur['id'], | |
148 | 'total_amount' => '3.00', | |
149 | 'financial_type_id' => 1, | |
150 | 'payment_instrument_id' => 1, | |
151 | 'currency' => 'USD', | |
152 | 'contact_id' => $this->individualCreate(), | |
153 | 'contribution_status_id' => 1, | |
154 | 'receive_date' => 'yesterday', | |
155 | ]); | |
156 | // Create a test contrib - should not be picked up as template for non-test recur | |
157 | $this->callAPISuccess('Contribution', 'create', [ | |
158 | 'contribution_recur_id' => $contributionRecur['id'], | |
159 | 'total_amount' => '3.00', | |
160 | 'financial_type_id' => 1, | |
161 | 'payment_instrument_id' => 1, | |
162 | 'currency' => 'USD', | |
163 | 'contact_id' => $this->individualCreate(), | |
164 | 'contribution_status_id' => 1, | |
165 | 'receive_date' => 'yesterday', | |
166 | 'is_test' => 1, | |
167 | ]); | |
168 | $fetchedTemplate = CRM_Contribute_BAO_ContributionRecur::getTemplateContribution($contributionRecur['id']); | |
169 | $this->assertEquals($firstContrib['id'], $fetchedTemplate['id']); | |
170 | } | |
171 | ||
172 | /** | |
173 | * Check non-test contributions aren't picked up as template for test recurs | |
174 | * | |
d486b01e PN |
175 | * @throws \API_Exception |
176 | * @throws \CRM_Core_Exception | |
177 | * @throws \CiviCRM_API3_Exception | |
178 | * @throws \Civi\API\Exception\UnauthorizedException | |
0b2f1f29 AS |
179 | */ |
180 | public function testGetTemplateContributionMatchTest() { | |
181 | $params = $this->_params; | |
182 | $params['is_test'] = 1; | |
183 | $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $params); | |
184 | // Create a first test contrib | |
185 | $firstContrib = $this->callAPISuccess('Contribution', 'create', [ | |
186 | 'contribution_recur_id' => $contributionRecur['id'], | |
187 | 'total_amount' => '3.00', | |
188 | 'financial_type_id' => 1, | |
189 | 'payment_instrument_id' => 1, | |
190 | 'currency' => 'USD', | |
191 | 'contact_id' => $this->individualCreate(), | |
192 | 'contribution_status_id' => 1, | |
193 | 'receive_date' => 'yesterday', | |
194 | 'is_test' => 1, | |
195 | ]); | |
196 | // Create a non-test contrib - should not be picked up as template for non-test recur | |
197 | // This shouldn't occur - a live contrib against a test recur, but that's not the point... | |
198 | $this->callAPISuccess('Contribution', 'create', [ | |
199 | 'contribution_recur_id' => $contributionRecur['id'], | |
200 | 'total_amount' => '3.00', | |
201 | 'financial_type_id' => 1, | |
202 | 'payment_instrument_id' => 1, | |
203 | 'currency' => 'USD', | |
204 | 'contact_id' => $this->individualCreate(), | |
205 | 'contribution_status_id' => 1, | |
206 | 'receive_date' => 'yesterday', | |
207 | 'is_test' => 0, | |
208 | ]); | |
209 | $fetchedTemplate = CRM_Contribute_BAO_ContributionRecur::getTemplateContribution($contributionRecur['id']); | |
210 | $this->assertEquals($firstContrib['id'], $fetchedTemplate['id']); | |
211 | } | |
212 | ||
f306dbeb AS |
213 | /** |
214 | * Test that is_template contribution is used where available | |
215 | * | |
d486b01e PN |
216 | * @throws \API_Exception |
217 | * @throws \CRM_Core_Exception | |
218 | * @throws \CiviCRM_API3_Exception | |
219 | * @throws \Civi\API\Exception\UnauthorizedException | |
f306dbeb AS |
220 | */ |
221 | public function testGetTemplateContributionNewTemplate() { | |
222 | $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params); | |
223 | // Create the template | |
224 | $templateContrib = $this->callAPISuccess('Contribution', 'create', [ | |
225 | 'contribution_recur_id' => $contributionRecur['id'], | |
226 | 'total_amount' => '3.00', | |
227 | 'financial_type_id' => 1, | |
228 | 'payment_instrument_id' => 1, | |
229 | 'currency' => 'USD', | |
230 | 'contact_id' => $this->individualCreate(), | |
231 | 'contribution_status_id' => 1, | |
232 | 'receive_date' => 'yesterday', | |
233 | 'is_template' => 1, | |
234 | ]); | |
235 | // Create another normal contrib | |
236 | $this->callAPISuccess('Contribution', 'create', [ | |
237 | 'contribution_recur_id' => $contributionRecur['id'], | |
238 | 'total_amount' => '3.00', | |
239 | 'financial_type_id' => 1, | |
240 | 'payment_instrument_id' => 1, | |
241 | 'currency' => 'USD', | |
242 | 'contact_id' => $this->individualCreate(), | |
243 | 'contribution_status_id' => 1, | |
244 | 'receive_date' => 'yesterday', | |
245 | ]); | |
246 | $fetchedTemplate = CRM_Contribute_BAO_ContributionRecur::getTemplateContribution($contributionRecur['id']); | |
247 | // Fetched template should be the is_template, not the latest contrib | |
248 | $this->assertEquals($fetchedTemplate['id'], $templateContrib['id']); | |
249 | } | |
250 | ||
d486b01e PN |
251 | /** |
252 | * Test to check if correct membership is auto renewed. | |
253 | * | |
254 | * @throws \CRM_Core_Exception | |
255 | */ | |
256 | public function testAutoRenewalWhenOneMemberIsDeceased() { | |
257 | $contactId1 = $this->individualCreate(); | |
258 | $contactId2 = $this->individualCreate(); | |
259 | $membershipOrganizationId = $this->organizationCreate(); | |
260 | ||
261 | $this->createExtraneousContribution(); | |
262 | $this->callAPISuccess('Contribution', 'create', [ | |
263 | 'contact_id' => $contactId1, | |
264 | 'receive_date' => '2010-01-20', | |
265 | 'financial_type_id' => 'Member Dues', | |
266 | 'contribution_status_id' => 'Completed', | |
267 | 'total_amount' => 150, | |
268 | ]); | |
269 | ||
270 | // create membership type | |
271 | $membershipTypeId1 = $this->callAPISuccess('MembershipType', 'create', [ | |
272 | 'domain_id' => 1, | |
273 | 'member_of_contact_id' => $membershipOrganizationId, | |
274 | 'financial_type_id' => 'Member Dues', | |
275 | 'duration_unit' => 'month', | |
276 | 'duration_interval' => 1, | |
277 | 'period_type' => 'rolling', | |
278 | 'minimum_fee' => 100, | |
279 | 'name' => 'Parent', | |
280 | ])['id']; | |
281 | ||
282 | $membershipTypeID = $this->callAPISuccess('MembershipType', 'create', [ | |
283 | 'domain_id' => 1, | |
284 | 'member_of_contact_id' => $membershipOrganizationId, | |
285 | 'financial_type_id' => 'Member Dues', | |
286 | 'duration_unit' => 'month', | |
287 | 'duration_interval' => 1, | |
288 | 'period_type' => 'rolling', | |
289 | 'minimum_fee' => 50, | |
290 | 'name' => 'Child', | |
291 | ])['id']; | |
292 | ||
293 | $contactIDs = [ | |
294 | $contactId1 => $membershipTypeId1, | |
295 | $contactId2 => $membershipTypeID, | |
296 | ]; | |
297 | ||
298 | $contributionRecurId = $this->callAPISuccess('contribution_recur', 'create', $this->_params)['id']; | |
299 | ||
300 | $priceFields = CRM_Price_BAO_PriceSet::getDefaultPriceSet('membership'); | |
301 | ||
302 | // prepare order api params. | |
303 | $params = [ | |
304 | 'contact_id' => $contactId1, | |
305 | 'receive_date' => '2010-01-20', | |
306 | 'financial_type_id' => 'Member Dues', | |
307 | 'contribution_status_id' => 'Pending', | |
308 | 'contribution_recur_id' => $contributionRecurId, | |
309 | 'total_amount' => 150, | |
310 | 'api.Payment.create' => ['total_amount' => 150], | |
311 | ]; | |
312 | ||
313 | foreach ($priceFields as $priceField) { | |
314 | $lineItems = []; | |
315 | $contactId = array_search($priceField['membership_type_id'], $contactIDs); | |
316 | $lineItems[1] = [ | |
317 | 'price_field_id' => $priceField['priceFieldID'], | |
318 | 'price_field_value_id' => $priceField['priceFieldValueID'], | |
319 | 'label' => $priceField['label'], | |
320 | 'field_title' => $priceField['label'], | |
321 | 'qty' => 1, | |
322 | 'unit_price' => $priceField['amount'], | |
323 | 'line_total' => $priceField['amount'], | |
324 | 'financial_type_id' => $priceField['financial_type_id'], | |
325 | 'entity_table' => 'civicrm_membership', | |
326 | 'membership_type_id' => $priceField['membership_type_id'], | |
327 | ]; | |
328 | $params['line_items'][] = [ | |
329 | 'line_item' => $lineItems, | |
330 | 'params' => [ | |
331 | 'contact_id' => $contactId, | |
332 | 'membership_type_id' => $priceField['membership_type_id'], | |
333 | 'source' => 'Payment', | |
334 | 'join_date' => '2020-04-28', | |
335 | 'start_date' => '2020-04-28', | |
336 | 'contribution_recur_id' => $contributionRecurId, | |
337 | 'status_id' => 'Pending', | |
338 | 'is_override' => 1, | |
339 | ], | |
340 | ]; | |
341 | } | |
342 | $order = $this->callAPISuccess('Order', 'create', $params); | |
343 | $contributionId = $order['id']; | |
344 | $membershipId1 = $this->callAPISuccessGetValue('Membership', [ | |
345 | 'contact_id' => $contactId1, | |
346 | 'membership_type_id' => $membershipTypeId1, | |
347 | 'return' => 'id', | |
348 | ]); | |
349 | ||
350 | $membershipId2 = $this->callAPISuccessGetValue('Membership', [ | |
351 | 'contact_id' => $contactId2, | |
352 | 'membership_type_id' => $membershipTypeID, | |
353 | 'return' => 'id', | |
354 | ]); | |
355 | ||
356 | // First renewal (2nd payment). | |
357 | $this->callAPISuccess('Contribution', 'repeattransaction', [ | |
358 | 'original_contribution_id' => $contributionId, | |
359 | 'contribution_status_id' => 'Completed', | |
360 | ]); | |
361 | ||
362 | // Second Renewal (3rd payment). | |
363 | $this->callAPISuccess('Contribution', 'repeattransaction', [ | |
364 | 'original_contribution_id' => $contributionId, | |
365 | 'contribution_status_id' => 'Completed', | |
366 | ]); | |
367 | ||
368 | // Third renewal (4th payment). | |
369 | $this->callAPISuccess('Contribution', 'repeattransaction', ['original_contribution_id' => $contributionId, 'contribution_status_id' => 'Completed']); | |
370 | ||
371 | // check line item and membership payment count. | |
372 | $this->validateAllCounts($membershipId1, 4); | |
373 | $this->validateAllCounts($membershipId2, 4); | |
374 | ||
375 | // check membership end date. | |
376 | foreach ([$membershipId1, $membershipId2] as $mId) { | |
377 | $endDate = $this->callAPISuccessGetValue('Membership', [ | |
378 | 'id' => $mId, | |
379 | 'return' => 'end_date', | |
380 | ]); | |
381 | $this->assertEquals($endDate, '2020-08-27', ts('End date incorrect.')); | |
382 | } | |
383 | ||
384 | // At this moment Contact 2 is deceased, but we wait until payment is recorded in civi before marking the contact deceased. | |
385 | // At payment Gateway we update the amount from 150 to 100 | |
386 | // IPN is recorded for subsequent payment (5th payment). | |
387 | $contribution = $this->callAPISuccess('Contribution', 'repeattransaction', [ | |
388 | 'original_contribution_id' => $contributionId, | |
389 | 'contribution_status_id' => 'Completed', | |
390 | 'total_amount' => '100', | |
391 | ]); | |
392 | ||
393 | // now we mark the contact2 as deceased. | |
394 | $this->callAPISuccess('Contact', 'create', [ | |
395 | 'id' => $contactId2, | |
396 | 'is_deceased' => 1, | |
397 | ]); | |
398 | ||
399 | // We delete latest membership payment and line item. | |
400 | $lineItemId = $this->callAPISuccessGetValue('LineItem', [ | |
401 | 'contribution_id' => $contribution['id'], | |
402 | 'entity_id' => $membershipId2, | |
403 | 'entity_table' => 'civicrm_membership', | |
404 | 'return' => 'id', | |
405 | ]); | |
406 | ||
407 | // No api to delete membership payment. | |
408 | CRM_Core_DAO::executeQuery(' | |
409 | DELETE FROM civicrm_membership_payment | |
410 | WHERE contribution_id = %1 | |
411 | AND membership_id = %2 | |
412 | ', [ | |
413 | 1 => [$contribution['id'], 'Integer'], | |
414 | 2 => [$membershipId2, 'Integer'], | |
415 | ]); | |
416 | ||
417 | $this->callAPISuccess('LineItem', 'delete', [ | |
418 | 'id' => $lineItemId, | |
419 | ]); | |
420 | ||
421 | // set membership recurring to null. | |
422 | $this->callAPISuccess('Membership', 'create', [ | |
423 | 'id' => $membershipId2, | |
424 | 'contribution_recur_id' => NULL, | |
425 | ]); | |
426 | ||
427 | // check line item and membership payment count. | |
428 | $this->validateAllCounts($membershipId1, 5); | |
429 | $this->validateAllCounts($membershipId2, 4); | |
430 | ||
431 | $checkAgainst = $this->callAPISuccessGetSingle('Membership', [ | |
432 | 'id' => $membershipId2, | |
433 | 'return' => ['end_date', 'status_id'], | |
434 | ]); | |
435 | ||
436 | // record next subsequent payment (6th payment). | |
437 | $this->callAPISuccess('Contribution', 'repeattransaction', [ | |
438 | 'original_contribution_id' => $contributionId, | |
439 | 'contribution_status_id' => 'Completed', | |
440 | 'total_amount' => '100', | |
441 | ]); | |
442 | ||
443 | // check membership id 1 is renewed | |
444 | $endDate = $this->callAPISuccessGetValue('Membership', [ | |
445 | 'id' => $membershipId1, | |
446 | 'return' => 'end_date', | |
447 | ]); | |
448 | $this->assertEquals($endDate, '2020-10-27', ts('End date incorrect.')); | |
449 | // check line item and membership payment count. | |
450 | $this->validateAllCounts($membershipId1, 6); | |
451 | $this->validateAllCounts($membershipId2, 4); | |
452 | ||
453 | // check if membership status and end date is not changed. | |
454 | $membership2 = $this->callAPISuccessGetSingle('Membership', [ | |
455 | 'id' => $membershipId2, | |
456 | 'return' => ['end_date', 'status_id'], | |
457 | ]); | |
458 | $this->assertSame($membership2, $checkAgainst); | |
459 | } | |
460 | ||
461 | /** | |
462 | * Check line item and membership payment count. | |
463 | * | |
464 | * @param int $membershipId | |
465 | * @param int $count | |
466 | * | |
467 | * @throws \CRM_Core_Exception | |
468 | */ | |
469 | public function validateAllCounts($membershipId, $count) { | |
470 | $memPayParams = [ | |
471 | 'membership_id' => $membershipId, | |
472 | ]; | |
473 | $lineItemParams = [ | |
474 | 'entity_id' => $membershipId, | |
475 | 'entity_table' => 'civicrm_membership', | |
476 | ]; | |
477 | $this->callAPISuccessGetCount('LineItem', $lineItemParams, $count); | |
478 | $this->callAPISuccessGetCount('MembershipPayment', $memPayParams, $count); | |
479 | } | |
480 | ||
bbf58b03 | 481 | } |