Skip to content

Commit

Permalink
fix(core): Correctly set OrderItem prices on tax zone change
Browse files Browse the repository at this point in the history
Fixes #1216
  • Loading branch information
michaelbromley committed Nov 10, 2021
1 parent c474d93 commit 731f8d9
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 31 deletions.
4 changes: 2 additions & 2 deletions e2e-common/test-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ if (process.env.E2E_DEBUG) {
// tslint:disable-next-line:no-console
console.log('E2E_DEBUG', process.env.E2E_DEBUG, ' - setting long timeout');
jest.setTimeout(1800 * 1000);
} else {
jest.setTimeout(15 * 1000);
}
/**
* Increase default timeout in CI because occasionally valid tests fail due to
* timeouts.
*/
if (process.env.CI) {
jest.setTimeout(30 * 1000);
} else {
jest.setTimeout(15 * 1000);
}

export const testConfig = () => {
Expand Down
30 changes: 30 additions & 0 deletions packages/core/e2e/graphql/generated-e2e-admin-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6405,6 +6405,21 @@ export type DeletePromotionAdHoc1MutationVariables = Exact<{ [key: string]: neve

export type DeletePromotionAdHoc1Mutation = { deletePromotion: Pick<DeletionResponse, 'result'> };

export type GetTaxRateListQueryVariables = Exact<{
options?: Maybe<TaxRateListOptions>;
}>;

export type GetTaxRateListQuery = {
taxRates: Pick<TaxRateList, 'totalItems'> & {
items: Array<
Pick<TaxRate, 'id' | 'name' | 'enabled' | 'value'> & {
category: Pick<TaxCategory, 'id' | 'name'>;
zone: Pick<Zone, 'id' | 'name'>;
}
>;
};
};

export type GetOrderListFulfillmentsQueryVariables = Exact<{ [key: string]: never }>;

export type GetOrderListFulfillmentsQuery = {
Expand Down Expand Up @@ -8738,6 +8753,21 @@ export namespace DeletePromotionAdHoc1 {
export type DeletePromotion = NonNullable<DeletePromotionAdHoc1Mutation['deletePromotion']>;
}

export namespace GetTaxRateList {
export type Variables = GetTaxRateListQueryVariables;
export type Query = GetTaxRateListQuery;
export type TaxRates = NonNullable<GetTaxRateListQuery['taxRates']>;
export type Items = NonNullable<
NonNullable<NonNullable<GetTaxRateListQuery['taxRates']>['items']>[number]
>;
export type Category = NonNullable<
NonNullable<NonNullable<NonNullable<GetTaxRateListQuery['taxRates']>['items']>[number]>['category']
>;
export type Zone = NonNullable<
NonNullable<NonNullable<NonNullable<GetTaxRateListQuery['taxRates']>['items']>[number]>['zone']
>;
}

export namespace GetOrderListFulfillments {
export type Variables = GetOrderListFulfillmentsQueryVariables;
export type Query = GetOrderListFulfillmentsQuery;
Expand Down
184 changes: 181 additions & 3 deletions packages/core/e2e/order-taxes.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,96 @@
/* tslint:disable:no-non-null-assertion */
import { summate } from '@vendure/common/lib/shared-utils';
import {
Channel,
Injector,
Order,
RequestContext,
TaxZoneStrategy,
TransactionalConnection,
Zone,
} from '@vendure/core';
import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
import gql from 'graphql-tag';
import path from 'path';

import { initialData } from '../../../e2e-common/e2e-initial-data';
import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';

import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
import { GetProductsWithVariantPrices, UpdateChannel } from './graphql/generated-e2e-admin-types';
import {
GetProductsWithVariantPrices,
GetTaxRateList,
UpdateChannel,
UpdateTaxRate,
} from './graphql/generated-e2e-admin-types';
import {
AddItemToOrder,
GetActiveOrderWithPriceData,
SetBillingAddress,
SetShippingAddress,
TestOrderFragmentFragment,
UpdatedOrderFragment,
} from './graphql/generated-e2e-shop-types';
import { GET_PRODUCTS_WITH_VARIANT_PRICES, UPDATE_CHANNEL } from './graphql/shared-definitions';
import { ADD_ITEM_TO_ORDER, GET_ACTIVE_ORDER_WITH_PRICE_DATA } from './graphql/shop-definitions';
import {
GET_PRODUCTS_WITH_VARIANT_PRICES,
UPDATE_CHANNEL,
UPDATE_TAX_RATE,
} from './graphql/shared-definitions';
import {
ADD_ITEM_TO_ORDER,
GET_ACTIVE_ORDER_WITH_PRICE_DATA,
SET_BILLING_ADDRESS,
SET_SHIPPING_ADDRESS,
} from './graphql/shop-definitions';
import { sortById } from './utils/test-order-utils';

/**
* Determines active tax zone based on:
*
* 1. billingAddress country, if set
* 2. else shippingAddress country, is set
* 3. else channel default tax zone.
*/
class TestTaxZoneStrategy implements TaxZoneStrategy {
private connection: TransactionalConnection;

init(injector: Injector): void | Promise<void> {
this.connection = injector.get(TransactionalConnection);
}

async determineTaxZone(
ctx: RequestContext,
zones: Zone[],
channel: Channel,
order?: Order,
): Promise<Zone> {
if (!order?.billingAddress?.countryCode && !order?.shippingAddress?.countryCode) {
return channel.defaultTaxZone;
}

const countryCode = order?.billingAddress?.countryCode || order?.shippingAddress?.countryCode;
const zoneForCountryCode = await this.getZoneForCountryCode(ctx, countryCode);
return zoneForCountryCode ?? channel.defaultTaxZone;
}

private getZoneForCountryCode(ctx: RequestContext, countryCode: string): Promise<Zone | undefined> {
return this.connection
.getRepository(ctx, Zone)
.createQueryBuilder('zone')
.leftJoin('zone.members', 'country')
.where('country.code = :countryCode', {
countryCode,
})
.getOne();
}
}

describe('Order taxes', () => {
const { server, adminClient, shopClient } = createTestEnvironment({
...testConfig(),
taxOptions: {
taxZoneStrategy: new TestTaxZoneStrategy(),
},
paymentOptions: {
paymentMethodHandlers: [testSuccessfulPaymentMethod],
},
Expand Down Expand Up @@ -137,6 +207,92 @@ describe('Order taxes', () => {
},
]);
});

// https://github.com/vendure-ecommerce/vendure/issues/1216
it('re-calculates OrderItem prices when shippingAddress causes activeTaxZone change', async () => {
const { taxRates } = await adminClient.query<GetTaxRateList.Query>(GET_TAX_RATE_LIST);
// Set the TaxRates to Asia to 0%
const taxRatesAsia = taxRates.items.filter(tr => tr.name.includes('Asia'));
for (const taxRate of taxRatesAsia) {
await adminClient.query<UpdateTaxRate.Mutation, UpdateTaxRate.Variables>(UPDATE_TAX_RATE, {
input: {
id: taxRate.id,
value: 0,
},
});
}

await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(
SET_SHIPPING_ADDRESS,
{
input: {
countryCode: 'CN',
streetLine1: '123 Lugu St',
city: 'Beijing',
province: 'Beijing',
postalCode: '12340',
},
},
);

const { activeOrder } = await shopClient.query<GetActiveOrderWithPriceData.Query>(
GET_ACTIVE_ORDER_WITH_PRICE_DATA,
);
expect(activeOrder?.totalWithTax).toBe(166);
expect(activeOrder?.total).toBe(166);
expect(activeOrder?.lines[0].taxRate).toBe(0);
expect(activeOrder?.lines[0].linePrice).toBe(166);
expect(activeOrder?.lines[0].lineTax).toBe(0);
expect(activeOrder?.lines[0].linePriceWithTax).toBe(166);
expect(activeOrder?.lines[0].unitPrice).toBe(83);
expect(activeOrder?.lines[0].unitPriceWithTax).toBe(83);
expect(activeOrder?.lines[0].items[0].unitPrice).toBe(83);
expect(activeOrder?.lines[0].items[0].unitPriceWithTax).toBe(83);
expect(activeOrder?.lines[0].items[0].taxRate).toBe(0);
expect(activeOrder?.lines[0].taxLines).toEqual([
{
description: 'Standard Tax Asia',
taxRate: 0,
},
]);
});

// https://github.com/vendure-ecommerce/vendure/issues/1216
it('re-calculates OrderItem prices when billingAddress causes activeTaxZone change', async () => {
await shopClient.query<SetBillingAddress.Mutation, SetBillingAddress.Variables>(
SET_BILLING_ADDRESS,
{
input: {
countryCode: 'US',
streetLine1: '123 Chad Street',
city: 'Houston',
province: 'Texas',
postalCode: '12345',
},
},
);

const { activeOrder } = await shopClient.query<GetActiveOrderWithPriceData.Query>(
GET_ACTIVE_ORDER_WITH_PRICE_DATA,
);
expect(activeOrder?.totalWithTax).toBe(200);
expect(activeOrder?.total).toBe(166);
expect(activeOrder?.lines[0].taxRate).toBe(20);
expect(activeOrder?.lines[0].linePrice).toBe(166);
expect(activeOrder?.lines[0].lineTax).toBe(34);
expect(activeOrder?.lines[0].linePriceWithTax).toBe(200);
expect(activeOrder?.lines[0].unitPrice).toBe(83);
expect(activeOrder?.lines[0].unitPriceWithTax).toBe(100);
expect(activeOrder?.lines[0].items[0].unitPrice).toBe(83);
expect(activeOrder?.lines[0].items[0].unitPriceWithTax).toBe(100);
expect(activeOrder?.lines[0].items[0].taxRate).toBe(20);
expect(activeOrder?.lines[0].taxLines).toEqual([
{
description: 'Standard Tax Americas',
taxRate: 20,
},
]);
});
});

it('taxSummary works', async () => {
Expand Down Expand Up @@ -193,3 +349,25 @@ describe('Order taxes', () => {
expect(taxSummaryBaseTotal + taxSummaryTaxTotal).toBe(activeOrder?.totalWithTax);
});
});

export const GET_TAX_RATE_LIST = gql`
query GetTaxRateList($options: TaxRateListOptions) {
taxRates(options: $options) {
items {
id
name
enabled
value
category {
id
name
}
zone {
id
name
}
}
totalItems
}
}
`;
2 changes: 2 additions & 0 deletions packages/core/e2e/utils/assert-throws-with-message.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { fail } from 'assert';

/**
* Helper method for creating tests which assert a given error message when the operation is attempted.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class ProductPriceApplicator {
}
const { taxZoneStrategy } = this.configService.taxOptions;
const zones = await this.requestCache.get(ctx, 'allZones', () => this.zoneService.findAll(ctx));
const activeTaxZone = await this.requestCache.get(ctx, 'activeTaxZone', () =>
const activeTaxZone = await this.requestCache.get(ctx, `activeTaxZone`, () =>
taxZoneStrategy.determineTaxZone(ctx, zones, ctx.channel, order),
);
if (!activeTaxZone) {
Expand Down
Loading

0 comments on commit 731f8d9

Please sign in to comment.