| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599 |
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
- import { pick } from '@vendure/common/lib/pick';
- import { mergeConfig } from '@vendure/core';
- import {
- createErrorResultGuard,
- createTestEnvironment,
- E2E_DEFAULT_CHANNEL_TOKEN,
- ErrorResultGuard,
- } from '@vendure/testing';
- import path from 'path';
- import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
- import { initialData } from '../../../e2e-common/e2e-initial-data';
- import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
- import { ProductVariantPrice, ProductVariantPriceUpdateStrategy, RequestContext } from '../src/index';
- import * as Codegen from './graphql/generated-e2e-admin-types';
- import {
- AssignProductsToChannelDocument,
- CreateChannelDocument,
- CreateProductDocument,
- CreateProductVariantsDocument,
- CurrencyCode,
- GetProductWithVariantsDocument,
- LanguageCode,
- UpdateChannelDocument,
- UpdateProductVariantsDocument,
- } from './graphql/generated-e2e-admin-types';
- import {
- AddItemToOrderDocument,
- AdjustItemQuantityDocument,
- GetActiveOrderDocument,
- TestOrderFragmentFragment,
- UpdatedOrderFragment,
- } from './graphql/generated-e2e-shop-types';
- import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
- class TestProductVariantPriceUpdateStrategy implements ProductVariantPriceUpdateStrategy {
- static syncAcrossChannels = false;
- static onCreatedSpy = vi.fn();
- static onUpdatedSpy = vi.fn();
- static onDeletedSpy = vi.fn();
- onPriceCreated(ctx: RequestContext, price: ProductVariantPrice, prices: ProductVariantPrice[]) {
- TestProductVariantPriceUpdateStrategy.onCreatedSpy(price, prices);
- return [];
- }
- onPriceUpdated(ctx: RequestContext, updatedPrice: ProductVariantPrice, prices: ProductVariantPrice[]) {
- TestProductVariantPriceUpdateStrategy.onUpdatedSpy(updatedPrice, prices);
- if (TestProductVariantPriceUpdateStrategy.syncAcrossChannels) {
- return prices
- .filter(p => p.currencyCode === updatedPrice.currencyCode)
- .map(p => ({
- id: p.id,
- price: updatedPrice.price,
- }));
- } else {
- return [];
- }
- }
- onPriceDeleted(ctx: RequestContext, deletedPrice: ProductVariantPrice, prices: ProductVariantPrice[]) {
- TestProductVariantPriceUpdateStrategy.onDeletedSpy(deletedPrice, prices);
- return [];
- }
- }
- describe('Product prices', () => {
- const { server, adminClient, shopClient } = createTestEnvironment(
- mergeConfig(
- { ...testConfig() },
- {
- catalogOptions: {
- productVariantPriceUpdateStrategy: new TestProductVariantPriceUpdateStrategy(),
- },
- },
- ),
- );
- let multiPriceProduct: Codegen.CreateProductMutation['createProduct'];
- let multiPriceVariant: NonNullable<
- Codegen.CreateProductVariantsMutation['createProductVariants'][number]
- >;
- const orderResultGuard: ErrorResultGuard<TestOrderFragmentFragment | UpdatedOrderFragment> =
- createErrorResultGuard(input => !!input.lines);
- const createChannelResultGuard: ErrorResultGuard<{ id: string }> = createErrorResultGuard(
- input => !!input.id,
- );
- beforeAll(async () => {
- await server.init({
- initialData,
- customerCount: 1,
- productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
- });
- await adminClient.asSuperAdmin();
- await adminClient.query(UpdateChannelDocument, {
- input: {
- id: 'T_1',
- availableCurrencyCodes: [CurrencyCode.USD, CurrencyCode.GBP, CurrencyCode.EUR],
- },
- });
- const { createProduct } = await adminClient.query(CreateProductDocument, {
- input: {
- translations: [
- {
- languageCode: LanguageCode.en,
- name: 'Cactus',
- slug: 'cactus',
- description: 'A prickly plant',
- },
- ],
- },
- });
- multiPriceProduct = createProduct;
- }, TEST_SETUP_TIMEOUT_MS);
- afterAll(async () => {
- await server.destroy();
- });
- it('create ProductVariant creates price in Channel default currency', async () => {
- const { createProductVariants } = await adminClient.query(CreateProductVariantsDocument, {
- input: [
- {
- productId: multiPriceProduct.id,
- sku: 'CACTUS-1',
- optionIds: [],
- translations: [{ languageCode: LanguageCode.de, name: 'Cactus' }],
- price: 1000,
- },
- ],
- });
- expect(createProductVariants.length).toBe(1);
- expect(createProductVariants[0]?.prices).toEqual([
- {
- currencyCode: CurrencyCode.USD,
- price: 1000,
- },
- ]);
- multiPriceVariant = createProductVariants[0]!;
- });
- it(
- 'updating ProductVariant with price in unavailable currency throws',
- assertThrowsWithMessage(async () => {
- await adminClient.query(UpdateProductVariantsDocument, {
- input: {
- id: multiPriceVariant.id,
- prices: [
- {
- currencyCode: CurrencyCode.JPY,
- price: 100000,
- },
- ],
- },
- });
- }, 'The currency "JPY" is not available in the current Channel'),
- );
- it('updates ProductVariant with multiple prices', async () => {
- await adminClient.query(UpdateProductVariantsDocument, {
- input: {
- id: multiPriceVariant.id,
- prices: [
- { currencyCode: CurrencyCode.USD, price: 1200 },
- { currencyCode: CurrencyCode.GBP, price: 900 },
- { currencyCode: CurrencyCode.EUR, price: 1100 },
- ],
- },
- });
- const { product } = await adminClient.query(GetProductWithVariantsDocument, {
- id: multiPriceProduct.id,
- });
- expect(product?.variants[0]?.prices.sort((a, b) => a.price - b.price)).toEqual([
- { currencyCode: CurrencyCode.GBP, price: 900 },
- { currencyCode: CurrencyCode.EUR, price: 1100 },
- { currencyCode: CurrencyCode.USD, price: 1200 },
- ]);
- });
- it('deletes a price in a non-default currency', async () => {
- await adminClient.query(UpdateProductVariantsDocument, {
- input: {
- id: multiPriceVariant.id,
- prices: [{ currencyCode: CurrencyCode.EUR, price: 1100, delete: true }],
- },
- });
- const { product } = await adminClient.query(GetProductWithVariantsDocument, {
- id: multiPriceProduct.id,
- });
- expect(product?.variants[0]?.prices.sort((a, b) => a.price - b.price)).toEqual([
- { currencyCode: CurrencyCode.GBP, price: 900 },
- { currencyCode: CurrencyCode.USD, price: 1200 },
- ]);
- });
- describe('DefaultProductVariantPriceSelectionStrategy', () => {
- it('defaults to default Channel currency', async () => {
- const { product } = await adminClient.query(GetProductWithVariantsDocument, {
- id: multiPriceProduct.id,
- });
- expect(product?.variants[0]?.price).toEqual(1200);
- expect(product?.variants[0]?.priceWithTax).toEqual(1200 * 1.2);
- expect(product?.variants[0]?.currencyCode).toEqual(CurrencyCode.USD);
- });
- it('uses query string to select currency', async () => {
- const { product } = await adminClient.query(
- GetProductWithVariantsDocument,
- {
- id: multiPriceProduct.id,
- },
- { currencyCode: 'GBP' },
- );
- expect(product?.variants[0]?.price).toEqual(900);
- expect(product?.variants[0]?.priceWithTax).toEqual(900 * 1.2);
- expect(product?.variants[0]?.currencyCode).toEqual(CurrencyCode.GBP);
- });
- it(
- 'throws if unrecognised currency code passed in query string',
- assertThrowsWithMessage(async () => {
- await adminClient.query(
- GetProductWithVariantsDocument,
- {
- id: multiPriceProduct.id,
- },
- { currencyCode: 'JPY' },
- );
- }, 'The currency "JPY" is not available in the current Channel'),
- );
- });
- describe('changing Order currencyCode', () => {
- beforeAll(async () => {
- await adminClient.query(UpdateProductVariantsDocument, {
- input: [
- {
- id: 'T_1',
- prices: [
- { currencyCode: CurrencyCode.USD, price: 1000 },
- { currencyCode: CurrencyCode.GBP, price: 900 },
- { currencyCode: CurrencyCode.EUR, price: 1100 },
- ],
- },
- {
- id: 'T_2',
- prices: [
- { currencyCode: CurrencyCode.USD, price: 2000 },
- { currencyCode: CurrencyCode.GBP, price: 1900 },
- { currencyCode: CurrencyCode.EUR, price: 2100 },
- ],
- },
- {
- id: 'T_3',
- prices: [
- { currencyCode: CurrencyCode.USD, price: 3000 },
- { currencyCode: CurrencyCode.GBP, price: 2900 },
- { currencyCode: CurrencyCode.EUR, price: 3100 },
- ],
- },
- ],
- });
- });
- it('create order in default currency', async () => {
- await shopClient.query(AddItemToOrderDocument, {
- productVariantId: 'T_1',
- quantity: 1,
- });
- await shopClient.query(AddItemToOrderDocument, {
- productVariantId: 'T_2',
- quantity: 1,
- });
- const { activeOrder } = await shopClient.query(GetActiveOrderDocument);
- expect(activeOrder?.lines[0]?.unitPrice).toBe(1000);
- expect(activeOrder?.lines[0]?.unitPriceWithTax).toBe(1200);
- expect(activeOrder?.lines[1]?.unitPrice).toBe(2000);
- expect(activeOrder?.lines[1]?.unitPriceWithTax).toBe(2400);
- expect(activeOrder?.currencyCode).toBe(CurrencyCode.USD);
- });
- it(
- 'updating an order in an unsupported currency throws',
- assertThrowsWithMessage(async () => {
- await shopClient.query(
- AddItemToOrderDocument,
- {
- productVariantId: 'T_1',
- quantity: 1,
- },
- { currencyCode: 'JPY' },
- );
- }, 'The currency "JPY" is not available in the current Channel'),
- );
- it('updating an order line with a new currency updates all lines to that currency', async () => {
- const { activeOrder } = await shopClient.query(GetActiveOrderDocument);
- const { adjustOrderLine } = await shopClient.query(
- AdjustItemQuantityDocument,
- {
- orderLineId: activeOrder!.lines[0]?.id,
- quantity: 2,
- },
- { currencyCode: 'GBP' },
- );
- orderResultGuard.assertSuccess(adjustOrderLine);
- expect(adjustOrderLine?.lines[0]?.unitPrice).toBe(900);
- expect(adjustOrderLine?.lines[0]?.unitPriceWithTax).toBe(1080);
- expect(adjustOrderLine?.lines[1]?.unitPrice).toBe(1900);
- expect(adjustOrderLine?.lines[1]?.unitPriceWithTax).toBe(2280);
- expect(adjustOrderLine.currencyCode).toBe('GBP');
- });
- it('adding a new order line with a new currency updates all lines to that currency', async () => {
- const { addItemToOrder } = await shopClient.query(
- AddItemToOrderDocument,
- {
- productVariantId: 'T_3',
- quantity: 1,
- },
- { currencyCode: 'EUR' },
- );
- orderResultGuard.assertSuccess(addItemToOrder);
- expect(addItemToOrder?.lines[0]?.unitPrice).toBe(1100);
- expect(addItemToOrder?.lines[0]?.unitPriceWithTax).toBe(1320);
- expect(addItemToOrder?.lines[1]?.unitPrice).toBe(2100);
- expect(addItemToOrder?.lines[1]?.unitPriceWithTax).toBe(2520);
- expect(addItemToOrder?.lines[2]?.unitPrice).toBe(3100);
- expect(addItemToOrder?.lines[2]?.unitPriceWithTax).toBe(3720);
- expect(addItemToOrder.currencyCode).toBe('EUR');
- });
- });
- describe('ProductVariantPriceUpdateStrategy', () => {
- const SECOND_CHANNEL_TOKEN = 'second_channel_token';
- const THIRD_CHANNEL_TOKEN = 'third_channel_token';
- beforeAll(async () => {
- const { createChannel: channel2Result } = await adminClient.query(CreateChannelDocument, {
- input: {
- code: 'second-channel',
- token: SECOND_CHANNEL_TOKEN,
- defaultLanguageCode: LanguageCode.en,
- currencyCode: CurrencyCode.GBP,
- pricesIncludeTax: true,
- defaultShippingZoneId: 'T_1',
- defaultTaxZoneId: 'T_1',
- },
- });
- createChannelResultGuard.assertSuccess(channel2Result);
- const { createChannel: channel3Result } = await adminClient.query(CreateChannelDocument, {
- input: {
- code: 'third-channel',
- token: THIRD_CHANNEL_TOKEN,
- defaultLanguageCode: LanguageCode.en,
- currencyCode: CurrencyCode.GBP,
- pricesIncludeTax: true,
- defaultShippingZoneId: 'T_1',
- defaultTaxZoneId: 'T_1',
- },
- });
- createChannelResultGuard.assertSuccess(channel3Result);
- await adminClient.query(AssignProductsToChannelDocument, {
- input: {
- channelId: channel2Result.id,
- productIds: [multiPriceProduct.id],
- },
- });
- await adminClient.query(AssignProductsToChannelDocument, {
- input: {
- channelId: channel3Result.id,
- productIds: [multiPriceProduct.id],
- },
- });
- });
- it('onPriceCreated() is called when a new price is created', async () => {
- await adminClient.asSuperAdmin();
- const onCreatedSpy = TestProductVariantPriceUpdateStrategy.onCreatedSpy;
- onCreatedSpy.mockClear();
- await adminClient.query(UpdateChannelDocument, {
- input: {
- id: 'T_1',
- availableCurrencyCodes: [
- CurrencyCode.USD,
- CurrencyCode.GBP,
- CurrencyCode.EUR,
- CurrencyCode.MYR,
- ],
- },
- });
- await adminClient.query(UpdateProductVariantsDocument, {
- input: {
- id: multiPriceVariant.id,
- prices: [{ currencyCode: CurrencyCode.MYR, price: 5500 }],
- },
- });
- expect(onCreatedSpy).toHaveBeenCalledTimes(1);
- expect(onCreatedSpy.mock.calls[0][0].currencyCode).toBe(CurrencyCode.MYR);
- expect(onCreatedSpy.mock.calls[0][0].price).toBe(5500);
- expect(onCreatedSpy.mock.calls[0][1].length).toBe(4);
- expect(getOrderedPricesArray(onCreatedSpy.mock.calls[0][1])).toEqual([
- {
- channelId: 1,
- currencyCode: 'USD',
- id: 35,
- price: 1200,
- },
- {
- channelId: 1,
- currencyCode: 'GBP',
- id: 36,
- price: 900,
- },
- {
- channelId: 2,
- currencyCode: 'GBP',
- id: 44,
- price: 1440,
- },
- {
- channelId: 3,
- currencyCode: 'GBP',
- id: 45,
- price: 1440,
- },
- ]);
- });
- it('onPriceUpdated() is called when a new price is created', async () => {
- adminClient.setChannelToken(THIRD_CHANNEL_TOKEN);
- TestProductVariantPriceUpdateStrategy.syncAcrossChannels = true;
- const onUpdatedSpy = TestProductVariantPriceUpdateStrategy.onUpdatedSpy;
- onUpdatedSpy.mockClear();
- await adminClient.query(UpdateProductVariantsDocument, {
- input: {
- id: multiPriceVariant.id,
- prices: [
- {
- currencyCode: CurrencyCode.GBP,
- price: 4242,
- },
- ],
- },
- });
- expect(onUpdatedSpy).toHaveBeenCalledTimes(1);
- expect(onUpdatedSpy.mock.calls[0][0].currencyCode).toBe(CurrencyCode.GBP);
- expect(onUpdatedSpy.mock.calls[0][0].price).toBe(4242);
- expect(onUpdatedSpy.mock.calls[0][1].length).toBe(5);
- expect(getOrderedPricesArray(onUpdatedSpy.mock.calls[0][1])).toEqual([
- {
- channelId: 1,
- currencyCode: 'USD',
- id: 35,
- price: 1200,
- },
- {
- channelId: 1,
- currencyCode: 'GBP',
- id: 36,
- price: 900,
- },
- {
- channelId: 2,
- currencyCode: 'GBP',
- id: 44,
- price: 1440,
- },
- {
- channelId: 3,
- currencyCode: 'GBP',
- id: 45,
- price: 4242,
- },
- {
- channelId: 1,
- currencyCode: 'MYR',
- id: 46,
- price: 5500,
- },
- ]);
- });
- it('syncing prices in other channels', async () => {
- const { product: productChannel3 } = await adminClient.query(GetProductWithVariantsDocument, {
- id: multiPriceProduct.id,
- });
- expect(productChannel3?.variants[0].prices).toEqual([
- { currencyCode: CurrencyCode.GBP, price: 4242 },
- ]);
- adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
- const { product: productChannel2 } = await adminClient.query(GetProductWithVariantsDocument, {
- id: multiPriceProduct.id,
- });
- expect(productChannel2?.variants[0].prices).toEqual([
- { currencyCode: CurrencyCode.GBP, price: 4242 },
- ]);
- adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
- const { product: productDefaultChannel } = await adminClient.query(
- GetProductWithVariantsDocument,
- {
- id: multiPriceProduct.id,
- },
- );
- expect(productDefaultChannel?.variants[0].prices).toEqual([
- { currencyCode: CurrencyCode.USD, price: 1200 },
- { currencyCode: CurrencyCode.GBP, price: 4242 },
- { currencyCode: CurrencyCode.MYR, price: 5500 },
- ]);
- });
- it('onPriceDeleted() is called when a price is deleted', async () => {
- adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
- const onDeletedSpy = TestProductVariantPriceUpdateStrategy.onDeletedSpy;
- onDeletedSpy.mockClear();
- const result = await adminClient.query(UpdateProductVariantsDocument, {
- input: {
- id: multiPriceVariant.id,
- prices: [
- {
- currencyCode: CurrencyCode.MYR,
- price: 4242,
- delete: true,
- },
- ],
- },
- });
- expect(result.updateProductVariants[0]?.prices).toEqual([
- { currencyCode: CurrencyCode.USD, price: 1200 },
- { currencyCode: CurrencyCode.GBP, price: 4242 },
- ]);
- expect(onDeletedSpy).toHaveBeenCalledTimes(1);
- expect(onDeletedSpy.mock.calls[0][0].currencyCode).toBe(CurrencyCode.MYR);
- expect(onDeletedSpy.mock.calls[0][0].price).toBe(5500);
- expect(onDeletedSpy.mock.calls[0][1].length).toBe(4);
- expect(getOrderedPricesArray(onDeletedSpy.mock.calls[0][1])).toEqual([
- {
- channelId: 1,
- currencyCode: 'USD',
- id: 35,
- price: 1200,
- },
- {
- channelId: 1,
- currencyCode: 'GBP',
- id: 36,
- price: 4242,
- },
- {
- channelId: 2,
- currencyCode: 'GBP',
- id: 44,
- price: 4242,
- },
- {
- channelId: 3,
- currencyCode: 'GBP',
- id: 45,
- price: 4242,
- },
- ]);
- });
- });
- });
- function getOrderedPricesArray(input: ProductVariantPrice[]) {
- return input
- .map(p => pick(p, ['channelId', 'currencyCode', 'price', 'id']))
- .sort((a, b) => (a.id < b.id ? -1 : 1));
- }
|