vendure icon indicating copy to clipboard operation
vendure copied to clipboard

Allow multiple shipping method inside an Order

Open giosueDelgado opened this issue 2 years ago • 0 comments

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();
    }
  }
}

giosueDelgado avatar Jul 13 '22 16:07 giosueDelgado