Skip to content

Commit

Permalink
feat(core): Introduce new ProductVariantPriceUpdateStrategy
Browse files Browse the repository at this point in the history
Closes #2651
  • Loading branch information
michaelbromley committed Feb 9, 2024
1 parent aa4eeb8 commit 9099f35
Show file tree
Hide file tree
Showing 8 changed files with 582 additions and 26 deletions.
308 changes: 305 additions & 3 deletions packages/core/e2e/product-prices.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
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 } from 'vitest';
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,
Expand All @@ -25,8 +35,48 @@ import {
} 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({ ...testConfig() });
const { server, adminClient, shopClient } = createTestEnvironment(
mergeConfig(
{ ...testConfig() },
{
catalogOptions: {
productVariantPriceUpdateStrategy: new TestProductVariantPriceUpdateStrategy(),
},
},
),
);

let multiPriceProduct: Codegen.CreateProductMutation['createProduct'];
let multiPriceVariant: NonNullable<
Expand All @@ -36,6 +86,10 @@ describe('Product prices', () => {
const orderResultGuard: ErrorResultGuard<TestOrderFragmentFragment | UpdatedOrderFragment> =
createErrorResultGuard(input => !!input.lines);

const createChannelResultGuard: ErrorResultGuard<{ id: string }> = createErrorResultGuard(
input => !!input.id,
);

beforeAll(async () => {
await server.init({
initialData,
Expand Down Expand Up @@ -294,4 +348,252 @@ describe('Product prices', () => {
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));
}
Loading

0 comments on commit 9099f35

Please sign in to comment.