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