Feat(order): order changes (#6614)

This is a PR to keep them relatively small. Very likely changes, validations and other features will be added.

What:
  Basic methods to cancel, confirm or decline order changes
  Apply order changes to modify and create a new version of an order

Things related to calculation, Order and Item totals are not covered in this PR. Properties won't match with definition, etc.

Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
Carlos R. L. Rodrigues
2024-03-07 15:24:05 -03:00
committed by GitHub
parent e4acde1aa2
commit 43399c8d0d
34 changed files with 2138 additions and 338 deletions

View File

@@ -8,5 +8,6 @@ export enum ChangeActionType {
RECEIVE_RETURN_ITEM = "RECEIVE_RETURN_ITEM",
RETURN_ITEM = "RETURN_ITEM",
SHIPPING_ADD = "SHIPPING_ADD",
SHIP_ITEM = "SHIP_ITEM",
WRITE_OFF_ITEM = "WRITE_OFF_ITEM",
}

View File

@@ -8,7 +8,9 @@ OrderChangeProcessing.registerActionType(ChangeActionType.CANCEL_RETURN, {
(item) => item.id === action.details.reference_id
)!
existing.return_requested_quantity -= action.details.quantity
existing.detail.return_requested_quantity ??= 0
existing.detail.return_requested_quantity -= action.details.quantity
return action.details.unit_price * action.details.quantity
},
@@ -17,7 +19,7 @@ OrderChangeProcessing.registerActionType(ChangeActionType.CANCEL_RETURN, {
(item) => item.id === action.details.reference_id
)!
existing.return_requested_quantity += action.details.quantity
existing.detail.return_requested_quantity += action.details.quantity
},
validate({ action, currentOrder }) {
const refId = action.details?.reference_id
@@ -28,6 +30,13 @@ OrderChangeProcessing.registerActionType(ChangeActionType.CANCEL_RETURN, {
)
}
if (!isDefined(action.amount) && !isDefined(action.details?.unit_price)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Unit price of item ${action.reference_id} is required if no action.amount is provided.`
)
}
const existing = currentOrder.items.find((item) => item.id === refId)
if (!existing) {
@@ -37,13 +46,17 @@ OrderChangeProcessing.registerActionType(ChangeActionType.CANCEL_RETURN, {
)
}
const notFulfilled =
(existing.quantity as number) - (existing.fulfilled_quantity as number)
if (action.details.quantity > notFulfilled) {
if (!action.details?.quantity) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Cannot fulfill more items than what was ordered."
`Quantity to cancel return of item ${refId} is required.`
)
}
if (action.details?.quantity > existing.detail?.return_requested_quantity) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cannot cancel more items than what was requested to return for item ${refId}.`
)
}
},

View File

@@ -8,17 +8,19 @@ OrderChangeProcessing.registerActionType(ChangeActionType.FULFILL_ITEM, {
(item) => item.id === action.details.reference_id
)!
existing.fulfilled_quantity += action.details.quantity
existing.detail.fulfilled_quantity ??= 0
existing.detail.fulfilled_quantity += action.details.quantity
},
revert({ action, currentOrder }) {
const existing = currentOrder.items.find(
(item) => item.id === action.reference_id
)!
existing.fulfilled_quantity -= action.details.quantity
existing.detail.fulfilled_quantity -= action.details.quantity
},
validate({ action, currentOrder }) {
const refId = action.details.reference_id
const refId = action.details?.reference_id
if (!isDefined(refId)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -34,10 +36,28 @@ OrderChangeProcessing.registerActionType(ChangeActionType.FULFILL_ITEM, {
)
}
if (action.details.quantity < 1) {
if (!action.details?.quantity) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Quantity must be greater than 0."
`Quantity to fulfill of item ${refId} is required.`
)
}
if (action.details?.quantity < 1) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Quantity of item ${refId} must be greater than 0.`
)
}
const notFulfilled =
(existing.quantity as number) -
(existing.detail?.fulfilled_quantity as number)
if (action.details?.quantity > notFulfilled) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cannot fulfill more items than what was ordered for item ${refId}.`
)
}
},

