medusajs / medusa

Building blocks for digital commerce
https://medusajs.com
MIT License
24.74k stars 2.44k forks source link

Confusion Around Fulfillment Status Update Logic #9292

Open erickirt opened 2 hours ago

erickirt commented 2 hours ago

I encountered a potential issue where the order’s fulfillment_status is set to "fulfilled" when all items in the fulfillment have a packed_at timestamp. This can cause confusion, as "fulfilled" typically implies the order has been shipped, not just packed. The current behavior of equating packed_at with the "fulfilled" status could be misleading, as it does not distinguish between an order being fully packed and actually shipped. Could you clarify if this is the intended behavior, or if a distinction between packed and shipped statuses is needed?

Link to file

erickirt commented 2 hours ago

Below are two versions of the getLastFulfillmentStatus function tailored to address the reported confusion around the fulfilled status when packed_at timestamps are present. Each version includes a concise explanation of the changes made.


Version 1: Adding Extra Statuses

Changes:

export const getLastFulfillmentStatus = (order: OrderDetailDTO) => {
  const FulfillmentStatus = {
    NOT_FULFILLED: "not_fulfilled",
    PARTIALLY_FULFILLED: "partially_fulfilled",
    PACKED: "packed", // New status added
    FULFILLED: "fulfilled",
    PARTIALLY_SHIPPED: "partially_shipped",
    SHIPPED: "shipped",
    DELIVERED: "delivered",
    PARTIALLY_DELIVERED: "partially_delivered",
    CANCELED: "canceled",
  }

  let fulfillmentStatus: { [key: string]: number } = {}

  for (const status in FulfillmentStatus) {
    fulfillmentStatus[FulfillmentStatus[status]] = 0
  }

  const statusMap = {
    canceled_at: FulfillmentStatus.CANCELED,
    delivered_at: FulfillmentStatus.DELIVERED,
    shipped_at: FulfillmentStatus.SHIPPED,
    packed_at: FulfillmentStatus.PACKED, // Updated mapping
  }

  for (const fulfillmentCollection of order.fulfillments) {
    for (const key in statusMap) {
      if (fulfillmentCollection[key]) {
        fulfillmentStatus[statusMap[key]] += 1
        break
      }
    }
  }

  const totalFulfillments = order.fulfillments.length
  const totalFulfillmentsExceptCanceled =
    totalFulfillments - fulfillmentStatus[FulfillmentStatus.CANCELED]

  const hasUnfulfilledItems = (order.items || [])?.filter(
    (i) =>
      isDefined(i?.detail?.raw_fulfilled_quantity) &&
      MathBN.lt(i.detail.raw_fulfilled_quantity, i.raw_quantity)
  ).length > 0

  if (fulfillmentStatus[FulfillmentStatus.DELIVERED] > 0) {
    if (
      fulfillmentStatus[FulfillmentStatus.DELIVERED] ===
        totalFulfillmentsExceptCanceled &&
      !hasUnfulfilledItems
    ) {
      return FulfillmentStatus.DELIVERED
    }

    return FulfillmentStatus.PARTIALLY_DELIVERED
  }

  if (fulfillmentStatus[FulfillmentStatus.SHIPPED] > 0) {
    if (
      fulfillmentStatus[FulfillmentStatus.SHIPPED] ===
        totalFulfillmentsExceptCanceled &&
      !hasUnfulfilledItems
    ) {
      return FulfillmentStatus.SHIPPED
    }

    return FulfillmentStatus.PARTIALLY_SHIPPED
  }

  if (fulfillmentStatus[FulfillmentStatus.PACKED] > 0) { // New condition
    if (
      fulfillmentStatus[FulfillmentStatus.PACKED] ===
        totalFulfillmentsExceptCanceled &&
      !hasUnfulfilledItems
    ) {
      return FulfillmentStatus.PACKED
    }

    return FulfillmentStatus.PARTIALLY_FULFILLED
  }

  if (fulfillmentStatus[FulfillmentStatus.FULFILLED] > 0) {
    if (
      fulfillmentStatus[FulfillmentStatus.FULFILLED] ===
        totalFulfillmentsExceptCanceled &&
      !hasUnfulfilledItems
    ) {
      return FulfillmentStatus.FULFILLED
    }

    return FulfillmentStatus.PARTIALLY_FULFILLED
  }

  if (
    fulfillmentStatus[FulfillmentStatus.CANCELED] > 0 &&
    fulfillmentStatus[FulfillmentStatus.CANCELED] === totalFulfillments
  ) {
    return FulfillmentStatus.CANCELED
  }

  return FulfillmentStatus.NOT_FULFILLED
}

