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