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