Skip to content

Commit

Permalink
fix(core): Fix querying order variant after removal from channel
Browse files Browse the repository at this point in the history
Fixes #2716
  • Loading branch information
michaelbromley committed Mar 5, 2024
1 parent dad7f98 commit e28ba3d
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 6 deletions.
100 changes: 100 additions & 0 deletions packages/core/e2e/product-channel.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
E2E_DEFAULT_CHANNEL_TOKEN,
ErrorResultGuard,
} from '@vendure/testing';
import { fail } from 'assert';
import gql from 'graphql-tag';
import path from 'path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

Expand Down Expand Up @@ -36,13 +38,18 @@ import {
UpdateProductDocument,
UpdateProductVariantsDocument,
} from './graphql/generated-e2e-admin-types';
import { AddItemToOrderMutation, AddItemToOrderMutationVariables } from './graphql/generated-e2e-shop-types';
import { ADD_ITEM_TO_ORDER } from './graphql/shop-definitions';
import { assertThrowsWithMessage } from './utils/assert-throws-with-message';

describe('ChannelAware Products and ProductVariants', () => {
const { server, adminClient, shopClient } = createTestEnvironment(testConfig());
const SECOND_CHANNEL_TOKEN = 'second_channel_token';
const THIRD_CHANNEL_TOKEN = 'third_channel_token';
let secondChannelAdminRole: CreateRoleMutation['createRole'];
const orderResultGuard: ErrorResultGuard<{ lines: Array<{ id: string }> }> = createErrorResultGuard(
input => !!input.lines,
);

beforeAll(async () => {
await server.init({
Expand Down Expand Up @@ -216,6 +223,99 @@ describe('ChannelAware Products and ProductVariants', () => {

expect(removeProductsFromChannel[0].channels.map(c => c.id)).toEqual(['T_1']);
});

// https://github.com/vendure-ecommerce/vendure/issues/2716
it('querying an Order with a variant that was since removed from the channel', async () => {
await adminClient.query(AssignProductsToChannelDocument, {
input: {
channelId: 'T_2',
productIds: [product1.id],
priceFactor: 1,
},
});

// Create an order in the second channel with the variant just assigned
shopClient.setChannelToken(SECOND_CHANNEL_TOKEN);
const { addItemToOrder } = await shopClient.query<
AddItemToOrderMutation,
AddItemToOrderMutationVariables
>(ADD_ITEM_TO_ORDER, {
productVariantId: product1.variants[0].id,
quantity: 1,
});
orderResultGuard.assertSuccess(addItemToOrder);

// Now remove that variant from the second channel
await adminClient.query(RemoveProductsFromChannelDocument, {
input: {
productIds: [product1.id],
channelId: 'T_2',
},
});

adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);

// If no price fields are requested on the ProductVariant, then the query will
// succeed even if the ProductVariant is no longer assigned to the channel.
const GET_ORDER_WITHOUT_VARIANT_PRICE = `
query GetOrderWithoutVariantPrice($id: ID!) {
order(id: $id) {
id
lines {
id
linePrice
productVariant {
id
name
}
}
}
}`;
const { order } = await adminClient.query(gql(GET_ORDER_WITHOUT_VARIANT_PRICE), {
id: addItemToOrder.id,
});

expect(order).toEqual({
id: 'T_1',
lines: [
{
id: 'T_1',
linePrice: 129900,
productVariant: {
id: 'T_1',
name: 'Laptop 13 inch 8GB',
},
},
],
});

try {
// The API will only throw if one of the price fields is requested in the query
const GET_ORDER_WITH_VARIANT_PRICE = `
query GetOrderWithVariantPrice($id: ID!) {
order(id: $id) {
id
lines {
id
linePrice
productVariant {
id
name
price
}
}
}
}`;
await adminClient.query(gql(GET_ORDER_WITH_VARIANT_PRICE), {
id: addItemToOrder.id,
});
fail(`Should have thrown`);
} catch (e: any) {
expect(e.message).toContain(
'No price information was found for ProductVariant ID "1" in the Channel "second-channel"',
);
}
});
});

describe('assigning ProductVariant to Channels', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common';
import { RequestContext } from '../../../api/common/request-context';
import { RequestContextCacheService } from '../../../cache/request-context-cache.service';
import { InternalServerError } from '../../../common/error/errors';
import { idsAreEqual } from '../../../common/utils';
import { ConfigService } from '../../../config/config.service';
import { Order } from '../../../entity/order/order.entity';
import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
Expand Down Expand Up @@ -51,19 +50,23 @@ export class ProductPriceApplicator {
* @description
* Populates the `price` field with the price for the specified channel. Make sure that
* the ProductVariant being passed in has its `taxCategory` relation joined.
*
* If the `throwIfNoPriceFound` option is set to `true`, then an error will be thrown if no
* price is found for the given Channel.
*/
async applyChannelPriceAndTax(
variant: ProductVariant,
ctx: RequestContext,
order?: Order,
throwIfNoPriceFound = false,
): Promise<ProductVariant> {
const { productVariantPriceSelectionStrategy, productVariantPriceCalculationStrategy } =
this.configService.catalogOptions;
const channelPrice = await productVariantPriceSelectionStrategy.selectPrice(
ctx,
variant.productVariantPrices,
);
if (!channelPrice) {
if (!channelPrice && throwIfNoPriceFound) {
throw new InternalServerError('error.no-price-found-for-channel', {
variantId: variant.id,
channel: ctx.channel.code,
Expand All @@ -86,7 +89,7 @@ export class ProductPriceApplicator {
);

const { price, priceIncludesTax } = await productVariantPriceCalculationStrategy.calculate({
inputPrice: channelPrice.price,
inputPrice: channelPrice?.price ?? 0,
taxCategory: variant.taxCategory,
productVariant: variant,
activeTaxZone,
Expand All @@ -96,7 +99,7 @@ export class ProductPriceApplicator {
variant.listPrice = price;
variant.listPriceIncludesTax = priceIncludesTax;
variant.taxRateApplied = applicableTaxRate;
variant.currencyCode = channelPrice.currencyCode;
variant.currencyCode = channelPrice?.currencyCode ?? ctx.currencyCode;
return variant;
}
}
5 changes: 3 additions & 2 deletions packages/core/src/service/services/product-variant.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ export class ProductVariantService {
);
variant.taxCategory = variantWithTaxCategory.taxCategory;
}
resolve(await this.applyChannelPriceAndTax(variant, ctx));
resolve(await this.applyChannelPriceAndTax(variant, ctx, undefined, true));
} catch (e: any) {
reject(e);
}
Expand Down Expand Up @@ -691,8 +691,9 @@ export class ProductVariantService {
variant: ProductVariant,
ctx: RequestContext,
order?: Order,
throwIfNoPriceFound = false,
): Promise<ProductVariant> {
return this.productPriceApplicator.applyChannelPriceAndTax(variant, ctx, order);
return this.productPriceApplicator.applyChannelPriceAndTax(variant, ctx, order, throwIfNoPriceFound);
}

/**
Expand Down

0 comments on commit e28ba3d

Please sign in to comment.