<?php /* +--------------------------------------------------------------------+ | Copyright CiviCRM LLC. All rights reserved. | | | | This work is published under the GNU AGPLv3 license with some | | permitted exceptions and without any warranty. For full license | | and copyright information, see https://civicrm.org/licensing | +--------------------------------------------------------------------+ */ use Civi\Api4\Contribution; use Civi\Api4\FinancialItem; /** * Test APIv3 civicrm_contribute_* functions * * @package CiviCRM_APIv3 * @subpackage API_Contribution * @group headless */ class api_v3_OrderTest extends CiviUnitTestCase { use CRMTraits_Financial_TaxTrait; protected $_financialTypeId = 1; protected static $phpunitStartedDate; protected static $skipStatusCalStillExists; /** * Setup function. */ public function setUp(): void { parent::setUp(); $this->_apiversion = 3; $this->individualCreate(); } /** * Clean up after each test. */ public function tearDown(): void { $this->quickCleanUpFinancialEntities(); $this->quickCleanup(['civicrm_uf_match']); parent::tearDown(); } /** * Test Get order api. */ public function testGetOrder(): void { $contribution = $this->addOrder(FALSE, 100); $params = ['contribution_id' => $contribution['id']]; $order = $this->callAPISuccess('Order', 'get', $params); $this->assertEquals(1, $order['count']); $expectedResult = [ $contribution['id'] => [ 'total_amount' => 100, 'contribution_id' => $contribution['id'], 'contribution_status' => 'Completed', 'net_amount' => 100, ], ]; $lineItems[] = [ 'entity_table' => 'civicrm_contribution', 'entity_id' => $contribution['id'], 'contribution_id' => $contribution['id'], 'unit_price' => 100, 'line_total' => 100, 'financial_type_id' => 1, ]; $this->checkPaymentResult($order, $expectedResult, $lineItems); } /** * Test Get Order api for participant contribution. * * @throws \CRM_Core_Exception */ public function testGetOrderParticipant(): void { $this->addOrder(FALSE, 100); $contribution = $this->createPartiallyPaidParticipantOrder(); $params = [ 'contribution_id' => $contribution['id'], ]; $order = $this->callAPISuccess('Order', 'get', $params); $this->assertCount(2, $order['values'][$contribution['id']]['line_items']); } /** * Function to assert db values. */ public function checkPaymentResult($results, $expectedResult, $lineItems = NULL): void { foreach ($expectedResult[$results['id']] as $key => $value) { $this->assertEquals($value, $results['values'][$results['id']][$key], "Unexpected value for '$key'"); } if ($lineItems) { foreach ($lineItems as $key => $items) { foreach ($items as $k => $item) { $this->assertEquals($item, $results['values'][$results['id']]['line_items'][$key][$k], "Unexpected value for line item $key, $k"); } } } } /** * Add order. * * @param bool $isPriceSet * @param float $amount * @param array $extraParams * * @return array */ public function addOrder(bool $isPriceSet, float $amount = 300.00, array $extraParams = []): array { $p = [ 'contact_id' => $this->ids['Contact']['individual_0'], 'receive_date' => '2010-01-20', 'total_amount' => $amount, 'financial_type_id' => $this->_financialTypeId, 'contribution_status_id' => 1, ]; if ($isPriceSet) { $priceFields = $this->createPriceSet(); foreach ($priceFields['values'] as $key => $priceField) { $lineItems[1][$key] = [ 'price_field_id' => $priceField['price_field_id'], 'price_field_value_id' => $priceField['id'], 'label' => $priceField['label'], 'field_title' => $priceField['label'], 'qty' => 1, 'unit_price' => $priceField['amount'], 'line_total' => $priceField['amount'], 'financial_type_id' => $priceField['financial_type_id'], ]; } $p['line_item'] = $lineItems; } $p = array_merge($extraParams, $p); return $this->callAPISuccess('Contribution', 'create', $p); } /** * Test create order api. */ public function testAddOrder(): void { $order = $this->addOrder(FALSE, 100); $params = [ 'contribution_id' => $order['id'], ]; $order = $this->callAPISuccess('Order', 'get', $params); $expectedResult = [ $order['id'] => [ 'total_amount' => 100, 'contribution_id' => $order['id'], 'contribution_status' => 'Completed', 'net_amount' => 100, ], ]; $lineItems[] = [ 'entity_table' => 'civicrm_contribution', 'entity_id' => $order['id'], 'contribution_id' => $order['id'], 'unit_price' => 100, 'line_total' => 100, 'financial_type_id' => 1, ]; $this->checkPaymentResult($order, $expectedResult, $lineItems); } /** * Test create order api for membership * * @throws \CRM_Core_Exception */ public function testAddOrderForMembership(): void { $membershipType = $this->membershipTypeCreate(); $membershipType1 = $this->membershipTypeCreate(); $membershipType = $membershipTypes = [$membershipType, $membershipType1]; $p = [ 'contact_id' => $this->ids['Contact']['individual_0'], 'receive_date' => '2010-01-20', 'financial_type_id' => 'Event Fee', 'contribution_status_id' => 'Pending', ]; $priceFields = $this->createPriceSet(); foreach ($priceFields['values'] as $key => $priceField) { $lineItems[$key] = [ 'price_field_id' => $priceField['price_field_id'], 'price_field_value_id' => $priceField['id'], 'label' => $priceField['label'], 'field_title' => $priceField['label'], 'qty' => 1, 'unit_price' => $priceField['amount'], 'line_total' => $priceField['amount'], 'financial_type_id' => $priceField['financial_type_id'], 'entity_table' => 'civicrm_membership', 'membership_type_id' => array_pop($membershipType), ]; } $p['line_items'][] = [ 'line_item' => [array_pop($lineItems)], 'params' => [ 'contact_id' => $this->ids['Contact']['individual_0'], 'membership_type_id' => array_pop($membershipTypes), 'join_date' => '2006-01-21', 'start_date' => '2006-01-21', 'end_date' => '2006-12-21', 'source' => 'Payment', 'is_override' => 1, ], ]; $order = $this->callAPISuccess('Order', 'create', $p); $params = [ 'contribution_id' => $order['id'], ]; $order = $this->callAPISuccess('Order', 'get', $params); $expectedResult = [ $order['id'] => [ 'total_amount' => 200, 'contribution_id' => $order['id'], 'contribution_status' => 'Pending Label**', 'net_amount' => 200, ], ]; $this->checkPaymentResult($order, $expectedResult); $membershipPayment = $this->callAPISuccessGetSingle('MembershipPayment', $params); $this->callAPISuccessGetSingle('Membership', ['id' => $membershipPayment['id']]); $this->callAPISuccess('Contribution', 'Delete', ['id' => $order['id']]); $p['line_items'][] = [ 'line_item' => [array_pop($lineItems)], 'params' => [ 'contact_id' => $this->ids['Contact']['individual_0'], 'membership_type_id' => array_pop($membershipTypes), 'join_date' => '2006-01-21', 'start_date' => '2006-01-21', 'end_date' => '2006-12-21', 'source' => 'Payment', 'is_override' => 1, 'status_id' => 'Pending', ], ]; $p['total_amount'] = 300; $order = $this->callAPISuccess('Order', 'create', $p); $expectedResult = [ $order['id'] => [ 'total_amount' => 300, 'contribution_status' => 'Pending Label**', 'net_amount' => 300, ], ]; $paymentMembership = [ 'contribution_id' => $order['id'], ]; $order = $this->callAPISuccess('Order', 'get', $paymentMembership); $this->checkPaymentResult($order, $expectedResult); $this->callAPISuccessGetCount('MembershipPayment', $paymentMembership, 2); $this->callAPISuccess('Payment', 'create', [ 'contribution_id' => $order['id'], 'payment_instrument_id' => 'Check', 'total_amount' => 300, ]); foreach (FinancialItem::get(FALSE) ->addJoin( 'LineItem AS line_item', 'INNER', NULL, ['entity_table', '=', '"civicrm_line_item"'], ['entity_id', '=', 'line_item.id'], ['line_item.contribution_id', '=', $order['id']] ) ->addSelect('status_id')->execute() as $item) { $this->assertEquals('Paid', CRM_Core_PseudoConstant::getName('CRM_Financial_BAO_FinancialItem', 'status_id', $item['status_id'])); } } /** * Test create order api for membership. * * @dataProvider dataForTestAddOrderForMembershipWithDates * * @param array $membershipExtraParams Optional additional params for the membership, * e.g. skipStatusCal or start_date. This can also have a 'renewalOf' key, in which * case we'll create an existing membership based on the values therein and try to renew it. * @param ?string $paymentDate - if set, a payment will be made on that date, completing the order. (Not implemented yet) * @param array $expectations */ public function testAddOrderForMembershipWithDates(array $membershipExtraParams, ?string $paymentDate, array $expectations): void { if (date('Y-m-d') > static::$phpunitStartedDate) { $this->markTestSkipped('Test run spanned 2 days so skipping test as results would be affected'); } if (date('Hi') > '2357') { $this->markTestSkipped("It‘s less than 2 minutes to midnight, test skipped as 'today' may change during test."); } if (isset($membershipExtraParams['skipStatusCal']) && !$this->skipStatusCalStillExists()) { $this->markTestSkipped('The test was skipped as skipStatusCal seems to have been removed, so this test is useless and should be removed.'); } $membershipType = $this->membershipTypeCreate(); if (isset($membershipExtraParams['renewalOf'])) { // Create a pre-existing membership $originalMembershipID = $this->callAPISuccess('Membership', 'create', $membershipExtraParams['renewalOf'] + [ 'contact_id' => $this->ids['Contact']['individual_0'], 'membership_type_id' => $membershipType, 'source' => 'Old', ])['id']; unset($membershipExtraParams['renewalOf']); // To make this Order a renewal, we provide the ID of the membership. $membershipExtraParams['id'] = $originalMembershipID; } // Use the 2nd and last price field value defined in the fixture, which has value 200 $priceFieldValue = end($this->createPriceSet()['values']); $orderCreateParams = [ 'contact_id' => $this->ids['Contact']['individual_0'], 'financial_type_id' => 'Member Dues', 'contribution_status_id' => 'Pending', 'line_items' => [[ 'line_item' => [[ 'price_field_id' => $priceFieldValue['price_field_id'], 'price_field_value_id' => $priceFieldValue['id'], 'label' => $priceFieldValue['label'], 'field_title' => $priceFieldValue['label'], 'qty' => 1, 'unit_price' => $priceFieldValue['amount'], 'line_total' => $priceFieldValue['amount'], 'financial_type_id' => $priceFieldValue['financial_type_id'], 'entity_table' => 'civicrm_membership', 'membership_type_id' => $membershipType, ], ], 'params' => [ 'contact_id' => $this->ids['Contact']['individual_0'], 'membership_type_id' => $membershipType, 'source' => 'Payment', ] + $membershipExtraParams, ], ], ]; $order = $this->callAPISuccess('Order', 'create', $orderCreateParams); // Create expected dates immediately before order creation to minimise chance of day changing over. //$expectedStart = date('Y-m-d'); $expectedEnd = date('Y-m-d', strtotime('+ 1 year - 1 day')); $order = $this->callAPISuccess('Order', 'get', ['id' => $order['id']]); $expectedResult = [ $order['id'] => [ 'total_amount' => 200, 'contribution_id' => $order['id'], 'contribution_status' => 'Pending Label**', 'net_amount' => 200, ], ]; $this->checkPaymentResult($order, $expectedResult); // If we are to make a payment to complete this order, do that now. if ($paymentDate) { $paymentCreateResult = $this->callAPISuccess('Payment', 'create', [ 'contribution_id' => $order['id'], 'total_amount' => 200, 'trxn_date' => $paymentDate, 'is_send_contribution_notification' => FALSE, ]); // Reload the order, check it's now completed. $order = $this->callAPISuccess('Order', 'get', ['id' => $order['id']]); $expectedResult[$order['id']]['contribution_status'] = 'Completed'; $this->checkPaymentResult($order, $expectedResult); } // Check membership details $membershipPayment = $this->callAPISuccessGetSingle('MembershipPayment', ['contribution_id' => $order['id']]); $membership = $this->callAPISuccessGetSingle('Membership', ['id' => $membershipPayment['id']]); if (isset($expectations['status_id'])) { $membership['status_id'] = \CRM_Member_PseudoConstant::membershipstatus()[$membership['status_id']]; } $actuals = []; foreach ($expectations as $field => $expectedValue) { $actuals[$field] = $membership[$field] ?? NULL; } $this->assertEquals($expectations, $actuals, "Membership expectations are not met"); // Clean up $this->callAPISuccess('Contribution', 'Delete', ['id' => $order['id']]); } /** * Provides data for testAddOrderForMembershipWithDates. * * As well as checking various things work as expected, this set of tests is * here to set out what IS expected behaviour. */ public function dataForTestAddOrderForMembershipWithDates(): array { // Prevent test mis-fires because of running over midnight. static::$phpunitStartedDate = date('Y-m-d'); $today = date('Y-m-d'); $aYearLater = date('Y-m-d', strtotime('+1 year')); $fromTodayForAYear = ['start_date' => $today, 'join_date' => $today, 'end_date' => date('Y-m-d', strtotime('+1 year - 1 day'))]; $historical = ['start_date' => '2020-01-01', 'join_date' => '2020-01-01', 'end_date' => '2020-12-31']; $currentMembership = [ 'join_date' => date('Y-m-d', strtotime('-9 months')), 'start_date' => date('Y-m-d', strtotime('-9 months')), 'end_date' => date('Y-m-d', strtotime('-9 months + 1 year - 1 day')), ]; $tests = [ // #0 Without dates, we should get a pending membership starting today. [[], NULL, $fromTodayForAYear + ['status_id' => 'Pending']], // #1 With our own dates. [$historical + ['skipStatusCal' => 1], NULL, $historical + ['status_id' => 'Pending']], // #2 Auto dates (from today), payment today. [[], $today , $fromTodayForAYear + ['status_id' => 'New']], // #3 With our own dates, payment today: i.e. payment date should not affect membership date. [$historical, $today, $historical + ['status_id' => 'Expired']], // #4 Renewal that is not yet paid: we would expect Order.create NOT to change // dates or status on the current membership (...until it gets paid, see next tests). [['renewalOf' => []], NULL, $fromTodayForAYear + ['status_id' => 'New']], // #5 Existing expired membership gets renewed today. [$historical + ['renewalOf' => $historical], $today, [ 'join_date' => $historical['join_date'], /* unchanged */ 'start_date' => $today, 'end_date' => $fromTodayForAYear['end_date'], 'status_id' => 'Current', /* Hmmm. */ ], ], // #6 Existing current membership gets renewed today. // We expect that it adds a concurrent membership year, rather than overlapping. [['renewalOf' => $currentMembership], $today, [ 'join_date' => $currentMembership['join_date'], /* unchanged */ 'start_date' => $currentMembership['start_date'], /* also unchanged */ 'end_date' => date('Y-m-d', strtotime("$currentMembership[end_date] +1 year")), 'status_id' => 'Current', ], ], ]; // Duplicate the tests with skipStatusCal: i.e. should return the same // results for all these tests which use the Order API. // (This deprecated parameter should only affect a direct api3 Membership.create call.) // Remove this code (and skipStatusCalStillExists etc) if/when that is finally deprecated. foreach ($tests as $test) { $test[0] += ['skipStatusCal' => 1]; $tests[] = $test; } return $tests; } /** * We can lose a bunch of tests when skipStatusCal is finally gone. */ public function skipStatusCalStillExists() { if (!isset(static::$skipStatusCalStillExists)) { $path = Civi::paths()->getPath('[civicrm.root]/api/v3/Membership.php'); static::$skipStatusCalStillExists = stripos(file_get_contents($path), 'skipStatusCal') !== FALSE; } return static::$skipStatusCalStillExists; } /** * Test create order api for participant * * @throws \CRM_Core_Exception */ public function testAddOrderForParticipant(): void { $this->eventCreatePaid(); $p = [ 'contact_id' => $this->ids['Contact']['individual_0'], 'receive_date' => '2010-01-20', 'financial_type_id' => $this->_financialTypeId, 'contribution_status_id' => 'Pending', ]; $priceFields = $this->createPriceSet(); foreach ($priceFields['values'] as $key => $priceField) { $lineItems[$key] = [ 'price_field_id' => $priceField['price_field_id'], 'price_field_value_id' => $priceField['id'], 'label' => $priceField['label'], 'field_title' => $priceField['label'], 'qty' => 1, 'unit_price' => $priceField['amount'], 'line_total' => $priceField['amount'], 'financial_type_id' => $priceField['financial_type_id'], 'entity_table' => 'civicrm_participant', ]; } $p['line_items'][] = [ 'line_item' => $lineItems, 'params' => [ 'contact_id' => $this->ids['Contact']['individual_0'], 'event_id' => $this->getEventID(), 'role_id' => 1, 'register_date' => '2007-07-21 00:00:00', 'source' => 'Online Event Registration: API Testing', ], ]; $order = $this->callAPISuccess('order', 'create', $p); $params = ['contribution_id' => $order['id']]; $order = $this->callAPISuccess('order', 'get', $params); $expectedResult = [ $order['id'] => [ 'total_amount' => 300, 'contribution_id' => $order['id'], 'contribution_status' => 'Pending Label**', 'net_amount' => 300, ], ]; $this->checkPaymentResult($order, $expectedResult); $paymentParticipant = $this->callAPISuccessGetSingle('ParticipantPayment', ['contribution_id' => $order['id']]); $participant = $this->callAPISuccessGetSingle('Participant', ['participant_id' => $paymentParticipant['participant_id']]); $this->assertEquals('Pending (incomplete transaction)', $participant['participant_status']); $this->callAPISuccess('Contribution', 'Delete', [ 'id' => $order['id'], ]); // Enable the "Pending from approval" status which is not enabled by default $pendingFromApprovalParticipantStatus = civicrm_api3('ParticipantStatusType', 'getsingle', [ 'name' => 'Pending from approval', ]); civicrm_api3('ParticipantStatusType', 'create', [ 'id' => $pendingFromApprovalParticipantStatus['id'], 'name' => 'Pending from approval', 'is_active' => 1, ]); $p['line_items'][] = [ 'line_item' => $lineItems, 'params' => [ 'contact_id' => $this->individualCreate(), 'event_id' => $this->getEventID(), 'role_id' => 1, 'register_date' => '2007-07-21 00:00:00', 'source' => 'Online Event Registration: API Testing', 'participant_status_id' => 'Pending from approval', ], ]; $order = $this->callAPISuccess('order', 'create', $p); $expectedResult = [ $order['id'] => [ 'total_amount' => 600, 'contribution_status' => 'Pending Label**', 'net_amount' => 600, ], ]; $orderParams = [ 'contribution_id' => $order['id'], ]; $order = $this->callAPISuccess('order', 'get', $orderParams); $this->checkPaymentResult($order, $expectedResult); $paymentParticipant = $this->callAPISuccess('ParticipantPayment', 'get', $orderParams)['values']; $this->assertCount(2, $paymentParticipant, 'Expected two participant payments'); $participant = $this->callAPISuccessGetSingle('Participant', ['participant_id' => end($paymentParticipant)['participant_id']]); $this->assertEquals('Pending from approval', $participant['participant_status']); $this->callAPISuccess('Contribution', 'Delete', [ 'id' => $order['id'], ]); } /** * Test create order api with line items. */ public function testAddOrderWithLineItems(): void { $order = $this->addOrder(TRUE); $params = [ 'contribution_id' => $order['id'], ]; $order = $this->callAPISuccess('order', 'get', $params); $expectedResult = [ $order['id'] => [ 'total_amount' => 300, 'contribution_id' => $order['id'], 'contribution_status' => 'Completed', 'net_amount' => 300, ], ]; $items[] = [ 'entity_table' => 'civicrm_contribution', 'entity_id' => $order['id'], 'contribution_id' => $order['id'], 'unit_price' => 100, 'line_total' => 100, ]; $items[] = [ 'entity_table' => 'civicrm_contribution', 'entity_id' => $order['id'], 'contribution_id' => $order['id'], 'unit_price' => 200, 'line_total' => 200, ]; $this->checkPaymentResult($order, $expectedResult, $items); $params = [ 'entity_table' => 'civicrm_contribution', 'entity_id' => $order['id'], ]; $eft = $this->callAPISuccess('EntityFinancialTrxn', 'get', $params); $this->assertEquals(300, $eft['values'][$eft['id']]['amount']); $params = [ 'entity_table' => 'civicrm_financial_item', 'financial_trxn_id' => $eft['values'][$eft['id']]['financial_trxn_id'], ]; $eft = $this->callAPISuccess('EntityFinancialTrxn', 'get', $params); $amounts = [200, 100]; foreach ($eft['values'] as $value) { $this->assertEquals($value['amount'], array_pop($amounts)); } $this->callAPISuccess('Contribution', 'Delete', [ 'id' => $order['id'], ]); } /** * Test delete order api. */ public function testDeleteOrder(): void { $order = $this->addOrder(FALSE, 100); $params = [ 'contribution_id' => $order['id'], ]; try { $this->callAPISuccess('order', 'delete', $params); $this->fail("Missed expected exception"); } catch (Exception $expected) { $this->callAPISuccess('Contribution', 'create', [ 'contribution_id' => $order['id'], 'is_test' => TRUE, ]); $this->callAPISuccess('order', 'delete', $params); $order = $this->callAPISuccess('order', 'get', $params); $this->assertEquals(0, $order['count']); } } /** * Test order api treats amount as inclusive when line items not set. * * @throws \CRM_Core_Exception */ public function testAPIOrderTaxSpecified(): void { $this->enableTaxAndInvoicing(); $this->createFinancialTypeWithSalesTax(); $order = $this->callAPISuccess('Order', 'create', [ 'total_amount' => 105, 'financial_type_id' => 'Test taxable financial Type', 'contact_id' => $this->individualCreate(), 'sequential' => 1, 'tax_amount' => 5, ])['values'][0]; $this->assertEquals(105, $order['total_amount']); $this->assertEquals(5, $order['tax_amount']); } /** * Test order api treats amount as inclusive when line items not set. * * @throws \CRM_Core_Exception */ public function testAPIOrderTaxNotSpecified(): void { $this->enableTaxAndInvoicing(); $this->createFinancialTypeWithSalesTax(); $order = $this->callAPISuccess('Order', 'create', [ 'total_amount' => 105, 'financial_type_id' => 'Test taxable financial Type', 'contact_id' => $this->individualCreate(), 'sequential' => 1, ])['values'][0]; $this->assertEquals(105, $order['total_amount']); $this->assertEquals(5, $order['tax_amount']); } /** * Test order api treats amount as inclusive when line items not set. * * @throws \CRM_Core_Exception */ public function testAPIContributionTaxSpecified(): void { $this->enableTaxAndInvoicing(); $this->createFinancialTypeWithSalesTax(); $order = $this->callAPISuccess('Contribution', 'create', [ 'total_amount' => 105, 'financial_type_id' => 'Test taxable financial Type', 'contact_id' => $this->individualCreate(), 'sequential' => 1, 'tax_amount' => 5, ])['values'][0]; $this->assertEquals(105, $order['total_amount']); $this->assertEquals(5, $order['tax_amount']); } /** * Test cancel order api. */ public function testCancelOrder(): void { $contribution = $this->addOrder(FALSE, 100); $params = [ 'contribution_id' => $contribution['id'], ]; $this->callAPISuccess('order', 'cancel', $params); $order = $this->callAPISuccess('Order', 'get', $params); $expectedResult = [ $contribution['id'] => [ 'total_amount' => 100, 'contribution_id' => $contribution['id'], 'contribution_status' => 'Cancelled', 'net_amount' => 100, ], ]; $this->checkPaymentResult($order, $expectedResult); $this->callAPISuccess('Contribution', 'Delete', [ 'id' => $contribution['id'], ]); } /** * Test an exception is thrown if line items do not add up to total_amount, no tax. */ public function testCreateOrderIfTotalAmountDoesNotMatchLineItemsAmountsIfNoTaxSupplied(): void { $params = [ 'contact_id' => $this->ids['Contact']['individual_0'], 'receive_date' => '2018-01-01', 'total_amount' => 50, 'financial_type_id' => $this->_financialTypeId, 'contribution_status_id' => 'Pending', 'line_items' => [ 0 => [ 'line_item' => [ '0' => [ 'price_field_id' => 1, 'price_field_value_id' => 1, 'label' => 'Test 1', 'field_title' => 'Test 1', 'qty' => 1, 'unit_price' => 40, 'line_total' => 40, 'financial_type_id' => 1, 'entity_table' => 'civicrm_contribution', ], ], ], ], ]; $this->callAPIFailure('Order', 'create', $params, "Line item total doesn't match total amount"); } /** * Test an exception is thrown if line items do not add up to total_amount, with tax. */ public function testCreateOrderIfTotalAmountDoesNotMatchLineItemsAmountsIfTaxSupplied(): void { $params = [ 'contact_id' => $this->ids['Contact']['individual_0'], 'receive_date' => '2018-01-01', 'total_amount' => 50, 'financial_type_id' => $this->_financialTypeId, 'contribution_status_id' => 'Pending', 'tax_amount' => 15, 'line_items' => [ 0 => [ 'line_item' => [ '0' => [ 'price_field_id' => 1, 'price_field_value_id' => 1, 'label' => 'Test 1', 'field_title' => 'Test 1', 'qty' => 1, 'unit_price' => 30, 'line_total' => 30, 'financial_type_id' => 1, 'entity_table' => 'civicrm_contribution', 'tax_amount' => 15, ], ], ], ], ]; $this->callAPIFailure('Order', 'create', $params, "Line item total doesn't match total amount."); } /** * @throws \CRM_Core_Exception */ public function testCreateOrderIfTotalAmountDoesMatchLineItemsAmountsAndTaxSupplied(): void { $this->enableTaxAndInvoicing(); $this->createFinancialTypeWithSalesTax(); $params = [ 'contact_id' => $this->ids['Contact']['individual_0'], 'receive_date' => '2018-01-01', 'total_amount' => 36.75, 'financial_type_id' => $this->_financialTypeId, 'contribution_status_id' => 'Pending', 'tax_amount' => 1.75, 'line_items' => [ 0 => [ 'line_item' => [ '0' => [ 'price_field_id' => 1, 'price_field_value_id' => 1, 'label' => 'Test 1', 'field_title' => 'Test 1', 'qty' => 1, 'unit_price' => 35, 'line_total' => 35, 'financial_type_id' => $this->ids['FinancialType']['taxable'], 'entity_table' => 'civicrm_contribution', 'tax_amount' => 1.75, ], ], ], ], ]; $order = $this->callAPISuccess('Order', 'create', $params); $this->assertEquals(1, $order['count']); } /** * Test that a contribution can be added in pending mode with a chained * payment. * * We have just deprecated creating an order with a status other than * pending. It makes sense to support adding a payment straight away by * chaining. */ public function testCreateWithChainedPayment(): void { $contributionID = $this->callAPISuccess('Order', 'create', ['contact_id' => $this->ids['Contact']['individual_0'], 'total_amount' => 5, 'financial_type_id' => 2, 'contribution_status_id' => 'Pending', 'api.Payment.create' => ['total_amount' => 5]])['id']; $this->assertEquals('Completed', $this->callAPISuccessGetValue('Contribution', ['id' => $contributionID, 'return' => 'contribution_status'])); } /** * Test creating an order with a mixture of taxable & non-taxable. * * @throws \CRM_Core_Exception */ public function testOrderWithMixedTax(): void { $this->enableTaxAndInvoicing(); $this->createFinancialTypeWithSalesTax('woo', [], ['tax_rate' => 19.3791]); $membershipTypeID = $this->membershipTypeCreate(); $contactID = $this->individualCreate(); $this->callAPISuccess('Order', 'create', [ 'contact_id' => $contactID, 'financial_type_id' => $this->ids['FinancialType']['woo'], 'payment_instrument_id' => 4, 'trxn_id' => 'WooCommerce Order - 1859', 'invoice_id' => '1859_woocommerce', 'receive_date' => '2021-05-05 23:24:02', 'contribution_status_id' => 'Pending', 'source' => 'Shop', 'note' => 'Fundraiser Dinner Ticket x 1, Student Membership x 1', 'line_items' => [ [ 'line_item' => [ [ 'price_field_id' => 1, 'unit_price' => 50.00, 'qty' => 1, 'line_total' => 50.00, 'tax_amount' => 9.69, 'label' => 'Fundraiser Dinner Ticket', 'financial_type_id' => $this->ids['FinancialType']['woo'], ], ], ], [ 'line_item' => [ [ 'price_field_id' => 1, 'unit_price' => 50.00, 'qty' => 1, 'line_total' => 50.00, 'tax_amount' => 0.00, 'label' => 'Student Membership', 'financial_type_id' => 2, 'entity_table' => 'civicrm_membership', 'membership_type_id' => $membershipTypeID, ], ], 'params' => [ 'membership_type_id' => $membershipTypeID, 'source' => 'Shop', 'contact_id' => $contactID, 'skipStatusCal' => 1, 'status_id' => 'Pending', ], ], ], 'tax_amount' => 9.69, ]); $contribution = Contribution::get(FALSE) ->addWhere('trxn_id', '=', 'WooCommerce Order - 1859') ->setSelect(['tax_amount', 'total_amount'])->execute()->first(); $this->assertEquals('9.69', $contribution['tax_amount']); $this->assertEquals('109.69', $contribution['total_amount']); Contribution::update()->setValues([ 'source' => 'new one', 'financial_type_id' => $this->ids['FinancialType']['woo'], ])->addWhere('id', '=', $contribution['id'])->execute(); $contribution = Contribution::get(FALSE) ->addWhere('trxn_id', '=', 'WooCommerce Order - 1859') ->setSelect(['tax_amount', 'total_amount'])->execute()->first(); $this->assertEquals('9.69', $contribution['tax_amount']); $this->assertEquals('109.69', $contribution['total_amount']); } }