View File

@@ -6,4 +6,5 @@ export * from "./item-remove"
export * from "./receive-damaged-return-item"
export * from "./receive-return-item"
export * from "./return-item"
export * from "./ship-item"
export * from "./shipping-add"

View File

@@ -10,12 +10,16 @@ OrderChangeProcessing.registerActionType(ChangeActionType.ITEM_ADD, {
)
if (existing) {
existing.detail.quantity ??= 0
existing.quantity += action.details.quantity
existing.detail.quantity += action.details.quantity
} else {
currentOrder.items.push({
id: action.reference_id!,
unit_price: action.details.unit_price,
quantity: action.details.quantity,
// detail: {}
} as VirtualOrder["items"][0])
}
@@ -29,6 +33,7 @@ OrderChangeProcessing.registerActionType(ChangeActionType.ITEM_ADD, {
if (existingIndex > -1) {
const existing = currentOrder.items[existingIndex]
existing.quantity -= action.details.quantity
existing.detail.quantity -= action.details.quantity
if (existing.quantity <= 0) {
currentOrder.items.splice(existingIndex, 1)
@@ -36,6 +41,7 @@ OrderChangeProcessing.registerActionType(ChangeActionType.ITEM_ADD, {
}
},
validate({ action }) {
const refId = action.reference_id
if (!isDefined(action.reference_id)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -43,17 +49,24 @@ OrderChangeProcessing.registerActionType(ChangeActionType.ITEM_ADD, {
)
}
if (!isDefined(action.details.unit_price)) {
if (!isDefined(action.amount) && !isDefined(action.details?.unit_price)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Unit price is required."
`Unit price of item ${refId} is required if no action.amount is provided.`
)
}
if (action.details.quantity < 1) {
if (!action.details?.quantity) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Quantity must be greater than 0."
`Quantity of item ${refId} is required.`
)
}
if (action.details?.quantity < 1) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Quantity of item ${refId} must be greater than 0.`
)
}
},

View File

@@ -11,7 +11,11 @@ OrderChangeProcessing.registerActionType(ChangeActionType.ITEM_REMOVE, {
)
const existing = currentOrder.items[existingIndex]
existing.detail.quantity ??= 0
existing.quantity -= action.details.quantity
existing.detail.quantity -= action.details.quantity
if (existing.quantity <= 0) {
currentOrder.items.splice(existingIndex, 1)
@@ -26,6 +30,7 @@ OrderChangeProcessing.registerActionType(ChangeActionType.ITEM_REMOVE, {
if (existing) {
existing.quantity += action.details.quantity
existing.detail.quantity += action.details.quantity
} else {
currentOrder.items.push({
id: action.reference_id!,
@@ -51,27 +56,35 @@ OrderChangeProcessing.registerActionType(ChangeActionType.ITEM_REMOVE, {
)
}
if (!isDefined(action.details.unit_price)) {
if (!isDefined(action.amount) && !isDefined(action.details?.unit_price)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Unit price is required."
`Unit price of item ${refId} is required if no action.amount is provided.`
)
}
if (action.details.quantity < 1) {
if (!action.details?.quantity) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Quantity must be greater than 0."
`Quantity of item ${refId} is required.`
)
}
if (action.details?.quantity < 1) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Quantity of item ${refId} must be greater than 0.`
)
}
const notFulfilled =
(existing.quantity as number) - (existing.fulfilled_quantity as number)
(existing.quantity as number) -
(existing.detail?.fulfilled_quantity as number)
if (action.details.quantity > notFulfilled) {
if (action.details?.quantity > notFulfilled) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Cannot remove fulfilled items."
`Cannot remove fulfilled item: Item ${refId}.`
)
}
},

View File

@@ -15,9 +15,11 @@ OrderChangeProcessing.registerActionType(
let toReturn = action.details.quantity
existing.return_dismissed_quantity ??= 0
existing.return_dismissed_quantity += toReturn
existing.return_requested_quantity -= toReturn
existing.detail.return_dismissed_quantity ??= 0
existing.detail.return_requested_quantity ??= 0
existing.detail.return_dismissed_quantity += toReturn
existing.detail.return_requested_quantity -= toReturn
if (previousEvents) {
for (const previousEvent of previousEvents) {
@@ -40,8 +42,8 @@ OrderChangeProcessing.registerActionType(
(item) => item.id === action.details.reference_id
)!
existing.return_dismissed_quantity -= action.details.quantity
existing.return_requested_quantity += action.details.quantity
existing.detail.return_dismissed_quantity -= action.details.quantity
existing.detail.return_requested_quantity += action.details.quantity
if (previousEvents) {
for (const previousEvent of previousEvents) {
@@ -76,11 +78,18 @@ OrderChangeProcessing.registerActionType(
)
}
const quantityRequested = existing?.return_requested_quantity || 0
if (action.details.quantity > quantityRequested) {
if (!action.details?.quantity) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Cannot receive more items than what was requested to be returned."
`Quantity to return of item ${refId} is required.`
)
}
const quantityRequested = existing?.detail.return_requested_quantity || 0
if (action.details?.quantity > quantityRequested) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cannot receive more items than what was requested to be returned for item ${refId}.`
)
}
},

View File

@@ -13,9 +13,11 @@ OrderChangeProcessing.registerActionType(ChangeActionType.RECEIVE_RETURN_ITEM, {
let toReturn = action.details.quantity
existing.return_received_quantity ??= 0
existing.return_received_quantity += toReturn
existing.return_requested_quantity -= toReturn
existing.detail.return_received_quantity ??= 0
existing.detail.return_requested_quantity ??= 0
existing.detail.return_received_quantity += toReturn
existing.detail.return_requested_quantity -= toReturn
if (previousEvents) {
for (const previousEvent of previousEvents) {
@@ -38,8 +40,8 @@ OrderChangeProcessing.registerActionType(ChangeActionType.RECEIVE_RETURN_ITEM, {
(item) => item.id === action.details.reference_id
)!
existing.return_received_quantity -= action.details.quantity
existing.return_requested_quantity += action.details.quantity
existing.detail.return_received_quantity -= action.details.quantity
existing.detail.return_requested_quantity += action.details.quantity
if (previousEvents) {
for (const previousEvent of previousEvents) {
@@ -74,11 +76,18 @@ OrderChangeProcessing.registerActionType(ChangeActionType.RECEIVE_RETURN_ITEM, {
)
}
const quantityRequested = existing?.return_requested_quantity || 0
if (action.details.quantity > quantityRequested) {
if (!action.details?.quantity) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Cannot receive more items than what was requested to be returned."
`Quantity to receive return of item ${refId} is required.`
)
}
const quantityRequested = existing?.detail?.return_requested_quantity || 0
if (action.details?.quantity > quantityRequested) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cannot receive more items than what was requested to be returned for item ${refId}.`
)
}
},

View File

@@ -10,8 +10,8 @@ OrderChangeProcessing.registerActionType(ChangeActionType.RETURN_ITEM, {
(item) => item.id === action.details.reference_id
)!
existing.return_requested_quantity ??= 0
existing.return_requested_quantity += action.details.quantity
existing.detail.return_requested_quantity ??= 0
existing.detail.return_requested_quantity += action.details.quantity
return existing.unit_price * action.details.quantity
},
@@ -20,7 +20,7 @@ OrderChangeProcessing.registerActionType(ChangeActionType.RETURN_ITEM, {
(item) => item.id === action.details.reference_id
)!
existing.return_requested_quantity -= action.details.quantity
existing.detail.return_requested_quantity -= action.details.quantity
},
validate({ action, currentOrder }) {
const refId = action.details?.reference_id
@@ -40,14 +40,21 @@ OrderChangeProcessing.registerActionType(ChangeActionType.RETURN_ITEM, {
)
}
const quantityAvailable =
(existing!.fulfilled_quantity ?? 0) -
(existing!.return_requested_quantity ?? 0)
if (action.details.quantity > quantityAvailable) {
if (!action.details?.quantity) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Cannot request to return more items than what was fulfilled."
`Quantity to return of item ${refId} is required.`
)
}
const quantityAvailable =
(existing!.detail?.shipped_quantity ?? 0) -
(existing!.detail?.return_requested_quantity ?? 0)
if (action.details?.quantity > quantityAvailable) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cannot request to return more items than what was shipped for item ${refId}.`
)
}
},

View File

@@ -0,0 +1,64 @@
import { MedusaError, isDefined } from "@medusajs/utils"
import { ChangeActionType } from "../action-key"
import { OrderChangeProcessing } from "../calculate-order-change"
OrderChangeProcessing.registerActionType(ChangeActionType.SHIP_ITEM, {
operation({ action, currentOrder }) {
const existing = currentOrder.items.find(
(item) => item.id === action.details.reference_id
)!
existing.detail.shipped_quantity ??= 0
existing.detail.shipped_quantity += action.details.quantity
},
revert({ action, currentOrder }) {
const existing = currentOrder.items.find(
(item) => item.id === action.reference_id
)!
existing.detail.shipped_quantity -= action.details.quantity
},
validate({ action, currentOrder }) {
const refId = action.details?.reference_id
if (!isDefined(refId)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Reference ID is required."
)
}
const existing = currentOrder.items.find((item) => item.id === refId)
if (!existing) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Reference ID "${refId}" not found.`
)
}
if (!action.details?.quantity) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Quantity to ship of item ${refId} is required.`
)
}
if (action.details?.quantity < 1) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Quantity of item ${refId} must be greater than 0.`
)
}
const notShipped =
(existing.detail?.fulfilled_quantity as number) -
(existing.detail?.shipped_quantity as number)
if (action.details?.quantity > notShipped) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cannot ship more items than what was fulfilled for item ${refId}.`
)
}
},
})

View File

@@ -14,7 +14,6 @@ OrderChangeProcessing.registerActionType(ChangeActionType.SHIPPING_ADD, {
})
currentOrder.shipping_methods = shipping
return action.amount
},
revert({ action, currentOrder }) {
const shipping = Array.isArray(currentOrder.shipping_methods)

View File

@@ -8,15 +8,15 @@ OrderChangeProcessing.registerActionType(ChangeActionType.WRITE_OFF_ITEM, {
(item) => item.id === action.details.reference_id
)!
existing.written_off_quantity ??= 0
existing.written_off_quantity += action.details.quantity
existing.detail.written_off_quantity ??= 0
existing.detail.written_off_quantity += action.details.quantity
},
revert({ action, currentOrder }) {
const existing = currentOrder.items.find(
(item) => item.id === action.details.reference_id
)!
existing.written_off_quantity -= action.details.quantity
existing.detail.written_off_quantity -= action.details.quantity
},
validate({ action, currentOrder }) {
const refId = action.details?.reference_id
@@ -36,8 +36,15 @@ OrderChangeProcessing.registerActionType(ChangeActionType.WRITE_OFF_ITEM, {
)
}
if (!action.details?.quantity) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Quantity to write-off item ${refId} is required.`
)
}
const quantityAvailable = existing!.quantity ?? 0
if (action.details.quantity > quantityAvailable) {
if (action.details?.quantity > quantityAvailable) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Cannot claim more items than what was ordered."

View File

@@ -1,14 +1,16 @@
import { OrderSummaryDTO } from "@medusajs/types"
import { isDefined } from "@medusajs/utils"
import {
ActionTypeDefinition,
EVENT_STATUS,
InternalOrderChangeEvent,
OrderChangeEvent,
OrderSummary,
OrderSummaryCalculated,
OrderTransaction,
VirtualOrder,
} from "@types"
type InternalOrderSummary = OrderSummary & {
type InternalOrderSummary = OrderSummaryCalculated & {
futureTemporarySum: number
}
@@ -55,8 +57,8 @@ export class OrderChangeProcessing {
pendingDifference: 0,
futureTemporarySum: 0,
differenceSum: 0,
currentOrderTotal: order.total as number,
originalOrderTotal: order.total as number,
currentOrderTotal: order?.summary?.total ?? 0,
originalOrderTotal: order?.summary?.total ?? 0,
transactionTotal,
}
}
@@ -181,7 +183,10 @@ export class OrderChangeProcessing {
if (typeof type.operation === "function") {
calculatedAmount = type.operation(params) as number
action.amount = calculatedAmount ?? 0
// the action.amount has priority over the calculated amount
if (!isDefined(action.amount)) {
action.amount = calculatedAmount ?? 0
}
}
// If an action commits previous ones, replay them with updated values
@@ -313,7 +318,7 @@ export class OrderChangeProcessing {
})
}
public getSummary(): OrderSummary {
public getSummary(): OrderSummaryDTO {
const summary = this.summary
const orderSummary = {
transactionTotal: summary.transactionTotal,
@@ -324,7 +329,35 @@ export class OrderChangeProcessing {
futureTemporaryDifference: summary.futureTemporaryDifference,
pendingDifference: summary.pendingDifference,
differenceSum: summary.differenceSum,
} as unknown as OrderSummaryDTO
/*
{
total: summary.currentOrderTotal
subtotal: number
total_tax: number
ordered_total: summary.originalOrderTotal
fulfilled_total: number
returned_total: number
return_request_total: number
write_off_total: number
projected_total: number
net_total: number
net_subtotal: number
net_total_tax: number
future_total: number
future_subtotal: number
future_total_tax: number
future_projected_total: number
balance: summary.pendingDifference
future_balance: number
}
*/
return orderSummary
}

View File

@@ -1,11 +1,11 @@
import { OrderTypes } from "@medusajs/types"
import { isDefined } from "@medusajs/utils"
import { deduplicate, isDefined } from "@medusajs/utils"
export function formatOrder(
order
): OrderTypes.OrderDTO | OrderTypes.OrderDTO[] {
const isArray = Array.isArray(order)
const orders = isArray ? order : [order]
const orders = [...(isArray ? order : [order])]
orders.map((order) => {
order.items = order.items?.map((orderItem) => {
@@ -21,6 +21,8 @@ export function formatOrder(
}
})
order.summary = order.summary?.[0]?.totals
return order
})
@@ -35,25 +37,27 @@ export function mapRepositoryToOrderModel(config) {
return
}
return [
...new Set<string>(
obj[type].sort().map((rel) => {
if (rel == "items.quantity") {
if (type === "fields") {
obj.populate.push("items.item")
}
return "items.item.quantity"
} else if (rel.includes("items.detail")) {
return rel.replace("items.detail", "items")
} else if (rel == "items") {
return "items.item"
} else if (rel.includes("items.") && !rel.includes("items.item")) {
return rel.replace("items.", "items.item.")
return deduplicate(
obj[type].sort().map((rel) => {
if (rel == "items.quantity") {
if (type === "fields") {
obj.populate.push("items.item")
}
return rel
})
),
]
return "items.item.quantity"
}
if (rel == "summary" && type === "fields") {
obj.populate.push("summary")
return "summary.totals"
} else if (rel.includes("items.detail")) {
return rel.replace("items.detail", "items")
} else if (rel == "items") {
return "items.item"
} else if (rel.includes("items.") && !rel.includes("items.item")) {
return rel.replace("items.", "items.item.")
}
return rel
})
)
}
conf.options.fields = replace(config.options, "fields")