cbec27ee0ab017e3b94d840847049047a149b199
[civicrm-core.git] / tests / phpunit / api / v3 / TaxContributionPageTest.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 use Civi\Api4\EntityFinancialTrxn;
13
14 /**
15 * Class api_v3_TaxContributionPageTest
16 * @group headless
17 */
18 class api_v3_TaxContributionPageTest extends CiviUnitTestCase {
19 protected $params;
20 protected $financialTypeID;
21 protected $financialAccountId;
22 protected $_entity = 'contribution_page';
23 protected $_priceSetParams = [];
24 protected $_paymentProcessorType;
25 protected $payParams = [];
26 protected $settingValue = [];
27 protected $setInvoiceSettings;
28 protected $_individualId;
29 protected $financialAccHalftax;
30 protected $financialtypeHalftax;
31 protected $financialRelationHalftax;
32 protected $halfFinancialAccId;
33 protected $halfFinancialTypeId;
34
35 protected $isValidateFinancialsOnPostAssert = FALSE;
36
37 /**
38 * @throws \CRM_Core_Exception
39 * @throws \CiviCRM_API3_Exception
40 */
41 public function setUp(): void {
42 parent::setUp();
43 $this->_individualId = $this->individualCreate();
44 $this->_orgId = $this->organizationCreate(NULL);
45
46 $this->ids['PaymentProcessor'] = $this->paymentProcessorCreate();
47 $this->params = [
48 'title' => 'Test Contribution Page',
49 'financial_type_id' => 1,
50 'payment_processor' => $this->ids['PaymentProcessor'],
51 'currency' => 'NZD',
52 'goal_amount' => 350,
53 'is_pay_later' => 1,
54 'pay_later_text' => 'I will pay later',
55 'pay_later_receipt' => 'I will pay later',
56 'is_monetary' => TRUE,
57 'is_billing_required' => TRUE,
58 ];
59
60 $this->_priceSetParams = [
61 'name' => 'tax_contribution',
62 'title' => 'contribution tax',
63 'is_active' => 1,
64 'help_pre' => 'Where does your goat sleep',
65 'help_post' => 'thank you for your time',
66 'extends' => 2,
67 'financial_type_id' => 3,
68 'is_quick_config' => 0,
69 'is_reserved' => 0,
70 ];
71 // Financial Account with 20% tax rate
72 $financialAccountSetparams = [
73 'name' => 'vat full tax rate account',
74 'contact_id' => $this->_orgId,
75 'financial_account_type_id' => 2,
76 'is_tax' => 1,
77 'tax_rate' => 20.00,
78 'is_reserved' => 0,
79 'is_active' => 1,
80 'is_default' => 0,
81 ];
82
83 $financialAccount = $this->callAPISuccess('financial_account', 'create', $financialAccountSetparams);
84 $this->financialAccountId = $financialAccount['id'];
85
86 // Financial type having 'Sales Tax Account is' with liability financial account
87 $this->financialTypeID = $this->callAPISuccess('FinancialType', 'create', [
88 'name' => 'grass_variety_1',
89 'is_reserved' => 0,
90 'is_active' => 1,
91 ])['id'];
92 $financialRelationParams = [
93 'entity_table' => 'civicrm_financial_type',
94 'entity_id' => $this->financialTypeID,
95 'account_relationship' => 10,
96 'financial_account_id' => $this->financialAccountId,
97 ];
98 CRM_Financial_BAO_FinancialTypeAccount::add($financialRelationParams);
99
100 // Financial type with 5% tax rate
101 $financialAccountHalfTax = [
102 'name' => 'vat half tax_rate account',
103 'contact_id' => $this->_orgId,
104 'financial_account_type_id' => 2,
105 'is_tax' => 1,
106 'tax_rate' => 5.00,
107 'is_reserved' => 0,
108 'is_active' => 1,
109 'is_default' => 0,
110 ];
111 $halfFinancialAccount = CRM_Financial_BAO_FinancialAccount::add($financialAccountHalfTax);
112 $this->halfFinancialAccId = $halfFinancialAccount->id;
113 $halfFinancialTypeHalfTax = [
114 'name' => 'grass_variety_2',
115 'is_reserved' => 0,
116 'is_active' => 1,
117 ];
118
119 $halfFinancialType = CRM_Financial_BAO_FinancialType::add($halfFinancialTypeHalfTax);
120 $this->halfFinancialTypeId = $halfFinancialType->id;
121 $financialRelationHalftax = [
122 'entity_table' => 'civicrm_financial_type',
123 'entity_id' => $this->halfFinancialTypeId,
124 'account_relationship' => 10,
125 'financial_account_id' => $this->halfFinancialAccId,
126 ];
127
128 CRM_Financial_BAO_FinancialTypeAccount::add($financialRelationHalftax);
129
130 // Enable component contribute setting
131 $this->enableTaxAndInvoicing();
132 }
133
134 /**
135 * Cleanup after function.
136 *
137 * @throws \CRM_Core_Exception
138 * @throws \CiviCRM_API3_Exception
139 */
140 public function tearDown(): void {
141 $this->quickCleanUpFinancialEntities();
142 parent::tearDown();
143 }
144
145 /**
146 * @throws \CRM_Core_Exception
147 */
148 public function setUpContributionPage(): void {
149 $contributionPageResult = $this->callAPISuccess($this->_entity, 'create', $this->params);
150 if (empty($this->ids['price_set'])) {
151 $priceSet = $this->callAPISuccess('price_set', 'create', $this->_priceSetParams);
152 $this->ids['price_set'][] = $priceSet['id'];
153 }
154 $priceSetID = $this->_price = reset($this->ids['price_set']);
155 CRM_Price_BAO_PriceSet::addTo('civicrm_contribution_page', $contributionPageResult['id'], $priceSetID);
156
157 if (empty($this->ids['price_field'])) {
158 $priceField = $this->callAPISuccess('price_field', 'create', [
159 'price_set_id' => $priceSetID,
160 'label' => 'Goat Breed',
161 'html_type' => 'Radio',
162 ]);
163 $this->ids['price_field'] = [$priceField['id']];
164 if (empty($this->ids['price_field_value'])) {
165 $this->callAPISuccess('price_field_value', 'create', [
166 'price_set_id' => $priceSetID,
167 'price_field_id' => $priceField['id'],
168 'label' => 'Long Haired Goat',
169 'amount' => 100,
170 'financial_type_id' => $this->financialTypeID,
171 ]);
172 $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', [
173 'price_set_id' => $priceSetID,
174 'price_field_id' => $priceField['id'],
175 'label' => 'Shoe-eating Goat',
176 'amount' => 300,
177 'financial_type_id' => $this->halfFinancialTypeId,
178 ]);
179 $this->ids['price_field_value'] = [$priceFieldValue['id']];
180 }
181 }
182 $this->ids['contribution_page'] = $contributionPageResult['id'];
183 }
184
185 /**
186 * Online and offline contribution from above created contribution page.
187 *
188 * @param string $thousandSeparator
189 * punctuation used to refer to thousands.
190 *
191 * @throws \API_Exception
192 * @throws \CRM_Core_Exception
193 * @dataProvider getThousandSeparators
194 *
195 */
196 public function testCreateContributionOnline(string $thousandSeparator): void {
197 $this->setCurrencySeparators($thousandSeparator);
198 $this->setUpContributionPage();
199 $params = [
200 'contact_id' => $this->_individualId,
201 'receive_date' => '20120511',
202 'total_amount' => $this->formatMoneyInput(100.00),
203 'financial_type_id' => $this->financialTypeID,
204 'contribution_page_id' => $this->ids['contribution_page'],
205 'payment_processor' => $this->ids['PaymentProcessor'],
206 'trxn_id' => 12345,
207 'invoice_id' => 67890,
208 'source' => 'SSF',
209 'contribution_status_id' => 1,
210 'sequential' => 1,
211 ];
212
213 $contribution = $this->callAPISuccess('Contribution', 'create', $params)['values'][0];
214 $this->assertEquals($this->_individualId, $contribution['contact_id']);
215 $this->assertEquals(120.00, $contribution['total_amount']);
216 $this->assertEquals($this->financialTypeID, $contribution['financial_type_id']);
217 $this->assertEquals(12345, $contribution['trxn_id']);
218 $this->assertEquals(67890, $contribution['invoice_id']);
219 $this->assertEquals('SSF', $contribution['source']);
220 $this->assertEquals(20, $contribution['tax_amount']);
221 $this->assertEquals(1, $contribution['contribution_status_id']);
222 $this->_checkFinancialRecords($contribution, 'online');
223 }
224
225 /**
226 * Create contribution with chained line items.
227 *
228 * @param string $thousandSeparator
229 * punctuation used to refer to thousands.
230 *
231 * @dataProvider getThousandSeparators
232 *
233 * @throws \CRM_Core_Exception
234 */
235 public function testCreateContributionChainedLineItems(string $thousandSeparator): void {
236 $this->setCurrencySeparators($thousandSeparator);
237 $this->setUpContributionPage();
238 $params = [
239 'contact_id' => $this->_individualId,
240 'receive_date' => '20120511',
241 'total_amount' => 400.00,
242 'financial_type_id' => $this->financialTypeID,
243 'trxn_id' => 12345,
244 'invoice_id' => 67890,
245 'source' => 'SSF',
246 'contribution_status_id' => 1,
247 'skipLineItem' => 1,
248 'api.line_item.create' => [
249 [
250 'price_field_id' => $this->ids['price_field'],
251 'qty' => 1,
252 'line_total' => '100',
253 'unit_price' => '100',
254 'financial_type_id' => $this->financialTypeID,
255 ],
256 [
257 'price_field_id' => $this->ids['price_field'],
258 'qty' => 1,
259 'line_total' => '300',
260 'unit_price' => '300',
261 'financial_type_id' => $this->halfFinancialTypeId,
262 ],
263 ],
264 ];
265
266 $contribution = $this->callAPISuccess('contribution', 'create', $params);
267
268 $lineItems = $this->callAPISuccess('line_item', 'get', [
269 'entity_id' => $contribution['id'],
270 'contribution_id' => $contribution['id'],
271 'entity_table' => 'civicrm_contribution',
272 'sequential' => 1,
273 ]);
274 $this->assertEquals(2, $lineItems['count']);
275 }
276
277 /**
278 * @throws \API_Exception
279 * @throws \CRM_Core_Exception
280 */
281 public function testCreateContributionPayLaterOnline(): void {
282 $this->setUpContributionPage();
283 $params = [
284 'contact_id' => $this->_individualId,
285 'receive_date' => '20120511',
286 'total_amount' => 100.00,
287 'financial_type_id' => $this->financialTypeID,
288 'contribution_page_id' => $this->ids['contribution_page'],
289 'trxn_id' => 12345,
290 'is_pay_later' => 1,
291 'invoice_id' => 67890,
292 'source' => 'SSF',
293 'contribution_status_id' => 2,
294 'sequential' => 1,
295 ];
296 $contribution = $this->callAPISuccess('Contribution', 'create', $params)['values'][0];
297 $this->assertEquals($contribution['contact_id'], $this->_individualId);
298 $this->assertEquals(120.00, $contribution['total_amount']);
299 $this->assertEquals($this->financialTypeID, $contribution['financial_type_id']);
300 $this->assertEquals(12345, $contribution['trxn_id']);
301 $this->assertEquals(67890, $contribution['invoice_id']);
302 $this->assertEquals('SSF', $contribution['source']);
303 $this->assertEquals(20, $contribution['tax_amount']);
304 $this->assertEquals(2, $contribution['contribution_status_id']);
305 $this->_checkFinancialRecords($contribution, 'payLater');
306 }
307
308 /**
309 * Test online pending contributions.
310 *
311 * @param string $thousandSeparator
312 * punctuation used to refer to thousands.
313 *
314 * @throws \API_Exception
315 * @throws \CRM_Core_Exception
316 * @throws \Civi\API\Exception\UnauthorizedException
317 * @dataProvider getThousandSeparators
318 *
319 */
320 public function testCreateContributionPendingOnline(string $thousandSeparator): void {
321 $this->setCurrencySeparators($thousandSeparator);
322 $this->setUpContributionPage();
323 $params = [
324 'contact_id' => $this->_individualId,
325 'receive_date' => '20120511',
326 'total_amount' => $this->formatMoneyInput(100.00),
327 'financial_type_id' => $this->financialTypeID,
328 'contribution_page_id' => $this->ids['contribution_page'],
329 'trxn_id' => 12345,
330 'invoice_id' => 67890,
331 'source' => 'SSF',
332 'contribution_status_id' => 2,
333 'sequential' => 1,
334 ];
335
336 $contribution = $this->callAPISuccess('Contribution', 'create', $params)['values'][0];
337 $this->assertEquals($this->_individualId, $contribution['contact_id']);
338 $this->assertEquals(120.00, $contribution['total_amount']);
339 $this->assertEquals($this->financialTypeID, $contribution['financial_type_id']);
340 $this->assertEquals(12345, $contribution['trxn_id']);
341 $this->assertEquals(67890, $contribution['invoice_id']);
342 $this->assertEquals('SSF', $contribution['source']);
343 $this->assertEquals(20, $contribution['tax_amount']);
344 $this->assertEquals(2, $contribution['contribution_status_id']);
345 $trxn = $this->getFinancialTransactionsForContribution($contribution['id']);
346 $this->assertCount(0, $trxn, 'No Trxn to be created until IPN callback');
347
348 $this->setCurrencySeparators($thousandSeparator);
349 }
350
351 /**
352 * Update a contribution.
353 *
354 * Function tests that line items, financial records are updated when
355 * contribution amount is changed
356 *
357 * @throws \CRM_Core_Exception
358 */
359 public function testCreateUpdateContributionChangeTotal(): void {
360 $this->setUpContributionPage();
361 $contributionParams = [
362 'contact_id' => $this->_individualId,
363 'receive_date' => '20120511',
364 'total_amount' => 100.00,
365 'financial_type_id' => $this->financialTypeID,
366 'source' => 'SSF',
367 'contribution_status_id' => 1,
368 ];
369 $contribution = $this->callAPISuccess('contribution', 'create', $contributionParams);
370 $lineItems = $this->callAPISuccess('line_item', 'getvalue', [
371 'entity_id' => $contribution['id'],
372 'entity_table' => 'civicrm_contribution',
373 'sequential' => 1,
374 'return' => 'line_total',
375 ]);
376 $this->assertEquals('100.00', $lineItems);
377 $trxnAmount = $this->_getFinancialTrxnAmount($contribution['id']);
378 $this->assertEquals('120.00', $trxnAmount);
379 $newParams = [
380 'id' => $contribution['id'],
381 // without tax rate i.e Donation
382 'financial_type_id' => 1,
383 'total_amount' => '300',
384 ];
385 $contribution = $this->callAPISuccess('contribution', 'create', $newParams);
386
387 $lineItems = $this->callAPISuccess('line_item', 'getvalue', [
388 'entity_id' => $contribution['id'],
389 'entity_table' => 'civicrm_contribution',
390 'sequential' => 1,
391 'return' => 'line_total',
392 ]);
393
394 $this->assertEquals('300.00', $lineItems);
395 $this->assertEquals('320.00', $this->_getFinancialTrxnAmount($contribution['id']));
396 $this->assertEquals('320.00', $this->_getFinancialItemAmount($contribution['id']));
397 }
398
399 /**
400 * @param int $contId
401 *
402 * @return null|string
403 */
404 public function _getFinancialTrxnAmount(int $contId): ?string {
405 $query = "SELECT
406 SUM( ft.total_amount ) AS total
407 FROM civicrm_financial_trxn AS ft
408 LEFT JOIN civicrm_entity_financial_trxn AS ceft ON ft.id = ceft.financial_trxn_id
409 WHERE ceft.entity_table = 'civicrm_contribution'
410 AND ceft.entity_id = {$contId}";
411 return CRM_Core_DAO::singleValueQuery($query);
412 }
413
414 /**
415 * @param int $contId
416 *
417 * @return null|string
418 */
419 public function _getFinancialItemAmount(int $contId): ?string {
420 $lineItem = key(CRM_Price_BAO_LineItem::getLineItems($contId, 'contribution'));
421 $query = "SELECT
422 SUM(amount)
423 FROM civicrm_financial_item
424 WHERE entity_table = 'civicrm_line_item'
425 AND entity_id = {$lineItem}";
426 return CRM_Core_DAO::singleValueQuery($query);
427 }
428
429 /**
430 * @param array $params
431 * @param string $context
432 *
433 * @throws \API_Exception
434 */
435 public function _checkFinancialRecords($params, $context): void {
436 $contributionID = $params['id'];
437 $trxn = $this->getFinancialTransactionsForContribution($contributionID);
438
439 $trxnParams = [
440 'id' => $trxn->first()['financial_trxn_id'],
441 ];
442 if ($context !== 'online' && $context !== 'payLater') {
443 $compareParams = [
444 'to_financial_account_id' => 6,
445 'total_amount' => 120,
446 'status_id' => 1,
447 ];
448 }
449 if ($context === 'online') {
450 $compareParams = [
451 'to_financial_account_id' => 12,
452 'total_amount' => 120,
453 'status_id' => 1,
454 ];
455 }
456 elseif ($context === 'payLater') {
457 $compareParams = [
458 'to_financial_account_id' => 7,
459 'total_amount' => 120,
460 'status_id' => 2,
461 ];
462 }
463 $this->assertDBCompareValues('CRM_Financial_DAO_FinancialTrxn', $trxnParams, $compareParams);
464 $entityParams = [
465 'financial_trxn_id' => $trxn->first()['financial_trxn_id'],
466 'entity_table' => 'civicrm_financial_item',
467 ];
468 $entityTrxn = current(CRM_Financial_BAO_FinancialItem::retrieveEntityFinancialTrxn($entityParams));
469 $compareParams = [
470 'amount' => 100,
471 'status_id' => 1,
472 'financial_account_id' => $this->_getFinancialAccountId($this->financialTypeID),
473 ];
474 if ($context === 'payLater') {
475 $compareParams = [
476 'amount' => 100,
477 'status_id' => 3,
478 'financial_account_id' => $this->_getFinancialAccountId($this->financialTypeID),
479 ];
480 }
481 $this->assertDBCompareValues('CRM_Financial_DAO_FinancialItem', [
482 'id' => $entityTrxn['entity_id'],
483 ], $compareParams);
484 }
485
486 /**
487 * @param int $financialTypeId
488 *
489 * @return int
490 */
491 public function _getFinancialAccountId(int $financialTypeId): ?int {
492 $accountRel = key(CRM_Core_PseudoConstant::accountOptionValues('account_relationship', NULL, " AND v.name LIKE 'Income Account is' "));
493
494 $searchParams = [
495 'entity_table' => 'civicrm_financial_type',
496 'entity_id' => $financialTypeId,
497 'account_relationship' => $accountRel,
498 ];
499
500 $result = [];
501 CRM_Financial_BAO_FinancialTypeAccount::retrieve($searchParams, $result);
502 return $result['financial_account_id'] ?? NULL;
503 }
504
505 /**
506 * Test deleting a contribution.
507 *
508 * (It is unclear why this is in this class - it seems like maybe it doesn't
509 * test anything not on the contribution test class & might be copy and
510 * paste....).
511 *
512 * @throws \CRM_Core_Exception
513 */
514 public function testDeleteContribution(): void {
515 $contributionID = $this->contributionCreate([
516 'contact_id' => $this->_individualId,
517 'trxn_id' => 12389,
518 'financial_type_id' => $this->financialTypeID,
519 'invoice_id' => 'abc',
520 ]);
521 $this->callAPISuccess('contribution', 'delete', ['id' => $contributionID]);
522 }
523
524 /**
525 * @param $contributionID
526 *
527 * @return \Civi\Api4\Generic\Result
528 * @throws \API_Exception
529 * @throws \Civi\API\Exception\UnauthorizedException
530 */
531 protected function getFinancialTransactionsForContribution($contributionID): \Civi\Api4\Generic\Result {
532 return EntityFinancialTrxn::get()
533 ->addWhere('id', '=', $contributionID)
534 ->addWhere('entity_table', '=', 'civicrm_contribution')
535 ->addSelect('*')->execute();
536 }
537
538 }