Tax fixes in unit test
[civicrm-core.git] / tests / phpunit / api / v3 / ContributionPageTest.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 use Civi\Api4\PriceFieldValue;
13
14 /**
15 * Test APIv3 civicrm_contribute_recur* functions
16 *
17 * @package CiviCRM_APIv3
18 * @subpackage API_Contribution
19 * @group headless
20 */
21 class api_v3_ContributionPageTest extends CiviUnitTestCase {
22 protected $testAmount = 34567;
23 protected $params;
24 protected $id = 0;
25 protected $contactIds = [];
26 protected $_entity = 'ContributionPage';
27 protected $contribution_result = NULL;
28 protected $_priceSetParams = [];
29 protected $_membershipBlockAmount = 2;
30 /**
31 * Payment processor details.
32 * @var array
33 */
34 protected $_paymentProcessor = [];
35
36 /**
37 * @var array
38 * - contribution_page
39 * - price_set
40 * - price_field
41 * - price_field_value
42 */
43 protected $_ids = [];
44
45 /**
46 * Should financials be checked after the test but before tear down.
47 *
48 * @var bool
49 */
50 protected $isValidateFinancialsOnPostAssert = TRUE;
51
52 /**
53 * Setup for test.
54 *
55 * @throws \CRM_Core_Exception
56 */
57 public function setUp(): void {
58 parent::setUp();
59 $this->contactIds[] = $this->individualCreate();
60 $this->params = [
61 'title' => 'Test Contribution Page',
62 'financial_type_id' => 1,
63 'currency' => 'NZD',
64 'goal_amount' => $this->testAmount,
65 'is_pay_later' => 1,
66 'pay_later_text' => 'Send check',
67 'is_monetary' => TRUE,
68 'is_email_receipt' => TRUE,
69 'receipt_from_email' => 'yourconscience@donate.com',
70 'receipt_from_name' => 'Ego Freud',
71 ];
72
73 $this->_priceSetParams = [
74 'is_quick_config' => 1,
75 'extends' => 'CiviContribute',
76 'financial_type_id' => 'Donation',
77 'title' => 'my Page',
78 ];
79 }
80
81 /**
82 * Tear down after test.
83 *
84 * @throws \CRM_Core_Exception
85 */
86 public function tearDown(): void {
87 foreach ($this->contactIds as $id) {
88 $this->callAPISuccess('contact', 'delete', ['id' => $id]);
89 }
90 $this->quickCleanUpFinancialEntities();
91 parent::tearDown();
92 }
93
94 /**
95 * Test creating a contribution page.
96 *
97 * @param int $version
98 *
99 * @dataProvider versionThreeAndFour
100 * @throws \CRM_Core_Exception
101 */
102 public function testCreateContributionPage($version) {
103 $this->basicCreateTest($version);
104 }
105
106 /**
107 * Test getting a contribution page.
108 *
109 * @param int $version
110 *
111 * @dataProvider versionThreeAndFour
112 * @throws \CRM_Core_Exception
113 */
114 public function testGetBasicContributionPage($version) {
115 $this->_apiversion = $version;
116 $createResult = $this->callAPISuccess($this->_entity, 'create', $this->params);
117 $this->id = $createResult['id'];
118 $getParams = [
119 'currency' => 'NZD',
120 'financial_type_id' => 1,
121 ];
122 $getResult = $this->callAPIAndDocument($this->_entity, 'get', $getParams, __FUNCTION__, __FILE__);
123 $this->assertEquals(1, $getResult['count']);
124 }
125
126 /**
127 * Test get with amount as a parameter.
128 *
129 * @throws \CRM_Core_Exception
130 */
131 public function testGetContributionPageByAmount() {
132 $createResult = $this->callAPISuccess($this->_entity, 'create', $this->params);
133 $this->id = $createResult['id'];
134 $getParams = [
135 // 3456
136 'amount' => '' . $this->testAmount,
137 'currency' => 'NZD',
138 'financial_type_id' => 1,
139 ];
140 $getResult = $this->callAPISuccess($this->_entity, 'get', $getParams);
141 $this->assertEquals(1, $getResult['count']);
142 }
143
144 /**
145 * Test page deletion.
146 *
147 * @param int $version
148 *
149 * @dataProvider versionThreeAndFour
150 * @throws \CRM_Core_Exception
151 */
152 public function testDeleteContributionPage($version) {
153 $this->basicDeleteTest($version);
154 }
155
156 /**
157 * Test getfields function.
158 *
159 * @throws \CRM_Core_Exception
160 */
161 public function testGetFieldsContributionPage() {
162 $result = $this->callAPISuccess($this->_entity, 'getfields', ['action' => 'create']);
163 $this->assertEquals(12, $result['values']['start_date']['type']);
164 }
165
166 /**
167 * Test form submission with basic price set.
168 *
169 * @throws \CRM_Core_Exception
170 */
171 public function testSubmit() {
172 $this->setUpContributionPage();
173 $submitParams = $this->getBasicSubmitParams();
174
175 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
176 $contribution = $this->callAPISuccess('contribution', 'getsingle', ['contribution_page_id' => $this->_ids['contribution_page']]);
177 //assert non-deductible amount
178 $this->assertEquals(5.00, $contribution['non_deductible_amount']);
179 }
180
181 /**
182 * Test form submission with basic price set.
183 *
184 * @throws \CRM_Core_Exception
185 */
186 public function testSubmitZeroDollar() {
187 $this->setUpContributionPage();
188 $priceFieldID = reset($this->_ids['price_field']);
189 $submitParams = [
190 'price_' . $priceFieldID => $this->_ids['price_field_value']['cheapskate'],
191 'id' => (int) $this->_ids['contribution_page'],
192 'amount' => 0,
193 'priceSetId' => $this->_ids['price_set'][0],
194 'payment_processor_id' => '',
195 ];
196
197 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
198 $contribution = $this->callAPISuccess('contribution', 'getsingle', ['contribution_page_id' => $this->_ids['contribution_page']]);
199
200 $this->assertEquals($this->formatMoneyInput(0), $contribution['non_deductible_amount']);
201 $this->assertEquals($this->formatMoneyInput(0), $contribution['total_amount']);
202 }
203
204 /**
205 * Test form submission with billing first & last name where the contact does NOT
206 * otherwise have one.
207 *
208 * @throws \CRM_Core_Exception
209 */
210 public function testSubmitNewBillingNameData() {
211 $this->setUpContributionPage();
212 $contact = $this->callAPISuccess('Contact', 'create', ['contact_type' => 'Individual', 'email' => 'wonderwoman@amazon.com']);
213 $priceFieldID = reset($this->_ids['price_field']);
214 $priceFieldValueID = reset($this->_ids['price_field_value']);
215 $submitParams = [
216 'price_' . $priceFieldID => $priceFieldValueID,
217 'id' => (int) $this->_ids['contribution_page'],
218 'amount' => 10,
219 'billing_first_name' => 'Wonder',
220 'billing_last_name' => 'Woman',
221 'contactID' => $contact['id'],
222 'email' => 'wonderwoman@amazon.com',
223 ];
224
225 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
226 $contact = $this->callAPISuccess('Contact', 'get', [
227 'id' => $contact['id'],
228 'return' => [
229 'first_name',
230 'last_name',
231 'sort_name',
232 'display_name',
233 ],
234 ]);
235 $this->assertEquals([
236 'first_name' => 'Wonder',
237 'last_name' => 'Woman',
238 'display_name' => 'Wonder Woman',
239 'sort_name' => 'Woman, Wonder',
240 'id' => $contact['id'],
241 'contact_id' => $contact['id'],
242 ], $contact['values'][$contact['id']]);
243
244 }
245
246 /**
247 * Test form submission with billing first & last name where the contact does
248 * otherwise have one and should not be overwritten.
249 *
250 * @throws \CRM_Core_Exception
251 */
252 public function testSubmitNewBillingNameDoNotOverwrite(): void {
253 $this->setUpContributionPage();
254 $contact = $this->callAPISuccess('Contact', 'create', [
255 'contact_type' => 'Individual',
256 'email' => 'wonderwoman@amazon.com',
257 'first_name' => 'Super',
258 'last_name' => 'Boy',
259 ]);
260 $priceFieldID = reset($this->_ids['price_field']);
261 $priceFieldValueID = reset($this->_ids['price_field_value']);
262 $submitParams = [
263 'price_' . $priceFieldID => $priceFieldValueID,
264 'id' => (int) $this->_ids['contribution_page'],
265 'amount' => 10,
266 'billing_first_name' => 'Wonder',
267 'billing_last_name' => 'Woman',
268 'contactID' => $contact['id'],
269 'email' => 'wonderwoman@amazon.com',
270 ];
271
272 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
273 $contact = $this->callAPISuccess('Contact', 'get', [
274 'id' => $contact['id'],
275 'return' => [
276 'first_name',
277 'last_name',
278 'sort_name',
279 'display_name',
280 ],
281 ]);
282 $this->assertEquals([
283 'first_name' => 'Super',
284 'last_name' => 'Boy',
285 'display_name' => 'Super Boy',
286 'sort_name' => 'Boy, Super',
287 'id' => $contact['id'],
288 'contact_id' => $contact['id'],
289 ], $contact['values'][$contact['id']]);
290
291 }
292
293 /**
294 * Test process with instant payment when more than one configured for the page.
295 *
296 * @see https://issues.civicrm.org/jira/browse/CRM-16923
297 *
298 * @throws \CiviCRM_API3_Exception
299 * @throws \CRM_Core_Exception
300 */
301 public function testSubmitRecurMultiProcessorInstantPayment() {
302 $this->setUpContributionPage();
303 $this->setupPaymentProcessor();
304 $paymentProcessor2ID = $this->paymentProcessorCreate([
305 'payment_processor_type_id' => 'Dummy',
306 'name' => 'processor 2',
307 'class_name' => 'Payment_Dummy',
308 'billing_mode' => 1,
309 ]);
310 $dummyPP = Civi\Payment\System::singleton()->getById($paymentProcessor2ID);
311 $dummyPP->setDoDirectPaymentResult([
312 'payment_status_id' => 1,
313 'trxn_id' => 'create_first_success',
314 'fee_amount' => .85,
315 ]);
316 $processor = $dummyPP->getPaymentProcessor();
317 $this->callAPISuccess('ContributionPage', 'create', [
318 'id' => $this->_ids['contribution_page'],
319 'payment_processor' => [$paymentProcessor2ID, $this->_ids['payment_processor']],
320 ]);
321
322 $priceFieldID = reset($this->_ids['price_field']);
323 $priceFieldValueID = reset($this->_ids['price_field_value']);
324 $submitParams = [
325 'price_' . $priceFieldID => $priceFieldValueID,
326 'id' => (int) $this->_ids['contribution_page'],
327 'amount' => 10,
328 'is_recur' => 1,
329 'frequency_interval' => 1,
330 'frequency_unit' => 'month',
331 'payment_processor_id' => $paymentProcessor2ID,
332 ];
333
334 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
335 $contribution = $this->callAPISuccess('contribution', 'getsingle', [
336 'contribution_page_id' => $this->_ids['contribution_page'],
337 'contribution_status_id' => 1,
338 ]);
339 $this->assertEquals('create_first_success', $contribution['trxn_id']);
340 $this->assertEquals(10, $contribution['total_amount']);
341 $this->assertEquals(.85, $contribution['fee_amount']);
342 $this->assertEquals(9.15, $contribution['net_amount']);
343 $this->_checkFinancialRecords([
344 'id' => $contribution['id'],
345 'total_amount' => $contribution['total_amount'],
346 'payment_instrument_id' => $processor['payment_instrument_id'],
347 ], 'online');
348 }
349
350 /**
351 * Test submit with a membership block in place.
352 *
353 * @throws \CRM_Core_Exception
354 */
355 public function testSubmitMembershipBlockNotSeparatePayment() {
356 $this->setUpMembershipContributionPage();
357 $submitParams = [
358 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
359 'id' => (int) $this->_ids['contribution_page'],
360 'billing_first_name' => 'Billy',
361 'billing_middle_name' => 'Goat',
362 'billing_last_name' => 'Gruff',
363 'selectMembership' => $this->_ids['membership_type'][0],
364 ];
365
366 $this->callAPIAndDocument('ContributionPage', 'submit', $submitParams, __FUNCTION__, __FILE__, 'submit contribution page', NULL);
367 $contribution = $this->callAPISuccess('contribution', 'getsingle', ['contribution_page_id' => $this->_ids['contribution_page']]);
368 $membershipPayment = $this->callAPISuccess('membership_payment', 'getsingle', ['contribution_id' => $contribution['id']]);
369 $this->callAPISuccessGetSingle('LineItem', ['contribution_id' => $contribution['id'], 'entity_id' => $membershipPayment['id']]);
370 }
371
372 /**
373 * Test submit with a membership block in place works with renewal.
374 *
375 * @throws \CRM_Core_Exception
376 */
377 public function testSubmitMembershipBlockNotSeparatePaymentProcessorInstantRenew() {
378 $this->setUpMembershipContributionPage();
379 $dummyPP = Civi\Payment\System::singleton()->getByProcessor($this->_paymentProcessor);
380 $dummyPP->setDoDirectPaymentResult(['payment_status_id' => 1]);
381 $submitParams = [
382 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
383 'id' => (int) $this->_ids['contribution_page'],
384 'billing_first_name' => 'Billy',
385 'billing_middle_name' => 'Goat',
386 'billing_last_name' => 'Gruff',
387 'selectMembership' => $this->_ids['membership_type'][0],
388 'payment_processor_id' => 1,
389 'credit_card_number' => '4111111111111111',
390 'credit_card_type' => 'Visa',
391 'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
392 'cvv2' => 123,
393 ];
394
395 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
396 $contribution = $this->callAPISuccess('contribution', 'getsingle', ['contribution_page_id' => $this->_ids['contribution_page']]);
397 $membershipPayment = $this->callAPISuccess('membership_payment', 'getsingle', ['contribution_id' => $contribution['id']]);
398 $this->callAPISuccessGetCount('LineItem', [
399 'entity_table' => 'civicrm_membership',
400 'entity_id' => $membershipPayment['id'],
401 ], 1);
402
403 $submitParams['contact_id'] = $contribution['contact_id'];
404
405 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
406 $this->callAPISuccessGetCount('LineItem', [
407 'entity_table' => 'civicrm_membership',
408 'entity_id' => $membershipPayment['id'],
409 ], 2);
410 $membership = $this->callAPISuccessGetSingle('Membership', [
411 'id' => $membershipPayment['membership_id'],
412 'return' => ['end_date', 'join_date', 'start_date'],
413 ]);
414 $this->assertEquals(date('Y-m-d'), $membership['start_date']);
415 $this->assertEquals(date('Y-m-d'), $membership['join_date']);
416 $this->assertEquals(date('Y-m-d', strtotime('+ 2 year - 1 day')), $membership['end_date']);
417 }
418
419 /**
420 * Test submit with a membership block in place.
421 *
422 * @throws \CRM_Core_Exception
423 */
424 public function testSubmitMembershipBlockNotSeparatePaymentWithEmail(): void {
425 $mut = new CiviMailUtils($this, TRUE);
426 $this->setUpMembershipContributionPage();
427 $this->addProfile('supporter_profile', $this->_ids['contribution_page']);
428
429 $submitParams = [
430 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
431 'id' => (int) $this->_ids['contribution_page'],
432 'billing_first_name' => 'Billy',
433 'billing_middle_name' => 'Goat',
434 'billing_last_name' => 'Gruff',
435 'selectMembership' => $this->_ids['membership_type'][0],
436 'email-Primary' => 'billy-goat@the-bridge.net',
437 'payment_processor_id' => $this->_paymentProcessor['id'],
438 'credit_card_number' => '4111111111111111',
439 'credit_card_type' => 'Visa',
440 'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
441 'cvv2' => 123,
442 ];
443
444 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
445 $contribution = $this->callAPISuccess('contribution', 'getsingle', ['contribution_page_id' => $this->_ids['contribution_page']]);
446 $this->callAPISuccess('membership_payment', 'getsingle', ['contribution_id' => $contribution['id']]);
447 $mut->checkMailLog([
448 'Membership Type: General',
449 'Test Frontend title',
450 ]);
451 $mut->stop();
452 $mut->clearMessages();
453 }
454
455 /**
456 * Test submit with a membership block in place.
457 *
458 * @throws \Exception
459 */
460 public function testSubmitMembershipBlockNotSeparatePaymentZeroDollarsWithEmail() {
461 $mut = new CiviMailUtils($this, TRUE);
462 $this->_ids['membership_type'] = [$this->membershipTypeCreate(['minimum_fee' => 0])];
463 $this->setUpMembershipContributionPage();
464 $this->addProfile('supporter_profile', $this->_ids['contribution_page']);
465 $priceFieldValueID = reset($this->_ids['price_field_value']);
466 PriceFieldValue::update()->addWhere('id', '=', $priceFieldValueID)->setValues(['amount' => 0])->execute();
467
468 $submitParams = [
469 'price_' . $this->_ids['price_field'][0] => $priceFieldValueID,
470 'id' => (int) $this->_ids['contribution_page'],
471 'billing_first_name' => 'Billy',
472 'billing_middle_name' => 'Goat',
473 'billing_last_name' => 'Gruffier',
474 'selectMembership' => $this->_ids['membership_type'][0],
475 'email-Primary' => 'billy-goat@the-new-bridge.net',
476 'payment_processor_id' => $this->params['payment_processor_id'],
477 ];
478
479 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
480 $contribution = $this->callAPISuccess('contribution', 'getsingle', ['contribution_page_id' => $this->_ids['contribution_page']]);
481 $this->callAPISuccess('membership_payment', 'getsingle', ['contribution_id' => $contribution['id']]);
482 //Assert only one mail is being sent.
483 $msgs = $mut->getAllMessages();
484 $this->assertCount(1, $msgs);
485
486 $mut->checkMailLog([
487 'Membership Type: General',
488 'Gruffier',
489 ], [
490 'Amount',
491 ]);
492 $mut->stop();
493 $mut->clearMessages();
494 }
495
496 /**
497 * Test submit with a pay later and check line item in mails.
498 *
499 * @throws \CRM_Core_Exception
500 */
501 public function testSubmitMembershipBlockIsSeparatePaymentPayLaterWithEmail() {
502 $mut = new CiviMailUtils($this, TRUE);
503 $this->setUpMembershipContributionPage(TRUE);
504 $submitParams = [
505 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
506 'id' => (int) $this->_ids['contribution_page'],
507 'billing_first_name' => 'Billy',
508 'billing_middle_name' => 'Goat',
509 'billing_last_name' => 'Gruff',
510 'is_pay_later' => 1,
511 'selectMembership' => $this->_ids['membership_type'][0],
512 'email-Primary' => 'billy-goat@the-bridge.net',
513 ];
514
515 $this->callAPISuccess('ContributionPage', 'submit', $submitParams);
516 $contributions = $this->callAPISuccess('Contribution', 'get', ['contribution_page_id' => $this->_ids['contribution_page']])['values'];
517 $this->assertCount(2, $contributions);
518 $this->callAPISuccess('membership_payment', 'getsingle', ['contribution_id' => ['IN' => array_keys($contributions)]]);
519 $mut->checkMailLog([
520 'Membership Amount -... $ 2.00',
521 ]);
522 $mut->stop();
523 $mut->clearMessages();
524 }
525
526 /**
527 * Test submit with a membership block in place.
528 *
529 * @throws \CRM_Core_Exception
530 */
531 public function testSubmitMembershipBlockIsSeparatePayment() {
532 $this->setUpMembershipContributionPage(TRUE);
533 $this->_ids['membership_type'] = [$this->membershipTypeCreate(['minimum_fee' => 2])];
534 $submitParams = [
535 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
536 'id' => (int) $this->_ids['contribution_page'],
537 'billing_first_name' => 'Billy',
538 'billing_middle_name' => 'Goat',
539 'billing_last_name' => 'Gruff',
540 'selectMembership' => $this->_ids['membership_type'][0],
541 ];
542
543 $this->callAPISuccess('ContributionPage', 'submit', $submitParams);
544 $contributions = $this->callAPISuccess('contribution', 'get', ['contribution_page_id' => $this->_ids['contribution_page']]);
545 $this->assertCount(2, $contributions['values']);
546 $lines = $this->callAPISuccess('LineItem', 'get', ['sequential' => 1]);
547 $this->assertEquals(2, $lines['values'][0]['line_total']);
548 $membershipPayment = $this->callAPISuccess('membership_payment', 'getsingle');
549 $this->assertTrue(in_array($membershipPayment['contribution_id'], array_keys($contributions['values'])));
550 $membership = $this->callAPISuccessGetSingle('membership', ['id' => $membershipPayment['membership_id']]);
551 $this->assertEquals($membership['contact_id'], $contributions['values'][$membershipPayment['contribution_id']]['contact_id']);
552 }
553
554 /**
555 * Test submit with a membership block in place.
556 *
557 * @throws \CRM_Core_Exception
558 */
559 public function testSubmitMembershipBlockIsSeparatePaymentWithPayLater() {
560 $this->setUpMembershipContributionPage(TRUE);
561 $this->_ids['membership_type'] = [$this->membershipTypeCreate(['minimum_fee' => 2])];
562 //Pay later
563 $submitParams = [
564 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
565 'id' => (int) $this->_ids['contribution_page'],
566 'billing_first_name' => 'Billy',
567 'billing_middle_name' => 'Goat',
568 'billing_last_name' => 'Gruff',
569 'is_pay_later' => 1,
570 'selectMembership' => $this->_ids['membership_type'],
571 ];
572
573 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
574 $contributions = $this->callAPISuccess('contribution', 'get', ['contribution_page_id' => $this->_ids['contribution_page']]);
575 $this->assertCount(2, $contributions['values']);
576 foreach ($contributions['values'] as $val) {
577 $this->assertEquals(CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending'), $val['contribution_status_id']);
578 }
579
580 //Membership should be in Pending state.
581 $membershipPayment = $this->callAPISuccess('membership_payment', 'getsingle', []);
582 $this->assertTrue(in_array($membershipPayment['contribution_id'], array_keys($contributions['values'])));
583 $membership = $this->callAPISuccessGetSingle('membership', ['id' => $membershipPayment['membership_id']]);
584 $pendingStatus = $this->callAPISuccessGetSingle('MembershipStatus', ['return' => ['id'], 'name' => 'Pending']);
585 $this->assertEquals($membership['status_id'], $pendingStatus['id']);
586 $this->assertEquals($membership['contact_id'], $contributions['values'][$membershipPayment['contribution_id']]['contact_id']);
587 }
588
589 /**
590 * Test submit with a membership block in place.
591 *
592 * @throws \CRM_Core_Exception
593 */
594 public function testSubmitMembershipBlockIsSeparatePaymentWithEmail() {
595 // Need to work on valid financials on this test.
596 $this->isValidateFinancialsOnPostAssert = FALSE;
597 $mut = new CiviMailUtils($this, TRUE);
598 $this->setUpMembershipContributionPage(TRUE);
599 $this->addProfile('supporter_profile', $this->_ids['contribution_page']);
600
601 $submitParams = [
602 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
603 'id' => (int) $this->_ids['contribution_page'],
604 'amount' => 10,
605 'billing_first_name' => 'Billy',
606 'billing_middle_name' => 'Goat',
607 'billing_last_name' => 'Gruff',
608 'selectMembership' => $this->_ids['membership_type'],
609 'email-Primary' => 'billy-goat@the-bridge.net',
610 'payment_processor_id' => $this->_paymentProcessor['id'],
611 'credit_card_number' => '4111111111111111',
612 'credit_card_type' => 'Visa',
613 'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
614 'cvv2' => 123,
615 ];
616
617 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
618 $contributions = $this->callAPISuccess('contribution', 'get', ['contribution_page_id' => $this->_ids['contribution_page']]);
619 $this->assertCount(2, $contributions['values']);
620 $membershipPayment = $this->callAPISuccess('membership_payment', 'getsingle', []);
621 $this->assertTrue(in_array($membershipPayment['contribution_id'], array_keys($contributions['values'])));
622 $membership = $this->callAPISuccessGetSingle('membership', ['id' => $membershipPayment['membership_id']]);
623 $this->assertEquals($membership['contact_id'], $contributions['values'][$membershipPayment['contribution_id']]['contact_id']);
624 // We should have two separate email messages, each with their own amount
625 // line and no total line.
626 $mut->checkAllMailLog(
627 [
628 'Amount: $ 2.00',
629 'Amount: $ 10.00',
630 'Membership Fee',
631 ],
632 [
633 'Total: $',
634 ]
635 );
636 $mut->stop();
637 $mut->clearMessages();
638 }
639
640 /**
641 * Test submit with a membership block in place.
642 *
643 * @throws \CRM_Core_Exception
644 */
645 public function testSubmitMembershipBlockIsSeparatePaymentZeroDollarsPayLaterWithEmail() {
646 // Need to work on valid financials on this test.
647 $this->isValidateFinancialsOnPostAssert = FALSE;
648 $mut = new CiviMailUtils($this, TRUE);
649 $this->_ids['membership_type'] = [$this->membershipTypeCreate(['minimum_fee' => 0])];
650 $this->setUpMembershipContributionPage(TRUE);
651 $this->addProfile('supporter_profile', $this->_ids['contribution_page']);
652
653 $submitParams = [
654 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
655 'id' => (int) $this->_ids['contribution_page'],
656 'amount' => 0,
657 'billing_first_name' => 'Billy',
658 'billing_middle_name' => 'Goat',
659 'billing_last_name' => 'Gruffalo',
660 'selectMembership' => $this->_ids['membership_type'],
661 'payment_processor_id' => 0,
662 'email-Primary' => 'gruffalo@the-bridge.net',
663 ];
664
665 $this->callAPIAndDocument('contribution_page', 'submit', $submitParams, __FUNCTION__, __FILE__, 'submit contribution page', NULL);
666 $contributions = $this->callAPISuccess('contribution', 'get', ['contribution_page_id' => $this->_ids['contribution_page']]);
667 $this->assertCount(2, $contributions['values']);
668 $membershipPayment = $this->callAPISuccess('membership_payment', 'getsingle', []);
669 $this->assertTrue(in_array($membershipPayment['contribution_id'], array_keys($contributions['values'])));
670 $membership = $this->callAPISuccessGetSingle('membership', ['id' => $membershipPayment['membership_id']]);
671 $this->assertEquals($membership['contact_id'], $contributions['values'][$membershipPayment['contribution_id']]['contact_id']);
672 $mut->checkMailLog([
673 'Gruffalo',
674 'General Membership: $ 0.00',
675 'Membership Fee',
676 ]);
677 $mut->stop();
678 $mut->clearMessages();
679 }
680
681 /**
682 * Test submit with a membership block in place.
683 *
684 * @throws \CRM_Core_Exception
685 */
686 public function testSubmitMembershipBlockTwoTypesIsSeparatePayment() {
687 // Need to work on valid financials on this test.
688 $this->isValidateFinancialsOnPostAssert = FALSE;
689 $this->_ids['membership_type'] = [$this->membershipTypeCreate(['minimum_fee' => 6])];
690 $this->_ids['membership_type'][] = $this->membershipTypeCreate(['name' => 'Student', 'minimum_fee' => 50]);
691 $this->setUpMembershipContributionPage(TRUE);
692 $submitParams = [
693 'price_' . $this->_ids['price_field'][0] => $this->_ids['price_field_value'][1],
694 'id' => (int) $this->_ids['contribution_page'],
695 'amount' => 10,
696 'billing_first_name' => 'Billy',
697 'billing_middle_name' => 'Goat',
698 'billing_last_name' => 'Gruff',
699 'selectMembership' => $this->_ids['membership_type'][1],
700 ];
701
702 $this->callAPIAndDocument('contribution_page', 'submit', $submitParams, __FUNCTION__, __FILE__, 'submit contribution page', NULL);
703 $contributions = $this->callAPISuccess('contribution', 'get', ['contribution_page_id' => $this->_ids['contribution_page']]);
704 $this->assertCount(2, $contributions['values']);
705 $ids = array_keys($contributions['values']);
706 $this->assertEquals('10.00', $contributions['values'][$ids[0]]['total_amount']);
707 $this->assertEquals('50.00', $contributions['values'][$ids[1]]['total_amount']);
708 $membershipPayment = $this->callAPISuccess('membership_payment', 'getsingle', []);
709 $this->assertArrayHasKey($membershipPayment['contribution_id'], $contributions['values']);
710 $membership = $this->callAPISuccessGetSingle('membership', ['id' => $membershipPayment['membership_id']]);
711 $this->assertEquals($membership['contact_id'], $contributions['values'][$membershipPayment['contribution_id']]['contact_id']);
712 }
713
714 /**
715 * Test submit with a membership block in place.
716 *
717 * We are expecting a separate payment for the membership vs the contribution.
718 *
719 * @throws \CRM_Core_Exception
720 * @throws \CiviCRM_API3_Exception
721 */
722 public function testSubmitMembershipBlockIsSeparatePaymentPaymentProcessorNow(): void {
723 // Need to work on valid financials on this test.
724 $this->isValidateFinancialsOnPostAssert = FALSE;
725 $mut = new CiviMailUtils($this, TRUE);
726 $this->setUpMembershipContributionPage(TRUE);
727 $processor = Civi\Payment\System::singleton()->getById($this->_paymentProcessor['id']);
728 $processor->setDoDirectPaymentResult(['payment_status_id' => 1, 'fee_amount' => .72]);
729 $submitParams = [
730 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
731 'id' => (int) $this->_ids['contribution_page'],
732 'amount' => 10,
733 'billing_first_name' => 'Billy',
734 'billing_middle_name' => 'Goat',
735 'billing_last_name' => 'Gruff',
736 'email-Primary' => 'henry@8th.king',
737 'selectMembership' => $this->_ids['membership_type'],
738 'payment_processor_id' => $this->_paymentProcessor['id'],
739 'credit_card_number' => '4111111111111111',
740 'credit_card_type' => 'Visa',
741 'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
742 'cvv2' => 123,
743 ];
744
745 $this->callAPIAndDocument('ContributionPage', 'submit', $submitParams, __FUNCTION__, __FILE__, 'submit contribution page', NULL);
746 $contributions = $this->callAPISuccess('contribution', 'get', [
747 'contribution_page_id' => $this->_ids['contribution_page'],
748 'contribution_status_id' => 1,
749 ]);
750 $this->assertCount(2, $contributions['values']);
751 $membershipPayment = $this->callAPISuccess('membership_payment', 'getsingle', []);
752 $this->assertArrayHasKey($membershipPayment['contribution_id'], $contributions['values']);
753 $membership = $this->callAPISuccessGetSingle('membership', ['id' => $membershipPayment['membership_id']]);
754 $this->assertEquals($membership['contact_id'], $contributions['values'][$membershipPayment['contribution_id']]['contact_id']);
755 $lineItem = $this->callAPISuccessGetSingle('LineItem', ['entity_table' => 'civicrm_membership']);
756 $this->assertEquals($membership['id'], $lineItem['entity_id']);
757 $this->assertEquals($membershipPayment['contribution_id'], $lineItem['contribution_id']);
758 $this->assertEquals(1, $lineItem['qty']);
759 $this->assertEquals(2, $lineItem['unit_price']);
760 $this->assertEquals(2, $lineItem['line_total']);
761 foreach ($contributions['values'] as $contribution) {
762 $this->assertEquals(.72, $contribution['fee_amount']);
763 $this->assertEquals($contribution['total_amount'] - .72, $contribution['net_amount']);
764 }
765 // The total string is currently absent & it seems worse with - although at some point
766 // it may have been intended
767 $mut->checkAllMailLog(['$ 2.00', 'Contribution Amount', '$ 10.00'], ['Total:']);
768 $mut->stop();
769 $mut->clearMessages();
770 }
771
772 /**
773 * Test submit with a membership block in place.
774 *
775 * Ensure a separate payment for the membership vs the contribution, with
776 * correct amounts.
777 *
778 * @param string $thousandSeparator
779 * punctuation used to refer to thousands.
780 *
781 * @throws \CRM_Core_Exception
782 * @throws \CiviCRM_API3_Exception
783 *
784 * @dataProvider getThousandSeparators
785 */
786 public function testSubmitMembershipBlockIsSeparatePaymentPaymentProcessorNowChargesCorrectAmounts($thousandSeparator): void {
787 $this->setCurrencySeparators($thousandSeparator);
788 $this->setUpMembershipContributionPage(TRUE);
789 $processor = Civi\Payment\System::singleton()->getById($this->_paymentProcessor['id']);
790 $processor->setDoDirectPaymentResult(['fee_amount' => .72]);
791 $test_uniq = uniqid();
792 $contributionPageAmount = 10;
793 $submitParams = [
794 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
795 'id' => (int) $this->_ids['contribution_page'],
796 'amount' => $contributionPageAmount,
797 'billing_first_name' => 'Billy',
798 'billing_middle_name' => 'Goat',
799 'billing_last_name' => 'Gruff',
800 'email-Primary' => 'henry@8th.king',
801 'selectMembership' => $this->_ids['membership_type'],
802 'payment_processor_id' => $this->_paymentProcessor['id'],
803 'credit_card_number' => '4111111111111111',
804 'credit_card_type' => 'Visa',
805 'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
806 'cvv2' => 123,
807 'TEST_UNIQ' => $test_uniq,
808 ];
809
810 // set custom hook
811 $this->hookClass->setHook('civicrm_alterPaymentProcessorParams', [$this, 'hook_civicrm_alterPaymentProcessorParams']);
812
813 $this->callAPISuccess('ContributionPage', 'submit', $submitParams);
814 $this->callAPISuccess('Contribution', 'get', [
815 'contribution_page_id' => $this->_ids['contribution_page'],
816 'contribution_status_id' => 1,
817 ]);
818
819 $result = civicrm_api3('SystemLog', 'get', [
820 'sequential' => 1,
821 'message' => ['LIKE' => "%{$test_uniq}%"],
822 ]);
823 $this->assertCount(2, $result['values'], "Expected exactly 2 log entries matching {$test_uniq}.");
824
825 // Examine logged entries to ensure correct values.
826 $contribution_ids = [];
827 $found_membership_amount = $found_contribution_amount = FALSE;
828 foreach ($result['values'] as $value) {
829 [$junk, $json] = explode("$test_uniq:", $value['message']);
830 $logged_contribution = json_decode($json, TRUE);
831 $contribution_ids[] = $logged_contribution['contributionID'];
832 if (!empty($logged_contribution['total_amount'])) {
833 $amount = $logged_contribution['total_amount'];
834 }
835 else {
836 $amount = $logged_contribution['amount'];
837 }
838
839 if ($amount == $this->_membershipBlockAmount) {
840 $found_membership_amount = TRUE;
841 }
842 if ($amount == $contributionPageAmount) {
843 $found_contribution_amount = TRUE;
844 }
845 }
846
847 $distinct_contribution_ids = array_unique($contribution_ids);
848 $this->assertCount(2, $distinct_contribution_ids, "Expected exactly 2 log contributions with distinct contributionIDs.");
849 $this->assertTrue($found_contribution_amount, "Expected one log contribution with amount '$contributionPageAmount' (the contribution page amount)");
850 $this->assertTrue($found_membership_amount, "Expected one log contribution with amount '$this->_membershipBlockAmount' (the membership amount)");
851 }
852
853 /**
854 * Test that when a transaction fails the pending contribution remains.
855 *
856 * An activity should also be created. CRM-16417.
857 *
858 * @throws \CRM_Core_Exception
859 */
860 public function testSubmitPaymentProcessorFailure() {
861 $this->setUpContributionPage();
862 $this->setupPaymentProcessor();
863 $this->createLoggedInUser();
864 $priceFieldID = reset($this->_ids['price_field']);
865 $priceFieldValueID = reset($this->_ids['price_field_value']);
866 $submitParams = [
867 'price_' . $priceFieldID => $priceFieldValueID,
868 'id' => (int) $this->_ids['contribution_page'],
869 'amount' => 10,
870 'payment_processor_id' => 1,
871 'credit_card_number' => '4111111111111111',
872 'credit_card_type' => 'Visa',
873 'credit_card_exp_date' => ['M' => 9, 'Y' => 2008],
874 'cvv2' => 123,
875 ];
876
877 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
878 $contribution = $this->callAPISuccessGetSingle('contribution', [
879 'contribution_page_id' => $this->_ids['contribution_page'],
880 'contribution_status_id' => 'Failed',
881 ]);
882
883 $this->callAPISuccessGetSingle('activity', [
884 'source_record_id' => $contribution['id'],
885 'activity_type_id' => 'Failed Payment',
886 ]);
887
888 }
889
890 /**
891 * Test submit recurring (yearly) membership with immediate confirmation (IATS style).
892 *
893 * - we process 2 membership transactions against with a recurring contribution against a contribution page with an immediate
894 * processor (IATS style - denoted by returning trxn_id)
895 * - the first creates a new membership, completed contribution, in progress recurring. Check these
896 * - create another - end date should be extended
897 *
898 * @throws \CRM_Core_Exception
899 */
900 public function testSubmitMembershipPriceSetPaymentPaymentProcessorRecurInstantPaymentYear() {
901 // Need to work on valid financials on this test.
902 $this->isValidateFinancialsOnPostAssert = FALSE;
903 $this->doSubmitMembershipPriceSetPaymentPaymentProcessorRecurInstantPayment(['duration_unit' => 'year', 'recur_frequency_unit' => 'year']);
904 }
905
906 /**
907 * Test submit recurring (monthly) membership with immediate confirmation (IATS style).
908 *
909 * - we process 2 membership transactions against with a recurring contribution against a contribution page with an immediate
910 * processor (IATS style - denoted by returning trxn_id)
911 * - the first creates a new membership, completed contribution, in progress recurring. Check these
912 * - create another - end date should be extended
913 *
914 * @throws \CRM_Core_Exception
915 */
916 public function testSubmitMembershipPriceSetPaymentPaymentProcessorRecurInstantPaymentMonth() {
917 // Need to work on valid financials on this test.
918 $this->isValidateFinancialsOnPostAssert = FALSE;
919 $this->doSubmitMembershipPriceSetPaymentPaymentProcessorRecurInstantPayment(['duration_unit' => 'month', 'recur_frequency_unit' => 'month']);
920 }
921
922 /**
923 * Test submit recurring (mismatched frequency unit) membership with immediate confirmation (IATS style).
924 *
925 * - we process 2 membership transactions against with a recurring contribution against a contribution page with an immediate
926 * processor (IATS style - denoted by returning trxn_id)
927 * - the first creates a new membership, completed contribution, in progress recurring. Check these
928 * - create another - end date should be extended
929 */
930 //public function testSubmitMembershipPriceSetPaymentPaymentProcessorRecurInstantPaymentDifferentFrequency() {
931 // $this->doSubmitMembershipPriceSetPaymentPaymentProcessorRecurInstantPayment(array('duration_unit' => 'year', 'recur_frequency_unit' => 'month'));
932 //}
933
934 /**
935 * Helper function for testSubmitMembershipPriceSetPaymentProcessorRecurInstantPayment*
936 * @param array $params
937 *
938 * @throws \CRM_Core_Exception
939 */
940 public function doSubmitMembershipPriceSetPaymentPaymentProcessorRecurInstantPayment($params = []) {
941 // Need to work on valid financials on this test.
942 $this->isValidateFinancialsOnPostAssert = FALSE;
943 $this->params['is_recur'] = 1;
944 $this->params['recur_frequency_unit'] = $params['recur_frequency_unit'];
945 $membershipTypeParams['duration_unit'] = $params['duration_unit'];
946 if ($params['recur_frequency_unit'] === $params['duration_unit']) {
947 $durationUnit = $params['duration_unit'];
948 }
949 else {
950 $durationUnit = NULL;
951 }
952 $this->setUpMembershipContributionPage(FALSE, FALSE, $membershipTypeParams);
953 $dummyPP = Civi\Payment\System::singleton()->getByProcessor($this->_paymentProcessor);
954 $dummyPP->setDoDirectPaymentResult(['payment_status_id' => 1, 'trxn_id' => 'create_first_success']);
955 $processor = $dummyPP->getPaymentProcessor();
956
957 if ($params['recur_frequency_unit'] === $params['duration_unit']) {
958 // Membership will be in "New" state because it will get confirmed as payment matches
959 $expectedMembershipStatus = 1;
960 }
961 else {
962 // Membership will still be in "Pending" state as it won't get confirmed as payment doesn't match
963 $expectedMembershipStatus = 5;
964 }
965
966 $submitParams = [
967 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
968 'id' => (int) $this->_ids['contribution_page'],
969 'amount' => 10,
970 'billing_first_name' => 'Billy',
971 'billing_middle_name' => 'Goat',
972 'billing_last_name' => 'Gruff',
973 'email' => 'billy@goat.gruff',
974 'selectMembership' => $this->_ids['membership_type'],
975 'payment_processor_id' => 1,
976 'credit_card_number' => '4111111111111111',
977 'credit_card_type' => 'Visa',
978 'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
979 'cvv2' => 123,
980 'is_recur' => 1,
981 'frequency_interval' => 1,
982 'frequency_unit' => $this->params['recur_frequency_unit'],
983 ];
984
985 $this->callAPIAndDocument('contribution_page', 'submit', $submitParams, __FUNCTION__, __FILE__, 'submit contribution page', NULL);
986 $contribution = $this->callAPISuccess('contribution', 'getsingle', [
987 'contribution_page_id' => $this->_ids['contribution_page'],
988 'contribution_status_id' => 1,
989 ]);
990 $this->assertEquals($processor['payment_instrument_id'], $contribution['payment_instrument_id']);
991
992 $this->assertEquals('create_first_success', $contribution['trxn_id']);
993 $membershipPayment = $this->callAPISuccess('membership_payment', 'getsingle', []);
994 $this->assertEquals($membershipPayment['contribution_id'], $contribution['id']);
995 $membership = $this->callAPISuccessGetSingle('membership', ['id' => $membershipPayment['membership_id']]);
996 $this->assertEquals($membership['contact_id'], $contribution['contact_id']);
997 $this->assertEquals($expectedMembershipStatus, $membership['status_id']);
998 $this->callAPISuccess('contribution_recur', 'getsingle', ['id' => $contribution['contribution_recur_id']]);
999 $this->assertEquals($contribution['contribution_recur_id'], $membership['contribution_recur_id']);
1000
1001 $this->callAPISuccess('line_item', 'getsingle', ['contribution_id' => $contribution['id'], 'entity_id' => $membership['id']]);
1002 //renew it with processor setting completed - should extend membership
1003 $submitParams['contact_id'] = $contribution['contact_id'];
1004 $dummyPP->setDoDirectPaymentResult(['payment_status_id' => 1, 'trxn_id' => 'create_second_success']);
1005 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
1006 $this->callAPISuccess('contribution', 'getsingle', [
1007 'id' => ['NOT IN' => [$contribution['id']]],
1008 'contribution_page_id' => $this->_ids['contribution_page'],
1009 'contribution_status_id' => 1,
1010 ]);
1011 $renewedMembership = $this->callAPISuccessGetSingle('membership', ['id' => $membershipPayment['membership_id']]);
1012 if ($durationUnit) {
1013 // We only have an end_date if frequency units match, otherwise membership won't be autorenewed and dates won't be calculated.
1014 $renewedMembershipEndDate = $this->membershipRenewalDate($durationUnit, $membership['end_date']);
1015 $this->assertEquals($renewedMembershipEndDate, $renewedMembership['end_date']);
1016 }
1017 $recurringContribution = $this->callAPISuccess('contribution_recur', 'getsingle', ['id' => $contribution['contribution_recur_id']]);
1018 $this->assertEquals($processor['payment_instrument_id'], $recurringContribution['payment_instrument_id']);
1019 $this->assertEquals(5, $recurringContribution['contribution_status_id']);
1020 }
1021
1022 /**
1023 * Test submit recurring membership with immediate confirmation (IATS style).
1024 *
1025 * - we process 2 membership transactions against with a recurring contribution against a contribution page with an immediate
1026 * processor (IATS style - denoted by returning trxn_id)
1027 * - the first creates a new membership, completed contribution, in progress recurring. Check these
1028 * - create another - end date should be extended
1029 *
1030 * @throws \CRM_Core_Exception
1031 */
1032 public function testSubmitMembershipComplexNonPriceSetPaymentPaymentProcessorRecurInstantPayment() {
1033 // Need to work on valid financials on this test.
1034 $this->isValidateFinancialsOnPostAssert = FALSE;
1035 $this->params['is_recur'] = 1;
1036 $this->params['recur_frequency_unit'] = $membershipTypeParams['duration_unit'] = 'year';
1037 // Add a membership so membership & contribution are not both 1.
1038 $preExistingMembershipID = $this->contactMembershipCreate(['contact_id' => $this->contactIds[0]]);
1039 $this->setUpMembershipContributionPage(FALSE, FALSE, $membershipTypeParams);
1040 $dummyPP = Civi\Payment\System::singleton()->getByProcessor($this->_paymentProcessor);
1041 $dummyPP->setDoDirectPaymentResult(['payment_status_id' => 1, 'trxn_id' => 'create_first_success']);
1042 $processor = $dummyPP->getPaymentProcessor();
1043
1044 $submitParams = [
1045 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
1046 'price_' . $this->_ids['price_field']['cont'] => 88,
1047 'id' => (int) $this->_ids['contribution_page'],
1048 'amount' => 10,
1049 'billing_first_name' => 'Billy',
1050 'billing_middle_name' => 'Goat',
1051 'billing_last_name' => 'Gruff',
1052 'email' => 'billy@goat.gruff',
1053 'selectMembership' => $this->_ids['membership_type'],
1054 'payment_processor_id' => 1,
1055 'credit_card_number' => '4111111111111111',
1056 'credit_card_type' => 'Visa',
1057 'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
1058 'cvv2' => 123,
1059 'is_recur' => 1,
1060 'frequency_interval' => 1,
1061 'frequency_unit' => $this->params['recur_frequency_unit'],
1062 ];
1063
1064 $this->callAPIAndDocument('contribution_page', 'submit', $submitParams, __FUNCTION__, __FILE__, 'submit contribution page', NULL);
1065 $contribution = $this->callAPISuccess('contribution', 'getsingle', [
1066 'contribution_page_id' => $this->_ids['contribution_page'],
1067 'contribution_status_id' => 1,
1068 ]);
1069 $this->assertEquals($processor['payment_instrument_id'], $contribution['payment_instrument_id']);
1070
1071 $this->assertEquals('create_first_success', $contribution['trxn_id']);
1072 $membershipPayment = $this->callAPISuccess('membership_payment', 'getsingle', []);
1073 $this->assertEquals($membershipPayment['contribution_id'], $contribution['id']);
1074 $membership = $this->callAPISuccessGetSingle('membership', ['id' => $membershipPayment['membership_id']]);
1075 $this->assertEquals($membership['contact_id'], $contribution['contact_id']);
1076 $this->assertEquals(1, $membership['status_id']);
1077 $this->callAPISuccess('contribution_recur', 'getsingle', ['id' => $contribution['contribution_recur_id']]);
1078
1079 $lines = $this->callAPISuccess('line_item', 'get', ['sequential' => 1, 'contribution_id' => $contribution['id']]);
1080 $this->assertEquals(2, $lines['count']);
1081 $this->assertEquals('civicrm_membership', $lines['values'][0]['entity_table']);
1082 $this->assertEquals($preExistingMembershipID + 1, $lines['values'][0]['entity_id']);
1083 $this->assertEquals('civicrm_contribution', $lines['values'][1]['entity_table']);
1084 $this->assertEquals($contribution['id'], $lines['values'][1]['entity_id']);
1085 $this->callAPISuccessGetSingle('MembershipPayment', ['contribution_id' => $contribution['id'], 'membership_id' => $preExistingMembershipID + 1]);
1086
1087 //renew it with processor setting completed - should extend membership
1088 $submitParams['contact_id'] = $contribution['contact_id'];
1089 $dummyPP->setDoDirectPaymentResult(['payment_status_id' => 1, 'trxn_id' => 'create_second_success']);
1090 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
1091 $renewContribution = $this->callAPISuccess('contribution', 'getsingle', [
1092 'id' => ['NOT IN' => [$contribution['id']]],
1093 'contribution_page_id' => $this->_ids['contribution_page'],
1094 'contribution_status_id' => 1,
1095 ]);
1096 $lines = $this->callAPISuccess('line_item', 'get', ['sequential' => 1, 'contribution_id' => $renewContribution['id']]);
1097 $this->assertEquals(2, $lines['count']);
1098 $this->assertEquals('civicrm_membership', $lines['values'][0]['entity_table']);
1099 $this->assertEquals($preExistingMembershipID + 1, $lines['values'][0]['entity_id']);
1100 $this->assertEquals('civicrm_contribution', $lines['values'][1]['entity_table']);
1101 $this->assertEquals($renewContribution['id'], $lines['values'][1]['entity_id']);
1102
1103 $renewedMembership = $this->callAPISuccessGetSingle('membership', ['id' => $membershipPayment['membership_id']]);
1104 $this->assertEquals(date('Y-m-d', strtotime('+ 1 ' . $this->params['recur_frequency_unit'], strtotime($membership['end_date']))), $renewedMembership['end_date']);
1105 $recurringContribution = $this->callAPISuccess('contribution_recur', 'getsingle', ['id' => $contribution['contribution_recur_id']]);
1106 $this->assertEquals($processor['payment_instrument_id'], $recurringContribution['payment_instrument_id']);
1107 $this->assertEquals(5, $recurringContribution['contribution_status_id']);
1108 }
1109
1110 /**
1111 * Test submit recurring membership with immediate confirmation (IATS style).
1112 *
1113 * - we process 2 membership transactions against with a recurring contribution against a contribution page with an immediate
1114 * processor (IATS style - denoted by returning trxn_id)
1115 * - the first creates a new membership, completed contribution, in progress recurring. Check these
1116 * - create another - end date should be extended
1117 *
1118 * @throws \CRM_Core_Exception
1119 */
1120 public function testSubmitMembershipComplexPriceSetPaymentPaymentProcessorRecurInstantPayment() {
1121 $this->params['is_recur'] = 1;
1122 $this->params['recur_frequency_unit'] = $membershipTypeParams['duration_unit'] = 'year';
1123 // Add a membership so membership & contribution are not both 1.
1124 $preExistingMembershipID = $this->contactMembershipCreate(['contact_id' => $this->contactIds[0]]);
1125 $this->createPriceSetWithPage();
1126 $this->addSecondOrganizationMembershipToPriceSet();
1127 $this->setupPaymentProcessor();
1128
1129 $dummyPP = Civi\Payment\System::singleton()->getByProcessor($this->_paymentProcessor);
1130 $dummyPP->setDoDirectPaymentResult(['payment_status_id' => 1, 'trxn_id' => 'create_first_success']);
1131 $processor = $dummyPP->getPaymentProcessor();
1132
1133 $submitParams = [
1134 'price_' . $this->_ids['price_field'][0] => $this->_ids['price_field_value']['cont'],
1135 'price_' . $this->_ids['price_field']['org1'] => $this->_ids['price_field_value']['org1'],
1136 'price_' . $this->_ids['price_field']['org2'] => $this->_ids['price_field_value']['org2'],
1137 'id' => (int) $this->_ids['contribution_page'],
1138 'billing_first_name' => 'Billy',
1139 'billing_middle_name' => 'Goat',
1140 'billing_last_name' => 'Gruff',
1141 'email' => 'billy@goat.gruff',
1142 'selectMembership' => NULL,
1143 'payment_processor_id' => 1,
1144 'credit_card_number' => '4111111111111111',
1145 'credit_card_type' => 'Visa',
1146 'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
1147 'cvv2' => 123,
1148 'frequency_interval' => 1,
1149 'frequency_unit' => $this->params['recur_frequency_unit'],
1150 ];
1151
1152 $this->callAPIAndDocument('contribution_page', 'submit', $submitParams, __FUNCTION__, __FILE__, 'submit contribution page', NULL);
1153 $contribution = $this->callAPISuccess('contribution', 'getsingle', [
1154 'contribution_page_id' => $this->_ids['contribution_page'],
1155 'contribution_status_id' => 1,
1156 ]);
1157 $this->assertEquals($processor['payment_instrument_id'], $contribution['payment_instrument_id']);
1158
1159 $this->assertEquals('create_first_success', $contribution['trxn_id']);
1160 $membershipPayments = $this->callAPISuccess('membership_payment', 'get', [
1161 'sequential' => 1,
1162 'contribution_id' => $contribution['id'],
1163 ]);
1164 $this->assertEquals(2, $membershipPayments['count']);
1165 $lines = $this->callAPISuccess('line_item', 'get', ['sequential' => 1, 'contribution_id' => $contribution['id']]);
1166 $this->assertEquals(3, $lines['count']);
1167 $this->assertEquals('civicrm_membership', $lines['values'][0]['entity_table']);
1168 $this->assertEquals($preExistingMembershipID + 1, $lines['values'][0]['entity_id']);
1169 $this->assertEquals('civicrm_contribution', $lines['values'][1]['entity_table']);
1170 $this->assertEquals($contribution['id'], $lines['values'][1]['entity_id']);
1171 $this->assertEquals('civicrm_membership', $lines['values'][2]['entity_table']);
1172 $this->assertEquals($preExistingMembershipID + 2, $lines['values'][2]['entity_id']);
1173
1174 $this->callAPISuccessGetSingle('MembershipPayment', ['contribution_id' => $contribution['id'], 'membership_id' => $preExistingMembershipID + 1]);
1175 $membership = $this->callAPISuccessGetSingle('membership', ['id' => $preExistingMembershipID + 1]);
1176
1177 //renew it with processor setting completed - should extend membership
1178 $submitParams['contact_id'] = $contribution['contact_id'];
1179 $dummyPP->setDoDirectPaymentResult(['payment_status_id' => 1, 'trxn_id' => 'create_second_success']);
1180 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
1181 $renewContribution = $this->callAPISuccess('contribution', 'getsingle', [
1182 'id' => ['NOT IN' => [$contribution['id']]],
1183 'contribution_page_id' => $this->_ids['contribution_page'],
1184 'contribution_status_id' => 1,
1185 ]);
1186 $lines = $this->callAPISuccess('line_item', 'get', ['sequential' => 1, 'contribution_id' => $renewContribution['id']]);
1187 $this->assertEquals(3, $lines['count']);
1188 $this->assertEquals('civicrm_membership', $lines['values'][0]['entity_table']);
1189 $this->assertEquals($preExistingMembershipID + 1, $lines['values'][0]['entity_id']);
1190 $this->assertEquals('civicrm_contribution', $lines['values'][1]['entity_table']);
1191 $this->assertEquals($renewContribution['id'], $lines['values'][1]['entity_id']);
1192
1193 $renewedMembership = $this->callAPISuccessGetSingle('membership', ['id' => $preExistingMembershipID + 1]);
1194 $this->assertEquals(date('Y-m-d', strtotime('+ 1 ' . $this->params['recur_frequency_unit'], strtotime($membership['end_date']))), $renewedMembership['end_date']);
1195 }
1196
1197 /**
1198 * Extend the price set with a second organisation's membership.
1199 *
1200 * @throws \CRM_Core_Exception
1201 */
1202 public function addSecondOrganizationMembershipToPriceSet() {
1203 $organization2ID = $this->organizationCreate();
1204 $membershipTypes = $this->callAPISuccess('MembershipType', 'get', []);
1205 $this->_ids['membership_type'] = array_keys($membershipTypes['values']);
1206 $this->_ids['membership_type']['org2'] = $this->membershipTypeCreate(['contact_id' => $organization2ID, 'name' => 'Org 2']);
1207 $priceField = $this->callAPISuccess('PriceField', 'create', [
1208 'price_set_id' => $this->_ids['price_set'],
1209 'html_type' => 'Radio',
1210 'name' => 'Org1 Price',
1211 'label' => 'Org1Price',
1212 ]);
1213 $this->_ids['price_field']['org1'] = $priceField['id'];
1214
1215 $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', [
1216 'name' => 'org1 amount',
1217 'label' => 'org 1 Amount',
1218 'amount' => 2,
1219 'financial_type_id' => 'Member Dues',
1220 'format.only_id' => TRUE,
1221 'membership_type_id' => reset($this->_ids['membership_type']),
1222 'price_field_id' => $priceField['id'],
1223 ]);
1224 $this->_ids['price_field_value']['org1'] = $priceFieldValue;
1225
1226 $priceField = $this->callAPISuccess('PriceField', 'create', [
1227 'price_set_id' => $this->_ids['price_set'],
1228 'html_type' => 'Radio',
1229 'name' => 'Org2 Price',
1230 'label' => 'Org2Price',
1231 ]);
1232 $this->_ids['price_field']['org2'] = $priceField['id'];
1233
1234 $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', [
1235 'name' => 'org2 amount',
1236 'label' => 'org 2 Amount',
1237 'amount' => 200,
1238 'financial_type_id' => 'Member Dues',
1239 'format.only_id' => TRUE,
1240 'membership_type_id' => $this->_ids['membership_type']['org2'],
1241 'price_field_id' => $priceField['id'],
1242 ]);
1243 $this->_ids['price_field_value']['org2'] = $priceFieldValue;
1244
1245 }
1246
1247 /**
1248 * Test submit recurring membership with immediate confirmation (IATS style).
1249 *
1250 * - we process 2 membership transactions against with a recurring contribution against a contribution page with an immediate
1251 * processor (IATS style - denoted by returning trxn_id)
1252 * - the first creates a new membership, completed contribution, in progress recurring. Check these
1253 * - create another - end date should be extended
1254 *
1255 * @throws \CRM_Core_Exception
1256 */
1257 public function testSubmitMembershipPriceSetPaymentPaymentProcessorSeparatePaymentRecurInstantPayment() {
1258
1259 $this->setUpMembershipContributionPage(TRUE);
1260 $dummyPP = Civi\Payment\System::singleton()->getByProcessor($this->_paymentProcessor);
1261 $dummyPP->setDoDirectPaymentResult(['payment_status_id' => 1, 'trxn_id' => 'create_first_success']);
1262
1263 $submitParams = [
1264 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
1265 'id' => (int) $this->_ids['contribution_page'],
1266 'amount' => 10,
1267 'billing_first_name' => 'Billy',
1268 'billing_middle_name' => 'Goat',
1269 'billing_last_name' => 'Gruff',
1270 'email' => 'billy@goat.gruff',
1271 'selectMembership' => $this->_ids['membership_type'][0],
1272 'payment_processor_id' => 1,
1273 'credit_card_number' => '4111111111111111',
1274 'credit_card_type' => 'Visa',
1275 'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
1276 'cvv2' => 123,
1277 'is_recur' => 1,
1278 'auto_renew' => TRUE,
1279 'frequency_interval' => 1,
1280 'frequency_unit' => 'month',
1281 ];
1282
1283 $this->callAPIAndDocument('ContributionPage', 'submit', $submitParams, __FUNCTION__, __FILE__, 'submit contribution page', NULL);
1284 $contribution = $this->callAPISuccess('contribution', 'get', [
1285 'contribution_page_id' => $this->_ids['contribution_page'],
1286 'contribution_status_id' => 1,
1287 ]);
1288
1289 $this->assertEquals(2, $contribution['count']);
1290 $membershipPayment = $this->callAPISuccess('membership_payment', 'getsingle', []);
1291 $this->callAPISuccessGetSingle('membership', ['id' => $membershipPayment['membership_id']]);
1292 $this->assertNotEmpty($contribution['values'][$membershipPayment['contribution_id']]['contribution_recur_id']);
1293 $this->callAPISuccess('contribution_recur', 'getsingle');
1294 }
1295
1296 /**
1297 * Test submit recurring membership with delayed confirmation (Authorize.net style)
1298 * - we process 2 membership transactions against with a recurring contribution against a contribution page with a delayed
1299 * processor (Authorize.net style - denoted by NOT returning trxn_id)
1300 * - the first creates a pending membership, pending contribution, penging recurring. Check these
1301 * - complete the transaction
1302 * - create another - end date should NOT be extended
1303 *
1304 * @throws \CRM_Core_Exception
1305 */
1306 public function testSubmitMembershipPriceSetPaymentPaymentProcessorRecurDelayed() {
1307 $this->params['is_recur'] = 1;
1308 $this->params['recur_frequency_unit'] = $membershipTypeParams['duration_unit'] = 'year';
1309 $this->setUpMembershipContributionPage();
1310 $dummyPP = Civi\Payment\System::singleton()->getByProcessor($this->_paymentProcessor);
1311 $dummyPP->setDoDirectPaymentResult(['payment_status_id' => 2]);
1312 $this->membershipTypeCreate(['name' => 'Student']);
1313
1314 // Add a contribution & a couple of memberships so the id will not be 1 & will differ from membership id.
1315 // This saves us from 'accidental success'.
1316 $this->contributionCreate(['contact_id' => $this->contactIds[0]]);
1317 $this->contactMembershipCreate(['contact_id' => $this->contactIds[0]]);
1318 $this->contactMembershipCreate(['contact_id' => $this->contactIds[0], 'membership_type_id' => 'Student']);
1319
1320 $submitParams = [
1321 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
1322 'id' => (int) $this->_ids['contribution_page'],
1323 'billing_first_name' => 'Billy',
1324 'billing_middle_name' => 'Goat',
1325 'billing_last_name' => 'Gruff',
1326 'email' => 'billy@goat.gruff',
1327 'selectMembership' => $this->_ids['membership_type'][0],
1328 'payment_processor_id' => 1,
1329 'credit_card_number' => '4111111111111111',
1330 'credit_card_type' => 'Visa',
1331 'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
1332 'cvv2' => 123,
1333 'is_recur' => 1,
1334 'frequency_interval' => 1,
1335 'frequency_unit' => $this->params['recur_frequency_unit'],
1336 ];
1337
1338 $this->callAPIAndDocument('contribution_page', 'submit', $submitParams, __FUNCTION__, __FILE__, 'submit contribution page', NULL);
1339 $contribution = $this->callAPISuccess('contribution', 'getsingle', [
1340 'contribution_page_id' => $this->_ids['contribution_page'],
1341 'contribution_status_id' => 2,
1342 ]);
1343
1344 $membershipPayment = $this->callAPISuccess('membership_payment', 'getsingle', []);
1345 $this->assertEquals($membershipPayment['contribution_id'], $contribution['id']);
1346 $membership = $this->callAPISuccessGetSingle('membership', ['id' => $membershipPayment['membership_id']]);
1347 $this->assertEquals($membership['contact_id'], $contribution['contact_id']);
1348 $this->assertEquals(5, $membership['status_id']);
1349
1350 $line = $this->callAPISuccess('line_item', 'getsingle', ['contribution_id' => $contribution['id']]);
1351 $this->assertEquals('civicrm_membership', $line['entity_table']);
1352 $this->assertEquals($membership['id'], $line['entity_id']);
1353
1354 $this->callAPISuccess('contribution', 'completetransaction', [
1355 'id' => $contribution['id'],
1356 'trxn_id' => 'ipn_called',
1357 'payment_processor_id' => $this->_paymentProcessor['id'],
1358 ]);
1359 $line = $this->callAPISuccess('line_item', 'getsingle', ['contribution_id' => $contribution['id']]);
1360 $this->assertEquals('civicrm_membership', $line['entity_table']);
1361 $this->assertEquals($membership['id'], $line['entity_id']);
1362
1363 $membership = $this->callAPISuccessGetSingle('membership', ['id' => $membershipPayment['membership_id']]);
1364 //renew it with processor setting completed - should extend membership
1365 $submitParams = array_merge($submitParams, [
1366 'contact_id' => $contribution['contact_id'],
1367 'is_recur' => 1,
1368 'frequency_interval' => 1,
1369 'frequency_unit' => $this->params['recur_frequency_unit'],
1370 ]);
1371
1372 $dummyPP->setDoDirectPaymentResult(['payment_status_id' => 2]);
1373 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
1374 $newContribution = $this->callAPISuccess('contribution', 'getsingle', [
1375 'id' => [
1376 'NOT IN' => [$contribution['id']],
1377 ],
1378 'contribution_page_id' => $this->_ids['contribution_page'],
1379 'contribution_status_id' => 2,
1380 ]);
1381 $line = $this->callAPISuccess('line_item', 'getsingle', ['contribution_id' => $newContribution['id']]);
1382 $this->assertEquals('civicrm_membership', $line['entity_table']);
1383 $this->assertEquals($membership['id'], $line['entity_id']);
1384
1385 $renewedMembership = $this->callAPISuccessGetSingle('membership', ['id' => $membershipPayment['membership_id']]);
1386 //no renewal as the date hasn't changed
1387 $this->assertEquals($membership['end_date'], $renewedMembership['end_date']);
1388 $recurringContribution = $this->callAPISuccess('contribution_recur', 'getsingle', ['id' => $newContribution['contribution_recur_id']]);
1389 $this->assertEquals(2, $recurringContribution['contribution_status_id']);
1390 }
1391
1392 /**
1393 * Test non-recur contribution with membership payment
1394 *
1395 * @throws \CRM_Core_Exception
1396 */
1397 public function testSubmitMembershipIsSeparatePaymentNotRecur() {
1398 //Create recur contribution page.
1399 $this->setUpMembershipContributionPage(TRUE, TRUE);
1400 $dummyPP = Civi\Payment\System::singleton()->getByProcessor($this->_paymentProcessor);
1401 $dummyPP->setDoDirectPaymentResult(['payment_status_id' => 1, 'trxn_id' => 'create_first_success']);
1402
1403 //Submit payment with recur disabled.
1404 $submitParams = [
1405 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
1406 'id' => (int) $this->_ids['contribution_page'],
1407 'amount' => 10,
1408 'frequency_interval' => 1,
1409 'frequency_unit' => 'month',
1410 'billing_first_name' => 'Billy',
1411 'billing_middle_name' => 'Goat',
1412 'billing_last_name' => 'Gruff',
1413 'email' => 'billy@goat.gruff',
1414 'selectMembership' => $this->_ids['membership_type'][0],
1415 'payment_processor_id' => 1,
1416 'credit_card_number' => '4111111111111111',
1417 'credit_card_type' => 'Visa',
1418 'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
1419 'cvv2' => 123,
1420 ];
1421
1422 //Assert if recur contribution is created.
1423 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
1424 $recur = $this->callAPISuccess('contribution_recur', 'get', []);
1425 $this->assertEmpty($recur['count']);
1426 }
1427
1428 /**
1429 * Set up membership contribution page.
1430 *
1431 * @param bool $isSeparatePayment
1432 * @param bool $isRecur
1433 * @param array $membershipTypeParams Parameters to pass to membershiptype.create API
1434 *
1435 * @throws \CRM_Core_Exception
1436 */
1437 public function setUpMembershipContributionPage($isSeparatePayment = FALSE, $isRecur = FALSE, $membershipTypeParams = []): void {
1438 $this->setUpMembershipBlockPriceSet($membershipTypeParams);
1439 $this->setupPaymentProcessor();
1440 $this->setUpContributionPage($isRecur);
1441
1442 $this->callAPISuccess('membership_block', 'create', [
1443 'entity_id' => $this->_ids['contribution_page'],
1444 'entity_table' => 'civicrm_contribution_page',
1445 'is_required' => TRUE,
1446 'is_active' => TRUE,
1447 'is_separate_payment' => $isSeparatePayment,
1448 'membership_type_default' => $this->_ids['membership_type'],
1449 ]);
1450 }
1451
1452 /**
1453 * Set up pledge block.
1454 */
1455 public function setUpPledgeBlock() {
1456 $params = [
1457 'entity_table' => 'civicrm_contribution_page',
1458 'entity_id' => $this->_ids['contribution_page'],
1459 'pledge_frequency_unit' => 'week',
1460 'is_pledge_interval' => 0,
1461 'pledge_start_date' => json_encode(['calendar_date' => date('Ymd', strtotime("+1 month"))]),
1462 ];
1463 $pledgeBlock = CRM_Pledge_BAO_PledgeBlock::create($params);
1464 $this->_ids['pledge_block_id'] = $pledgeBlock->id;
1465 }
1466
1467 /**
1468 * The default data set does not include a complete default membership price set - not quite sure why.
1469 *
1470 * This function ensures it exists & populates $this->_ids with it's data
1471 *
1472 * @throws \CRM_Core_Exception
1473 */
1474 public function setUpMembershipBlockPriceSet($membershipTypeParams = []) {
1475 $this->_ids['price_set'][] = $this->callAPISuccess('price_set', 'getvalue', [
1476 'name' => 'default_membership_type_amount',
1477 'return' => 'id',
1478 ]);
1479 if (empty($this->_ids['membership_type'])) {
1480 $membershipTypeParams = array_merge([
1481 'minimum_fee' => 2,
1482 ], $membershipTypeParams);
1483 $this->_ids['membership_type'] = [$this->membershipTypeCreate($membershipTypeParams)];
1484 }
1485 $priceField = $this->callAPISuccess('price_field', 'create', [
1486 'price_set_id' => reset($this->_ids['price_set']),
1487 'name' => 'membership_amount',
1488 'label' => 'Membership Amount',
1489 'html_type' => 'Radio',
1490 'sequential' => 1,
1491 ]);
1492 $this->_ids['price_field'][] = $priceField['id'];
1493
1494 foreach ($this->_ids['membership_type'] as $membershipTypeID) {
1495 $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', [
1496 'name' => 'membership_amount',
1497 'label' => 'Membership Amount',
1498 'amount' => $this->_membershipBlockAmount,
1499 'financial_type_id' => 'Donation',
1500 'format.only_id' => TRUE,
1501 'membership_type_id' => $membershipTypeID,
1502 'price_field_id' => $priceField['id'],
1503 ]);
1504 $this->_ids['price_field_value'][] = $priceFieldValue;
1505 }
1506 if (!empty($this->_ids['membership_type']['org2'])) {
1507 $priceField = $this->callAPISuccess('price_field', 'create', [
1508 'price_set_id' => reset($this->_ids['price_set']),
1509 'name' => 'membership_org2',
1510 'label' => 'Membership Org2',
1511 'html_type' => 'Checkbox',
1512 'sequential' => 1,
1513 ]);
1514 $this->_ids['price_field']['org2'] = $priceField['id'];
1515
1516 $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', [
1517 'name' => 'membership_org2',
1518 'label' => 'Membership org 2',
1519 'amount' => 55,
1520 'financial_type_id' => 'Member Dues',
1521 'format.only_id' => TRUE,
1522 'membership_type_id' => $this->_ids['membership_type']['org2'],
1523 'price_field_id' => $priceField['id'],
1524 ]);
1525 $this->_ids['price_field_value']['org2'] = $priceFieldValue;
1526 }
1527 $priceField = $this->callAPISuccess('price_field', 'create', [
1528 'price_set_id' => reset($this->_ids['price_set']),
1529 'name' => 'Contribution',
1530 'label' => 'Contribution',
1531 'html_type' => 'Text',
1532 'sequential' => 1,
1533 'is_enter_qty' => 1,
1534 ]);
1535 $this->_ids['price_field']['cont'] = $priceField['id'];
1536 $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', [
1537 'name' => 'contribution',
1538 'label' => 'Give me money',
1539 'amount' => 88,
1540 'financial_type_id' => 'Donation',
1541 'format.only_id' => TRUE,
1542 'price_field_id' => $priceField['id'],
1543 ]);
1544 $this->_ids['price_field_value'][] = $priceFieldValue;
1545 }
1546
1547 /**
1548 * Add text field other amount to the price set.
1549 *
1550 * @throws \CRM_Core_Exception
1551 */
1552 public function addOtherAmountFieldToMembershipPriceSet() {
1553 $this->_ids['price_field']['other_amount'] = $this->callAPISuccess('price_field', 'create', [
1554 'price_set_id' => reset($this->_ids['price_set']),
1555 'name' => 'other_amount',
1556 'label' => 'Other Amount',
1557 'html_type' => 'Text',
1558 'format.only_id' => TRUE,
1559 'sequential' => 1,
1560 ]);
1561 $this->_ids['price_field_value']['other_amount'] = $this->callAPISuccess('price_field_value', 'create', [
1562 'financial_type_id' => 'Donation',
1563 'format.only_id' => TRUE,
1564 'label' => 'Other Amount',
1565 'amount' => 1,
1566 'price_field_id' => $this->_ids['price_field']['other_amount'],
1567 ]);
1568 }
1569
1570 /**
1571 * Help function to set up contribution page with some defaults.
1572 *
1573 * @param bool $isRecur
1574 *
1575 * @throws \CRM_Core_Exception
1576 */
1577 public function setUpContributionPage($isRecur = FALSE) {
1578 if ($isRecur) {
1579 $this->params['is_recur'] = 1;
1580 $this->params['recur_frequency_unit'] = 'month';
1581 }
1582 $this->params['frontend_title'] = 'Test Frontend title';
1583 $contributionPageResult = $this->callAPISuccess($this->_entity, 'create', $this->params);
1584 if (empty($this->_ids['price_set'])) {
1585 $priceSet = $this->callAPISuccess('price_set', 'create', $this->_priceSetParams);
1586 $this->_ids['price_set'][] = $priceSet['id'];
1587 }
1588 $priceSetID = reset($this->_ids['price_set']);
1589 CRM_Price_BAO_PriceSet::addTo('civicrm_contribution_page', $contributionPageResult['id'], $priceSetID);
1590
1591 if (empty($this->_ids['price_field'])) {
1592 $priceField = $this->callAPISuccess('price_field', 'create', [
1593 'price_set_id' => $priceSetID,
1594 'label' => 'Goat Breed',
1595 'html_type' => 'Radio',
1596 ]);
1597 $this->_ids['price_field'] = [$priceField['id']];
1598 }
1599 if (empty($this->_ids['price_field_value'])) {
1600 $this->callAPISuccess('price_field_value', 'create', [
1601 'price_set_id' => $priceSetID,
1602 'price_field_id' => $priceField['id'],
1603 'label' => 'Long Haired Goat',
1604 'financial_type_id' => 'Donation',
1605 'amount' => 20,
1606 'non_deductible_amount' => 15,
1607 ]);
1608 $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', [
1609 'price_set_id' => $priceSetID,
1610 'price_field_id' => $priceField['id'],
1611 'label' => 'Shoe-eating Goat',
1612 'financial_type_id' => 'Donation',
1613 'amount' => 10,
1614 'non_deductible_amount' => 5,
1615 ]);
1616 $this->_ids['price_field_value'] = [$priceFieldValue['id']];
1617
1618 $this->_ids['price_field_value']['cheapskate'] = $this->callAPISuccess('price_field_value', 'create', [
1619 'price_set_id' => $priceSetID,
1620 'price_field_id' => $priceField['id'],
1621 'label' => 'Stingy Goat',
1622 'financial_type_id' => 'Donation',
1623 'amount' => 0,
1624 'non_deductible_amount' => 0,
1625 ])['id'];
1626 }
1627 $this->_ids['contribution_page'] = $contributionPageResult['id'];
1628 }
1629
1630 /**
1631 * Helper function to set up contribution page which can be used to purchase a
1632 * membership type for different intervals.
1633 *
1634 * @throws \CRM_Core_Exception
1635 */
1636 public function setUpMultiIntervalMembershipContributionPage() {
1637 // Need to work on valid financials on this test.
1638 $this->isValidateFinancialsOnPostAssert = FALSE;
1639 $this->setupPaymentProcessor();
1640 $contributionPage = $this->callAPISuccess($this->_entity, 'create', $this->params);
1641 $this->_ids['contribution_page'] = $contributionPage['id'];
1642
1643 $this->_ids['membership_type'] = $this->membershipTypeCreate([
1644 // force auto-renew
1645 'auto_renew' => 2,
1646 'duration_unit' => 'month',
1647 ]);
1648
1649 $priceSet = $this->callAPISuccess('PriceSet', 'create', [
1650 'is_quick_config' => 0,
1651 'extends' => 'CiviMember',
1652 'financial_type_id' => 'Member Dues',
1653 'title' => 'CRM-21177',
1654 ]);
1655 $this->_ids['price_set'] = $priceSet['id'];
1656
1657 $priceField = $this->callAPISuccess('price_field', 'create', [
1658 'price_set_id' => $this->_ids['price_set'],
1659 'name' => 'membership_type',
1660 'label' => 'Membership Type',
1661 'html_type' => 'Radio',
1662 ]);
1663 $this->_ids['price_field'] = $priceField['id'];
1664
1665 $priceFieldValueMonthly = $this->callAPISuccess('price_field_value', 'create', [
1666 'name' => 'CRM-21177_Monthly',
1667 'label' => 'CRM-21177 - Monthly',
1668 'amount' => 20,
1669 'membership_num_terms' => 1,
1670 'membership_type_id' => $this->_ids['membership_type'],
1671 'price_field_id' => $this->_ids['price_field'],
1672 'financial_type_id' => 'Member Dues',
1673 ]);
1674 $this->_ids['price_field_value_monthly'] = $priceFieldValueMonthly['id'];
1675
1676 $priceFieldValueYearly = $this->callAPISuccess('price_field_value', 'create', [
1677 'name' => 'CRM-21177_Yearly',
1678 'label' => 'CRM-21177 - Yearly',
1679 'amount' => 200,
1680 'membership_num_terms' => 12,
1681 'membership_type_id' => $this->_ids['membership_type'],
1682 'price_field_id' => $this->_ids['price_field'],
1683 'financial_type_id' => 'Member Dues',
1684 ]);
1685 $this->_ids['price_field_value_yearly'] = $priceFieldValueYearly['id'];
1686
1687 CRM_Price_BAO_PriceSet::addTo('civicrm_contribution_page', $this->_ids['contribution_page'], $this->_ids['price_set']);
1688
1689 $this->callAPISuccess('membership_block', 'create', [
1690 'entity_id' => $this->_ids['contribution_page'],
1691 'entity_table' => 'civicrm_contribution_page',
1692 'is_required' => TRUE,
1693 'is_separate_payment' => FALSE,
1694 'is_active' => TRUE,
1695 'membership_type_default' => $this->_ids['membership_type'],
1696 ]);
1697 }
1698
1699 /**
1700 * Test submit with a membership block in place.
1701 *
1702 * @throws \CRM_Core_Exception
1703 */
1704 public function testSubmitMultiIntervalMembershipContributionPage() {
1705 $this->setUpMultiIntervalMembershipContributionPage();
1706 $submitParams = [
1707 'price_' . $this->_ids['price_field'] => $this->_ids['price_field_value_monthly'],
1708 'id' => (int) $this->_ids['contribution_page'],
1709 'amount' => 20,
1710 'first_name' => 'Billy',
1711 'last_name' => 'Gruff',
1712 'email' => 'billy@goat.gruff',
1713 'payment_processor_id' => $this->_ids['payment_processor'],
1714 'credit_card_number' => '4111111111111111',
1715 'credit_card_type' => 'Visa',
1716 'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
1717 'cvv2' => 123,
1718 'auto_renew' => 1,
1719 ];
1720 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
1721
1722 $submitParams['price_' . $this->_ids['price_field']] = $this->_ids['price_field_value_yearly'];
1723 $this->callAPISuccess('contribution_page', 'submit', $submitParams);
1724
1725 $contribution = $this->callAPISuccess('Contribution', 'get', [
1726 'contribution_page_id' => $this->_ids['contribution_page'],
1727 'sequential' => 1,
1728 'api.ContributionRecur.getsingle' => [],
1729 ]);
1730 $this->assertEquals(1, $contribution['values'][0]['api.ContributionRecur.getsingle']['frequency_interval']);
1731 //$this->assertEquals(12, $contribution['values'][1]['api.ContributionRecur.getsingle']['frequency_interval']);
1732 }
1733
1734 /**
1735 * Create a payment processor instance.
1736 *
1737 * @throws \CRM_Core_Exception
1738 */
1739 protected function setupPaymentProcessor() {
1740 $this->params['payment_processor_id'] = $this->_ids['payment_processor'] = $this->paymentProcessorCreate([
1741 'payment_processor_type_id' => 'Dummy',
1742 'class_name' => 'Payment_Dummy',
1743 'billing_mode' => 1,
1744 ]);
1745 $this->_paymentProcessor = $this->callAPISuccess('payment_processor', 'getsingle', ['id' => $this->params['payment_processor_id']]);
1746 }
1747
1748 /**
1749 * Test submit recurring pledge.
1750 *
1751 * - we process 1 pledge with a future start date. A recur contribution and the pledge should be created with first payment date in the future.
1752 *
1753 * @throws \CRM_Core_Exception
1754 */
1755 public function testSubmitPledgePaymentPaymentProcessorRecurFuturePayment() {
1756 // Need to work on valid financials on this test.
1757 $this->isValidateFinancialsOnPostAssert = FALSE;
1758 $this->params['adjust_recur_start_date'] = TRUE;
1759 $this->params['is_pay_later'] = FALSE;
1760 $this->setUpContributionPage();
1761 $this->setUpPledgeBlock();
1762 $this->setupPaymentProcessor();
1763 $dummyPP = Civi\Payment\System::singleton()->getByProcessor($this->_paymentProcessor);
1764 $dummyPP->setDoDirectPaymentResult(['payment_status_id' => 1, 'trxn_id' => 'create_first_success']);
1765
1766 $submitParams = [
1767 'id' => (int) $this->_ids['contribution_page'],
1768 'amount' => 100,
1769 'billing_first_name' => 'Billy',
1770 'billing_middle_name' => 'Goat',
1771 'billing_last_name' => 'Gruff',
1772 'email' => 'billy@goat.gruff',
1773 'payment_processor_id' => 1,
1774 'credit_card_number' => '4111111111111111',
1775 'credit_card_type' => 'Visa',
1776 'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
1777 'cvv2' => 123,
1778 'pledge_frequency_interval' => 1,
1779 'pledge_frequency_unit' => 'week',
1780 'pledge_installments' => 3,
1781 'is_pledge' => TRUE,
1782 'pledge_block_id' => (int) $this->_ids['pledge_block_id'],
1783 ];
1784
1785 $this->callAPIAndDocument('contribution_page', 'submit', $submitParams, __FUNCTION__, __FILE__, 'submit contribution page', NULL);
1786
1787 // Check if contribution created.
1788 $contribution = $this->callAPISuccess('contribution', 'getsingle', [
1789 'contribution_page_id' => $this->_ids['contribution_page'],
1790 // Will be pending when actual payment processor is used (dummy processor does not support future payments).
1791 'contribution_status_id' => 'Completed',
1792 ]);
1793
1794 $this->assertEquals('create_first_success', $contribution['trxn_id']);
1795
1796 // Check if pledge created.
1797 $pledge = $this->callAPISuccess('pledge', 'getsingle', []);
1798 $this->assertEquals(date('Ymd', strtotime($pledge['pledge_start_date'])), date('Ymd', strtotime("+1 month")));
1799 $this->assertEquals($pledge['pledge_amount'], 300.00);
1800
1801 // Check if pledge payments created.
1802 $params = [
1803 'pledge_id' => $pledge['id'],
1804 ];
1805 $pledgePayment = $this->callAPISuccess('pledge_payment', 'get', $params);
1806 $this->assertEquals($pledgePayment['count'], 3);
1807 $this->assertEquals(date('Ymd', strtotime($pledgePayment['values'][1]['scheduled_date'])), date('Ymd', strtotime("+1 month")));
1808 $this->assertEquals($pledgePayment['values'][1]['scheduled_amount'], 100.00);
1809 // Will be pending when actual payment processor is used (dummy processor does not support future payments).
1810 $this->assertEquals($pledgePayment['values'][1]['status_id'], 1);
1811
1812 // Check contribution recur record.
1813 $recur = $this->callAPISuccess('contribution_recur', 'getsingle', ['id' => $contribution['contribution_recur_id']]);
1814 $this->assertEquals(date('Ymd', strtotime($recur['start_date'])), date('Ymd', strtotime("+1 month")));
1815 $this->assertEquals($recur['amount'], 100.00);
1816 // In progress status.
1817 $this->assertEquals($recur['contribution_status_id'], 5);
1818 }
1819
1820 /**
1821 * Test submit pledge payment.
1822 *
1823 * - test submitting a pledge payment using contribution form.
1824 *
1825 * @throws \CRM_Core_Exception
1826 */
1827 public function testSubmitPledgePayment() {
1828 // Need to work on valid financials on this test.
1829 $this->isValidateFinancialsOnPostAssert = FALSE;
1830 $this->testSubmitPledgePaymentPaymentProcessorRecurFuturePayment();
1831 $pledge = $this->callAPISuccess('pledge', 'getsingle', []);
1832 $params = [
1833 'pledge_id' => $pledge['id'],
1834 ];
1835 $submitParams = [
1836 'id' => (int) $pledge['pledge_contribution_page_id'],
1837 'pledge_amount' => [2 => 1],
1838 'billing_first_name' => 'Billy',
1839 'billing_middle_name' => 'Goat',
1840 'billing_last_name' => 'Gruff',
1841 'email' => 'billy@goat.gruff',
1842 'payment_processor_id' => 1,
1843 'credit_card_number' => '4111111111111111',
1844 'credit_card_type' => 'Visa',
1845 'credit_card_exp_date' => ['M' => 9, 'Y' => 2040],
1846 'cvv2' => 123,
1847 'pledge_id' => $pledge['id'],
1848 'cid' => $pledge['contact_id'],
1849 'contact_id' => $pledge['contact_id'],
1850 'amount' => 100.00,
1851 'is_pledge' => TRUE,
1852 'pledge_block_id' => $this->_ids['pledge_block_id'],
1853 ];
1854 $pledgePayment = $this->callAPISuccess('pledge_payment', 'get', $params);
1855 $this->assertEquals($pledgePayment['values'][2]['status_id'], 2);
1856
1857 $this->callAPIAndDocument('contribution_page', 'submit', $submitParams, __FUNCTION__, __FILE__, 'submit contribution page', NULL);
1858
1859 // Check if contribution created.
1860 $contribution = $this->callAPISuccess('contribution', 'getsingle', [
1861 'contribution_page_id' => $pledge['pledge_contribution_page_id'],
1862 'contribution_status_id' => 'Completed',
1863 'contact_id' => $pledge['contact_id'],
1864 'contribution_recur_id' => ['IS NULL' => 1],
1865 ]);
1866
1867 $this->assertEquals(100.00, $contribution['total_amount']);
1868 $pledgePayment = $this->callAPISuccess('pledge_payment', 'get', $params);
1869 $this->assertEquals($pledgePayment['values'][2]['status_id'], 1, 'This pledge payment should have been completed');
1870 $this->assertEquals($pledgePayment['values'][2]['contribution_id'], $contribution['id']);
1871 }
1872
1873 /**
1874 * Test form submission with multiple option price set.
1875 *
1876 * @param string $thousandSeparator
1877 * punctuation used to refer to thousands.
1878 *
1879 * @dataProvider getThousandSeparators
1880 * @throws \CRM_Core_Exception
1881 */
1882 public function testSubmitContributionPageWithPriceSet($thousandSeparator) {
1883 $this->setCurrencySeparators($thousandSeparator);
1884 $this->_priceSetParams['is_quick_config'] = 0;
1885 $this->setUpContributionPage();
1886 $submitParams = [
1887 'price_' . $this->_ids['price_field'][0] => reset($this->_ids['price_field_value']),
1888 'id' => (int) $this->_ids['contribution_page'],
1889 'first_name' => 'Billy',
1890 'last_name' => 'Gruff',
1891 'email' => 'billy@goat.gruff',
1892 'is_pay_later' => TRUE,
1893 ];
1894 $this->addPriceFields($submitParams);
1895
1896 $this->callAPISuccess('ContributionPage', 'submit', $submitParams);
1897 $contribution = $this->callAPISuccessGetSingle('contribution', [
1898 'contribution_page_id' => $this->_ids['contribution_page'],
1899 'contribution_status_id' => 'Pending',
1900 ]);
1901 $this->assertEquals(80, $contribution['total_amount']);
1902 $lineItems = $this->callAPISuccess('LineItem', 'get', [
1903 'contribution_id' => $contribution['id'],
1904 ]);
1905 $this->assertEquals(3, $lineItems['count']);
1906 $totalLineAmount = 0;
1907 foreach ($lineItems['values'] as $lineItem) {
1908 $totalLineAmount += $lineItem['line_total'];
1909 }
1910 $this->assertEquals(80, $totalLineAmount);
1911 }
1912
1913 /**
1914 * Function to add additional price fields to priceset.
1915 *
1916 * @param array $params
1917 *
1918 * @throws \CRM_Core_Exception
1919 */
1920 public function addPriceFields(&$params) {
1921 $priceSetID = reset($this->_ids['price_set']);
1922 $priceField = $this->callAPISuccess('price_field', 'create', [
1923 'price_set_id' => $priceSetID,
1924 'label' => 'Chicken Breed',
1925 'html_type' => 'CheckBox',
1926 ]);
1927 $priceFieldValue1 = $this->callAPISuccess('price_field_value', 'create', [
1928 'price_set_id' => $priceSetID,
1929 'price_field_id' => $priceField['id'],
1930 'label' => 'Shoe-eating chicken -1',
1931 'financial_type_id' => 'Donation',
1932 'amount' => 30,
1933 ]);
1934 $priceFieldValue2 = $this->callAPISuccess('price_field_value', 'create', [
1935 'price_set_id' => $priceSetID,
1936 'price_field_id' => $priceField['id'],
1937 'label' => 'Shoe-eating chicken -2',
1938 'financial_type_id' => 'Donation',
1939 'amount' => 40,
1940 ]);
1941 $params['price_' . $priceField['id']] = [
1942 $priceFieldValue1['id'] => 1,
1943 $priceFieldValue2['id'] => 1,
1944 ];
1945 }
1946
1947 /**
1948 * Test Tax Amount is calculated properly when using PriceSet with Field Type = Text/Numeric Quantity
1949 *
1950 * The created contribution has 3 line items
1951 *
1952 * |qty | unit_price| line_total| tax |total including tax|
1953 * | 1 | 10 | 10 | 0 | 10 |
1954 * | 180 | 16.95 | 3051 |305.1 | 3356.1|
1955 * | 110 | 2.95 | 324.5 | 32.45 | 356.95|
1956 *
1957 * Contribution total = 3723.05
1958 * made up of tax 337.55
1959 * non tax 3385.5
1960 * @param string $thousandSeparator
1961 * punctuation used to refer to thousands.
1962 *
1963 * @dataProvider getThousandSeparators
1964 * @throws \CRM_Core_Exception
1965 */
1966 public function testSubmitContributionPageWithPriceSetQuantity(string $thousandSeparator): void {
1967 $this->setCurrencySeparators($thousandSeparator);
1968 $this->_priceSetParams['is_quick_config'] = 0;
1969 $this->enableTaxAndInvoicing();
1970 $financialType = $this->createFinancialType();
1971 $financialTypeId = $financialType['id'];
1972 // This function sets the Tax Rate at 10% - it currently has no way to pass Tax Rate into it - so let's work with 10%
1973 $this->addTaxAccountToFinancialType($financialType['id']);
1974
1975 $this->setUpContributionPage();
1976 $submitParams = [
1977 'id' => (int) $this->_ids['contribution_page'],
1978 'first_name' => 'J',
1979 'last_name' => 'T',
1980 'email' => 'JT@ohcanada.ca',
1981 'is_pay_later' => TRUE,
1982 'receive_date' => date('Y-m-d H:i:s'),
1983 ];
1984
1985 // Add Existing PriceField
1986 // This is a Shoe-eating Goat; qty = 1; unit_price = $10.00; There is no sales tax on Goats
1987 $submitParams['price_' . $this->_ids['price_field'][0]] = reset($this->_ids['price_field_value']);
1988
1989 // Create additional PriceSet/PriceField
1990 $priceSetID = reset($this->_ids['price_set']);
1991 $priceField = $this->callAPISuccess('price_field', 'create', [
1992 'price_set_id' => $priceSetID,
1993 'label' => 'Printing Rights',
1994 'html_type' => 'Text',
1995 ]);
1996
1997 $this->callAPISuccess('PriceFieldValue', 'create', [
1998 'price_set_id' => $priceSetID,
1999 'price_field_id' => $priceField['id'],
2000 'label' => 'Printing Rights',
2001 'financial_type_id' => $financialTypeId,
2002 'amount' => '16.95',
2003 ]);
2004 $priceFieldId = $priceField['id'];
2005
2006 // Set quantity for our test
2007 $submitParams['price_' . $priceFieldId] = 180;
2008
2009 $priceField = $this->callAPISuccess('PriceField', 'create', [
2010 'price_set_id' => $priceSetID,
2011 'label' => 'Another Line Item',
2012 'html_type' => 'Text',
2013 ]);
2014
2015 $this->callAPISuccess('price_field_value', 'create', [
2016 'price_set_id' => $priceSetID,
2017 'price_field_id' => $priceField['id'],
2018 'label' => 'Another Line Item',
2019 'financial_type_id' => $financialTypeId,
2020 'amount' => '2.95',
2021 ]);
2022 $priceFieldId = $priceField['id'];
2023
2024 // Set quantity for our test
2025 $submitParams['price_' . $priceFieldId] = 110;
2026
2027 // This is the correct Tax Amount - use it later to compare to what the CiviCRM Core came up with at the LineItem level
2028 $submitParams['tax_amount'] = (180 * 16.95 * 0.10 + 110 * 2.95 * 0.10);
2029
2030 $this->callAPISuccess('ContributionPage', 'submit', $submitParams);
2031 $this->validateAllContributions();
2032
2033 $contribution = $this->callAPISuccessGetSingle('Contribution', [
2034 'contribution_page_id' => $this->_ids['contribution_page'],
2035 ]);
2036
2037 // Retrieve the lineItem that belongs to the Goat
2038 $lineItem1 = $this->callAPISuccessGetSingle('LineItem', [
2039 'contribution_id' => $contribution['id'],
2040 'label' => 'Shoe-eating Goat',
2041 ]);
2042
2043 // Retrieve the lineItem that belongs to the Printing Rights and check the tax_amount CiviCRM Core calculated for it
2044 $lineItem2 = $this->callAPISuccessGetSingle('LineItem', [
2045 'contribution_id' => $contribution['id'],
2046 'label' => 'Printing Rights',
2047 ]);
2048
2049 // Retrieve the lineItem that belongs to the Another Line Item and check the tax_amount CiviCRM Core calculated for it
2050 $lineItem3 = $this->callAPISuccessGetSingle('LineItem', [
2051 'contribution_id' => $contribution['id'],
2052 'label' => 'Another Line Item',
2053 ]);
2054
2055 $this->assertEquals($lineItem1['line_total'] + $lineItem2['line_total'] + $lineItem3['line_total'], round(10 + 180 * 16.95 + 110 * 2.95, 2), 'Line Item Total is incorrect.');
2056 $this->assertEquals(round($lineItem1['tax_amount'] + $lineItem2['tax_amount'] + $lineItem3['tax_amount'], 2), round(180 * 16.95 * 0.10 + 110 * 2.95 * 0.10, 2), 'Wrong Sales Tax Amount is calculated and stored.');
2057 }
2058
2059 /**
2060 * Test validating a contribution page submit.
2061 *
2062 * @throws \CRM_Core_Exception
2063 */
2064 public function testValidate() {
2065 $this->setUpContributionPage();
2066 $errors = $this->callAPISuccess('ContributionPage', 'validate', array_merge($this->getBasicSubmitParams(), ['action' => 'submit']))['values'];
2067 $this->assertEmpty($errors);
2068 }
2069
2070 /**
2071 * Test validating a contribution page submit in POST context.
2072 *
2073 * A likely use case for the validation is when the is being submitted and some handling is
2074 * to be done before processing but the validity of input needs to be checked first.
2075 *
2076 * For example Paypal Checkout will replace the confirm button with it's own but we are able to validate
2077 * before paypal launches it's modal. In this case the $_REQUEST is post but we need validation to succeed.
2078 *
2079 * @throws \CRM_Core_Exception
2080 */
2081 public function testValidatePost() {
2082 $_SERVER['REQUEST_METHOD'] = 'POST';
2083 $this->setUpContributionPage();
2084 $errors = $this->callAPISuccess('ContributionPage', 'validate', array_merge($this->getBasicSubmitParams(), ['action' => 'submit']))['values'];
2085 $this->assertEmpty($errors);
2086 unset($_SERVER['REQUEST_METHOD']);
2087 }
2088
2089 /**
2090 * Test that an error is generated if required fields are not submitted.
2091 *
2092 * @throws \CRM_Core_Exception
2093 */
2094 public function testValidateOutputOnMissingRecurFields() {
2095 $this->params['is_recur_interval'] = 1;
2096 $this->setUpContributionPage(TRUE);
2097 $submitParams = array_merge($this->getBasicSubmitParams(), ['action' => 'submit']);
2098 $submitParams['is_recur'] = 1;
2099 $submitParams['frequency_interval'] = '';
2100 $submitParams['frequency_unit'] = '';
2101 $errors = $this->callAPISuccess('ContributionPage', 'validate', $submitParams)['values'];
2102 $this->assertEquals('Please enter a number for how often you want to make this recurring contribution (EXAMPLE: Every 3 months).', $errors['frequency_interval']);
2103 }
2104
2105 /**
2106 * Implements hook_civicrm_alterPaymentProcessorParams().
2107 *
2108 * @throws \Exception
2109 */
2110 public function hook_civicrm_alterPaymentProcessorParams($paymentObj, &$rawParams, &$cookedParams) {
2111 // Ensure total_amount are the same if they're both given.
2112 $total_amount = $rawParams['total_amount'] ?? NULL;
2113 $amount = $rawParams['amount'] ?? NULL;
2114 if (!empty($total_amount) && !empty($amount) && $total_amount != $amount) {
2115 throw new Exception("total_amount '$total_amount' and amount '$amount' differ.");
2116 }
2117
2118 // Log parameters for later debugging and testing.
2119 $message = __FUNCTION__ . ": {$rawParams['TEST_UNIQ']}:";
2120 $log_params = array_intersect_key($rawParams, [
2121 'amount' => 1,
2122 'total_amount' => 1,
2123 'contributionID' => 1,
2124 ]);
2125 $message .= json_encode($log_params);
2126 $log = new CRM_Utils_SystemLogger();
2127 $log->debug($message, $_REQUEST);
2128 }
2129
2130 /**
2131 * Get the params for a basic simple submit.
2132 *
2133 * @return array
2134 */
2135 protected function getBasicSubmitParams() {
2136 $priceFieldID = reset($this->_ids['price_field']);
2137 $priceFieldValueID = reset($this->_ids['price_field_value']);
2138 return [
2139 'price_' . $priceFieldID => $priceFieldValueID,
2140 'id' => (int) $this->_ids['contribution_page'],
2141 'amount' => 10,
2142 'priceSetId' => $this->_ids['price_set'][0],
2143 'payment_processor_id' => 0,
2144 ];
2145 }
2146
2147 }