re-re-fix test
[civicrm-core.git] / tests / phpunit / CRM / Contribute / BAO / ContributionRecurTest.php
CommitLineData
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 16class 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',
fd684278 334 'join_date' => date('Y-m') . '-28',
335 'start_date' => date('Y-m') . '-28',
d486b01e
PN
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
e9b9bb9c 375 $expectedDate = $this->getYearAndMonthFromOffset(4);
d486b01e
PN
376 // check membership end date.
377 foreach ([$membershipId1, $membershipId2] as $mId) {
378 $endDate = $this->callAPISuccessGetValue('Membership', [
379 'id' => $mId,
380 'return' => 'end_date',
381 ]);
e9b9bb9c 382 $this->assertEquals("{$expectedDate['year']}-{$expectedDate['month']}-27", $endDate, ts('End date incorrect.'));
d486b01e
PN
383 }
384
385 // At this moment Contact 2 is deceased, but we wait until payment is recorded in civi before marking the contact deceased.
386 // At payment Gateway we update the amount from 150 to 100
387 // IPN is recorded for subsequent payment (5th payment).
388 $contribution = $this->callAPISuccess('Contribution', 'repeattransaction', [
389 'original_contribution_id' => $contributionId,
390 'contribution_status_id' => 'Completed',
391 'total_amount' => '100',
392 ]);
393
394 // now we mark the contact2 as deceased.
395 $this->callAPISuccess('Contact', 'create', [
396 'id' => $contactId2,
397 'is_deceased' => 1,
398 ]);
399
400 // We delete latest membership payment and line item.
401 $lineItemId = $this->callAPISuccessGetValue('LineItem', [
402 'contribution_id' => $contribution['id'],
403 'entity_id' => $membershipId2,
404 'entity_table' => 'civicrm_membership',
405 'return' => 'id',
406 ]);
407
408 // No api to delete membership payment.
409 CRM_Core_DAO::executeQuery('
410 DELETE FROM civicrm_membership_payment
411 WHERE contribution_id = %1
412 AND membership_id = %2
413 ', [
414 1 => [$contribution['id'], 'Integer'],
415 2 => [$membershipId2, 'Integer'],
416 ]);
417
418 $this->callAPISuccess('LineItem', 'delete', [
419 'id' => $lineItemId,
420 ]);
421
422 // set membership recurring to null.
423 $this->callAPISuccess('Membership', 'create', [
424 'id' => $membershipId2,
425 'contribution_recur_id' => NULL,
426 ]);
427
428 // check line item and membership payment count.
429 $this->validateAllCounts($membershipId1, 5);
430 $this->validateAllCounts($membershipId2, 4);
431
432 $checkAgainst = $this->callAPISuccessGetSingle('Membership', [
433 'id' => $membershipId2,
434 'return' => ['end_date', 'status_id'],
435 ]);
436
437 // record next subsequent payment (6th payment).
438 $this->callAPISuccess('Contribution', 'repeattransaction', [
439 'original_contribution_id' => $contributionId,
440 'contribution_status_id' => 'Completed',
441 'total_amount' => '100',
442 ]);
443
444 // check membership id 1 is renewed
445 $endDate = $this->callAPISuccessGetValue('Membership', [
446 'id' => $membershipId1,
447 'return' => 'end_date',
448 ]);
e9b9bb9c 449 $expectedDate = $this->getYearAndMonthFromOffset(6);
450 $this->assertEquals("{$expectedDate['year']}-{$expectedDate['month']}-27", $endDate, ts('End date incorrect.'));
d486b01e
PN
451 // check line item and membership payment count.
452 $this->validateAllCounts($membershipId1, 6);
453 $this->validateAllCounts($membershipId2, 4);
454
455 // check if membership status and end date is not changed.
456 $membership2 = $this->callAPISuccessGetSingle('Membership', [
457 'id' => $membershipId2,
458 'return' => ['end_date', 'status_id'],
459 ]);
460 $this->assertSame($membership2, $checkAgainst);
461 }
462
463 /**
464 * Check line item and membership payment count.
465 *
466 * @param int $membershipId
467 * @param int $count
468 *
469 * @throws \CRM_Core_Exception
470 */
471 public function validateAllCounts($membershipId, $count) {
472 $memPayParams = [
473 'membership_id' => $membershipId,
474 ];
475 $lineItemParams = [
476 'entity_id' => $membershipId,
477 'entity_table' => 'civicrm_membership',
478 ];
479 $this->callAPISuccessGetCount('LineItem', $lineItemParams, $count);
480 $this->callAPISuccessGetCount('MembershipPayment', $memPayParams, $count);
481 }
482
e9b9bb9c 483 /**
484 * Given a number of months offset, get the year and month.
485 * Note the way php arithmetic works, using strtotime('+x months') doesn't
486 * work because it will roll over the day accounting for different number
487 * of days in the month, but we want the same day of the month, x months
488 * from now.
489 * e.g. July 31 + 4 months will return Dec 1 if using php functions, but
490 * we want Nov 31.
491 *
492 * @param int $offset
493 * @param int $year Optional input year to start
494 * @param int $month Optional input month to start
495 *
496 * @return array
497 * ['year' => int, 'month' => int]
498 */
499 private function getYearAndMonthFromOffset(int $offset, int $year = NULL, int $month = NULL) {
500 $dateInfo = [
501 'year' => $year ?? date('Y'),
502 'month' => ($month ?? date('m')) + $offset,
503 ];
504 if ($dateInfo['month'] > 12) {
505 $dateInfo['year']++;
506 $dateInfo['month'] -= 12;
507 }
508 if ($dateInfo['month'] < 10) {
509 $dateInfo['month'] = "0{$dateInfo['month']}";
510 }
511
512 return $dateInfo;
513 }
514
515 /**
516 * Test getYearAndMonthFromOffset
517 * @dataProvider yearMonthProvider
518 *
519 * @param array $input
520 * @param array $expected
521 */
522 public function testGetYearAndMonthFromOffset($input, $expected) {
523 $this->assertEquals($expected, $this->getYearAndMonthFromOffset($input[0], $input[1], $input[2]));
524 }
525
526 /**
527 * data provider for testGetYearAndMonthFromOffset
528 */
529 public function yearMonthProvider() {
530 return [
531 // input = offset, year, current month
532 ['input' => [4, 2020, 1], 'output' => ['year' => '2020', 'month' => '05']],
533 ['input' => [6, 2020, 1], 'output' => ['year' => '2020', 'month' => '07']],
534 ['input' => [4, 2020, 2], 'output' => ['year' => '2020', 'month' => '06']],
535 ['input' => [6, 2020, 2], 'output' => ['year' => '2020', 'month' => '08']],
536 ['input' => [4, 2020, 3], 'output' => ['year' => '2020', 'month' => '07']],
537 ['input' => [6, 2020, 3], 'output' => ['year' => '2020', 'month' => '09']],
538 ['input' => [4, 2020, 4], 'output' => ['year' => '2020', 'month' => '08']],
539 ['input' => [6, 2020, 4], 'output' => ['year' => '2020', 'month' => '10']],
540 ['input' => [4, 2020, 5], 'output' => ['year' => '2020', 'month' => '09']],
541 ['input' => [6, 2020, 5], 'output' => ['year' => '2020', 'month' => '11']],
542 ['input' => [4, 2020, 6], 'output' => ['year' => '2020', 'month' => '10']],
543 ['input' => [6, 2020, 6], 'output' => ['year' => '2020', 'month' => '12']],
544 ['input' => [4, 2020, 7], 'output' => ['year' => '2020', 'month' => '11']],
545 ['input' => [6, 2020, 7], 'output' => ['year' => '2021', 'month' => '01']],
546 ['input' => [4, 2020, 8], 'output' => ['year' => '2020', 'month' => '12']],
547 ['input' => [6, 2020, 8], 'output' => ['year' => '2021', 'month' => '02']],
548 ['input' => [4, 2020, 9], 'output' => ['year' => '2021', 'month' => '01']],
549 ['input' => [6, 2020, 9], 'output' => ['year' => '2021', 'month' => '03']],
550 ['input' => [4, 2020, 10], 'output' => ['year' => '2021', 'month' => '02']],
551 ['input' => [6, 2020, 10], 'output' => ['year' => '2021', 'month' => '04']],
552 ['input' => [4, 2020, 11], 'output' => ['year' => '2021', 'month' => '03']],
553 ['input' => [6, 2020, 11], 'output' => ['year' => '2021', 'month' => '05']],
554 ['input' => [4, 2020, 12], 'output' => ['year' => '2021', 'month' => '04']],
555 ['input' => [6, 2020, 12], 'output' => ['year' => '2021', 'month' => '06']],
556 ];
557 }
558
bbf58b03 559}