Explanation:


Version 2: Adjusting Fulfillment Logic Without Extra Statuses

Changes:

export const getLastFulfillmentStatus = (order: OrderDetailDTO) => {
  const FulfillmentStatus = {
    NOT_FULFILLED: "not_fulfilled",
    PARTIALLY_FULFILLED: "partially_fulfilled",
    FULFILLED: "fulfilled",
    PARTIALLY_SHIPPED: "partially_shipped",
    SHIPPED: "shipped",
    DELIVERED: "delivered",
    PARTIALLY_DELIVERED: "partially_delivered",
    CANCELED: "canceled",
  }

  let fulfillmentStatus: { [key: string]: number } = {}

  for (const status in FulfillmentStatus) {
    fulfillmentStatus[FulfillmentStatus[status]] = 0
  }

  const statusMap = {
    canceled_at: FulfillmentStatus.CANCELED,
    delivered_at: FulfillmentStatus.DELIVERED,
    shipped_at: FulfillmentStatus.SHIPPED,
    packed_at: FulfillmentStatus.PARTIALLY_FULFILLED, // Changed mapping
  }

  for (const fulfillmentCollection of order.fulfillments) {
    for (const key in statusMap) {
      if (fulfillmentCollection[key]) {
        fulfillmentStatus[statusMap[key]] += 1
        break
      }
    }
  }

  const totalFulfillments = order.fulfillments.length
  const totalFulfillmentsExceptCanceled =
    totalFulfillments - fulfillmentStatus[FulfillmentStatus.CANCELED]

  const hasUnfulfilledItems = (order.items || [])?.filter(
    (i) =>
      isDefined(i?.detail?.raw_fulfilled_quantity) &&
      MathBN.lt(i.detail.raw_fulfilled_quantity, i.raw_quantity)
  ).length > 0

  if (fulfillmentStatus[FulfillmentStatus.DELIVERED] > 0) {
    if (
      fulfillmentStatus[FulfillmentStatus.DELIVERED] ===
        totalFulfillmentsExceptCanceled &&
      !hasUnfulfilledItems
    ) {
      return FulfillmentStatus.DELIVERED
    }

    return FulfillmentStatus.PARTIALLY_DELIVERED
  }

  if (fulfillmentStatus[FulfillmentStatus.SHIPPED] > 0) {
    if (
      fulfillmentStatus[FulfillmentStatus.SHIPPED] ===
        totalFulfillmentsExceptCanceled &&
      !hasUnfulfilledItems
    ) {
      return FulfillmentStatus.SHIPPED
    }

    return FulfillmentStatus.PARTIALLY_SHIPPED
  }

  if (fulfillmentStatus[FulfillmentStatus.FULFILLED] > 0) {
    if (
      fulfillmentStatus[FulfillmentStatus.FULFILLED] ===
        totalFulfillmentsExceptCanceled &&
      !hasUnfulfilledItems
    ) {
      return FulfillmentStatus.FULFILLED
    }

    return FulfillmentStatus.PARTIALLY_FULFILLED
  }

  if (fulfillmentStatus[FulfillmentStatus.PARTIALLY_FULFILLED] > 0) { // New condition
    return FulfillmentStatus.PARTIALLY_FULFILLED
  }

  if (
    fulfillmentStatus[FulfillmentStatus.CANCELED] > 0 &&
    fulfillmentStatus[FulfillmentStatus.CANCELED] === totalFulfillments
  ) {
    return FulfillmentStatus.CANCELED
  }

  return FulfillmentStatus.NOT_FULFILLED
}

Explanation:


Summary of Changes:

  1. Version 1:

    • Added PACKED Status to clearly differentiate between packed and fulfilled states.
    • Updated Status Mapping to assign packed_at to the new PACKED status.
  2. Version 2:

    • Mapped packed_at to PARTIALLY_FULFILLED instead of FULFILLED.
    • Introduced Logic to Prevent Premature Fulfillment, ensuring that packing does not automatically mark the order as fulfilled.