Merge pull request #19884 from eileenmcnaughton/unit8less
[civicrm-core.git] / tests / phpunit / CRM / Member / Form / MembershipTest.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 * File for the MembershipTest class
14 *
15 * (PHP 5)
16 *
17 * @author Walt Haas <walt@dharmatech.org> (801) 534-1262
18 */
19
20 use Civi\Api4\FinancialType;
21
22 /**
23 * Test CRM_Member_Form_Membership functions.
24 *
25 * @package CiviCRM
26 * @group headless
27 */
28 class CRM_Member_Form_MembershipTest extends CiviUnitTestCase {
29
30 use CRMTraits_Financial_OrderTrait;
31 use CRMTraits_Financial_PriceSetTrait;
32
33 /**
34 * @var int
35 */
36 protected $_individualId;
37 protected $_contribution;
38 protected $_financialTypeId = 1;
39 protected $_entity = 'Membership';
40 protected $_params;
41 protected $_ids = [];
42 protected $_paymentProcessorID;
43
44 /**
45 * Membership type ID for annual fixed membership.
46 *
47 * @var int
48 */
49 protected $membershipTypeAnnualFixedID;
50
51 /**
52 * Parameters to create payment processor.
53 *
54 * @var array
55 */
56 protected $_processorParams = [];
57
58 /**
59 * ID of created membership.
60 *
61 * @var int
62 */
63 protected $_membershipID;
64
65 /**
66 * Payment instrument mapping.
67 *
68 * @var array
69 */
70 protected $paymentInstruments = [];
71
72 /**
73 * @var CiviMailUtils
74 */
75 protected $mut;
76
77 /**
78 * Test setup for every test.
79 *
80 * Connect to the database, truncate the tables that will be used
81 * and redirect stdin to a temporary file.
82 *
83 * @throws \CRM_Core_Exception
84 * @throws \CiviCRM_API3_Exception
85 */
86 public function setUp() {
87 $this->_apiversion = 3;
88 parent::setUp();
89
90 $this->_individualId = $this->individualCreate();
91 $this->_paymentProcessorID = $this->processorCreate();
92
93 $this->ids['contact']['organization'] = $this->organizationCreate();
94 $this->ids['contact']['organization2'] = $this->organizationCreate();
95 $this->ids['relationship_type']['member'] = $this->callAPISuccess('RelationshipType', 'create', [
96 'name_a_b' => 'Member of',
97 'label_a_b' => 'Member of',
98 'name_b_a' => 'Member is',
99 'label_b_a' => 'Member is',
100 'contact_type_a' => 'Individual',
101 'contact_type_b' => 'Organization',
102 ])['id'];
103 $this->ids['membership_type']['AnnualFixed'] = $this->callAPISuccess('MembershipType', 'create', [
104 'domain_id' => 1,
105 'name' => 'AnnualFixed',
106 'member_of_contact_id' => $this->ids['contact']['organization'],
107 'duration_unit' => 'year',
108 'minimum_fee' => 50,
109 'duration_interval' => 1,
110 'period_type' => 'fixed',
111 'fixed_period_start_day' => '101',
112 'fixed_period_rollover_day' => '1231',
113 'relationship_type_id' => [$this->ids['relationship_type']['member']],
114 'relationship_direction' => ['b_a'],
115 'financial_type_id' => 2,
116 ])['id'];
117
118 $this->ids['membership_type']['AnnualRolling'] = $this->callAPISuccess('MembershipType', 'create', [
119 'name' => 'AnnualRolling',
120 'member_of_contact_id' => $this->ids['contact']['organization'],
121 'duration_unit' => 'year',
122 'duration_interval' => 1,
123 'period_type' => 'rolling',
124 'relationship_type_id' => [$this->ids['relationship_type']['member']],
125 'relationship_direction' => ['b_a'],
126 'financial_type_id' => 'Member Dues',
127 ])['id'];
128
129 $this->ids['membership_type']['AnnualRollingOrg2'] = $this->callAPISuccess('MembershipType', 'create', [
130 'name' => 'AnnualRolling1',
131 'member_of_contact_id' => $this->ids['contact']['organization2'],
132 'duration_unit' => 'year',
133 'duration_interval' => 1,
134 'period_type' => 'rolling',
135 'relationship_type_id' => [$this->ids['relationship_type']['member']],
136 'relationship_direction' => ['b_a'],
137 'financial_type_id' => 'Member Dues',
138 ])['id'];
139
140 $this->ids['membership_type']['lifetime'] = $this->callAPISuccess('MembershipType', 'create', [
141 'name' => 'Lifetime',
142 'member_of_contact_id' => $this->ids['contact']['organization'],
143 'duration_unit' => 'lifetime',
144 'duration_interval' => 1,
145 'relationship_type_id' => $this->ids['relationship_type']['member'],
146 'relationship_direction' => 'b_a',
147 'financial_type_id' => 'Member Dues',
148 'period_type' => 'rolling',
149 ])['id'];
150
151 $instruments = $this->callAPISuccess('Contribution', 'getoptions', ['field' => 'payment_instrument_id']);
152 $this->paymentInstruments = $instruments['values'];
153 }
154
155 /**
156 * Clean up after each test.
157 *
158 * @throws \CRM_Core_Exception
159 */
160 public function tearDown() {
161 $this->quickCleanUpFinancialEntities();
162 $this->quickCleanup(
163 [
164 'civicrm_relationship',
165 'civicrm_membership_type',
166 'civicrm_membership',
167 'civicrm_uf_match',
168 'civicrm_email',
169 ]
170 );
171 $this->callAPISuccess('Contact', 'delete', ['id' => $this->ids['contact']['organization'], 'skip_undelete' => TRUE]);
172 $this->callAPISuccess('RelationshipType', 'delete', ['id' => $this->ids['relationship_type']['member']]);
173 }
174
175 /**
176 * Test CRM_Member_Form_Membership::formRule() with a parameter
177 * that has an empty contact_select_id value
178 *
179 * @throws \CiviCRM_API3_Exception
180 * @throws \CRM_Core_Exception
181 */
182 public function testFormRuleEmptyContact(): void {
183 $params = [
184 'contact_select_id' => 0,
185 'membership_type_id' => [1 => NULL],
186 ];
187 $files = [];
188 $obj = new CRM_Member_Form_Membership();
189 $rc = CRM_Member_Form_Membership::formRule($params, $files, $obj);
190 $this->assertType('array', $rc);
191 $this->assertTrue(array_key_exists('membership_type_id', $rc));
192
193 $params['membership_type_id'] = [1 => 3];
194 $rc = CRM_Member_Form_Membership::formRule($params, $files, $obj);
195 $this->assertType('array', $rc);
196 $this->assertTrue(array_key_exists('join_date', $rc));
197 }
198
199 /**
200 * Test that form rule fails if start date is before join date.
201 *
202 * Test CRM_Member_Form_Membership::formRule() with a parameter
203 * that has an start date before the join date and a rolling
204 * membership type.
205 */
206 public function testFormRuleRollingEarlyStart() {
207 $unixNow = time();
208 $unixYesterday = $unixNow - (24 * 60 * 60);
209 $ymdYesterday = date('Y-m-d', $unixYesterday);
210 $params = [
211 'join_date' => date('Y-m-d'),
212 'start_date' => $ymdYesterday,
213 'end_date' => '',
214 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualRolling']],
215 ];
216 $files = [];
217 $obj = new CRM_Member_Form_Membership();
218 $rc = CRM_Member_Form_Membership::formRule($params, $files, $obj);
219 $this->assertType('array', $rc);
220 $this->assertTrue(array_key_exists('start_date', $rc));
221 }
222
223 /**
224 * Test CRM_Member_Form_Membership::formRule() with a parameter
225 * that has an end date before the start date and a rolling
226 * membership type
227 */
228 public function testFormRuleRollingEarlyEnd() {
229 $unixNow = time();
230 $unixYesterday = $unixNow - (24 * 60 * 60);
231 $ymdYesterday = date('Y-m-d', $unixYesterday);
232 $params = [
233 'join_date' => date('Y-m-d'),
234 'start_date' => date('Y-m-d'),
235 'end_date' => $ymdYesterday,
236 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualRolling']],
237 ];
238 $files = [];
239 $obj = new CRM_Member_Form_Membership();
240 $rc = CRM_Member_Form_Membership::formRule($params, $files, $obj);
241 $this->assertType('array', $rc);
242 $this->assertTrue(array_key_exists('end_date', $rc));
243 }
244
245 /**
246 * Test CRM_Member_Form_Membership::formRule() with end date but no start date and a rolling membership type.
247 */
248 public function testFormRuleRollingEndNoStart() {
249 $unixNow = time();
250 $unixYearFromNow = $unixNow + (365 * 24 * 60 * 60);
251 $ymdYearFromNow = date('Y-m-d', $unixYearFromNow);
252 $params = [
253 'join_date' => date('Y-m-d'),
254 'start_date' => '',
255 'end_date' => $ymdYearFromNow,
256 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualRolling']],
257 ];
258 $files = [];
259 $obj = new CRM_Member_Form_Membership();
260 $rc = $obj::formRule($params, $files, $obj);
261 $this->assertType('array', $rc);
262 $this->assertTrue(array_key_exists('start_date', $rc));
263 }
264
265 /**
266 * Test CRM_Member_Form_Membership::formRule() with a parameter
267 * that has an end date and a lifetime membership type
268 */
269 public function testFormRuleRollingLifetimeEnd() {
270 $unixNow = time();
271 $unixYearFromNow = $unixNow + (365 * 24 * 60 * 60);
272 $params = [
273 'join_date' => date('Y-m-d'),
274 'start_date' => date('Y-m-d'),
275 'end_date' => date('Y-m-d',
276 $unixYearFromNow
277 ),
278 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['lifetime']],
279 ];
280 $files = [];
281 $obj = new CRM_Member_Form_Membership();
282 $rc = $obj::formRule($params, $files, $obj);
283 $this->assertType('array', $rc);
284 $this->assertTrue(array_key_exists('status_id', $rc));
285 }
286
287 /**
288 * Test CRM_Member_Form_Membership::formRule() with a parameter
289 * that has permanent override and no status
290 *
291 * @throws \CiviCRM_API3_Exception
292 * @throws \CRM_Core_Exception
293 */
294 public function testFormRulePermanentOverrideWithNoStatus() {
295 $params = [
296 'join_date' => date('Y-m-d'),
297 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualFixed']],
298 'is_override' => TRUE,
299 ];
300 $files = [];
301 $obj = new CRM_Member_Form_Membership();
302 $rc = $obj::formRule($params, $files, $obj);
303 $this->assertType('array', $rc);
304 $this->assertTrue(array_key_exists('status_id', $rc));
305 }
306
307 public function testFormRuleUntilDateOverrideWithValidOverrideEndDate() {
308 $params = [
309 'join_date' => date('Y-m-d'),
310 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualFixed']],
311 'is_override' => TRUE,
312 'status_id' => 1,
313 'status_override_end_date' => date('Y-m-d'),
314 ];
315 $files = [];
316 $membershipForm = new CRM_Member_Form_Membership();
317 $validationResponse = CRM_Member_Form_Membership::formRule($params, $files, $membershipForm);
318 $this->assertTrue($validationResponse);
319 }
320
321 public function testFormRuleUntilDateOverrideWithNoOverrideEndDate() {
322 $params = [
323 'join_date' => date('Y-m-d'),
324 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualFixed']],
325 'is_override' => CRM_Member_StatusOverrideTypes::UNTIL_DATE,
326 'status_id' => 1,
327 ];
328 $files = [];
329 $membershipForm = new CRM_Member_Form_Membership();
330 $validationResponse = CRM_Member_Form_Membership::formRule($params, $files, $membershipForm);
331 $this->assertType('array', $validationResponse);
332 $this->assertEquals('Please enter the Membership override end date.', $validationResponse['status_override_end_date']);
333 }
334
335 /**
336 * Test CRM_Member_Form_Membership::formRule() with a join date
337 * of one month from now and a rolling membership type
338 */
339 public function testFormRuleRollingJoin1MonthFromNow() {
340 $unixNow = time();
341 $unix1MFmNow = $unixNow + (31 * 24 * 60 * 60);
342 $params = [
343 'join_date' => date('Y-m-d', $unix1MFmNow),
344 'start_date' => '',
345 'end_date' => '',
346 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualRolling']],
347 ];
348 $files = [];
349 $obj = new CRM_Member_Form_Membership();
350 $rc = $obj::formRule($params, $files, $obj);
351
352 // Should have found no valid membership status.
353 $this->assertType('array', $rc);
354 $this->assertTrue(array_key_exists('_qf_default', $rc));
355 }
356
357 /**
358 * Test CRM_Member_Form_Membership::formRule() with a join date of today and a rolling membership type.
359 */
360 public function testFormRuleRollingJoinToday() {
361 $params = [
362 'join_date' => date('Y-m-d'),
363 'start_date' => '',
364 'end_date' => '',
365 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualRolling']],
366 ];
367 $files = [];
368 $obj = new CRM_Member_Form_Membership();
369 $rc = $obj::formRule($params, $files, $obj);
370
371 // Should have found New membership status
372 $this->assertTrue($rc);
373 }
374
375 /**
376 * Test CRM_Member_Form_Membership::formRule() with a join date
377 * of one month ago and a rolling membership type
378 */
379 public function testFormRuleRollingJoin1MonthAgo() {
380 $unixNow = time();
381 $unix1MAgo = $unixNow - (31 * 24 * 60 * 60);
382 $params = [
383 'join_date' => date('Y-m-d', $unix1MAgo),
384 'start_date' => '',
385 'end_date' => '',
386 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualRolling']],
387 ];
388 $files = [];
389 $obj = new CRM_Member_Form_Membership();
390 $rc = $obj::formRule($params, $files, $obj);
391
392 // Should have found New membership status.
393 $this->assertTrue($rc);
394 }
395
396 /**
397 * Test CRM_Member_Form_Membership::formRule() with a join date of six months ago and a rolling membership type.
398 */
399 public function testFormRuleRollingJoin6MonthsAgo() {
400 $unixNow = time();
401 $unix6MAgo = $unixNow - (180 * 24 * 60 * 60);
402 $params = [
403 'join_date' => date('Y-m-d', $unix6MAgo),
404 'start_date' => '',
405 'end_date' => '',
406 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualRolling']],
407 ];
408 $files = [];
409 $obj = new CRM_Member_Form_Membership();
410 $rc = $obj::formRule($params, $files, $obj);
411
412 // Should have found Current membership status.
413 $this->assertTrue($rc);
414 }
415
416 /**
417 * Test CRM_Member_Form_Membership::formRule() with a join date
418 * of one year+ ago and a rolling membership type
419 */
420 public function testFormRuleRollingJoin1YearAgo() {
421 $unixNow = time();
422 $unix1YAgo = $unixNow - (370 * 24 * 60 * 60);
423 $params = [
424 'join_date' => date('Y-m-d', $unix1YAgo),
425 'start_date' => '',
426 'end_date' => '',
427 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualRolling']],
428 ];
429 $files = [];
430 $obj = new CRM_Member_Form_Membership();
431 $rc = $obj::formRule($params, $files, $obj);
432
433 // Should have found Grace membership status
434 $this->assertTrue($rc);
435 }
436
437 /**
438 * Test CRM_Member_Form_Membership::formRule() with a join date
439 * of two years ago and a rolling membership type
440 */
441 public function testFormRuleRollingJoin2YearsAgo() {
442 $unixNow = time();
443 $unix2YAgo = $unixNow - (2 * 365 * 24 * 60 * 60);
444 $params = [
445 'join_date' => date('Y-m-d', $unix2YAgo),
446 'start_date' => '',
447 'end_date' => '',
448 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualRolling']],
449 ];
450 $files = [];
451 $obj = new CRM_Member_Form_Membership();
452 $rc = $obj::formRule($params, $files, $obj);
453
454 // Should have found Expired membership status
455 $this->assertTrue($rc);
456 }
457
458 /**
459 * Test CRM_Member_Form_Membership::formRule() with a current status.
460 *
461 * The setup is a join date of six months ago and a fixed membership type.
462 */
463 public function testFormRuleFixedJoin6MonthsAgo() {
464 $unixNow = time();
465 $unix6MAgo = $unixNow - (180 * 24 * 60 * 60);
466 $params = [
467 'join_date' => date('Y-m-d', $unix6MAgo),
468 'start_date' => '',
469 'end_date' => '',
470 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualFixed']],
471 ];
472 $files = [];
473 $obj = new CRM_Member_Form_Membership();
474 $rc = $obj::formRule($params, $files, $obj);
475
476 // Should have found Current membership status
477 $this->assertTrue($rc);
478 }
479
480 /**
481 * Test the submit function of the membership form.
482 *
483 * @param string $thousandSeparator
484 *
485 * @throws \CRM_Core_Exception
486 * @throws \CiviCRM_API3_Exception
487 *
488 * @dataProvider getThousandSeparators
489 */
490 public function testSubmit(string $thousandSeparator) {
491 CRM_Core_Session::singleton()->getStatus(TRUE);
492 $this->setCurrencySeparators($thousandSeparator);
493 $form = $this->getForm();
494 $form->preProcess();
495 $this->mut = new CiviMailUtils($this, TRUE);
496 $form->_mode = 'test';
497 $this->createLoggedInUser();
498 $params = [
499 'cid' => $this->_individualId,
500 'join_date' => date('Y-m-d'),
501 'start_date' => '',
502 'end_date' => '',
503 // This format reflects the organisation & then the type.
504 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualFixed']],
505 'auto_renew' => '0',
506 'max_related' => '',
507 'num_terms' => '1',
508 'source' => '',
509 'total_amount' => $this->formatMoneyInput(1234.56),
510 //Member dues, see data.xml
511 'financial_type_id' => '2',
512 'soft_credit_type_id' => '',
513 'soft_credit_contact_id' => '',
514 'from_email_address' => '"Demonstrators Anonymous" <info@example.org>',
515 'payment_processor_id' => $this->_paymentProcessorID,
516 'credit_card_number' => '4111111111111111',
517 'cvv2' => '123',
518 'credit_card_exp_date' => [
519 'M' => '9',
520 'Y' => date('Y', strtotime('+ 2 years')),
521 ],
522 'credit_card_type' => 'Visa',
523 'billing_first_name' => 'Test',
524 'billing_middlename' => 'Last',
525 'billing_street_address-5' => '10 Test St',
526 'billing_city-5' => 'Test',
527 'billing_state_province_id-5' => '1003',
528 'billing_postal_code-5' => '90210',
529 'billing_country_id-5' => '1228',
530 'send_receipt' => TRUE,
531 'receipt_text' => 'Receipt text',
532 ];
533 $form->_contactID = $this->_individualId;
534 $form->testSubmit($params);
535 $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_individualId]);
536 $this->callAPISuccessGetCount('ContributionRecur', ['contact_id' => $this->_individualId], 0);
537 $contribution = $this->callAPISuccess('Contribution', 'get', [
538 'contact_id' => $this->_individualId,
539 'is_test' => TRUE,
540 ]);
541
542 //CRM-20264 : Check that CC type and number (last 4 digit) is stored during backoffice membership payment
543 $lastFinancialTrxnId = CRM_Core_BAO_FinancialTrxn::getFinancialTrxnId($contribution['id'], 'DESC');
544 $financialTrxn = $this->callAPISuccessGetSingle(
545 'FinancialTrxn',
546 [
547 'id' => $lastFinancialTrxnId['financialTrxnId'],
548 'return' => ['card_type_id', 'pan_truncation'],
549 ]
550 );
551 $this->assertEquals(1, $financialTrxn['card_type_id']);
552 $this->assertEquals(1111, $financialTrxn['pan_truncation']);
553
554 $this->callAPISuccessGetCount('LineItem', [
555 'entity_id' => $membership['id'],
556 'entity_table' => 'civicrm_membership',
557 'contribution_id' => $contribution['id'],
558 ], 1);
559
560 $this->_checkFinancialRecords([
561 'id' => $contribution['id'],
562 'total_amount' => 1234.56,
563 'financial_account_id' => 2,
564 'payment_instrument_id' => $this->callAPISuccessGetValue('PaymentProcessor', [
565 'id' => $this->_paymentProcessorID,
566 'return' => 'payment_instrument_id',
567 ]),
568 ], 'online');
569 $this->mut->checkMailLog([
570 CRM_Utils_Money::format('1234.56'),
571 'Receipt text',
572 ]);
573 $this->mut->stop();
574 $this->assertEquals([
575 [
576 'text' => 'AnnualFixed membership for Mr. Anthony Anderson II has been added. The new membership End Date is December 31st, ' . date('Y') . '. A membership confirmation and receipt has been sent to anthony_anderson@civicrm.org.',
577 'title' => 'Complete',
578 'type' => 'success',
579 'options' => NULL,
580 ],
581 ], CRM_Core_Session::singleton()->getStatus());
582 }
583
584 /**
585 * Test the submit function of the membership form on membership type change.
586 * Check if the related contribuion is also updated if the minimum_fee didn't match
587 *
588 * @throws \CRM_Core_Exception
589 * @throws \CiviCRM_API3_Exception
590 */
591 public function testContributionUpdateOnMembershipTypeChange(): void {
592 // Step 1: Create a Membership via backoffice whose with 50.00 payment
593 $form = $this->getForm();
594 $form->preProcess();
595 $this->mut = new CiviMailUtils($this, TRUE);
596 $this->createLoggedInUser();
597 $priceSet = $this->callAPISuccess('PriceSet', 'Get', ["extends" => "CiviMember"]);
598 $form->set('priceSetId', $priceSet['id']);
599 CRM_Price_BAO_PriceSet::buildPriceSet($form);
600 $params = [
601 'cid' => $this->_individualId,
602 'join_date' => date('Y-m-d'),
603 'start_date' => '',
604 'end_date' => '',
605 // This format reflects the first being the organisation & the $this->ids['membership_type']['AnnualFixed'] being the type.
606 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualFixed']],
607 'record_contribution' => 1,
608 'total_amount' => 50,
609 'receive_date' => date('Y-m-d', time()) . ' 20:36:00',
610 'payment_instrument_id' => array_search('Check', $this->paymentInstruments),
611 'contribution_status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'),
612 //Member dues, see data.xml
613 'financial_type_id' => '2',
614 'payment_processor_id' => $this->_paymentProcessorID,
615 ];
616 $form->_contactID = $this->_individualId;
617 $form->testSubmit($params);
618 $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_individualId]);
619 // check the membership status after partial payment, if its Pending
620 $this->assertEquals(array_search('New', CRM_Member_PseudoConstant::membershipStatus()), $membership['status_id']);
621 $contribution = $this->callAPISuccessGetSingle('Contribution', [
622 'contact_id' => $this->_individualId,
623 ]);
624 $this->assertEquals('Completed', $contribution['contribution_status']);
625 $this->assertEquals(50.00, $contribution['total_amount']);
626 $this->assertEquals(50.00, $contribution['net_amount']);
627
628 // Step 2: Change the membership type whose minimum free is less than earlier membership
629 $secondMembershipType = $this->callAPISuccess('membership_type', 'create', [
630 'domain_id' => 1,
631 'name' => 'Second Test Membership',
632 'member_of_contact_id' => $this->ids['contact']['organization'],
633 'duration_unit' => 'month',
634 'minimum_fee' => 25,
635 'duration_interval' => 1,
636 'period_type' => 'fixed',
637 'fixed_period_start_day' => '101',
638 'fixed_period_rollover_day' => '1231',
639 'relationship_type_id' => 20,
640 'financial_type_id' => 2,
641 ]);
642 Civi::settings()->set('update_contribution_on_membership_type_change', TRUE);
643 $form = $this->getForm();
644 $form->preProcess();
645 $form->_id = $membership['id'];
646 $form->set('priceSetId', $priceSet['id']);
647 CRM_Price_BAO_PriceSet::buildPriceSet($form);
648 $form->_action = CRM_Core_Action::UPDATE;
649 $params = [
650 'cid' => $this->_individualId,
651 'join_date' => date('Y-m-d'),
652 'start_date' => '',
653 'end_date' => '',
654 // This format reflects the first number being the organisation & the 25 being the type.
655 'membership_type_id' => [$this->ids['contact']['organization'], $secondMembershipType['id']],
656 'status_id' => 1,
657 'receive_date' => date('Y-m-d', time()) . ' 20:36:00',
658 'payment_instrument_id' => array_search('Check', $this->paymentInstruments),
659 //Member dues, see data.xml
660 'financial_type_id' => '2',
661 'payment_processor_id' => $this->_paymentProcessorID,
662 ];
663 $form->_contactID = $this->_individualId;
664 $form->testSubmit($params);
665 $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_individualId]);
666 // check the membership status after partial payment, if its Pending
667 $contribution = $this->callAPISuccessGetSingle('Contribution', [
668 'contact_id' => $this->_individualId,
669 ]);
670 $payment = CRM_Contribute_BAO_Contribution::getPaymentInfo($membership['id'], 'membership', FALSE);
671 // Check the contribution status on membership type change whose minimum fee was less than earlier membership
672 $this->assertEquals('Pending refund', $contribution['contribution_status']);
673 // Earlier paid amount
674 $this->assertEquals(50, $payment['paid']);
675 // balance remaining
676 $this->assertEquals(-25, $payment['balance']);
677
678 //Update to lifetime membership.
679 $params['membership_type_id'] = [$this->ids['contact']['organization'], $this->ids['membership_type']['lifetime']];
680 $form->testSubmit($params);
681 $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_individualId]);
682 $this->assertEquals($this->ids['membership_type']['lifetime'], $membership['membership_type_id']);
683 $this->assertTrue(empty($membership['end_date']), 'Lifetime Membership on the individual has an End date.');
684 }
685
686 /**
687 * Test the submit function of the membership form for partial payment.
688 *
689 * @param string $thousandSeparator
690 * punctuation used to refer to thousands.
691 *
692 * @throws \CRM_Core_Exception
693 * @throws \CiviCRM_API3_Exception
694 * @dataProvider getThousandSeparators
695 */
696 public function testSubmitPartialPayment(string $thousandSeparator): void {
697 $this->setCurrencySeparators($thousandSeparator);
698 // Step 1: submit a partial payment for a membership via backoffice
699 $form = $this->getForm();
700 $form->preProcess();
701 $this->mut = new CiviMailUtils($this, TRUE);
702 $this->createLoggedInUser();
703 $priceSet = $this->callAPISuccess('PriceSet', 'Get', ["extends" => "CiviMember"]);
704 $form->set('priceSetId', $priceSet['id']);
705
706 CRM_Price_BAO_PriceSet::buildPriceSet($form);
707 $params = [
708 'cid' => $this->_individualId,
709 'join_date' => date('Y-m-d'),
710 'start_date' => '',
711 'end_date' => '',
712 // This format reflects the first number being the organisation & the second being the type.
713 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualFixed']],
714 'receive_date' => date('Y-m-d', time()) . ' 20:36:00',
715 'record_contribution' => 1,
716 'total_amount' => $this->formatMoneyInput(50),
717 'payment_instrument_id' => array_search('Check', $this->paymentInstruments, TRUE),
718 'contribution_status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending'),
719 //Member dues, see data.xml
720 'financial_type_id' => '2',
721 'payment_processor_id' => $this->_paymentProcessorID,
722 ];
723 $form->_contactID = $this->_individualId;
724 $form->testSubmit($params);
725 $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_individualId]);
726 // check the membership status after partial payment, if its Pending
727 $this->assertEquals(array_search('Pending', CRM_Member_PseudoConstant::membershipStatus(), TRUE), $membership['status_id']);
728 $contribution = $this->callAPISuccessGetSingle('Contribution', ['contact_id' => $this->_individualId]);
729 $this->callAPISuccess('Payment', 'create', ['contribution_id' => $contribution['id'], 'total_amount' => 25, 'payment_instrument_id' => 'Cash']);
730 $contribution = $this->callAPISuccessGetSingle('Contribution', ['id' => $contribution['id']]);
731 $this->assertEquals('Partially paid', $contribution['contribution_status']);
732
733 // Step 2: submit the other half of the partial payment
734 // via AdditionalPayment form to complete the related contribution
735 $form = new CRM_Contribute_Form_AdditionalPayment();
736 $submitParams = [
737 'contribution_id' => $contribution['contribution_id'],
738 'contact_id' => $this->_individualId,
739 'total_amount' => $this->formatMoneyInput(25),
740 'currency' => 'USD',
741 'financial_type_id' => 2,
742 'receive_date' => '2015-04-21 23:27:00',
743 'trxn_date' => '2017-04-11 13:05:11',
744 'payment_processor_id' => 0,
745 'payment_instrument_id' => array_search('Check', $this->paymentInstruments, TRUE),
746 'check_number' => 'check-12345',
747 ];
748 $form->cid = $this->_individualId;
749 $form->testSubmit($submitParams);
750 $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_individualId]);
751 // check the membership status after additional payment, if its changed to 'New'
752 $this->assertEquals(array_search('New', CRM_Member_PseudoConstant::membershipStatus(), TRUE), $membership['status_id']);
753
754 // check the contribution status and net amount after additional payment
755 $contribution = $this->callAPISuccessGetSingle('Contribution', [
756 'contact_id' => $this->_individualId,
757 ]);
758 $this->assertEquals('Completed', $contribution['contribution_status']);
759 $this->validateAllPayments();
760 }
761
762 /**
763 * Test the submit function of the membership form.
764 *
765 * @throws \API_Exception
766 * @throws \CRM_Core_Exception
767 * @throws \CiviCRM_API3_Exception
768 */
769 public function testSubmitRecur(): void {
770 CRM_Core_Session::singleton()->getStatus(TRUE);
771 $pendingVal = $this->callAPISuccessGetValue('OptionValue', [
772 'return' => 'id',
773 'option_group_id' => 'contribution_status',
774 'label' => 'Pending Label**',
775 ]);
776 //Update label for Pending contribution status.
777 $this->callAPISuccess('OptionValue', 'create', [
778 'id' => $pendingVal,
779 'label' => 'PendingEdited',
780 ]);
781
782 $this->callAPISuccess('MembershipType', 'create', [
783 'id' => $this->ids['membership_type']['AnnualFixed'],
784 'duration_unit' => 'month',
785 'duration_interval' => 1,
786 'auto_renew' => TRUE,
787 ]);
788 $params = $this->getBaseSubmitParams();
789 // Change financial_type_id to test our override flows through to the line item.
790 $params['financial_type_id'] = FinancialType::get(FALSE)->addWhere('id', '!=', $params['financial_type_id'])->addSelect('id')->execute()->first()['id'];
791 $form = $this->getForm();
792 $this->createLoggedInUser();
793 $form->_mode = 'test';
794 $form->_contactID = $this->_individualId;
795 $form->testSubmit($params);
796 $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_individualId]);
797 $this->callAPISuccessGetCount('ContributionRecur', ['contact_id' => $this->_individualId], 1);
798
799 $contribution = $this->callAPISuccess('Contribution', 'get', [
800 'contact_id' => $this->_individualId,
801 'is_test' => TRUE,
802 ]);
803
804 //Check if Membership Payment is recorded.
805 $this->callAPISuccessGetCount('MembershipPayment', [
806 'membership_id' => $membership['id'],
807 'contribution_id' => $contribution['id'],
808 ], 1);
809
810 // CRM-16992.
811 $this->callAPISuccessGetCount('LineItem', [
812 'entity_id' => $membership['id'],
813 'entity_table' => 'civicrm_membership',
814 'contribution_id' => $contribution['id'],
815 'financial_type_id' => $params['financial_type_id'],
816 ], 1);
817 $this->assertEquals([
818 [
819 'text' => 'AnnualFixed membership for Mr. Anthony Anderson II has been added. The new membership End Date is ' . date('F jS, Y', strtotime('last day of this month')) . '.',
820 'title' => 'Complete',
821 'type' => 'success',
822 'options' => NULL,
823 ],
824 ], CRM_Core_Session::singleton()->getStatus());
825 }
826
827 /**
828 * Test submit recurring with two line items.
829 *
830 * @throws \CRM_Core_Exception
831 * @throws \CiviCRM_API3_Exception
832 */
833 public function testSubmitRecurTwoRows(): void {
834 $pfvIDs = $this->createMembershipPriceSet();
835 $form = $this->getForm();
836 $form->_mode = 'live';
837 $priceParams = [
838 'price_' . $this->getPriceFieldID() => $pfvIDs,
839 'price_set_id' => $this->getPriceSetID(),
840 'frequency_interval' => 1,
841 'frequency_unit' => 'month',
842 'membership_type_id' => NULL,
843 // Set financial type id to null to check it is retrieved from the price set.
844 'financial_type_id' => NULL,
845 ];
846 $form->testSubmit(array_merge($this->getBaseSubmitParams(), $priceParams));
847 $memberships = $this->callAPISuccess('Membership', 'get')['values'];
848 $this->assertCount(2, $memberships);
849 $this->callAPISuccessGetSingle('Contribution', ['financial_type_id' => 1]);
850 $this->callAPISuccessGetCount('MembershipPayment', [], 2);
851 $lines = $this->callAPISuccess('LineItem', 'get', ['sequential' => 1])['values'];
852 $this->assertCount(2, $lines);
853 $this->assertEquals('civicrm_membership', $lines[0]['entity_table']);
854 $this->assertEquals('civicrm_membership', $lines[1]['entity_table']);
855
856 }
857
858 /**
859 * CRM-20946: Test the financial entires especially the reversed amount,
860 * after related Contribution is cancelled
861 *
862 * @throws \CRM_Core_Exception
863 * @throws \CiviCRM_API3_Exception
864 */
865 public function testFinancialEntriesOnCancelledContribution(): void {
866 // Create two memberships for individual $this->_individualId, via a price set in the back end.
867 $this->createTwoMembershipsViaPriceSetInBackEnd($this->_individualId);
868
869 // cancel the related contribution via API
870 $contribution = $this->callAPISuccessGetSingle('Contribution', [
871 'contact_id' => $this->_individualId,
872 'contribution_status_id' => 2,
873 ]);
874 $this->callAPISuccess('Contribution', 'create', [
875 'id' => $contribution['id'],
876 'contribution_status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_DAO_Contribution', 'contribution_status_id', 'Cancelled'),
877 ]);
878
879 // fetch financial_trxn ID of the related contribution
880 $sql = "SELECT financial_trxn_id
881 FROM civicrm_entity_financial_trxn
882 WHERE entity_id = %1 AND entity_table = 'civicrm_contribution'
883 ORDER BY id DESC
884 LIMIT 1
885 ";
886 $financialTrxnID = CRM_Core_DAO::singleValueQuery($sql, [1 => [$contribution['id'], 'Int']]);
887
888 // fetch entity_financial_trxn records and compare their cancelled records
889 $result = $this->callAPISuccess('EntityFinancialTrxn', 'Get', [
890 'financial_trxn_id' => $financialTrxnID,
891 'entity_table' => 'civicrm_financial_item',
892 ]);
893 // compare the reversed amounts of respective memberships after cancelling contribution
894 $cancelledMembershipAmounts = [
895 -259.00,
896 -20.00,
897 ];
898 $count = 0;
899 foreach ($result['values'] as $record) {
900 $this->assertEquals($cancelledMembershipAmounts[$count], $record['amount']);
901 $count++;
902 }
903 }
904
905 /**
906 * Test the submit function of the membership form.
907 *
908 * @throws \CRM_Core_Exception
909 */
910 public function testSubmitPayLaterWithBilling() {
911 $form = $this->getForm();
912 $form->preProcess();
913 $this->createLoggedInUser();
914 $params = [
915 'cid' => $this->_individualId,
916 'join_date' => date('Y-m-d'),
917 'start_date' => '',
918 'end_date' => '',
919 // This format reflects the first number being the organisation & the second being the type.
920 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualFixed']],
921 'auto_renew' => '0',
922 'max_related' => '',
923 'num_terms' => '2',
924 'source' => '',
925 'total_amount' => '50.00',
926 //Member dues, see data.xml
927 'financial_type_id' => '2',
928 'soft_credit_type_id' => '',
929 'soft_credit_contact_id' => '',
930 'payment_instrument_id' => 4,
931 'from_email_address' => '"Demonstrators Anonymous" <info@example.org>',
932 'receipt_text_signup' => 'Thank you text',
933 'payment_processor_id' => $this->_paymentProcessorID,
934 'record_contribution' => TRUE,
935 'trxn_id' => 777,
936 'contribution_status_id' => 2,
937 'billing_first_name' => 'Test',
938 'billing_middlename' => 'Last',
939 'billing_street_address-5' => '10 Test St',
940 'billing_city-5' => 'Test',
941 'billing_state_province_id-5' => '1003',
942 'billing_postal_code-5' => '90210',
943 'billing_country_id-5' => '1228',
944 ];
945 $form->_contactID = $this->_individualId;
946
947 $form->testSubmit($params);
948 $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_individualId]);
949 $contribution = $this->callAPISuccessGetSingle('Contribution', [
950 'contact_id' => $this->_individualId,
951 'contribution_status_id' => 2,
952 ]);
953 $this->assertEquals($contribution['trxn_id'], 777);
954
955 $this->callAPISuccessGetCount('LineItem', [
956 'entity_id' => $membership['id'],
957 'entity_table' => 'civicrm_membership',
958 'contribution_id' => $contribution['id'],
959 ], 1);
960 $this->callAPISuccessGetSingle('address', [
961 'contact_id' => $this->_individualId,
962 'street_address' => '10 Test St',
963 'postal_code' => 90210,
964 ]);
965 }
966
967 /**
968 * Test if membership is updated to New after contribution
969 * is updated from Partially paid to Completed.
970 *
971 * @throws \CRM_Core_Exception
972 * @throws \CiviCRM_API3_Exception
973 */
974 public function testSubmitUpdateMembershipFromPartiallyPaid() {
975 $memStatus = CRM_Member_BAO_Membership::buildOptions('status_id', 'validate');
976
977 //Perform a pay later membership contribution.
978 $this->testSubmitPayLaterWithBilling();
979 $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_individualId]);
980 $this->assertEquals($membership['status_id'], array_search('Pending', $memStatus));
981 $contribution = $this->callAPISuccessGetSingle('MembershipPayment', [
982 'membership_id' => $membership['id'],
983 ]);
984 $prevContribution = $this->callAPISuccessGetSingle('Contribution', ['id' => $contribution['id']]);
985 $this->callAPISuccess('Payment', 'create', [
986 'contribution_id' => $contribution['contribution_id'],
987 'payment_instrument_id' => 'Cash',
988 'total_amount' => 5,
989 ]);
990
991 // Complete the contribution from offline form.
992 $form = new CRM_Contribute_Form_Contribution();
993 $submitParams = [
994 'id' => $contribution['contribution_id'],
995 'contribution_status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'),
996 'price_set_id' => 0,
997 ];
998 $fields = ['total_amount', 'net_amount', 'financial_type_id', 'receive_date', 'contact_id', 'payment_instrument_id'];
999 foreach ($fields as $val) {
1000 $submitParams[$val] = $prevContribution[$val];
1001 }
1002 $form->testSubmit($submitParams, CRM_Core_Action::UPDATE);
1003
1004 //Check if Membership is updated to New.
1005 $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_individualId]);
1006 $this->assertEquals($membership['status_id'], array_search('New', $memStatus));
1007 }
1008
1009 /**
1010 * Test the submit function of the membership form.
1011 *
1012 * @throws \CiviCRM_API3_Exception
1013 * @throws \CRM_Core_Exception
1014 */
1015 public function testSubmitRecurCompleteInstant() {
1016 $form = $this->getForm();
1017 $mut = new CiviMailUtils($this, TRUE);
1018 $processor = Civi\Payment\System::singleton()->getById($this->_paymentProcessorID);
1019 $processor->setDoDirectPaymentResult([
1020 'payment_status_id' => 1,
1021 'trxn_id' => 'kettles boil water',
1022 'fee_amount' => .14,
1023 ]);
1024 $processorDetail = $processor->getPaymentProcessor();
1025 $this->callAPISuccess('MembershipType', 'create', [
1026 'id' => $this->ids['membership_type']['AnnualFixed'],
1027 'duration_unit' => 'month',
1028 'duration_interval' => 1,
1029 'auto_renew' => TRUE,
1030 ]);
1031 $form->preProcess();
1032 $this->createLoggedInUser();
1033 $params = $this->getBaseSubmitParams();
1034 $form->_mode = 'test';
1035 $form->_contactID = $this->_individualId;
1036 $form->testSubmit($params);
1037 $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_individualId]);
1038 $this->callAPISuccessGetCount('ContributionRecur', ['contact_id' => $this->_individualId], 1);
1039
1040 $contribution = $this->callAPISuccess('Contribution', 'getsingle', [
1041 'contact_id' => $this->_individualId,
1042 'is_test' => TRUE,
1043 ]);
1044
1045 $this->assertEquals(.14, $contribution['fee_amount']);
1046 $this->assertEquals('kettles boil water', $contribution['trxn_id']);
1047 $this->assertEquals($processorDetail['payment_instrument_id'], $contribution['payment_instrument_id']);
1048
1049 $this->callAPISuccessGetCount('LineItem', [
1050 'entity_id' => $membership['id'],
1051 'entity_table' => 'civicrm_membership',
1052 'contribution_id' => $contribution['id'],
1053 ], 1);
1054 $mut->checkMailLog([
1055 '===========================================================
1056 Billing Name and Address
1057 ===========================================================
1058 Test
1059 10 Test St
1060 Test, AR 90210
1061 US',
1062 '===========================================================
1063 Membership Information
1064 ===========================================================
1065 Membership Type: AnnualFixed
1066 Membership Start Date: ',
1067 '===========================================================
1068 Credit Card Information
1069 ===========================================================
1070 Visa
1071 ************1111
1072 Expires: ',
1073 ]);
1074 $mut->stop();
1075
1076 }
1077
1078 /**
1079 * CRM-20955, CRM-20966:
1080 * Test creating two memberships with inheritance via price set in the back end,
1081 * checking that the correct primary & secondary memberships, contributions, line items
1082 * & membership_payment records are created.
1083 * Uses some data from tests/phpunit/CRM/Member/Form/dataset/data.xml .
1084 *
1085 * @throws \CRM_Core_Exception
1086 * @throws \CiviCRM_API3_Exception
1087 */
1088 public function testTwoInheritedMembershipsViaPriceSetInBackend(): void {
1089 // Create an organization and give it a "Member of" relationship to $this->_individualId.
1090 $orgID = $this->organizationCreate();
1091 $relationship = $this->callAPISuccess('Relationship', 'create', [
1092 'contact_id_a' => $this->_individualId,
1093 'contact_id_b' => $orgID,
1094 'relationship_type_id' => $this->ids['relationship_type']['member'],
1095 'is_active' => 1,
1096 ]);
1097
1098 // Create two memberships for the organization, via a price set in the back end.
1099 $this->createTwoMembershipsViaPriceSetInBackEnd($orgID);
1100
1101 // Check the primary memberships on the organization.
1102 $orgMembershipResult = $this->callAPISuccess('membership', 'get', [
1103 'contact_id' => $orgID,
1104 ]);
1105 $this->assertEquals(2, $orgMembershipResult['count'], '2 primary memberships should have been created on the organization.');
1106 $primaryMembershipIds = [];
1107 foreach ($orgMembershipResult['values'] as $membership) {
1108 $primaryMembershipIds[] = $membership['id'];
1109 $this->assertTrue(empty($membership['owner_membership_id']), 'Membership on the organization has owner_membership_id so is inherited.');
1110 }
1111
1112 // CRM-20955: check that correct inherited memberships were created for the individual,
1113 // for both of the primary memberships.
1114 $individualMembershipResult = $this->callAPISuccess('membership', 'get', [
1115 'contact_id' => $this->_individualId,
1116 ]);
1117 $this->assertEquals(2, $individualMembershipResult['count'], "2 inherited memberships should have been created on the individual.");
1118 foreach ($individualMembershipResult['values'] as $membership) {
1119 $this->assertNotEmpty($membership['owner_membership_id'], "Membership on the individual lacks owner_membership_id so is not inherited.");
1120 $this->assertNotContains($membership['id'], $primaryMembershipIds, "Inherited membership id should not be the id of a primary membership.");
1121 $this->assertContains($membership['owner_membership_id'], $primaryMembershipIds, "Inherited membership owner_membership_id should be the id of a primary membership.");
1122 }
1123
1124 // CRM-20966: check that the correct membership contribution, line items
1125 // & membership_payment records were created for the organization.
1126 $contributionResult = $this->callAPISuccess('contribution', 'get', [
1127 'contact_id' => $orgID,
1128 'sequential' => 1,
1129 'api.line_item.get' => [],
1130 'api.membership_payment.get' => [],
1131 ]);
1132 $this->assertEquals(1, $contributionResult['count'], "One contribution should have been created for the organization's memberships.");
1133
1134 $this->assertEquals(2, $contributionResult['values'][0]['api.line_item.get']['count'], "2 line items should have been created for the organization's memberships.");
1135 foreach ($contributionResult['values'][0]['api.line_item.get']['values'] as $lineItem) {
1136 $this->assertEquals('civicrm_membership', $lineItem['entity_table'], "Membership line item's entity_table should be 'civicrm_membership'.");
1137 $this->assertContains($lineItem['entity_id'], $primaryMembershipIds, "Membership line item's entity_id should be the id of a primary membership.");
1138 }
1139
1140 $this->assertEquals(2, $contributionResult['values'][0]['api.membership_payment.get']['count'], "2 membership payment records should have been created for the organization's memberships.");
1141 foreach ($contributionResult['values'][0]['api.membership_payment.get']['values'] as $membershipPayment) {
1142 $this->assertEquals($contributionResult['values'][0]['id'], $membershipPayment['contribution_id'], "membership payment's contribution ID should be the ID of the organization's membership contribution.");
1143 $this->assertContains($membershipPayment['membership_id'], $primaryMembershipIds, "membership payment's membership ID should be the ID of a primary membership.");
1144 }
1145 // Check for orphan line items.
1146 $this->callAPISuccessGetCount('LineItem', [], 2);
1147
1148 // CRM-20966: check that deleting relationship used for inheritance does not delete contribution.
1149 $this->callAPISuccess('relationship', 'delete', [
1150 'id' => $relationship['id'],
1151 ]);
1152
1153 $contributionResultAfterRelationshipDelete = $this->callAPISuccess('contribution', 'get', [
1154 'id' => $contributionResult['values'][0]['id'],
1155 'contact_id' => $orgID,
1156 ]);
1157 $this->assertEquals(1, $contributionResultAfterRelationshipDelete['count'], "Contribution has been wrongly deleted.");
1158 }
1159
1160 /**
1161 * dev/core/issues/860:
1162 * Test creating two memberships via price set in the back end with a discount,
1163 * checking that the line items have correct amounts.
1164 *
1165 * @throws \CRM_Core_Exception
1166 * @throws \CiviCRM_API3_Exception
1167 */
1168 public function testTwoMembershipsViaPriceSetInBackendWithDiscount(): void {
1169 // Register buildAmount hook to apply discount.
1170 $this->hookClass->setHook('civicrm_buildAmount', [$this, 'buildAmountMembershipDiscount']);
1171 $this->enableTaxAndInvoicing();
1172 $this->addTaxAccountToFinancialType(2);
1173 // Create two memberships for individual $this->_individualId, via a price set in the back end.
1174 $this->createTwoMembershipsViaPriceSetInBackEnd($this->_individualId);
1175 $contribution = $this->callAPISuccessGetSingle('Contribution', [
1176 'contact_id' => $this->_individualId,
1177 ]);
1178
1179 // Note: we can't check for the contribution total being discounted, because the total is set
1180 // when the contribution is created via $form->testSubmit(), but buildAmount isn't called
1181 // until testSubmit() runs. Fixing that might involve making testSubmit() more sophisticated,
1182 // or just hacking total_amount for this case.
1183
1184 $lineItemResult = $this->callAPISuccess('LineItem', 'get', [
1185 'contribution_id' => $contribution['id'],
1186 ]);
1187 $this->assertEquals(2, $lineItemResult['count']);
1188 $discountedItems = 0;
1189 foreach ($lineItemResult['values'] as $lineItem) {
1190 $this->assertEquals($lineItem['line_total'] * .1, $lineItem['tax_amount']);
1191 if (CRM_Utils_String::startsWith($lineItem['label'], 'Long Haired Goat')) {
1192 $this->assertEquals(15.0, $lineItem['line_total']);
1193 $this->assertEquals('Long Haired Goat - one leg free!', $lineItem['label']);
1194 $discountedItems++;
1195 }
1196 }
1197 $this->assertEquals(1, $discountedItems);
1198 }
1199
1200 /**
1201 * Implements hook_civicrm_buildAmount() for testTwoMembershipsViaPriceSetInBackendWithDiscount().
1202 */
1203 public function buildAmountMembershipDiscount($pageType, &$form, &$amount) {
1204 foreach ($amount as $id => $priceField) {
1205 if (is_array($priceField['options'])) {
1206 foreach ($priceField['options'] as $optionId => $option) {
1207 if ($option['membership_type_id'] == $this->ids['membership_type']['AnnualRolling']) {
1208 // Long Haired Goat membership discount.
1209 $amount[$id]['options'][$optionId]['amount'] = $option['amount'] * 0.75;
1210 $amount[$id]['options'][$optionId]['label'] = $option['label'] . ' - one leg free!';
1211 }
1212 }
1213 }
1214 }
1215 }
1216
1217 /**
1218 * Get a membership form object.
1219 *
1220 * We need to instantiate the form to run preprocess, which means we have to
1221 * trick it about the request method.
1222 *
1223 * @return \CRM_Member_Form_Membership
1224 * @throws \CRM_Core_Exception
1225 * @throws \CiviCRM_API3_Exception
1226 */
1227 protected function getForm() {
1228 if (isset($_REQUEST['cid'])) {
1229 unset($_REQUEST['cid']);
1230 }
1231 $form = $this->getFormObject('CRM_Member_Form_Membership');
1232 $form->preProcess();
1233 return $form;
1234 }
1235
1236 /**
1237 * @return array
1238 */
1239 protected function getBaseSubmitParams() {
1240 $params = [
1241 'cid' => $this->_individualId,
1242 'price_set_id' => 0,
1243 'join_date' => date('Y-m-d'),
1244 'start_date' => '',
1245 'end_date' => '',
1246 'campaign_id' => '',
1247 // This format reflects the first number being the organisation & the second being the type.
1248 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualFixed']],
1249 'auto_renew' => '1',
1250 'is_recur' => 1,
1251 'max_related' => 0,
1252 'num_terms' => '1',
1253 'source' => '',
1254 'total_amount' => '77.00',
1255 //Member dues, see data.xml
1256 'financial_type_id' => '2',
1257 'soft_credit_type_id' => 11,
1258 'soft_credit_contact_id' => '',
1259 'from_email_address' => '"Demonstrators Anonymous" <info@example.org>',
1260 'receipt_text' => 'Thank you text',
1261 'payment_processor_id' => $this->_paymentProcessorID,
1262 'credit_card_number' => '4111111111111111',
1263 'cvv2' => '123',
1264 'credit_card_exp_date' => [
1265 'M' => '9',
1266 'Y' => date('Y') + 1,
1267 ],
1268 'credit_card_type' => 'Visa',
1269 'billing_first_name' => 'Test',
1270 'billing_middlename' => 'Last',
1271 'billing_street_address-5' => '10 Test St',
1272 'billing_city-5' => 'Test',
1273 'billing_state_province_id-5' => '1003',
1274 'billing_postal_code-5' => '90210',
1275 'billing_country_id-5' => '1228',
1276 'send_receipt' => 1,
1277 ];
1278 return $params;
1279 }
1280
1281 /**
1282 * Scenario builder:
1283 * create two memberships for the same individual, via a price set in the back end.
1284 *
1285 * @param int $contactId Id of contact on which the memberships will be created.
1286 *
1287 * @throws \CRM_Core_Exception
1288 * @throws \CiviCRM_API3_Exception
1289 */
1290 protected function createTwoMembershipsViaPriceSetInBackEnd($contactId): void {
1291 $form = $this->getForm();
1292 $form->preProcess();
1293 $this->createLoggedInUser();
1294 $pfvIDs = $this->createMembershipPriceSet();
1295
1296 // register for both of these memberships via backoffice membership form submission
1297 $params = [
1298 'cid' => $contactId,
1299 'join_date' => date('Y-m-d'),
1300 'start_date' => '',
1301 'end_date' => '',
1302 "price_" . $this->getPriceFieldID() => $pfvIDs,
1303 'price_set_id' => $this->getPriceSetID(),
1304 'membership_type_id' => [1 => 0],
1305 'auto_renew' => '0',
1306 'max_related' => '',
1307 'num_terms' => '2',
1308 'source' => '',
1309 'total_amount' => '30.00',
1310 //Member dues, see data.xml
1311 'financial_type_id' => '2',
1312 'soft_credit_type_id' => '',
1313 'soft_credit_contact_id' => '',
1314 'payment_instrument_id' => 4,
1315 'from_email_address' => '"Demonstrators Anonymous" <info@example.org>',
1316 'receipt_text_signup' => 'Thank you text',
1317 'payment_processor_id' => $this->_paymentProcessorID,
1318 'record_contribution' => TRUE,
1319 'trxn_id' => 777,
1320 'contribution_status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_DAO_Contribution', 'contribution_status_id', 'Pending'),
1321 'billing_first_name' => 'Test',
1322 'billing_middlename' => 'Last',
1323 'billing_street_address-5' => '10 Test St',
1324 'billing_city-5' => 'Test',
1325 'billing_state_province_id-5' => '1003',
1326 'billing_postal_code-5' => '90210',
1327 'billing_country_id-5' => '1228',
1328 ];
1329 $form->testSubmit($params);
1330 }
1331
1332 /**
1333 * Test membership status overrides when contribution is cancelled.
1334 *
1335 * @throws \CRM_Core_Exception
1336 * @throws \CiviCRM_API3_Exception
1337 */
1338 public function testContributionFormStatusUpdate(): void {
1339
1340 $this->_contactID = $this->createLoggedInUser();
1341 $this->createContributionAndMembershipOrder();
1342
1343 $params = [
1344 'total_amount' => 50,
1345 'financial_type_id' => 2,
1346 'contact_id' => $this->_contactID,
1347 'payment_instrument_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'payment_instrument_id', 'Check'),
1348 'contribution_status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Cancelled'),
1349 ];
1350
1351 //Update Contribution to Cancelled.
1352 $form = new CRM_Contribute_Form_Contribution();
1353 $form->_id = $params['id'] = $this->ids['Contribution'][0];
1354 $form->_mode = NULL;
1355 $form->_contactID = $this->_individualId;
1356 $form->testSubmit($params, CRM_Core_Action::UPDATE);
1357 $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_contactID]);
1358
1359 //Assert membership status overrides when the contribution cancelled.
1360 $this->assertEquals(TRUE, $membership['is_override']);
1361 $this->assertEquals($membership['status_id'], $this->callAPISuccessGetValue('MembershipStatus', [
1362 'return' => 'id',
1363 'name' => 'Cancelled',
1364 ]));
1365 }
1366
1367 /**
1368 * CRM-21656: Test the submit function of the membership form if Sales Tax is enabled.
1369 * This test simulates what happens when one hits Edit on a Contribution that has both LineItems and Sales Tax components
1370 * Without making any Edits -> check that the LineItem data remain the same
1371 * In addition (a data-integrity check) -> check that the LineItem data add up to the data at the Contribution level
1372 *
1373 * @throws \CRM_Core_Exception
1374 * @throws \CiviCRM_API3_Exception
1375 */
1376 public function testLineItemAmountOnSalesTax() {
1377 $this->enableTaxAndInvoicing();
1378 $this->addTaxAccountToFinancialType(2);
1379 $form = $this->getForm();
1380 $form->preProcess();
1381 $this->mut = new CiviMailUtils($this, TRUE);
1382 $this->createLoggedInUser();
1383 $priceSet = $this->callAPISuccess('PriceSet', 'Get', ['extends' => 'CiviMember']);
1384 $form->set('priceSetId', $priceSet['id']);
1385 // we are simulating the creation of a Price Set in Administer -> CiviContribute -> Manage Price Sets so set is_quick_config = 0
1386 $this->callAPISuccess('PriceSet', 'Create', ['id' => $priceSet['id'], 'is_quick_config' => 0]);
1387 // clean the price options static variable to repopulate the options, in order to fetch tax information
1388 \Civi::$statics['CRM_Price_BAO_PriceField']['priceOptions'] = NULL;
1389 CRM_Price_BAO_PriceSet::buildPriceSet($form);
1390 // rebuild the price set form variable to include the tax information against each price options
1391 $form->_priceSet = current(CRM_Price_BAO_PriceSet::getSetDetail($priceSet['id']));
1392 $params = [
1393 'cid' => $this->_individualId,
1394 'join_date' => date('Y-m-d'),
1395 'start_date' => '',
1396 'end_date' => '',
1397 // This format reflects the first number being the organisation & the second being the type.
1398 'membership_type_id' => [$this->ids['contact']['organization'], $this->ids['membership_type']['AnnualFixed']],
1399 'record_contribution' => 1,
1400 'total_amount' => 55,
1401 'receive_date' => date('Y-m-d') . ' 20:36:00',
1402 'payment_instrument_id' => array_search('Check', $this->paymentInstruments, TRUE),
1403 'contribution_status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'),
1404 //Member dues, see data.xml
1405 'financial_type_id' => 2,
1406 'payment_processor_id' => $this->_paymentProcessorID,
1407 ];
1408 $form->_contactID = $this->_individualId;
1409 $form->testSubmit($params);
1410
1411 $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_individualId]);
1412 $lineItem = $this->callAPISuccessGetSingle('LineItem', ['entity_id' => $membership['id'], 'entity_table' => 'civicrm_membership']);
1413 $this->assertEquals(1, $lineItem['qty']);
1414 $this->assertEquals(50.00, $lineItem['unit_price']);
1415 $this->assertEquals(50.00, $lineItem['line_total']);
1416 $this->assertEquals(5.00, $lineItem['tax_amount']);
1417
1418 // Simply save the 'Edit Contribution' form
1419 $form = new CRM_Contribute_Form_Contribution();
1420 $form->_context = 'membership';
1421 $form->_values = $this->callAPISuccessGetSingle('Contribution', ['id' => $lineItem['contribution_id'], 'return' => ['total_amount', 'net_amount', 'fee_amount', 'tax_amount']]);
1422 $form->testSubmit([
1423 'contact_id' => $this->_individualId,
1424 'id' => $lineItem['contribution_id'],
1425 'financial_type_id' => 2,
1426 'contribution_status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'),
1427 ],
1428 CRM_Core_Action::UPDATE);
1429
1430 // ensure that the LineItem data remain the same
1431 $lineItem = $this->callAPISuccessGetSingle('LineItem', ['entity_id' => $membership['id'], 'entity_table' => 'civicrm_membership']);
1432 $this->assertEquals(1, $lineItem['qty']);
1433 $this->assertEquals(50.00, $lineItem['unit_price']);
1434 $this->assertEquals(50.00, $lineItem['line_total']);
1435 $this->assertEquals(5.00, $lineItem['tax_amount']);
1436
1437 // ensure that the LineItem data add up to the data at the Contribution level
1438 $contribution = $this->callAPISuccessGetSingle('Contribution',
1439 [
1440 'contribution_id' => 1,
1441 'return' => ['tax_amount', 'total_amount'],
1442 ]
1443 );
1444 $this->assertEquals($contribution['total_amount'], $lineItem['line_total'] + $lineItem['tax_amount']);
1445 $this->assertEquals($contribution['tax_amount'], $lineItem['tax_amount']);
1446
1447 $financialItems = $this->callAPISuccess('FinancialItem', 'get', []);
1448 $financialItems_sum = 0;
1449 foreach ($financialItems['values'] as $financialItem) {
1450 $financialItems_sum += $financialItem['amount'];
1451 }
1452 $this->assertEquals($contribution['total_amount'], $financialItems_sum);
1453 }
1454
1455 /**
1456 * Test that membership end_date is correct for multiple terms for pending contribution
1457 *
1458 * @throws CiviCRM_API3_Exception
1459 * @throws \CRM_Core_Exception
1460 * @throws \Exception
1461 */
1462 public function testCreatePendingWithMultipleTerms() {
1463 CRM_Core_Session::singleton()->getStatus(TRUE);
1464 $this->mut = new CiviMailUtils($this, TRUE);
1465 $this->createLoggedInUser();
1466 $membershipTypeAnnualRolling = $this->callAPISuccess('membership_type', 'create', [
1467 'domain_id' => 1,
1468 'name' => 'AnnualRollingNew',
1469 'member_of_contact_id' => $this->ids['contact']['organization'],
1470 'duration_unit' => 'year',
1471 'minimum_fee' => 50,
1472 'duration_interval' => 1,
1473 'period_type' => 'rolling',
1474 'relationship_type_id' => 20,
1475 'relationship_direction' => 'b_a',
1476 'financial_type_id' => 2,
1477 ]);
1478 $params = [
1479 'cid' => $this->_individualId,
1480 'join_date' => date('Y-m-d'),
1481 'start_date' => '',
1482 'end_date' => '',
1483 'contribution_status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending'),
1484 'membership_type_id' => [$this->ids['contact']['organization'], $membershipTypeAnnualRolling['id']],
1485 'max_related' => '',
1486 'num_terms' => '3',
1487 'record_contribution' => 1,
1488 'source' => '',
1489 'total_amount' => $this->formatMoneyInput(150.00),
1490 'financial_type_id' => '2',
1491 'soft_credit_type_id' => '11',
1492 'soft_credit_contact_id' => '',
1493 'from_email_address' => '"Demonstrators Anonymous" <info@example.org>',
1494 'receipt_text' => '',
1495 ];
1496 $form = $this->getForm();
1497 $form->preProcess();
1498 $form->_contactID = $this->_individualId;
1499 $form->testSubmit($params);
1500 $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_individualId]);
1501 $contribution = $this->callAPISuccess('Contribution', 'get', [
1502 'contact_id' => $this->_individualId,
1503 ]);
1504 $endDate = (new DateTime(date('Y-m-d')))->modify('+3 years')->modify('-1 day');
1505 $endDate = $endDate->format("Y-m-d");
1506
1507 $this->assertEquals($endDate, $membership['end_date'], 'Membership end date should be ' . $endDate);
1508 $this->assertEquals(1, count($contribution['values']), 'Pending contribution should be created.');
1509 $contribution = $contribution['values'][$contribution['id']];
1510 $additionalPaymentForm = new CRM_Contribute_Form_AdditionalPayment();
1511 $additionalPaymentForm->testSubmit([
1512 'total_amount' => 150.00,
1513 'trxn_date' => date("Y-m-d H:i:s"),
1514 'payment_instrument_id' => array_search('Check', $this->paymentInstruments),
1515 'check_number' => 'check-12345',
1516 'trxn_id' => '',
1517 'currency' => 'USD',
1518 'fee_amount' => '',
1519 'financial_type_id' => 1,
1520 'net_amount' => '',
1521 'payment_processor_id' => 0,
1522 'contact_id' => $this->_individualId,
1523 'contribution_id' => $contribution['id'],
1524 ]);
1525 $membership = $this->callAPISuccessGetSingle('Membership', ['contact_id' => $this->_individualId]);
1526 $contribution = $this->callAPISuccess('Contribution', 'get', [
1527 'contact_id' => $this->_individualId,
1528 'contribution_status_id' => CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'),
1529 ]);
1530 $this->assertEquals($endDate, $membership['end_date'], 'Membership end date should be same (' . $endDate . ') after payment');
1531 $this->assertCount(1, $contribution['values'], 'Completed contribution should be fetched.');
1532 }
1533
1534 /**
1535 * Test Membership Payment owned by other contact, membership view should show all contribution records in listing.
1536 * is other contact.
1537 *
1538 * @throws \CRM_Core_Exception
1539 * @throws \CiviCRM_API3_Exception
1540 * @throws \Exception
1541 */
1542 public function testMembershipViewContributionOwnerDifferent() {
1543 // Membership Owner
1544 $contactId1 = $this->individualCreate();
1545
1546 // Contribution Onwer
1547 $contactId2 = $this->individualCreate();
1548
1549 // create new membership type
1550 $membershipTypeAnnualFixed = $this->callAPISuccess('MembershipType', 'create', [
1551 'domain_id' => 1,
1552 'name' => 'AnnualFixed 2',
1553 'member_of_contact_id' => $this->organizationCreate(),
1554 'duration_unit' => 'year',
1555 'minimum_fee' => 50,
1556 'duration_interval' => 1,
1557 'period_type' => 'fixed',
1558 'fixed_period_start_day' => '101',
1559 'fixed_period_rollover_day' => '1231',
1560 'financial_type_id' => 2,
1561 ]);
1562
1563 // create Membership
1564 $membershipId = $this->contactMembershipCreate([
1565 'contact_id' => $contactId1,
1566 'membership_type_id' => $membershipTypeAnnualFixed['id'],
1567 'status_id' => 'New',
1568 ]);
1569
1570 // 1st Payment
1571 $contriParams = [
1572 'membership_id' => $membershipId,
1573 'total_amount' => 25,
1574 'financial_type_id' => 2,
1575 'contact_id' => $contactId2,
1576 'receive_date' => '2020-08-08',
1577 ];
1578 $contribution1 = CRM_Member_BAO_Membership::recordMembershipContribution($contriParams);
1579
1580 // 2nd Payment
1581 $contriParams = [
1582 'membership_id' => $membershipId,
1583 'total_amount' => 25,
1584 'financial_type_id' => 2,
1585 'contact_id' => $contactId2,
1586 'receive_date' => '2020-07-08',
1587 ];
1588 $contribution2 = CRM_Member_BAO_Membership::recordMembershipContribution($contriParams);
1589
1590 // View Membership record
1591 $membershipViewForm = new CRM_Member_Form_MembershipView();
1592 $membershipViewForm->controller = new CRM_Core_Controller_Simple('CRM_Member_Form_MembershipView', 'View Membership');
1593 $membershipViewForm->set('id', $membershipId);
1594 $membershipViewForm->set('context', 'membership');
1595 $membershipViewForm->controller->setEmbedded(TRUE);
1596 $membershipViewForm->preProcess();
1597
1598 // get contribution rows related to membership payments
1599 $templateVar = $membershipViewForm::getTemplate()->get_template_vars('rows');
1600
1601 $this->assertEquals($templateVar[0]['contribution_id'], $contribution1->id);
1602 $this->assertEquals($templateVar[0]['contact_id'], $contactId2);
1603
1604 $this->assertEquals($templateVar[1]['contribution_id'], $contribution2->id);
1605 $this->assertEquals($templateVar[1]['contact_id'], $contactId2);
1606 $this->assertEquals(count($templateVar), 2);
1607 }
1608
1609 }