If a template contribution is updated we need to update the amount on the recurring...
[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
ae16be86 12use Civi\Api4\ContributionRecur;
13
7fe37828
EM
14/**
15 * Class CRM_Contribute_BAO_ContributionRecurTest
acb109b7 16 * @group headless
7fe37828 17 */
bbf58b03 18class CRM_Contribute_BAO_ContributionRecurTest extends CiviUnitTestCase {
bbf58b03 19
d486b01e
PN
20 use CRMTraits_Financial_OrderTrait;
21
a33b79a2
EM
22 protected $isValidateFinancialsOnPostAssert = TRUE;
23
d486b01e
PN
24 /**
25 * Set up for test.
26 *
ae16be86 27 * @throws \CiviCRM_API3_Exception
d486b01e 28 */
ae16be86 29 public function setUp(): void {
bbf58b03 30 parent::setUp();
ae16be86 31 $this->ids['payment_processor'] = $this->paymentProcessorCreate();
9099cab3 32 $this->_params = [
bbf58b03
EM
33 'contact_id' => $this->individualCreate(),
34 'amount' => 3.00,
35 'frequency_unit' => 'week',
36 'frequency_interval' => 1,
37 'installments' => 2,
38 'start_date' => 'yesterday',
39 'create_date' => 'yesterday',
40 'modified_date' => 'yesterday',
41 'cancel_date' => NULL,
42 'end_date' => '+ 2 weeks',
43 'processor_id' => '643411460836',
44 'trxn_id' => 'e0d0808e26f3e661c6c18eb7c039d363',
45 'invoice_id' => 'e0d0808e26f3e661c6c18eb7c039d363',
46 'contribution_status_id' => 1,
47 'is_test' => 0,
48 'cycle_day' => 1,
49 'next_sched_contribution_date' => '+ 1 week',
50 'failure_count' => 0,
51 'failure_retry_date' => NULL,
52 'auto_renew' => 0,
53 'currency' => 'USD',
ae16be86 54 'payment_processor_id' => $this->ids['payment_processor'],
bbf58b03
EM
55 'is_email_receipt' => 1,
56 'financial_type_id' => 1,
57 'payment_instrument_id' => 1,
58 'campaign_id' => NULL,
9099cab3 59 ];
bbf58b03
EM
60 }
61
d486b01e
PN
62 /**
63 * Cleanup after test.
64 *
65 * @throws \CRM_Core_Exception
66 */
ae16be86 67 public function teardown():void {
d486b01e 68 $this->quickCleanUpFinancialEntities();
bbf58b03
EM
69 }
70
71 /**
fe482240
EM
72 * Test that an object can be retrieved & saved (per CRM-14986).
73 *
74 * This has been causing a DB error so we are checking for absence of error
d486b01e
PN
75 *
76 * @throws \CRM_Core_Exception
bbf58b03 77 */
ae16be86 78 public function testFindSave(): void {
bbf58b03
EM
79 $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params);
80 $dao = new CRM_Contribute_BAO_ContributionRecur();
81 $dao->id = $contributionRecur['id'];
82 $dao->find(TRUE);
83 $dao->is_email_receipt = 0;
84 $dao->save();
85 }
86
87 /**
fe482240
EM
88 * Test cancellation works per CRM-14986.
89 *
90 * We are checking for absence of error.
d486b01e
PN
91 *
92 * @throws \CRM_Core_Exception
bbf58b03 93 */
ae16be86 94 public function testCancelRecur(): void {
bbf58b03 95 $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params);
9cfc631e 96 CRM_Contribute_BAO_ContributionRecur::cancelRecurContribution(['id' => $contributionRecur['id']]);
bbf58b03
EM
97 }
98
748bcfb2 99 /**
0b2f1f29 100 * Test checking if contribution recur object can allow for changes to financial types.
748bcfb2 101 *
ae16be86 102 * @throws \CRM_Core_Exception|\CiviCRM_API3_Exception
748bcfb2 103 */
ae16be86 104 public function testSupportFinancialTypeChange(): void {
748bcfb2 105 $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params);
9099cab3 106 $this->callAPISuccess('Contribution', 'create', [
748bcfb2
SL
107 'contribution_recur_id' => $contributionRecur['id'],
108 'total_amount' => '3.00',
109 'financial_type_id' => 1,
110 'payment_instrument_id' => 1,
111 'currency' => 'USD',
112 'contact_id' => $this->individualCreate(),
113 'contribution_status_id' => 1,
46f459f2 114 'receive_date' => 'yesterday',
9099cab3 115 ]);
748bcfb2
SL
116 $this->assertTrue(CRM_Contribute_BAO_ContributionRecur::supportsFinancialTypeChange($contributionRecur['id']));
117 }
118
3d6bf1a7
EE
119 /**
120 * Test we don't change unintended fields on API edit
d486b01e
PN
121 *
122 * @throws \CRM_Core_Exception
3d6bf1a7 123 */
ae16be86 124 public function testUpdateRecur(): void {
3d6bf1a7
EE
125 $createParams = $this->_params;
126 $createParams['currency'] = 'XAU';
127 $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $createParams);
9099cab3 128 $editParams = [
3d6bf1a7
EE
129 'id' => $contributionRecur['id'],
130 'end_date' => '+ 4 weeks',
9099cab3 131 ];
3d6bf1a7
EE
132 $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $editParams);
133 $dao = new CRM_Contribute_BAO_ContributionRecur();
134 $dao->id = $contributionRecur['id'];
135 $dao->find(TRUE);
136 $this->assertEquals('XAU', $dao->currency, 'Edit clobbered recur currency');
137 }
138
0b2f1f29
AS
139 /**
140 * Check test contributions aren't picked up as template for non-test recurs
141 *
d486b01e
PN
142 * @throws \API_Exception
143 * @throws \CRM_Core_Exception
144 * @throws \CiviCRM_API3_Exception
145 * @throws \Civi\API\Exception\UnauthorizedException
0b2f1f29 146 */
ae16be86 147 public function testGetTemplateContributionMatchTest1(): void {
0b2f1f29
AS
148 $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params);
149 // Create a first contrib
150 $firstContrib = $this->callAPISuccess('Contribution', 'create', [
151 'contribution_recur_id' => $contributionRecur['id'],
152 'total_amount' => '3.00',
153 'financial_type_id' => 1,
154 'payment_instrument_id' => 1,
155 'currency' => 'USD',
156 'contact_id' => $this->individualCreate(),
157 'contribution_status_id' => 1,
158 'receive_date' => 'yesterday',
159 ]);
160 // Create a test contrib - should not be picked up as template for non-test recur
161 $this->callAPISuccess('Contribution', 'create', [
162 'contribution_recur_id' => $contributionRecur['id'],
163 'total_amount' => '3.00',
164 'financial_type_id' => 1,
165 'payment_instrument_id' => 1,
166 'currency' => 'USD',
167 'contact_id' => $this->individualCreate(),
168 'contribution_status_id' => 1,
169 'receive_date' => 'yesterday',
170 'is_test' => 1,
171 ]);
172 $fetchedTemplate = CRM_Contribute_BAO_ContributionRecur::getTemplateContribution($contributionRecur['id']);
173 $this->assertEquals($firstContrib['id'], $fetchedTemplate['id']);
174 }
175
176 /**
177 * Check non-test contributions aren't picked up as template for test recurs
178 *
d486b01e
PN
179 * @throws \API_Exception
180 * @throws \CRM_Core_Exception
181 * @throws \CiviCRM_API3_Exception
182 * @throws \Civi\API\Exception\UnauthorizedException
0b2f1f29 183 */
ae16be86 184 public function testGetTemplateContributionMatchTest(): void {
0b2f1f29
AS
185 $params = $this->_params;
186 $params['is_test'] = 1;
187 $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $params);
188 // Create a first test contrib
189 $firstContrib = $this->callAPISuccess('Contribution', 'create', [
190 'contribution_recur_id' => $contributionRecur['id'],
191 'total_amount' => '3.00',
192 'financial_type_id' => 1,
193 'payment_instrument_id' => 1,
194 'currency' => 'USD',
195 'contact_id' => $this->individualCreate(),
196 'contribution_status_id' => 1,
197 'receive_date' => 'yesterday',
198 'is_test' => 1,
199 ]);
200 // Create a non-test contrib - should not be picked up as template for non-test recur
201 // This shouldn't occur - a live contrib against a test recur, but that's not the point...
202 $this->callAPISuccess('Contribution', 'create', [
203 'contribution_recur_id' => $contributionRecur['id'],
204 'total_amount' => '3.00',
205 'financial_type_id' => 1,
206 'payment_instrument_id' => 1,
207 'currency' => 'USD',
208 'contact_id' => $this->individualCreate(),
209 'contribution_status_id' => 1,
210 'receive_date' => 'yesterday',
211 'is_test' => 0,
212 ]);
213 $fetchedTemplate = CRM_Contribute_BAO_ContributionRecur::getTemplateContribution($contributionRecur['id']);
214 $this->assertEquals($firstContrib['id'], $fetchedTemplate['id']);
215 }
216
7d238e35
JJ
217 /**
218 * Check whether template contribution is created based on the first contribution.
219 *
220 * There are three contributions created. Each of them with a different value at a custom field.
221 * The first contribution created should be copied as a template contribution.
222 * The other two should not be used as a template.
223 *
224 * Then we delete the template contribution and make sure a new one exists.
225 * At that time the second contribution should be used a template as that is the most recent one (according to the date).
226 *
227 * @throws \API_Exception
228 * @throws \CRM_Core_Exception
229 * @throws \CiviCRM_API3_Exception
230 * @throws \Civi\API\Exception\UnauthorizedException
231 */
232 public function testCreateTemplateContributionFromFirstContributionTest(): void {
233 $custom_group = $this->customGroupCreate(['extends' => 'Contribution', 'name' => 'template']);
234 $custom_field = $this->customFieldCreate(['custom_group_id' => $custom_group['id'], 'name' => 'field']);
235
236 $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params);
237 // Create a first test contrib
238 $date = new DateTime();
239 $firstContrib = $this->callAPISuccess('Contribution', 'create', [
240 'contribution_recur_id' => $contributionRecur['id'],
241 'total_amount' => '3.00',
242 'financial_type_id' => 1,
243 'payment_instrument_id' => 1,
244 'currency' => 'USD',
245 'contact_id' => $this->_params['contact_id'],
246 'contribution_status_id' => 1,
247 'receive_date' => $date->format('YmdHis'),
248 'custom_' . $custom_field['id'] => 'First Contribution',
249 ]);
250 $date->modify('+2 days');
251 $secondContrib = $this->callAPISuccess('Contribution', 'create', [
252 'contribution_recur_id' => $contributionRecur['id'],
253 'total_amount' => '3.00',
254 'financial_type_id' => 1,
255 'payment_instrument_id' => 1,
256 'currency' => 'USD',
257 'contact_id' => $this->_params['contact_id'],
258 'contribution_status_id' => 1,
259 'receive_date' => $date->format('YmdHis'),
260 'custom_' . $custom_field['id'] => 'Second and most recent Contribution',
261 ]);
262
263 $date->modify('-1 week');
264 $thirdContrib = $this->callAPISuccess('Contribution', 'create', [
265 'contribution_recur_id' => $contributionRecur['id'],
266 'total_amount' => '3.00',
267 'financial_type_id' => 1,
268 'payment_instrument_id' => 1,
269 'currency' => 'USD',
270 'contact_id' => $this->_params['contact_id'],
271 'contribution_status_id' => 1,
272 'receive_date' => $date->format('YmdHis'),
273 'custom_' . $custom_field['id'] => 'Third Contribution',
274 ]);
275
276 // Make sure a template contribution exists.
277 $templateContributionId = CRM_Contribute_BAO_ContributionRecur::ensureTemplateContributionExists($contributionRecur['id']);
278 $fetchedTemplate = CRM_Contribute_BAO_ContributionRecur::getTemplateContribution($contributionRecur['id']);
279 $templateContribution = \Civi\Api4\Contribution::get(FALSE)
280 ->addSelect('*', 'custom.*')
281 ->addWhere('contribution_recur_id', '=', $contributionRecur['id'])
282 ->addWhere('is_template', '=', 1)
283 ->addWhere('is_test', '=', 0)
284 ->addOrderBy('id', 'DESC')
285 ->execute();
286
287 $this->assertNotEquals($firstContrib['id'], $fetchedTemplate['id']);
288 $this->assertNotEquals($secondContrib['id'], $fetchedTemplate['id']);
289 $this->assertNotEquals($thirdContrib['id'], $fetchedTemplate['id']);
290 $this->assertEquals($templateContributionId, $fetchedTemplate['id']);
291 $this->assertTrue($fetchedTemplate['is_template']);
292 $this->assertFalse($fetchedTemplate['is_test']);
293 $this->assertEquals(1, $templateContribution->count());
294 $templateContribution = $templateContribution->first();
295 $this->assertNotNull($templateContribution['template.field']);
296 $this->assertEquals('Second and most recent Contribution', $templateContribution['template.field']);
29b23231
SL
297 $this->callAPISuccess('CustomField', 'delete', ['id' => $custom_field['id']]);
298 $this->callAPISuccess('CustomGroup', 'delete', ['id' => $custom_group['id']]);
7d238e35
JJ
299 }
300
f306dbeb
AS
301 /**
302 * Test that is_template contribution is used where available
303 *
d486b01e 304 * @throws \API_Exception
d486b01e
PN
305 * @throws \CiviCRM_API3_Exception
306 * @throws \Civi\API\Exception\UnauthorizedException
f306dbeb 307 */
ae16be86 308 public function testGetTemplateContributionNewTemplate(): void {
f306dbeb
AS
309 $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params);
310 // Create the template
311 $templateContrib = $this->callAPISuccess('Contribution', 'create', [
312 'contribution_recur_id' => $contributionRecur['id'],
313 'total_amount' => '3.00',
314 'financial_type_id' => 1,
c80d977f 315 'source' => 'Template Contribution',
f306dbeb 316 'payment_instrument_id' => 1,
aed0a241 317 'currency' => 'AUD',
f306dbeb
AS
318 'contact_id' => $this->individualCreate(),
319 'contribution_status_id' => 1,
320 'receive_date' => 'yesterday',
321 'is_template' => 1,
322 ]);
323 // Create another normal contrib
324 $this->callAPISuccess('Contribution', 'create', [
325 'contribution_recur_id' => $contributionRecur['id'],
326 'total_amount' => '3.00',
327 'financial_type_id' => 1,
c80d977f 328 'source' => 'Non-template Contribution',
f306dbeb
AS
329 'payment_instrument_id' => 1,
330 'currency' => 'USD',
331 'contact_id' => $this->individualCreate(),
332 'contribution_status_id' => 1,
333 'receive_date' => 'yesterday',
334 ]);
335 $fetchedTemplate = CRM_Contribute_BAO_ContributionRecur::getTemplateContribution($contributionRecur['id']);
336 // Fetched template should be the is_template, not the latest contrib
337 $this->assertEquals($fetchedTemplate['id'], $templateContrib['id']);
c80d977f
JP
338
339 $repeatContribution = $this->callAPISuccess('Contribution', 'repeattransaction', [
340 'contribution_status_id' => 'Completed',
341 'contribution_recur_id' => $contributionRecur['id'],
342 ]);
343 $this->assertEquals('Template Contribution', $repeatContribution['values'][$repeatContribution['id']]['source']);
aed0a241 344 $this->assertEquals('AUD', $repeatContribution['values'][$repeatContribution['id']]['currency']);
f306dbeb
AS
345 }
346
e5b9a6bd
MW
347 /**
348 * Test that is_template contribution is used where available
349 *
350 * @throws \API_Exception
351 * @throws \CiviCRM_API3_Exception
352 * @throws \Civi\API\Exception\UnauthorizedException
353 */
354 public function testTemplateContributionUpdatesRecur(): void {
355 $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', $this->_params);
356 $contributionRecur = reset($contributionRecur['values']);
357 // Create the template
358 $templateContrib = $this->callAPISuccess('Contribution', 'create', [
359 'contribution_recur_id' => $contributionRecur['id'],
360 'total_amount' => '3.00',
361 'financial_type_id' => 1,
362 'source' => 'Template Contribution',
363 'payment_instrument_id' => 1,
364 'currency' => 'AUD',
365 'contact_id' => $this->individualCreate(),
366 'contribution_status_id' => 1,
367 'receive_date' => 'yesterday',
368 'is_template' => 1,
369 ]);
370 $this->callAPISuccess('Contribution', 'create', [
371 'id' => $templateContrib['id'],
372 'contribution_recur_id' => $contributionRecur['id'],
373 'total_amount' => '2.00',
374 'currency' => 'USD',
375 ]);
376 $updatedContributionRecur = \Civi\Api4\ContributionRecur::get(FALSE)
377 ->addWhere('id', '=', $contributionRecur['id'])
378 ->execute()
379 ->first();
380 $this->assertEquals('USD', $updatedContributionRecur['currency']);
381 $this->assertEquals('2.00', $updatedContributionRecur['amount']);
382 $this->assertGreaterThan(
383 strtotime($contributionRecur['modified_date']),
384 strtotime($updatedContributionRecur['modified_date'])
385 );
386 }
387
d486b01e
PN
388 /**
389 * Test to check if correct membership is auto renewed.
390 *
ae16be86 391 * @throws \CRM_Core_Exception|\CiviCRM_API3_Exception
d486b01e 392 */
ae16be86 393 public function testAutoRenewalWhenOneMemberIsDeceased(): void {
d486b01e
PN
394 $contactId1 = $this->individualCreate();
395 $contactId2 = $this->individualCreate();
396 $membershipOrganizationId = $this->organizationCreate();
397
398 $this->createExtraneousContribution();
399 $this->callAPISuccess('Contribution', 'create', [
400 'contact_id' => $contactId1,
401 'receive_date' => '2010-01-20',
402 'financial_type_id' => 'Member Dues',
403 'contribution_status_id' => 'Completed',
404 'total_amount' => 150,
405 ]);
406
407 // create membership type
ae16be86 408 $membershipTypeId1 = (int) $this->callAPISuccess('MembershipType', 'create', [
d486b01e
PN
409 'domain_id' => 1,
410 'member_of_contact_id' => $membershipOrganizationId,
411 'financial_type_id' => 'Member Dues',
412 'duration_unit' => 'month',
413 'duration_interval' => 1,
414 'period_type' => 'rolling',
415 'minimum_fee' => 100,
416 'name' => 'Parent',
417 ])['id'];
418
ae16be86 419 $membershipTypeID = (int) $this->callAPISuccess('MembershipType', 'create', [
d486b01e
PN
420 'domain_id' => 1,
421 'member_of_contact_id' => $membershipOrganizationId,
422 'financial_type_id' => 'Member Dues',
423 'duration_unit' => 'month',
424 'duration_interval' => 1,
425 'period_type' => 'rolling',
426 'minimum_fee' => 50,
427 'name' => 'Child',
428 ])['id'];
429
430 $contactIDs = [
431 $contactId1 => $membershipTypeId1,
432 $contactId2 => $membershipTypeID,
433 ];
434
435 $contributionRecurId = $this->callAPISuccess('contribution_recur', 'create', $this->_params)['id'];
436
437 $priceFields = CRM_Price_BAO_PriceSet::getDefaultPriceSet('membership');
438
439 // prepare order api params.
440 $params = [
441 'contact_id' => $contactId1,
442 'receive_date' => '2010-01-20',
443 'financial_type_id' => 'Member Dues',
d486b01e
PN
444 'contribution_recur_id' => $contributionRecurId,
445 'total_amount' => 150,
446 'api.Payment.create' => ['total_amount' => 150],
447 ];
448
449 foreach ($priceFields as $priceField) {
450 $lineItems = [];
ae16be86 451 $contactId = array_search((int) $priceField['membership_type_id'], $contactIDs, TRUE);
d486b01e
PN
452 $lineItems[1] = [
453 'price_field_id' => $priceField['priceFieldID'],
454 'price_field_value_id' => $priceField['priceFieldValueID'],
455 'label' => $priceField['label'],
456 'field_title' => $priceField['label'],
457 'qty' => 1,
458 'unit_price' => $priceField['amount'],
459 'line_total' => $priceField['amount'],
460 'financial_type_id' => $priceField['financial_type_id'],
461 'entity_table' => 'civicrm_membership',
462 'membership_type_id' => $priceField['membership_type_id'],
463 ];
464 $params['line_items'][] = [
465 'line_item' => $lineItems,
466 'params' => [
467 'contact_id' => $contactId,
468 'membership_type_id' => $priceField['membership_type_id'],
469 'source' => 'Payment',
dadbf1e3 470 'join_date' => date('Y-m', strtotime('1 month ago')) . '-28',
fd684278 471 'start_date' => date('Y-m') . '-28',
d486b01e
PN
472 'contribution_recur_id' => $contributionRecurId,
473 'status_id' => 'Pending',
474 'is_override' => 1,
475 ],
476 ];
477 }
478 $order = $this->callAPISuccess('Order', 'create', $params);
479 $contributionId = $order['id'];
480 $membershipId1 = $this->callAPISuccessGetValue('Membership', [
481 'contact_id' => $contactId1,
482 'membership_type_id' => $membershipTypeId1,
483 'return' => 'id',
484 ]);
485
486 $membershipId2 = $this->callAPISuccessGetValue('Membership', [
487 'contact_id' => $contactId2,
488 'membership_type_id' => $membershipTypeID,
489 'return' => 'id',
490 ]);
491
492 // First renewal (2nd payment).
493 $this->callAPISuccess('Contribution', 'repeattransaction', [
494 'original_contribution_id' => $contributionId,
495 'contribution_status_id' => 'Completed',
496 ]);
497
498 // Second Renewal (3rd payment).
499 $this->callAPISuccess('Contribution', 'repeattransaction', [
500 'original_contribution_id' => $contributionId,
501 'contribution_status_id' => 'Completed',
502 ]);
503
504 // Third renewal (4th payment).
505 $this->callAPISuccess('Contribution', 'repeattransaction', ['original_contribution_id' => $contributionId, 'contribution_status_id' => 'Completed']);
506
507 // check line item and membership payment count.
508 $this->validateAllCounts($membershipId1, 4);
509 $this->validateAllCounts($membershipId2, 4);
510
e9b9bb9c 511 $expectedDate = $this->getYearAndMonthFromOffset(4);
d486b01e
PN
512 // check membership end date.
513 foreach ([$membershipId1, $membershipId2] as $mId) {
514 $endDate = $this->callAPISuccessGetValue('Membership', [
515 'id' => $mId,
516 'return' => 'end_date',
517 ]);
e9b9bb9c 518 $this->assertEquals("{$expectedDate['year']}-{$expectedDate['month']}-27", $endDate, ts('End date incorrect.'));
d486b01e
PN
519 }
520
521 // At this moment Contact 2 is deceased, but we wait until payment is recorded in civi before marking the contact deceased.
522 // At payment Gateway we update the amount from 150 to 100
523 // IPN is recorded for subsequent payment (5th payment).
524 $contribution = $this->callAPISuccess('Contribution', 'repeattransaction', [
525 'original_contribution_id' => $contributionId,
526 'contribution_status_id' => 'Completed',
527 'total_amount' => '100',
528 ]);
529
530 // now we mark the contact2 as deceased.
531 $this->callAPISuccess('Contact', 'create', [
532 'id' => $contactId2,
533 'is_deceased' => 1,
534 ]);
535
d486b01e
PN
536 // set membership recurring to null.
537 $this->callAPISuccess('Membership', 'create', [
538 'id' => $membershipId2,
539 'contribution_recur_id' => NULL,
540 ]);
541
a33b79a2
EM
542 $this->callAPISuccess('Contribution', 'delete', ['id' => $contribution['id']]);
543 unset($params['line_items'][1]);
544 $params['total_amount'] = 100;
545 $params['line_items'][0]['params']['id'] = $membershipId1;
546 $params['api.Payment.create']['total_amount'] = 100;
547
548 $order = $this->callAPISuccess('Order', 'create', $params);
549
d486b01e
PN
550 // check line item and membership payment count.
551 $this->validateAllCounts($membershipId1, 5);
552 $this->validateAllCounts($membershipId2, 4);
553
554 $checkAgainst = $this->callAPISuccessGetSingle('Membership', [
555 'id' => $membershipId2,
556 'return' => ['end_date', 'status_id'],
557 ]);
558
559 // record next subsequent payment (6th payment).
560 $this->callAPISuccess('Contribution', 'repeattransaction', [
a33b79a2 561 'original_contribution_id' => $order['id'],
d486b01e
PN
562 'contribution_status_id' => 'Completed',
563 'total_amount' => '100',
564 ]);
565
566 // check membership id 1 is renewed
567 $endDate = $this->callAPISuccessGetValue('Membership', [
568 'id' => $membershipId1,
569 'return' => 'end_date',
570 ]);
e9b9bb9c 571 $expectedDate = $this->getYearAndMonthFromOffset(6);
572 $this->assertEquals("{$expectedDate['year']}-{$expectedDate['month']}-27", $endDate, ts('End date incorrect.'));
d486b01e
PN
573 // check line item and membership payment count.
574 $this->validateAllCounts($membershipId1, 6);
575 $this->validateAllCounts($membershipId2, 4);
576
577 // check if membership status and end date is not changed.
578 $membership2 = $this->callAPISuccessGetSingle('Membership', [
579 'id' => $membershipId2,
580 'return' => ['end_date', 'status_id'],
581 ]);
582 $this->assertSame($membership2, $checkAgainst);
583 }
584
585 /**
586 * Check line item and membership payment count.
587 *
588 * @param int $membershipId
589 * @param int $count
590 *
591 * @throws \CRM_Core_Exception
592 */
ae16be86 593 public function validateAllCounts(int $membershipId, int $count): void {
d486b01e
PN
594 $memPayParams = [
595 'membership_id' => $membershipId,
596 ];
597 $lineItemParams = [
598 'entity_id' => $membershipId,
599 'entity_table' => 'civicrm_membership',
a33b79a2 600 'contribution_id' => ['>' => 0],
d486b01e
PN
601 ];
602 $this->callAPISuccessGetCount('LineItem', $lineItemParams, $count);
603 $this->callAPISuccessGetCount('MembershipPayment', $memPayParams, $count);
604 }
605
e9b9bb9c 606 /**
607 * Given a number of months offset, get the year and month.
608 * Note the way php arithmetic works, using strtotime('+x months') doesn't
609 * work because it will roll over the day accounting for different number
610 * of days in the month, but we want the same day of the month, x months
611 * from now.
612 * e.g. July 31 + 4 months will return Dec 1 if using php functions, but
613 * we want Nov 31.
614 *
615 * @param int $offset
ae16be86 616 * @param int|null $year Optional input year to start
617 * @param int|null $month Optional input month to start
e9b9bb9c 618 *
619 * @return array
620 * ['year' => int, 'month' => int]
621 */
ae16be86 622 private function getYearAndMonthFromOffset(int $offset, int $year = NULL, int $month = NULL): array {
e9b9bb9c 623 $dateInfo = [
ae16be86 624 'year' => $year ?? (int) date('Y'),
625 'month' => ($month ?? (int) date('m')) + $offset,
e9b9bb9c 626 ];
627 if ($dateInfo['month'] > 12) {
628 $dateInfo['year']++;
629 $dateInfo['month'] -= 12;
630 }
631 if ($dateInfo['month'] < 10) {
632 $dateInfo['month'] = "0{$dateInfo['month']}";
633 }
634
635 return $dateInfo;
636 }
637
638 /**
639 * Test getYearAndMonthFromOffset
ae16be86 640 *
e9b9bb9c 641 * @dataProvider yearMonthProvider
642 *
643 * @param array $input
644 * @param array $expected
645 */
ae16be86 646 public function testGetYearAndMonthFromOffset(array $input, array $expected): void {
e9b9bb9c 647 $this->assertEquals($expected, $this->getYearAndMonthFromOffset($input[0], $input[1], $input[2]));
648 }
649
650 /**
651 * data provider for testGetYearAndMonthFromOffset
652 */
ae16be86 653 public function yearMonthProvider(): array {
e9b9bb9c 654 return [
655 // input = offset, year, current month
656 ['input' => [4, 2020, 1], 'output' => ['year' => '2020', 'month' => '05']],
657 ['input' => [6, 2020, 1], 'output' => ['year' => '2020', 'month' => '07']],
658 ['input' => [4, 2020, 2], 'output' => ['year' => '2020', 'month' => '06']],
659 ['input' => [6, 2020, 2], 'output' => ['year' => '2020', 'month' => '08']],
660 ['input' => [4, 2020, 3], 'output' => ['year' => '2020', 'month' => '07']],
661 ['input' => [6, 2020, 3], 'output' => ['year' => '2020', 'month' => '09']],
662 ['input' => [4, 2020, 4], 'output' => ['year' => '2020', 'month' => '08']],
663 ['input' => [6, 2020, 4], 'output' => ['year' => '2020', 'month' => '10']],
664 ['input' => [4, 2020, 5], 'output' => ['year' => '2020', 'month' => '09']],
665 ['input' => [6, 2020, 5], 'output' => ['year' => '2020', 'month' => '11']],
666 ['input' => [4, 2020, 6], 'output' => ['year' => '2020', 'month' => '10']],
667 ['input' => [6, 2020, 6], 'output' => ['year' => '2020', 'month' => '12']],
668 ['input' => [4, 2020, 7], 'output' => ['year' => '2020', 'month' => '11']],
669 ['input' => [6, 2020, 7], 'output' => ['year' => '2021', 'month' => '01']],
670 ['input' => [4, 2020, 8], 'output' => ['year' => '2020', 'month' => '12']],
671 ['input' => [6, 2020, 8], 'output' => ['year' => '2021', 'month' => '02']],
672 ['input' => [4, 2020, 9], 'output' => ['year' => '2021', 'month' => '01']],
673 ['input' => [6, 2020, 9], 'output' => ['year' => '2021', 'month' => '03']],
674 ['input' => [4, 2020, 10], 'output' => ['year' => '2021', 'month' => '02']],
675 ['input' => [6, 2020, 10], 'output' => ['year' => '2021', 'month' => '04']],
676 ['input' => [4, 2020, 11], 'output' => ['year' => '2021', 'month' => '03']],
677 ['input' => [6, 2020, 11], 'output' => ['year' => '2021', 'month' => '05']],
678 ['input' => [4, 2020, 12], 'output' => ['year' => '2021', 'month' => '04']],
679 ['input' => [6, 2020, 12], 'output' => ['year' => '2021', 'month' => '06']],
680 ];
681 }
682
eba1a1d7
SP
683 /**
684 * Test Recurring Contribution Email Receipt Flag
685 *
686 * @throws \CRM_Core_Exception
687 */
ae16be86 688 public function testContributionEmailReceipt(): void {
eba1a1d7
SP
689 $createParams = $this->_params;
690 unset($createParams['trxn_id'], $createParams['invoice_id']);
691
692 // pass null value to is_email_receipt
693 $createParams['is_email_receipt'] = NULL;
ae16be86 694 $recurring1 = $this->callAPISuccess('ContributionRecur', 'create', $createParams);
695 $recurring1Get = $this->callAPISuccess('ContributionRecur', 'getsingle', ['id' => $recurring1['id']]);
eba1a1d7
SP
696 // default is_email_receipt column value is 1
697 $this->assertEquals('1', $recurring1Get['is_email_receipt']);
698
699 // pass empty value to is_email_receipt
700 $createParams['is_email_receipt'] = '';
ae16be86 701 $recurring2 = $this->callAPISuccess('ContributionRecur', 'create', $createParams);
702 $recurring2 = ContributionRecur::get(FALSE)->addWhere('id', '=', $recurring2['id'])->addSelect('is_email_receipt')->execute()->first();
703 $this->assertEquals(NULL, $recurring2['is_email_receipt']);
eba1a1d7 704
ae16be86 705 // Pass 0 value to is_email_receipt.
eba1a1d7 706 $createParams['is_email_receipt'] = 0;
ae16be86 707 $recurring3 = $this->callAPISuccess('ContributionRecur', 'create', $createParams);
708 $recurring3Get = $this->callAPISuccess('ContributionRecur', 'getsingle', ['id' => $recurring3['id']]);
eba1a1d7
SP
709 $this->assertEquals('0', $recurring3Get['is_email_receipt']);
710 }
711
bbf58b03 712}