vendure
vendure copied to clipboard
Allow multiple shipping method inside an Order
Is your feature request related to a problem? Please describe. Currently, the order uses only one shippingmethod, according to this: https://vendure-ecommerce.slack.com/archives/CKYMF0ZTJ/p1653916373970219 It should be very useful to have multiple shipping methods on a single order.
So for example, if we have two shipping methods we can have 2 items on that shipping method and 3 other items in another shipping method.
Describe the solution you'd like
For now, I implemented this feature by adding a custom field on orderline (each Orderline has its relation to the shipping line). Then made a customized version of setShippingMethod and addItemToOrder adding a new parameter that also set this relation.
There is also a manage that if in a shippingmethod there are any more orderline the shippingmethod is removed.
Describe alternatives you've considered
Additional context Some code to start to extend the actual logic:
import { Args, Mutation, Resolver } from "@nestjs/graphql";
import {
ActiveOrderService,
Allow,
Ctx,
EntityNotFoundError,
ErrorResultUnion,
ID,
idsAreEqual,
InternalServerError,
isGraphQlErrorResult,
Logger,
Order,
OrderLine,
OrderService,
Permission,
RequestContext,
ShippingLine,
Transaction,
TransactionalConnection,
UserInputError,
} from "@vendure/core";
import {
IneligibleShippingMethodError,
NoActiveOrderError,
OrderModificationError,
} from "@vendure/core/dist/common/error/generated-graphql-shop-errors";
import {
SetOrderShippingMethodResult,
UpdateOrderItemsResult,
} from "@vendure/common/lib/generated-shop-types";
import { orderRelation } from "../../../config/order.config";
@Resolver()
export class CheckoutResolver {
constructor(
private orderService: OrderService,
private activeOrderService: ActiveOrderService,
private connection: TransactionalConnection
) {}
@Transaction()
@Mutation()
@Allow(Permission.UpdateOrder, Permission.Owner)
async addItemToOrderCustom(
@Ctx() ctx: RequestContext,
@Args()
args: {
productVariantId: ID;
quantity: number;
shippingMethodId: ID;
}
): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
const order = await this.activeOrderService.getOrderFromContext(ctx, true);
const result = await this.orderService.addItemToOrder(
ctx,
order.id,
args.productVariantId,
args.quantity,
(args as any).customFields
);
if (isGraphQlErrorResult(result)) {
return result;
}
const orderLineIdAdded = result.lines.find(item => item.productVariant.id === args.productVariantId);
if (orderLineIdAdded === undefined) {
throw new InternalServerError("error.internal-error-cant-find-item-added");
}
await this.setOrderLineShippingMethodCustom(ctx, {
shippingMethodId: args.shippingMethodId,
orderLineId: orderLineIdAdded.id
});
return result;
}
@Transaction()
@Mutation()
@Allow(Permission.Owner)
async setOrderLineShippingMethodCustom(
@Ctx() ctx: RequestContext,
@Args() args: { shippingMethodId: ID; orderLineId: ID }
): Promise<ErrorResultUnion<SetOrderShippingMethodResult, Order>> {
if (ctx.authorizedAsOwnerOnly) {
const sessionOrder = await this.activeOrderService.getOrderFromContext(
ctx
);
if (sessionOrder) {
return this.setShippingMethod(
ctx,
sessionOrder.id,
args.shippingMethodId,
args.orderLineId
);
}
}
return new NoActiveOrderError();
}
async setShippingMethod(
ctx: RequestContext,
orderId: ID,
shippingMethodId: ID,
orderLineId: ID
): Promise<ErrorResultUnion<SetOrderShippingMethodResult, Order>> {
const order = await this.getOrderOrThrow(ctx, orderId);
const validationError = this.assertAddingItemsState(order);
if (validationError) {
return validationError;
}
const shippingMethods = await this.orderService.getEligibleShippingMethods(ctx, orderId);
const shippingMethod = shippingMethods.find(shippingMethod => shippingMethod.id === shippingMethodId);
if (!shippingMethod) {
return new IneligibleShippingMethodError();
}
let orderLine: OrderLine = await this.getOrderLineOrThrow(
order,
orderLineId
);
let shippingLine: ShippingLine | undefined = await this.getShippingLine(
order,
shippingMethodId
); // order.shippingLines[0];
if (shippingLine) {
// Just set the shipping method
// shippingLine.shippingMethod = shippingMethod;
} else {
shippingLine = await this.connection
.getRepository(ctx, ShippingLine)
.save(
new ShippingLine({
shippingMethod,
order,
adjustments: [],
listPrice: 0,
listPriceIncludesTax: ctx.channel.pricesIncludeTax,
taxLines: [],
})
);
order.shippingLines.push(shippingLine);
await this.connection.getRepository(ctx, ShippingLine).save(shippingLine);
}
// If I just have this shipping line I assign the orderitem to that shipping line
// @ts-ignore TODO remove
orderLine.customFields.shippingLine = shippingLine;
await this.connection.getRepository(ctx, OrderLine).save(orderLine);
Logger.debug("Order update: " + JSON.stringify(order.id) + JSON.stringify(order.code));
// @ts-ignore
order.lines.forEach((item) => console.log("ORDERLINE ID: " + item.id + " SHIPPINGLINE: " + item.customFields.shippingLine?.id + " NAME: " + item.productVariant.name));
order.shippingLines.forEach((item) => console.log("SHIPPINGLINE ID: " +item.id + " METHODID: " + item.shippingMethodId));
// Clear shipping line with no items
let shippingMethodsOnOrder: ID[] = order.lines
// @ts-ignore TODO remove
.map((line: OrderLine) => line.customFields?.shippingLine?.id)
// set unique list (optimization)
.filter((v, i, a) => a.indexOf(v) === i);
Logger.debug("Order lines: " + JSON.stringify( order.lines.length) + " -- found: " + shippingMethodsOnOrder);
shippingMethodsOnOrder.forEach(async (shippingMethodIdToCheck: ID) => {
if (shippingMethodIdToCheck === undefined || shippingMethodIdToCheck == null) {
return;
}
let count = this.countOrderItemByShippingMethod(
order,
shippingMethodIdToCheck
);
Logger.debug("Counter remove: " + count + " -- found: " + shippingMethodIdToCheck);
if (count < 1) {
Logger.debug("Foudn remove: " + count + " -- found: " + shippingMethodIdToCheck);
// remove shipping line
let shippingLineToRemove: number = await this.getIndexShippingLine(
order,
shippingMethodIdToCheck
);
if (shippingLineToRemove >= 0) {
Logger.debug("Delete remove: " + shippingLineToRemove + " -- found: " + shippingMethodIdToCheck);
await this.connection
.getRepository(ctx, ShippingLine)
.delete(order.shippingLines[shippingLineToRemove].id);
order.shippingLines.splice(shippingLineToRemove, 1);
}
}
});
Logger.debug("Final update: " + JSON.stringify(order.id) + JSON.stringify(order.code));
// @ts-ignore
order.lines.forEach((item) => console.log("ORDERLINE ID: " + item.id + " SHIPPINGLINE: " + item.customFields.shippingLine?.id + " NAME: " + item.productVariant.name));
order.shippingLines.forEach((item) => console.log("SHIPPINGLINE ID: " +item.id + " METHODID: " + item.shippingMethodId));
// Save for recalculate
await this.connection
.getRepository(ctx, Order)
.save(order, { reload: false });
await this.orderService.applyPriceAdjustments(ctx, order); // TODO made some test
return this.connection.getRepository(ctx, Order).save(order);
}
private getOrderLineOrThrow(order: Order, orderLineId: ID): OrderLine {
const orderLine = order.lines.find((line) =>
idsAreEqual(line.id, orderLineId)
);
if (!orderLine) {
throw new UserInputError(`error.order-does-not-contain-line-with-id`, {
id: orderLineId,
});
}
return orderLine;
}
private getShippingLine(
order: Order,
shippingMethodId: ID
): ShippingLine | undefined {
return order.shippingLines.find(
(shippingLine) => shippingLine.shippingMethodId == shippingMethodId
);
}
private getIndexShippingLine(order: Order, shippingMethodId: ID): number {
return order.shippingLines.findIndex(
(item) => item.shippingMethodId === shippingMethodId
);
}
private countOrderItemByShippingMethod(
order: Order,
shippingMethodId: ID
): number {
return order.shippingLines.filter(
(shippingLine) => shippingLine.shippingMethodId == shippingMethodId
).length;
}
private async getOrderOrThrow(
ctx: RequestContext,
orderId: ID
): Promise<Order> {
const order = await this.orderService.findOne(ctx, orderId);
if (!order) {
throw new EntityNotFoundError("Order", orderId);
}
return order;
}
/**
* Returns error if the Order is not in the "AddingItems" state.
*/
private assertAddingItemsState(order: Order) {
if (order.state !== "AddingItems") {
return new OrderModificationError();
}
}
}