Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
14200b1
fix(core): Remove shipping lines from active orders when deleted or u…
kwerie Mar 11, 2026
dbb35d6
Merge branch 'master' into fix/shipping-line-deletion
kwerie Mar 11, 2026
dfcbf6e
chore(core): Update issue ref
kwerie Mar 11, 2026
a8ed3be
fix(core): Do not remove shipping lines from order when shipping meth…
kwerie Mar 11, 2026
7975d82
fix(core): Recalculate active orders when shipping method is unassign…
kwerie Mar 13, 2026
d97b39c
test(core): Remove duplicate query
kwerie Mar 13, 2026
23a1826
test(core): Remove mutation import
kwerie Mar 13, 2026
1984363
chore(core): Construct per channel context to avoid default channel c…
kwerie Mar 13, 2026
096c66c
test(core): Resolve CodeRabbit nitpick
kwerie Mar 13, 2026
f6cc8f8
fix(core): Use event ctx directly and manually remove shipping lines
kwerie Mar 13, 2026
ec6a1ae
Merge branch 'master' into fix/shipping-line-deletion
kwerie Mar 13, 2026
0c8567c
Merge branch 'master' into fix/shipping-line-deletion
kwerie Mar 16, 2026
3d7a333
fix(core): Properly delete shipping lines on channel unassignment
kwerie Mar 16, 2026
e3f5dbe
Merge remote-tracking branch 'refs/remotes/origin/fix/shipping-line-d…
kwerie Mar 17, 2026
83f03e6
Merge branch 'master' into fix/shipping-line-deletion
kwerie Mar 17, 2026
34e61f4
Merge branch 'master' into fix/shipping-line-deletion
kwerie Mar 23, 2026
149352e
Merge branch 'master' into fix/shipping-line-deletion
kwerie Apr 2, 2026
52237e7
Merge branch 'master' into fix/shipping-line-deletion
kwerie Apr 2, 2026
993ce26
Merge branch 'master' into fix/shipping-line-deletion
kwerie Apr 3, 2026
21094c1
Merge branch 'master' into fix/shipping-line-deletion
kwerie Apr 10, 2026
bfded41
Merge branch 'master' into fix/shipping-line-deletion
kwerie Apr 16, 2026
3212a80
Merge branch 'master' into fix/shipping-line-deletion
kwerie Apr 16, 2026
f9d7833
Merge branch 'master' into fix/shipping-line-deletion
kwerie Apr 30, 2026
1494e6e
Merge branch 'master' into fix/shipping-line-deletion
kwerie May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 223 additions & 2 deletions packages/core/e2e/shipping-method.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,30 @@ import { initialData } from '../../../e2e-common/e2e-initial-data';
import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
import { manualFulfillmentHandler } from '../src/config/fulfillment/manual-fulfillment-handler';

