Skip to content

Commit

Permalink
fix(core): Fix coupon code validation across multiple channels
Browse files Browse the repository at this point in the history
Relates to #2052. When multiple channels have promotions with the same
coupon code, this could cause validation to incorrectly fail
because the DB lookup was not limiting to the active channel.
  • Loading branch information
michaelbromley committed Oct 14, 2024
1 parent 73cb190 commit e57cc1b
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 30 deletions.
189 changes: 159 additions & 30 deletions packages/core/e2e/order-promotion.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -925,9 +925,8 @@ describe('Promotions applied to Orders', () => {
expect(removeCouponCode!.total).toBe(2200);
expect(removeCouponCode!.totalWithTax).toBe(2640);

const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(getItemSale1Line(activeOrder!.lines).discounts.length).toBe(0);
expect(activeOrder!.total).toBe(2200);
expect(activeOrder!.totalWithTax).toBe(2640);
Expand Down Expand Up @@ -986,9 +985,8 @@ describe('Promotions applied to Orders', () => {
expect(removeCouponCode!.total).toBe(2200);
expect(removeCouponCode!.totalWithTax).toBe(2640);

const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(getItemSale1Line(activeOrder!.lines).discounts.length).toBe(0);
expect(activeOrder!.total).toBe(2200);
expect(activeOrder!.totalWithTax).toBe(2640);
Expand Down Expand Up @@ -1534,9 +1532,8 @@ describe('Promotions applied to Orders', () => {

await addGuestCustomerToOrder();

const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(activeOrder!.couponCodes).toEqual([]);
expect(activeOrder!.totalWithTax).toBe(6000);
});
Expand Down Expand Up @@ -1627,9 +1624,8 @@ describe('Promotions applied to Orders', () => {

await logInAsRegisteredCustomer();

const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(activeOrder!.totalWithTax).toBe(6000);
expect(activeOrder!.couponCodes).toEqual([]);
});
Expand Down Expand Up @@ -1883,9 +1879,8 @@ describe('Promotions applied to Orders', () => {
expect(addItemToOrder.discounts.length).toBe(1);
expect(addItemToOrder.discounts[0].description).toBe('Test Promo');

const { activeOrder: check1 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder: check1 } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(check1!.discounts.length).toBe(1);
expect(check1!.discounts[0].description).toBe('Test Promo');

Expand All @@ -1899,9 +1894,8 @@ describe('Promotions applied to Orders', () => {
orderResultGuard.assertSuccess(removeOrderLine);
expect(removeOrderLine.discounts.length).toBe(0);

const { activeOrder: check2 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder: check2 } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(check2!.discounts.length).toBe(0);
});

Expand Down Expand Up @@ -2043,9 +2037,8 @@ describe('Promotions applied to Orders', () => {
quantity: 1,
});

const { activeOrder: check1 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder: check1 } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);

expect(check1!.lines[0].discountedUnitPriceWithTax).toBe(0);
expect(check1!.totalWithTax).toBe(0);
Expand All @@ -2055,9 +2048,8 @@ describe('Promotions applied to Orders', () => {
CodegenShop.ApplyCouponCodeMutationVariables
>(APPLY_COUPON_CODE, { couponCode: couponCode2 });

const { activeOrder: check2 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder: check2 } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(check2!.lines[0].discountedUnitPriceWithTax).toBe(0);
expect(check2!.totalWithTax).toBe(0);
});
Expand All @@ -2080,9 +2072,8 @@ describe('Promotions applied to Orders', () => {
quantity: 1,
});

const { activeOrder: check1 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder: check1 } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);

expect(check1!.lines[0].discountedUnitPriceWithTax).toBe(0);
expect(check1!.totalWithTax).toBe(0);
Expand All @@ -2092,14 +2083,152 @@ describe('Promotions applied to Orders', () => {
CodegenShop.ApplyCouponCodeMutationVariables
>(APPLY_COUPON_CODE, { couponCode: couponCode2 });

const { activeOrder: check2 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder: check2 } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(check2!.lines[0].discountedUnitPriceWithTax).toBe(0);
expect(check2!.totalWithTax).toBe(0);
});
});

// https://github.com/vendure-ecommerce/vendure/issues/2052
describe('multi-channel usage', () => {
const SECOND_CHANNEL_TOKEN = 'second_channel_token';
const THIRD_CHANNEL_TOKEN = 'third_channel_token';
const promoCode = 'TEST_COMMON_CODE';

async function createChannelAndAssignProducts(code: string, token: string) {
const result = await adminClient.query<
Codegen.CreateChannelMutation,
Codegen.CreateChannelMutationVariables
>(CREATE_CHANNEL, {
input: {
code,
token,
defaultLanguageCode: LanguageCode.en,
currencyCode: CurrencyCode.GBP,
pricesIncludeTax: true,
defaultShippingZoneId: 'T_1',
defaultTaxZoneId: 'T_1',
},
});

await adminClient.query<
Codegen.AssignProductsToChannelMutation,
Codegen.AssignProductsToChannelMutationVariables
>(ASSIGN_PRODUCT_TO_CHANNEL, {
input: {
channelId: (result.createChannel as Codegen.ChannelFragment).id,
priceFactor: 1,
productIds: products.map(p => p.id),
},
});

return result.createChannel as Codegen.ChannelFragment;
}

async function addItemAndApplyPromoCode() {
await shopClient.asAnonymousUser();
await shopClient.query<
CodegenShop.AddItemToOrderMutation,
CodegenShop.AddItemToOrderMutationVariables
>(ADD_ITEM_TO_ORDER, {
productVariantId: getVariantBySlug('item-5000').id,
quantity: 1,
});

const { applyCouponCode } = await shopClient.query<
CodegenShop.ApplyCouponCodeMutation,
CodegenShop.ApplyCouponCodeMutationVariables
>(APPLY_COUPON_CODE, {
couponCode: promoCode,
});

orderResultGuard.assertSuccess(applyCouponCode);
return applyCouponCode;
}

beforeAll(async () => {
await createChannelAndAssignProducts('second-channel', SECOND_CHANNEL_TOKEN);
await createChannelAndAssignProducts('third-channel', THIRD_CHANNEL_TOKEN);
});

it('create promotion in second channel', async () => {
adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);

const result = await createPromotion({
enabled: true,
name: 'common-promotion-second-channel',
couponCode: promoCode,
actions: [
{
code: orderPercentageDiscount.code,
arguments: [{ name: 'discount', value: '20' }],
},
],
conditions: [],
});

expect(result.name).toBe('common-promotion-second-channel');
});

it('create promotion in third channel', async () => {
adminClient.setChannelToken(THIRD_CHANNEL_TOKEN);

const result = await createPromotion({
enabled: true,
name: 'common-promotion-third-channel',
couponCode: promoCode,
actions: [
{
code: orderPercentageDiscount.code,
arguments: [{ name: 'discount', value: '20' }],
},
],
conditions: [],
});

expect(result.name).toBe('common-promotion-third-channel');
});

it('applies promotion in second channel', async () => {
shopClient.setChannelToken(SECOND_CHANNEL_TOKEN);

const result = await addItemAndApplyPromoCode();
expect(result.discounts.length).toBe(1);
expect(result.discounts[0].description).toBe('common-promotion-second-channel');
});

it('applies promotion in third channel', async () => {
shopClient.setChannelToken(THIRD_CHANNEL_TOKEN);

const result = await addItemAndApplyPromoCode();
expect(result.discounts.length).toBe(1);
expect(result.discounts[0].description).toBe('common-promotion-third-channel');
});

it('applies promotion from current channel, not default channel', async () => {
adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
const defaultChannelPromotion = await createPromotion({
enabled: true,
name: 'common-promotion-default-channel',
couponCode: promoCode,
actions: [
{
code: orderPercentageDiscount.code,
arguments: [{ name: 'discount', value: '20' }],
},
],
conditions: [],
});

shopClient.setChannelToken(SECOND_CHANNEL_TOKEN);

const result = await addItemAndApplyPromoCode();
expect(result.discounts.length).toBe(1);
expect(result.discounts[0].description).toBe('common-promotion-second-channel');
});
});

async function getProducts() {
const result = await adminClient.query<Codegen.GetProductsWithVariantPricesQuery>(
GET_PRODUCTS_WITH_VARIANT_PRICES,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/service/services/promotion.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ export class PromotionService {
couponCode,
enabled: true,
deletedAt: IsNull(),
channels: { id: ctx.channelId },
},
relations: ['channels'],
});
Expand Down

0 comments on commit e57cc1b

Please sign in to comment.