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