order-taxes.e2e-spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. /* tslint:disable:no-non-null-assertion */
  2. import { summate } from '@vendure/common/lib/shared-utils';
  3. import {
  4. Channel,
  5. Injector,
  6. Order,
  7. RequestContext,
  8. TaxZoneStrategy,
  9. TransactionalConnection,
  10. Zone,
  11. } from '@vendure/core';
  12. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  13. import gql from 'graphql-tag';
  14. import path from 'path';
  15. import { initialData } from '../../../e2e-common/e2e-initial-data';
  16. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  17. import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
  18. import {
  19. GetProductsWithVariantPrices,
  20. GetTaxRateList,
  21. UpdateChannel,
  22. UpdateTaxRate,
  23. } from './graphql/generated-e2e-admin-types';
  24. import {
  25. AddItemToOrder,
  26. GetActiveOrderWithPriceData,
  27. SetBillingAddress,
  28. SetShippingAddress,
  29. TestOrderFragmentFragment,
  30. UpdatedOrderFragment,
  31. } from './graphql/generated-e2e-shop-types';
  32. import {
  33. GET_PRODUCTS_WITH_VARIANT_PRICES,
  34. UPDATE_CHANNEL,
  35. UPDATE_TAX_RATE,
  36. } from './graphql/shared-definitions';
  37. import {
  38. ADD_ITEM_TO_ORDER,
  39. GET_ACTIVE_ORDER_WITH_PRICE_DATA,
  40. SET_BILLING_ADDRESS,
  41. SET_SHIPPING_ADDRESS,
  42. } from './graphql/shop-definitions';
  43. import { sortById } from './utils/test-order-utils';
  44. /**
  45. * Determines active tax zone based on:
  46. *
  47. * 1. billingAddress country, if set
  48. * 2. else shippingAddress country, is set
  49. * 3. else channel default tax zone.
  50. */
  51. class TestTaxZoneStrategy implements TaxZoneStrategy {
  52. private connection: TransactionalConnection;
  53. init(injector: Injector): void | Promise<void> {
  54. this.connection = injector.get(TransactionalConnection);
  55. }
  56. async determineTaxZone(
  57. ctx: RequestContext,
  58. zones: Zone[],
  59. channel: Channel,
  60. order?: Order,
  61. ): Promise<Zone> {
  62. if (!order?.billingAddress?.countryCode && !order?.shippingAddress?.countryCode) {
  63. return channel.defaultTaxZone;
  64. }
  65. const countryCode = order?.billingAddress?.countryCode || order?.shippingAddress?.countryCode;
  66. const zoneForCountryCode = await this.getZoneForCountryCode(ctx, countryCode);
  67. return zoneForCountryCode ?? channel.defaultTaxZone;
  68. }
  69. private getZoneForCountryCode(ctx: RequestContext, countryCode: string): Promise<Zone | undefined> {
  70. return this.connection
  71. .getRepository(ctx, Zone)
  72. .createQueryBuilder('zone')
  73. .leftJoin('zone.members', 'country')
  74. .where('country.code = :countryCode', {
  75. countryCode,
  76. })
  77. .getOne();
  78. }
  79. }
  80. describe('Order taxes', () => {
  81. const { server, adminClient, shopClient } = createTestEnvironment({
  82. ...testConfig(),
  83. taxOptions: {
  84. taxZoneStrategy: new TestTaxZoneStrategy(),
  85. },
  86. paymentOptions: {
  87. paymentMethodHandlers: [testSuccessfulPaymentMethod],
  88. },
  89. });
  90. type OrderSuccessResult = UpdatedOrderFragment | TestOrderFragmentFragment;
  91. const orderResultGuard: ErrorResultGuard<OrderSuccessResult> = createErrorResultGuard(
  92. input => !!input.lines,
  93. );
  94. let products: GetProductsWithVariantPrices.Items[];
  95. beforeAll(async () => {
  96. await server.init({
  97. initialData: {
  98. ...initialData,
  99. paymentMethods: [
  100. {
  101. name: testSuccessfulPaymentMethod.code,
  102. handler: { code: testSuccessfulPaymentMethod.code, arguments: [] },
  103. },
  104. ],
  105. },
  106. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-order-taxes.csv'),
  107. customerCount: 2,
  108. });
  109. await adminClient.asSuperAdmin();
  110. const result = await adminClient.query<GetProductsWithVariantPrices.Query>(
  111. GET_PRODUCTS_WITH_VARIANT_PRICES,
  112. );
  113. products = result.products.items;
  114. }, TEST_SETUP_TIMEOUT_MS);
  115. afterAll(async () => {
  116. await server.destroy();
  117. });
  118. describe('Channel.pricesIncludeTax = false', () => {
  119. beforeAll(async () => {
  120. await adminClient.query<UpdateChannel.Mutation, UpdateChannel.Variables>(UPDATE_CHANNEL, {
  121. input: {
  122. id: 'T_1',
  123. pricesIncludeTax: false,
  124. },
  125. });
  126. await shopClient.asAnonymousUser();
  127. });
  128. it('prices are correct', async () => {
  129. const variant = products.sort(sortById)[0].variants.sort(sortById)[0];
  130. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  131. productVariantId: variant.id,
  132. quantity: 2,
  133. });
  134. const { activeOrder } = await shopClient.query<GetActiveOrderWithPriceData.Query>(
  135. GET_ACTIVE_ORDER_WITH_PRICE_DATA,
  136. );
  137. expect(activeOrder?.totalWithTax).toBe(240);
  138. expect(activeOrder?.total).toBe(200);
  139. expect(activeOrder?.lines[0].taxRate).toBe(20);
  140. expect(activeOrder?.lines[0].linePrice).toBe(200);
  141. expect(activeOrder?.lines[0].lineTax).toBe(40);
  142. expect(activeOrder?.lines[0].linePriceWithTax).toBe(240);
  143. expect(activeOrder?.lines[0].unitPrice).toBe(100);
  144. expect(activeOrder?.lines[0].unitPriceWithTax).toBe(120);
  145. expect(activeOrder?.lines[0].items[0].unitPrice).toBe(100);
  146. expect(activeOrder?.lines[0].items[0].unitPriceWithTax).toBe(120);
  147. expect(activeOrder?.lines[0].items[0].taxRate).toBe(20);
  148. expect(activeOrder?.lines[0].taxLines).toEqual([
  149. {
  150. description: 'Standard Tax Europe',
  151. taxRate: 20,
  152. },
  153. ]);
  154. });
  155. });
  156. describe('Channel.pricesIncludeTax = true', () => {
  157. beforeAll(async () => {
  158. await adminClient.query<UpdateChannel.Mutation, UpdateChannel.Variables>(UPDATE_CHANNEL, {
  159. input: {
  160. id: 'T_1',
  161. pricesIncludeTax: true,
  162. },
  163. });
  164. await shopClient.asAnonymousUser();
  165. });
  166. it('prices are correct', async () => {
  167. const variant = products[0].variants[0];
  168. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  169. productVariantId: variant.id,
  170. quantity: 2,
  171. });
  172. const { activeOrder } = await shopClient.query<GetActiveOrderWithPriceData.Query>(
  173. GET_ACTIVE_ORDER_WITH_PRICE_DATA,
  174. );
  175. expect(activeOrder?.totalWithTax).toBe(200);
  176. expect(activeOrder?.total).toBe(166);
  177. expect(activeOrder?.lines[0].taxRate).toBe(20);
  178. expect(activeOrder?.lines[0].linePrice).toBe(166);
  179. expect(activeOrder?.lines[0].lineTax).toBe(34);
  180. expect(activeOrder?.lines[0].linePriceWithTax).toBe(200);
  181. expect(activeOrder?.lines[0].unitPrice).toBe(83);
  182. expect(activeOrder?.lines[0].unitPriceWithTax).toBe(100);
  183. expect(activeOrder?.lines[0].items[0].unitPrice).toBe(83);
  184. expect(activeOrder?.lines[0].items[0].unitPriceWithTax).toBe(100);
  185. expect(activeOrder?.lines[0].items[0].taxRate).toBe(20);
  186. expect(activeOrder?.lines[0].taxLines).toEqual([
  187. {
  188. description: 'Standard Tax Europe',
  189. taxRate: 20,
  190. },
  191. ]);
  192. });
  193. // https://github.com/vendure-ecommerce/vendure/issues/1216
  194. it('re-calculates OrderItem prices when shippingAddress causes activeTaxZone change', async () => {
  195. const { taxRates } = await adminClient.query<GetTaxRateList.Query>(GET_TAX_RATE_LIST);
  196. // Set the TaxRates to Asia to 0%
  197. const taxRatesAsia = taxRates.items.filter(tr => tr.name.includes('Asia'));
  198. for (const taxRate of taxRatesAsia) {
  199. await adminClient.query<UpdateTaxRate.Mutation, UpdateTaxRate.Variables>(UPDATE_TAX_RATE, {
  200. input: {
  201. id: taxRate.id,
  202. value: 0,
  203. },
  204. });
  205. }
  206. await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(
  207. SET_SHIPPING_ADDRESS,
  208. {
  209. input: {
  210. countryCode: 'CN',
  211. streetLine1: '123 Lugu St',
  212. city: 'Beijing',
  213. province: 'Beijing',
  214. postalCode: '12340',
  215. },
  216. },
  217. );
  218. const { activeOrder } = await shopClient.query<GetActiveOrderWithPriceData.Query>(
  219. GET_ACTIVE_ORDER_WITH_PRICE_DATA,
  220. );
  221. expect(activeOrder?.totalWithTax).toBe(166);
  222. expect(activeOrder?.total).toBe(166);
  223. expect(activeOrder?.lines[0].taxRate).toBe(0);
  224. expect(activeOrder?.lines[0].linePrice).toBe(166);
  225. expect(activeOrder?.lines[0].lineTax).toBe(0);
  226. expect(activeOrder?.lines[0].linePriceWithTax).toBe(166);
  227. expect(activeOrder?.lines[0].unitPrice).toBe(83);
  228. expect(activeOrder?.lines[0].unitPriceWithTax).toBe(83);
  229. expect(activeOrder?.lines[0].items[0].unitPrice).toBe(83);
  230. expect(activeOrder?.lines[0].items[0].unitPriceWithTax).toBe(83);
  231. expect(activeOrder?.lines[0].items[0].taxRate).toBe(0);
  232. expect(activeOrder?.lines[0].taxLines).toEqual([
  233. {
  234. description: 'Standard Tax Asia',
  235. taxRate: 0,
  236. },
  237. ]);
  238. });
  239. // https://github.com/vendure-ecommerce/vendure/issues/1216
  240. it('re-calculates OrderItem prices when billingAddress causes activeTaxZone change', async () => {
  241. await shopClient.query<SetBillingAddress.Mutation, SetBillingAddress.Variables>(
  242. SET_BILLING_ADDRESS,
  243. {
  244. input: {
  245. countryCode: 'US',
  246. streetLine1: '123 Chad Street',
  247. city: 'Houston',
  248. province: 'Texas',
  249. postalCode: '12345',
  250. },
  251. },
  252. );
  253. const { activeOrder } = await shopClient.query<GetActiveOrderWithPriceData.Query>(
  254. GET_ACTIVE_ORDER_WITH_PRICE_DATA,
  255. );
  256. expect(activeOrder?.totalWithTax).toBe(200);
  257. expect(activeOrder?.total).toBe(166);
  258. expect(activeOrder?.lines[0].taxRate).toBe(20);
  259. expect(activeOrder?.lines[0].linePrice).toBe(166);
  260. expect(activeOrder?.lines[0].lineTax).toBe(34);
  261. expect(activeOrder?.lines[0].linePriceWithTax).toBe(200);
  262. expect(activeOrder?.lines[0].unitPrice).toBe(83);
  263. expect(activeOrder?.lines[0].unitPriceWithTax).toBe(100);
  264. expect(activeOrder?.lines[0].items[0].unitPrice).toBe(83);
  265. expect(activeOrder?.lines[0].items[0].unitPriceWithTax).toBe(100);
  266. expect(activeOrder?.lines[0].items[0].taxRate).toBe(20);
  267. expect(activeOrder?.lines[0].taxLines).toEqual([
  268. {
  269. description: 'Standard Tax Americas',
  270. taxRate: 20,
  271. },
  272. ]);
  273. });
  274. });
  275. it('taxSummary works', async () => {
  276. await adminClient.query<UpdateChannel.Mutation, UpdateChannel.Variables>(UPDATE_CHANNEL, {
  277. input: {
  278. id: 'T_1',
  279. pricesIncludeTax: false,
  280. },
  281. });
  282. await shopClient.asAnonymousUser();
  283. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  284. productVariantId: products[0].variants[0].id,
  285. quantity: 2,
  286. });
  287. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  288. productVariantId: products[1].variants[0].id,
  289. quantity: 2,
  290. });
  291. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  292. productVariantId: products[2].variants[0].id,
  293. quantity: 2,
  294. });
  295. const { activeOrder } = await shopClient.query<GetActiveOrderWithPriceData.Query>(
  296. GET_ACTIVE_ORDER_WITH_PRICE_DATA,
  297. );
  298. expect(activeOrder?.taxSummary).toEqual([
  299. {
  300. description: 'Standard Tax Europe',
  301. taxRate: 20,
  302. taxBase: 200,
  303. taxTotal: 40,
  304. },
  305. {
  306. description: 'Reduced Tax Europe',
  307. taxRate: 10,
  308. taxBase: 200,
  309. taxTotal: 20,
  310. },
  311. {
  312. description: 'Zero Tax Europe',
  313. taxRate: 0,
  314. taxBase: 200,
  315. taxTotal: 0,
  316. },
  317. ]);
  318. // ensure that the summary total add up to the overall totals
  319. const taxSummaryBaseTotal = summate(activeOrder!.taxSummary, 'taxBase');
  320. const taxSummaryTaxTotal = summate(activeOrder!.taxSummary, 'taxTotal');
  321. expect(taxSummaryBaseTotal).toBe(activeOrder?.total);
  322. expect(taxSummaryBaseTotal + taxSummaryTaxTotal).toBe(activeOrder?.totalWithTax);
  323. });
  324. });
  325. export const GET_TAX_RATE_LIST = gql`
  326. query GetTaxRateList($options: TaxRateListOptions) {
  327. taxRates(options: $options) {
  328. items {
  329. id
  330. name
  331. enabled
  332. value
  333. category {
  334. id
  335. name
  336. }
  337. zone {
  338. id
  339. name
  340. }
  341. }
  342. totalItems
  343. }
  344. }
  345. `;