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