import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
import { SHIPPING_METHOD_FRAGMENT } from './graphql/fragments';
import * as Codegen from './graphql/generated-e2e-admin-types';
import { DeletionResult, LanguageCode } from './graphql/generated-e2e-admin-types';
import {
ASSIGN_PRODUCTVARIANT_TO_CHANNEL,
CREATE_CHANNEL,
CREATE_SHIPPING_METHOD,
DELETE_SHIPPING_METHOD,
GET_ORDER,
GET_SHIPPING_METHOD_LIST,
UPDATE_SHIPPING_METHOD,
} from './graphql/shared-definitions';
import { GET_ACTIVE_SHIPPING_METHODS } from './graphql/shop-definitions';
import {
ADD_ITEM_TO_ORDER,
ADD_PAYMENT,
GET_ACTIVE_ORDER,
GET_ACTIVE_SHIPPING_METHODS,
SET_CUSTOMER,
SET_SHIPPING_ADDRESS,
SET_SHIPPING_METHOD,
TRANSITION_TO_STATE,
} from './graphql/shop-definitions';
import { CREATE_PAYMENT_METHOD } from './payment-method.e2e-spec';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const TEST_METADATA = {
foo: 'bar',
Expand All @@ -46,6 +60,9 @@ const calculatorWithMetadata = new ShippingCalculator({
describe('ShippingMethod resolver', () => {
const { server, adminClient, shopClient } = createTestEnvironment({
...testConfig(),
paymentOptions: {
paymentMethodHandlers: [testSuccessfulPaymentMethod],
},
shippingOptions: {
shippingEligibilityCheckers: [defaultShippingEligibilityChecker],
shippingCalculators: [defaultShippingCalculator, calculatorWithMetadata],
Expand All @@ -54,7 +71,15 @@ describe('ShippingMethod resolver', () => {

beforeAll(async () => {
await server.init({
initialData,
initialData: {
...initialData,
paymentMethods: [
{
name: testSuccessfulPaymentMethod.code,
handler: { code: testSuccessfulPaymentMethod.code, arguments: [] },
},
],
},
productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
customerCount: 1,
});
Expand Down Expand Up @@ -515,6 +540,184 @@ describe('ShippingMethod resolver', () => {
expect(activeShippingMethods[0].name).toBe('Active Method');
expect(activeShippingMethods[0].description).toBe('This is an active shipping method');
});

// https://github.com/vendure-ecommerce/vendure/issues/4492
describe('shipping line removal on channel unassign', () => {
let channelId: string;
let shippingMethodId: string;

beforeAll(async () => {
// Create a new channel
const { createChannel } = await adminClient.query(CREATE_CHANNEL, {
input: {
code: 'shipping-test-channel',
token: 'shipping-test-channel-token',
defaultLanguageCode: LanguageCode.en,
currencyCode: 'USD',
pricesIncludeTax: false,
defaultShippingZoneId: 'T_1',
defaultTaxZoneId: 'T_1',
},
});
channelId = createChannel.id;

// Create a shipping method and assign it to the new channel
const { createShippingMethod } = await adminClient.query<
Codegen.CreateShippingMethodMutation,
Codegen.CreateShippingMethodMutationVariables
>(CREATE_SHIPPING_METHOD, {
input: {
code: 'channel-test-method',
fulfillmentHandler: manualFulfillmentHandler.code,
checker: {
code: defaultShippingEligibilityChecker.code,
arguments: [{ name: 'orderMinimum', value: '0' }],
},
calculator: {
code: defaultShippingCalculator.code,
arguments: [
{ name: 'rate', value: '500' },
{ name: 'includesTax', value: 'auto' },
{ name: 'taxRate', value: '0' },
],
},
translations: [
{ languageCode: LanguageCode.en, name: 'Channel Test Method', description: '' },
],
},
});
shippingMethodId = createShippingMethod.id;

await adminClient.query(ASSIGN_SHIPPING_METHODS_TO_CHANNEL, {
input: {
channelId,
shippingMethodIds: [shippingMethodId],
},
});

// Assign product variant to the new channel
await adminClient.query(ASSIGN_PRODUCTVARIANT_TO_CHANNEL, {
input: {
channelId,
productVariantIds: ['T_1'],
},
});
});

it('recalculates active orders when shipping method is unassigned from channel', async () => {
// Create an active order in the new channel with the shipping method
shopClient.setChannelToken('shipping-test-channel-token');
await shopClient.asAnonymousUser();
await shopClient.query(ADD_ITEM_TO_ORDER, {
productVariantId: 'T_1',
quantity: 1,
});
await shopClient.query(SET_SHIPPING_METHOD, { id: [shippingMethodId] });

// Verify shipping line is present and totals include shipping
const { activeOrder: orderBefore } = await shopClient.query(GET_ACTIVE_ORDER);
expect(orderBefore.shippingLines).toHaveLength(1);
expect(orderBefore.shippingLines[0].shippingMethod.id).toBe(shippingMethodId);
expect(orderBefore.shipping).toBe(500);
expect(orderBefore.total).toBe(Number(orderBefore.subTotal) + 500);

// Remove the shipping method from the channel
await adminClient.query(REMOVE_SHIPPING_METHODS_FROM_CHANNEL, {
input: {
channelId,
shippingMethodIds: [shippingMethodId],
},
});

// Verify the shipping line has been removed and totals recalculated
const { activeOrder: orderAfter } = await shopClient.query(GET_ACTIVE_ORDER);
expect(orderAfter.shippingLines).toHaveLength(0);
expect(orderAfter.shipping).toBe(0);
expect(orderAfter.shippingWithTax).toBe(0);
expect(orderAfter.total).toBe(orderAfter.subTotal);
expect(orderAfter.totalWithTax).toBe(orderAfter.subTotalWithTax);

// Reset shop client to default channel
shopClient.setChannelToken('e2e-default-channel');
});

it('historical orders still resolve shipping method after unassignment', async () => {
// Re-assign the shipping method to the channel so we can create a completed order
await adminClient.query(ASSIGN_SHIPPING_METHODS_TO_CHANNEL, {
input: {
channelId,
shippingMethodIds: [shippingMethodId],
},
});

// Create a payment method in the test channel
adminClient.setChannelToken('shipping-test-channel-token');
await adminClient.query(CREATE_PAYMENT_METHOD, {
input: {
code: 'test-payment-method',
translations: [
{ languageCode: LanguageCode.en, name: 'Test Payment Method', description: '' },
],
enabled: true,
handler: {
code: testSuccessfulPaymentMethod.code,
arguments: [],
},
},
});
adminClient.setChannelToken('e2e-default-channel');

// Create and complete an order
shopClient.setChannelToken('shipping-test-channel-token');
await shopClient.asAnonymousUser();
await shopClient.query(ADD_ITEM_TO_ORDER, {
productVariantId: 'T_1',
quantity: 1,
});
await shopClient.query(SET_SHIPPING_METHOD, { id: [shippingMethodId] });
await shopClient.query(SET_CUSTOMER, {
input: {
firstName: 'Test',
lastName: 'Customer',
emailAddress: 'shipping-test@test.com',
},
});
await shopClient.query(SET_SHIPPING_ADDRESS, {
input: {
streetLine1: '1 Test Street',
countryCode: 'GB',
},
});
await shopClient.query(TRANSITION_TO_STATE, { state: 'ArrangingPayment' });
const { addPaymentToOrder: completedOrder } = await shopClient.query(ADD_PAYMENT, {
input: {
method: testSuccessfulPaymentMethod.code,
metadata: {},
},
});

// Remove the shipping method from the channel again
await adminClient.query(REMOVE_SHIPPING_METHODS_FROM_CHANNEL, {
input: {
channelId,
shippingMethodIds: [shippingMethodId],
},
});

// Verify the historical order still resolves the shipping method
adminClient.setChannelToken('shipping-test-channel-token');
const { order } = await adminClient.query(GET_ORDER, {
id: completedOrder.id,
});
expect(order.shippingLines).toHaveLength(1);
expect(order.shippingLines[0].shippingMethod.id).toBe(shippingMethodId);
expect(order.shippingLines[0].shippingMethod.name).toBe('Channel Test Method');

// Reset clients to default channel
shopClient.setChannelToken('e2e-default-channel');
adminClient.setChannelToken('e2e-default-channel');
});
});
});

const GET_SHIPPING_METHOD = gql`
Expand Down Expand Up @@ -583,3 +786,21 @@ export const TEST_ELIGIBLE_SHIPPING_METHODS = gql`
}
}
`;

const ASSIGN_SHIPPING_METHODS_TO_CHANNEL = gql`
mutation AssignShippingMethodsToChannel($input: AssignShippingMethodsToChannelInput!) {
assignShippingMethodsToChannel(input: $input) {
id
name
}
}
`;

const REMOVE_SHIPPING_METHODS_FROM_CHANNEL = gql`
mutation RemoveShippingMethodsFromChannel($input: RemoveShippingMethodsFromChannelInput!) {
removeShippingMethodsFromChannel(input: $input) {
id
name
}
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class ShippingLineEntityResolver {
// Does not need to be decoded because it is an internal property
// which is never exposed to the outside world.
const shippingMethodId = shippingLine.shippingMethodId;
return this.shippingMethodService.findOne(ctx, shippingMethodId, true);
return this.shippingMethodService.findOne(ctx, shippingMethodId, true, [], false);
} else {
return null;
}
Expand Down
55 changes: 54 additions & 1 deletion packages/core/src/service/services/order.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,11 @@ import { Promotion } from '../../entity/promotion/promotion.entity';
import { Refund } from '../../entity/refund/refund.entity';
import { Session } from '../../entity/session/session.entity';
import { ShippingLine } from '../../entity/shipping-line/shipping-line.entity';
import { ShippingMethod } from '../../entity/shipping-method/shipping-method.entity';
import { Surcharge } from '../../entity/surcharge/surcharge.entity';
import { User } from '../../entity/user/user.entity';
import { EventBus } from '../../event-bus/event-bus';
import { ChangeChannelEvent } from '../../event-bus/events/change-channel-event';
import { CouponCodeEvent } from '../../event-bus/events/coupon-code-event';
import { OrderEvent } from '../../event-bus/events/order-event';
import { OrderLineEvent } from '../../event-bus/events/order-line-event';
Expand Down Expand Up @@ -164,7 +166,19 @@ export class OrderService {
private requestCache: RequestContextCacheService,
private translator: TranslatorService,
private stockLevelService: StockLevelService,
) {}
) {
this.eventBus.registerBlockingEventHandler({
id: 'order-service-remove-shipping-method-from-active-orders',
event: ChangeChannelEvent,
handler: async event => {
if (event.entityType === ShippingMethod && event.type === 'removed') {
await this.removeShippingMethodFromActiveOrders(
event as ChangeChannelEvent<ShippingMethod>,
);
}
},
});
}

/**
* @description
Expand Down Expand Up @@ -2192,4 +2206,43 @@ export class OrderService {
.add(idToAdd);
}
}

private async removeShippingMethodFromActiveOrders(event: ChangeChannelEvent<ShippingMethod>) {
const shippingMethodId = event.entity.id;
const { ctx } = event;

const affectedOrders = await this.connection
.getRepository(ctx, Order)
.createQueryBuilder('order')
.innerJoin('order.channels', 'channel', 'channel.id IN (:...channelIds)', {
channelIds: event.channelIds,
})
.innerJoin(
'order.shippingLines',
'shippingLine',
'shippingLine.shippingMethodId = :shippingMethodId',
{ shippingMethodId },
)
.where('order.active = :active', { active: true })
.getMany();

if (affectedOrders.length === 0) {
return;
}

const orders = await this.connection.getRepository(ctx, Order).find({
where: { id: In(affectedOrders.map(o => o.id)) },
relations: [
'lines',
'lines.productVariant',
'lines.productVariant.productVariantPrices',
'shippingLines',
'surcharges',
],
});

for (const order of orders) {
await this.applyPriceAdjustments(ctx, order);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
32 changes: 23 additions & 9 deletions packages/core/src/service/services/shipping-method.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,31 @@ export class ShippingMethodService {
shippingMethodId: ID,
includeDeleted = false,
relations: RelationPaths<ShippingMethod> = [],
filterOnChannel = true,
): Promise<Translated<ShippingMethod> | undefined> {
const shippingMethod = await this.connection.findOneInChannel(
ctx,
ShippingMethod,
shippingMethodId,
ctx.channelId,
{
let shippingMethod: ShippingMethod | undefined | null;

if (!filterOnChannel) {
shippingMethod = await this.connection.getRepository(ctx, ShippingMethod).findOne({
where: {
id: shippingMethodId,
deletedAt: includeDeleted ? undefined : IsNull(),
},
relations,
...(includeDeleted === false ? { where: { deletedAt: IsNull() } } : {}),
},
);
});
} else {
shippingMethod = await this.connection.findOneInChannel(
ctx,
ShippingMethod,
shippingMethodId,
ctx.channelId,
{
relations,
...(includeDeleted === false ? { where: { deletedAt: IsNull() } } : {}),
},
);
}

return (shippingMethod && this.translator.translate(shippingMethod, ctx)) ?? undefined;
}

Expand Down
Loading