feat(order): order change actions engine (#6467)
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
export enum ChangeActionType {
|
||||
CANCEL = "CANCEL",
|
||||
CANCEL_RETURN = "CANCEL_RETURN",
|
||||
FULFILL_ITEM = "FULFILL_ITEM",
|
||||
ITEM_ADD = "ITEM_ADD",
|
||||
ITEM_REMOVE = "ITEM_REMOVE",
|
||||
RECEIVE_DAMAGED_RETURN_ITEM = "RECEIVE_DAMAGED_RETURN_ITEM",
|
||||
RECEIVE_RETURN_ITEM = "RECEIVE_RETURN_ITEM",
|
||||
RETURN_ITEM = "RETURN_ITEM",
|
||||
SHIPPING_ADD = "SHIPPING_ADD",
|
||||
WRITE_OFF_ITEM = "WRITE_OFF_ITEM",
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { MedusaError, isDefined } from "@medusajs/utils"
|
||||
import { ChangeActionType } from "../action-key"
|
||||
import { OrderChangeProcessing } from "../calculate-order-change"
|
||||
|
||||
OrderChangeProcessing.registerActionType(ChangeActionType.CANCEL_RETURN, {
|
||||
operation({ action, currentOrder }) {
|
||||
const existing = currentOrder.items.find(
|
||||
(item) => item.id === action.details.reference_id
|
||||
)!
|
||||
|
||||
existing.return_requested_quantity -= action.details.quantity
|
||||
|
||||
return action.details.unit_price * action.details.quantity
|
||||
},
|
||||
revert({ action, currentOrder }) {
|
||||
const existing = currentOrder.items.find(
|
||||
(item) => item.id === action.details.reference_id
|
||||
)!
|
||||
|
||||
existing.return_requested_quantity += action.details.quantity
|
||||
},
|
||||
validate({ action, currentOrder }) {
|
||||
const refId = action.details?.reference_id
|
||||
if (!isDefined(refId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Details 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.`
|
||||
)
|
||||
}
|
||||
|
||||
const notFulfilled =
|
||||
(existing.quantity as number) - (existing.fulfilled_quantity as number)
|
||||
|
||||
if (action.details.quantity > notFulfilled) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Cannot fulfill more items than what was ordered."
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ChangeActionType } from "../action-key"
|
||||
import { OrderChangeProcessing } from "../calculate-order-change"
|
||||
|
||||
OrderChangeProcessing.registerActionType(ChangeActionType.CANCEL, {
|
||||
void: true,
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import { MedusaError, isDefined } from "@medusajs/utils"
|
||||
import { ChangeActionType } from "../action-key"
|
||||
import { OrderChangeProcessing } from "../calculate-order-change"
|
||||
|
||||
OrderChangeProcessing.registerActionType(ChangeActionType.FULFILL_ITEM, {
|
||||
operation({ action, currentOrder }) {
|
||||
const existing = currentOrder.items.find(
|
||||
(item) => item.id === action.details.reference_id
|
||||
)!
|
||||
|
||||
existing.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
|
||||
},
|
||||
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 < 1) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Quantity must be greater than 0."
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from "./cancel"
|
||||
export * from "./cancel-return"
|
||||
export * from "./fulfill-item"
|
||||
export * from "./item-add"
|
||||
export * from "./item-remove"
|
||||
export * from "./receive-damaged-return-item"
|
||||
export * from "./receive-return-item"
|
||||
export * from "./return-item"
|
||||
export * from "./shipping-add"
|
||||
@@ -0,0 +1,60 @@
|
||||
import { MedusaError, isDefined } from "@medusajs/utils"
|
||||
import { VirtualOrder } from "@types"
|
||||
import { ChangeActionType } from "../action-key"
|
||||
import { OrderChangeProcessing } from "../calculate-order-change"
|
||||
|
||||
OrderChangeProcessing.registerActionType(ChangeActionType.ITEM_ADD, {
|
||||
operation({ action, currentOrder }) {
|
||||
const existing = currentOrder.items.find(
|
||||
(item) => item.id === action.reference_id
|
||||
)
|
||||
|
||||
if (existing) {
|
||||
existing.quantity += action.details.quantity
|
||||
} else {
|
||||
currentOrder.items.push({
|
||||
id: action.reference_id!,
|
||||
unit_price: action.details.unit_price,
|
||||
quantity: action.details.quantity,
|
||||
} as VirtualOrder["items"][0])
|
||||
}
|
||||
|
||||
return action.details.unit_price * action.details.quantity
|
||||
},
|
||||
revert({ action, currentOrder }) {
|
||||
const existingIndex = currentOrder.items.findIndex(
|
||||
(item) => item.id === action.reference_id
|
||||
)
|
||||
|
||||
if (existingIndex > -1) {
|
||||
const existing = currentOrder.items[existingIndex]
|
||||
existing.quantity -= action.details.quantity
|
||||
|
||||
if (existing.quantity <= 0) {
|
||||
currentOrder.items.splice(existingIndex, 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
validate({ action }) {
|
||||
if (!isDefined(action.reference_id)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Reference ID is required."
|
||||
)
|
||||
}
|
||||
|
||||
if (!isDefined(action.details.unit_price)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Unit price is required."
|
||||
)
|
||||
}
|
||||
|
||||
if (action.details.quantity < 1) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Quantity must be greater than 0."
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
import { MedusaError, isDefined } from "@medusajs/utils"
|
||||
import { VirtualOrder } from "@types"
|
||||
import { ChangeActionType } from "../action-key"
|
||||
import { OrderChangeProcessing } from "../calculate-order-change"
|
||||
|
||||
OrderChangeProcessing.registerActionType(ChangeActionType.ITEM_REMOVE, {
|
||||
isDeduction: true,
|
||||
operation({ action, currentOrder }) {
|
||||
const existingIndex = currentOrder.items.findIndex(
|
||||
(item) => item.id === action.reference_id
|
||||
)
|
||||
|
||||
const existing = currentOrder.items[existingIndex]
|
||||
existing.quantity -= action.details.quantity
|
||||
|
||||
if (existing.quantity <= 0) {
|
||||
currentOrder.items.splice(existingIndex, 1)
|
||||
}
|
||||
|
||||
return existing.unit_price * action.details.quantity
|
||||
},
|
||||
revert({ action, currentOrder }) {
|
||||
const existing = currentOrder.items.find(
|
||||
(item) => item.id === action.reference_id
|
||||
)
|
||||
|
||||
if (existing) {
|
||||
existing.quantity += action.details.quantity
|
||||
} else {
|
||||
currentOrder.items.push({
|
||||
id: action.reference_id!,
|
||||
unit_price: action.details.unit_price,
|
||||
quantity: action.details.quantity,
|
||||
} as VirtualOrder["items"][0])
|
||||
}
|
||||
},
|
||||
validate({ action, currentOrder }) {
|
||||
const refId = action.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 (!isDefined(action.details.unit_price)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Unit price is required."
|
||||
)
|
||||
}
|
||||
|
||||
if (action.details.quantity < 1) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Quantity must be greater than 0."
|
||||
)
|
||||
}
|
||||
|
||||
const notFulfilled =
|
||||
(existing.quantity as number) - (existing.fulfilled_quantity as number)
|
||||
|
||||
if (action.details.quantity > notFulfilled) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Cannot remove fulfilled items."
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
import { MedusaError, isDefined } from "@medusajs/utils"
|
||||
import { EVENT_STATUS } from "@types"
|
||||
import { ChangeActionType } from "../action-key"
|
||||
import { OrderChangeProcessing } from "../calculate-order-change"
|
||||
|
||||
OrderChangeProcessing.registerActionType(
|
||||
ChangeActionType.RECEIVE_DAMAGED_RETURN_ITEM,
|
||||
{
|
||||
isDeduction: true,
|
||||
commitsAction: "return_item",
|
||||
operation({ action, currentOrder, previousEvents }) {
|
||||
const existing = currentOrder.items.find(
|
||||
(item) => item.id === action.details.reference_id
|
||||
)!
|
||||
|
||||
let toReturn = action.details.quantity
|
||||
|
||||
existing.return_dismissed_quantity ??= 0
|
||||
existing.return_dismissed_quantity += toReturn
|
||||
existing.return_requested_quantity -= toReturn
|
||||
|
||||
if (previousEvents) {
|
||||
for (const previousEvent of previousEvents) {
|
||||
previousEvent.original_ = JSON.parse(JSON.stringify(previousEvent))
|
||||
|
||||
let ret = Math.min(toReturn, previousEvent.details.quantity)
|
||||
toReturn -= ret
|
||||
|
||||
previousEvent.details.quantity -= ret
|
||||
if (previousEvent.details.quantity <= 0) {
|
||||
previousEvent.status = EVENT_STATUS.DONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return existing.unit_price * action.details.quantity
|
||||
},
|
||||
revert({ action, currentOrder, previousEvents }) {
|
||||
const existing = currentOrder.items.find(
|
||||
(item) => item.id === action.details.reference_id
|
||||
)!
|
||||
|
||||
existing.return_dismissed_quantity -= action.details.quantity
|
||||
existing.return_requested_quantity += action.details.quantity
|
||||
|
||||
if (previousEvents) {
|
||||
for (const previousEvent of previousEvents) {
|
||||
if (!previousEvent.original_) {
|
||||
continue
|
||||
}
|
||||
|
||||
previousEvent.details = JSON.parse(
|
||||
JSON.stringify(previousEvent.original_.details)
|
||||
)
|
||||
delete previousEvent.original_
|
||||
|
||||
previousEvent.status = EVENT_STATUS.PENDING
|
||||
}
|
||||
}
|
||||
},
|
||||
validate({ action, currentOrder }) {
|
||||
const refId = action.details?.reference_id
|
||||
if (!isDefined(refId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Details 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.`
|
||||
)
|
||||
}
|
||||
|
||||
const quantityRequested = existing?.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."
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
import { MedusaError, isDefined } from "@medusajs/utils"
|
||||
import { EVENT_STATUS } from "@types"
|
||||
import { ChangeActionType } from "../action-key"
|
||||
import { OrderChangeProcessing } from "../calculate-order-change"
|
||||
|
||||
OrderChangeProcessing.registerActionType(ChangeActionType.RECEIVE_RETURN_ITEM, {
|
||||
isDeduction: true,
|
||||
commitsAction: "return_item",
|
||||
operation({ action, currentOrder, previousEvents }) {
|
||||
const existing = currentOrder.items.find(
|
||||
(item) => item.id === action.details.reference_id
|
||||
)!
|
||||
|
||||
let toReturn = action.details.quantity
|
||||
|
||||
existing.return_received_quantity ??= 0
|
||||
existing.return_received_quantity += toReturn
|
||||
existing.return_requested_quantity -= toReturn
|
||||
|
||||
if (previousEvents) {
|
||||
for (const previousEvent of previousEvents) {
|
||||
previousEvent.original_ = JSON.parse(JSON.stringify(previousEvent))
|
||||
|
||||
let ret = Math.min(toReturn, previousEvent.details.quantity)
|
||||
toReturn -= ret
|
||||
|
||||
previousEvent.details.quantity -= ret
|
||||
if (previousEvent.details.quantity <= 0) {
|
||||
previousEvent.status = EVENT_STATUS.DONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return existing.unit_price * action.details.quantity
|
||||
},
|
||||
revert({ action, currentOrder, previousEvents }) {
|
||||
const existing = currentOrder.items.find(
|
||||
(item) => item.id === action.details.reference_id
|
||||
)!
|
||||
|
||||
existing.return_received_quantity -= action.details.quantity
|
||||
existing.return_requested_quantity += action.details.quantity
|
||||
|
||||
if (previousEvents) {
|
||||
for (const previousEvent of previousEvents) {
|
||||
if (!previousEvent.original_) {
|
||||
continue
|
||||
}
|
||||
|
||||
previousEvent.details = JSON.parse(
|
||||
JSON.stringify(previousEvent.original_.details)
|
||||
)
|
||||
delete previousEvent.original_
|
||||
|
||||
previousEvent.status = EVENT_STATUS.PENDING
|
||||
}
|
||||
}
|
||||
},
|
||||
validate({ action, currentOrder }) {
|
||||
const refId = action.details?.reference_id
|
||||
if (!isDefined(refId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Details 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.`
|
||||
)
|
||||
}
|
||||
|
||||
const quantityRequested = existing?.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."
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
import { MedusaError, isDefined } from "@medusajs/utils"
|
||||
import { ChangeActionType } from "../action-key"
|
||||
import { OrderChangeProcessing } from "../calculate-order-change"
|
||||
|
||||
OrderChangeProcessing.registerActionType(ChangeActionType.RETURN_ITEM, {
|
||||
isDeduction: true,
|
||||
awaitRequired: true,
|
||||
operation({ action, currentOrder }) {
|
||||
const existing = currentOrder.items.find(
|
||||
(item) => item.id === action.details.reference_id
|
||||
)!
|
||||
|
||||
existing.return_requested_quantity ??= 0
|
||||
existing.return_requested_quantity += action.details.quantity
|
||||
|
||||
return existing.unit_price * action.details.quantity
|
||||
},
|
||||
revert({ action, currentOrder }) {
|
||||
const existing = currentOrder.items.find(
|
||||
(item) => item.id === action.details.reference_id
|
||||
)!
|
||||
|
||||
existing.return_requested_quantity -= action.details.quantity
|
||||
},
|
||||
validate({ action, currentOrder }) {
|
||||
const refId = action.details?.reference_id
|
||||
if (!isDefined(refId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Details 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.`
|
||||
)
|
||||
}
|
||||
|
||||
const quantityAvailable =
|
||||
(existing!.fulfilled_quantity ?? 0) -
|
||||
(existing!.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 fulfilled."
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { MedusaError, isDefined } from "@medusajs/utils"
|
||||
import { ChangeActionType } from "../action-key"
|
||||
import { OrderChangeProcessing } from "../calculate-order-change"
|
||||
|
||||
OrderChangeProcessing.registerActionType(ChangeActionType.SHIPPING_ADD, {
|
||||
operation({ action, currentOrder }) {
|
||||
const shipping = Array.isArray(currentOrder.shipping_methods)
|
||||
? currentOrder.shipping_methods
|
||||
: [currentOrder.shipping_methods]
|
||||
|
||||
shipping.push({
|
||||
id: action.reference_id!,
|
||||
price: action.amount as number,
|
||||
})
|
||||
|
||||
currentOrder.shipping_methods = shipping
|
||||
return action.amount
|
||||
},
|
||||
revert({ action, currentOrder }) {
|
||||
const shipping = Array.isArray(currentOrder.shipping_methods)
|
||||
? currentOrder.shipping_methods
|
||||
: [currentOrder.shipping_methods]
|
||||
|
||||
const existingIndex = shipping.findIndex(
|
||||
(item) => item.id === action.reference_id
|
||||
)
|
||||
|
||||
if (existingIndex > -1) {
|
||||
shipping.splice(existingIndex, 1)
|
||||
}
|
||||
},
|
||||
validate({ action }) {
|
||||
if (!action.reference_id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Reference ID is required."
|
||||
)
|
||||
}
|
||||
|
||||
if (!isDefined(action.amount)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Amount is required."
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { MedusaError, isDefined } from "@medusajs/utils"
|
||||
import { ChangeActionType } from "../action-key"
|
||||
import { OrderChangeProcessing } from "../calculate-order-change"
|
||||
|
||||
OrderChangeProcessing.registerActionType(ChangeActionType.WRITE_OFF_ITEM, {
|
||||
operation({ action, currentOrder }) {
|
||||
const existing = currentOrder.items.find(
|
||||
(item) => item.id === action.details.reference_id
|
||||
)!
|
||||
|
||||
existing.written_off_quantity ??= 0
|
||||
existing.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
|
||||
},
|
||||
validate({ action, currentOrder }) {
|
||||
const refId = action.details?.reference_id
|
||||
if (!isDefined(refId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Details 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.`
|
||||
)
|
||||
}
|
||||
|
||||
const quantityAvailable = existing!.quantity ?? 0
|
||||
if (action.details.quantity > quantityAvailable) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Cannot claim more items than what was ordered."
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,353 @@
|
||||
import {
|
||||
ActionTypeDefinition,
|
||||
EVENT_STATUS,
|
||||
InternalOrderChangeEvent,
|
||||
OrderChangeEvent,
|
||||
OrderSummary,
|
||||
OrderTransaction,
|
||||
VirtualOrder,
|
||||
} from "@types"
|
||||
|
||||
type InternalOrderSummary = OrderSummary & {
|
||||
futureTemporarySum: number
|
||||
}
|
||||
|
||||
export class OrderChangeProcessing {
|
||||
private static typeDefinition: { [key: string]: ActionTypeDefinition } = {}
|
||||
private static defaultConfig = {
|
||||
awaitRequired: false,
|
||||
isDeduction: false,
|
||||
}
|
||||
|
||||
private order: VirtualOrder
|
||||
private transactions: OrderTransaction[]
|
||||
private actions: InternalOrderChangeEvent[]
|
||||
|
||||
private actionsProcessed: { [key: string]: InternalOrderChangeEvent[] } = {}
|
||||
private groupTotal: Record<string, number> = {}
|
||||
private summary: InternalOrderSummary
|
||||
|
||||
public static registerActionType(key: string, type: ActionTypeDefinition) {
|
||||
OrderChangeProcessing.typeDefinition[key] = type
|
||||
}
|
||||
|
||||
constructor({
|
||||
order,
|
||||
transactions,
|
||||
actions,
|
||||
}: {
|
||||
order: VirtualOrder
|
||||
transactions: OrderTransaction[]
|
||||
actions: InternalOrderChangeEvent[]
|
||||
}) {
|
||||
this.order = JSON.parse(JSON.stringify(order))
|
||||
this.transactions = JSON.parse(JSON.stringify(transactions ?? []))
|
||||
this.actions = JSON.parse(JSON.stringify(actions ?? []))
|
||||
|
||||
const transactionTotal = transactions.reduce((acc, transaction) => {
|
||||
return acc + transaction.amount
|
||||
}, 0)
|
||||
|
||||
this.summary = {
|
||||
futureDifference: 0,
|
||||
futureTemporaryDifference: 0,
|
||||
temporaryDifference: 0,
|
||||
pendingDifference: 0,
|
||||
futureTemporarySum: 0,
|
||||
differenceSum: 0,
|
||||
currentOrderTotal: order.total as number,
|
||||
originalOrderTotal: order.total as number,
|
||||
transactionTotal,
|
||||
}
|
||||
}
|
||||
|
||||
private isEventActive(action: InternalOrderChangeEvent): boolean {
|
||||
const status = action.status
|
||||
return (
|
||||
status === undefined ||
|
||||
status === EVENT_STATUS.PENDING ||
|
||||
status === EVENT_STATUS.DONE
|
||||
)
|
||||
}
|
||||
private isEventDone(action: InternalOrderChangeEvent): boolean {
|
||||
const status = action.status
|
||||
return status === EVENT_STATUS.DONE
|
||||
}
|
||||
|
||||
private isEventPending(action: InternalOrderChangeEvent): boolean {
|
||||
const status = action.status
|
||||
return status === undefined || status === EVENT_STATUS.PENDING
|
||||
}
|
||||
|
||||
public processActions() {
|
||||
for (const action of this.actions) {
|
||||
this.processAction_(action)
|
||||
}
|
||||
|
||||
const summary = this.summary
|
||||
for (const action of this.actions) {
|
||||
if (!this.isEventActive(action)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const type = {
|
||||
...OrderChangeProcessing.defaultConfig,
|
||||
...OrderChangeProcessing.typeDefinition[action.action],
|
||||
}
|
||||
|
||||
const amount = action.amount! * (type.isDeduction ? -1 : 1)
|
||||
|
||||
if (action.group_id && !action.evaluationOnly) {
|
||||
this.groupTotal[action.group_id] ??= 0
|
||||
this.groupTotal[action.group_id] += amount
|
||||
}
|
||||
|
||||
if (type.awaitRequired && !this.isEventDone(action)) {
|
||||
if (action.evaluationOnly) {
|
||||
summary.futureTemporarySum += amount
|
||||
} else {
|
||||
summary.temporaryDifference += amount
|
||||
}
|
||||
}
|
||||
|
||||
if (action.evaluationOnly) {
|
||||
summary.futureDifference += amount
|
||||
} else {
|
||||
if (!this.isEventDone(action) && !action.group_id) {
|
||||
summary.differenceSum += amount
|
||||
}
|
||||
summary.currentOrderTotal += amount
|
||||
}
|
||||
}
|
||||
|
||||
const groupSum = Object.values(this.groupTotal).reduce((acc, amount) => {
|
||||
return acc + amount
|
||||
}, 0)
|
||||
|
||||
summary.differenceSum += groupSum
|
||||
|
||||
summary.transactionTotal = this.transactions.reduce((acc, transaction) => {
|
||||
return acc + transaction.amount
|
||||
}, 0)
|
||||
|
||||
summary.futureTemporaryDifference =
|
||||
summary.futureDifference - summary.futureTemporarySum
|
||||
|
||||
summary.temporaryDifference =
|
||||
summary.differenceSum - summary.temporaryDifference
|
||||
|
||||
summary.pendingDifference =
|
||||
summary.currentOrderTotal - summary.transactionTotal
|
||||
}
|
||||
|
||||
private processAction_(
|
||||
action: InternalOrderChangeEvent,
|
||||
isReplay = false
|
||||
): number | void {
|
||||
const type = {
|
||||
...OrderChangeProcessing.defaultConfig,
|
||||
...OrderChangeProcessing.typeDefinition[action.action],
|
||||
}
|
||||
|
||||
this.actionsProcessed[action.action] ??= []
|
||||
|
||||
if (!isReplay) {
|
||||
this.actionsProcessed[action.action].push(action)
|
||||
}
|
||||
|
||||
let previousEvents: InternalOrderChangeEvent[] | undefined
|
||||
if (type.commitsAction) {
|
||||
previousEvents = (this.actionsProcessed[type.commitsAction] ?? []).filter(
|
||||
(ac_) =>
|
||||
ac_.reference_id === action.reference_id &&
|
||||
ac_.status !== EVENT_STATUS.VOIDED
|
||||
)
|
||||
}
|
||||
|
||||
let calculatedAmount: number = action.amount ?? 0
|
||||
const params = {
|
||||
actions: this.actions,
|
||||
action,
|
||||
previousEvents,
|
||||
currentOrder: this.order,
|
||||
summary: this.summary,
|
||||
transactions: this.transactions,
|
||||
type,
|
||||
}
|
||||
if (typeof type.validate === "function") {
|
||||
type.validate(params)
|
||||
}
|
||||
|
||||
if (typeof type.operation === "function") {
|
||||
calculatedAmount = type.operation(params) as number
|
||||
|
||||
action.amount = calculatedAmount ?? 0
|
||||
}
|
||||
|
||||
// If an action commits previous ones, replay them with updated values
|
||||
if (type.commitsAction) {
|
||||
for (const previousEvent of previousEvents ?? []) {
|
||||
this.processAction_(previousEvent, true)
|
||||
}
|
||||
}
|
||||
|
||||
if (action.resolve) {
|
||||
if (action.resolve.reference_id) {
|
||||
this.resolveReferences(action)
|
||||
}
|
||||
const groupId = action.resolve.group_id ?? "__default"
|
||||
if (action.resolve.group_id) {
|
||||
// resolve all actions in the same group
|
||||
this.resolveGroup(action)
|
||||
}
|
||||
if (action.resolve.amount && !action.evaluationOnly) {
|
||||
this.groupTotal[groupId] ??= 0
|
||||
this.groupTotal[groupId] -= action.resolve.amount
|
||||
}
|
||||
}
|
||||
|
||||
return calculatedAmount
|
||||
}
|
||||
|
||||
private resolveReferences(self: InternalOrderChangeEvent) {
|
||||
const resolve = self.resolve
|
||||
const resolveType = OrderChangeProcessing.typeDefinition[self.action]
|
||||
|
||||
Object.keys(this.actionsProcessed).forEach((actionKey) => {
|
||||
const type = OrderChangeProcessing.typeDefinition[actionKey]
|
||||
|
||||
const actions = this.actionsProcessed[actionKey]
|
||||
|
||||
for (const action of actions) {
|
||||
if (
|
||||
action === self ||
|
||||
!this.isEventPending(action) ||
|
||||
action.reference_id !== resolve?.reference_id
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (type.revert && (action.evaluationOnly || resolveType.void)) {
|
||||
let previousEvents: InternalOrderChangeEvent[] | undefined
|
||||
if (type.commitsAction) {
|
||||
previousEvents = (
|
||||
this.actionsProcessed[type.commitsAction] ?? []
|
||||
).filter(
|
||||
(ac_) =>
|
||||
ac_.reference_id === action.reference_id &&
|
||||
ac_.status !== EVENT_STATUS.VOIDED
|
||||
)
|
||||
}
|
||||
|
||||
type.revert({
|
||||
actions: this.actions,
|
||||
action,
|
||||
previousEvents,
|
||||
currentOrder: this.order,
|
||||
summary: this.summary,
|
||||
transactions: this.transactions,
|
||||
type,
|
||||
})
|
||||
|
||||
for (const previousEvent of previousEvents ?? []) {
|
||||
this.processAction_(previousEvent, true)
|
||||
}
|
||||
|
||||
action.status =
|
||||
action.evaluationOnly || resolveType.void
|
||||
? EVENT_STATUS.VOIDED
|
||||
: EVENT_STATUS.DONE
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private resolveGroup(self: InternalOrderChangeEvent) {
|
||||
const resolve = self.resolve
|
||||
|
||||
Object.keys(this.actionsProcessed).forEach((actionKey) => {
|
||||
const type = OrderChangeProcessing.typeDefinition[actionKey]
|
||||
const actions = this.actionsProcessed[actionKey]
|
||||
for (const action of actions) {
|
||||
if (!resolve?.group_id || action?.group_id !== resolve.group_id) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
type.revert &&
|
||||
action.status !== EVENT_STATUS.DONE &&
|
||||
action.status !== EVENT_STATUS.VOIDED &&
|
||||
(action.evaluationOnly || type.void)
|
||||
) {
|
||||
let previousEvents: InternalOrderChangeEvent[] | undefined
|
||||
if (type.commitsAction) {
|
||||
previousEvents = (
|
||||
this.actionsProcessed[type.commitsAction] ?? []
|
||||
).filter(
|
||||
(ac_) =>
|
||||
ac_.reference_id === action.reference_id &&
|
||||
ac_.status !== EVENT_STATUS.VOIDED
|
||||
)
|
||||
}
|
||||
|
||||
type.revert({
|
||||
actions: this.actions,
|
||||
action: action,
|
||||
previousEvents,
|
||||
currentOrder: this.order,
|
||||
summary: this.summary,
|
||||
transactions: this.transactions,
|
||||
type: OrderChangeProcessing.typeDefinition[action.action],
|
||||
})
|
||||
|
||||
for (const previousEvent of previousEvents ?? []) {
|
||||
this.processAction_(previousEvent, true)
|
||||
}
|
||||
|
||||
action.status =
|
||||
action.evaluationOnly || type.void
|
||||
? EVENT_STATUS.VOIDED
|
||||
: EVENT_STATUS.DONE
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public getSummary(): OrderSummary {
|
||||
const summary = this.summary
|
||||
const orderSummary = {
|
||||
transactionTotal: summary.transactionTotal,
|
||||
originalOrderTotal: summary.originalOrderTotal,
|
||||
currentOrderTotal: summary.currentOrderTotal,
|
||||
temporaryDifference: summary.temporaryDifference,
|
||||
futureDifference: summary.futureDifference,
|
||||
futureTemporaryDifference: summary.futureTemporaryDifference,
|
||||
pendingDifference: summary.pendingDifference,
|
||||
differenceSum: summary.differenceSum,
|
||||
}
|
||||
|
||||
return orderSummary
|
||||
}
|
||||
|
||||
public getCurrentOrder(): VirtualOrder {
|
||||
return this.order
|
||||
}
|
||||
}
|
||||
|
||||
export function calculateOrderChange({
|
||||
order,
|
||||
transactions = [],
|
||||
actions = [],
|
||||
}: {
|
||||
order: VirtualOrder
|
||||
transactions?: OrderTransaction[]
|
||||
actions?: OrderChangeEvent[]
|
||||
}) {
|
||||
const calc = new OrderChangeProcessing({ order, transactions, actions })
|
||||
calc.processActions()
|
||||
|
||||
return {
|
||||
summary: calc.getSummary(),
|
||||
order: calc.getCurrentOrder(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./action-key"
|
||||
export * from "./actions"
|
||||
export * from "./calculate-order-change"
|
||||
@@ -0,0 +1,86 @@
|
||||
import { OrderTypes } from "@medusajs/types"
|
||||
import { isDefined } from "@medusajs/utils"
|
||||
|
||||
export function formatOrder(
|
||||
order
|
||||
): OrderTypes.OrderDTO | OrderTypes.OrderDTO[] {
|
||||
const isArray = Array.isArray(order)
|
||||
const orders = isArray ? order : [order]
|
||||
|
||||
orders.map((order) => {
|
||||
order.items = order.items?.map((orderItem) => {
|
||||
const detail = { ...orderItem }
|
||||
delete detail.order
|
||||
delete detail.item
|
||||
|
||||
return {
|
||||
...orderItem.item,
|
||||
quantity: detail.quantity,
|
||||
raw_quantity: detail.raw_quantity,
|
||||
detail,
|
||||
}
|
||||
})
|
||||
|
||||
return order
|
||||
})
|
||||
|
||||
return isArray ? orders : orders[0]
|
||||
}
|
||||
|
||||
export function mapRepositoryToOrderModel(config) {
|
||||
const conf = { ...config }
|
||||
|
||||
function replace(obj, type): string[] | undefined {
|
||||
if (!isDefined(obj[type])) {
|
||||
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 rel
|
||||
})
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
conf.options.fields = replace(config.options, "fields")
|
||||
conf.options.populate = replace(config.options, "populate")
|
||||
|
||||
if (conf.where?.items) {
|
||||
const original = { ...conf.where.items }
|
||||
if (original.detail) {
|
||||
delete conf.where.items.detail
|
||||
}
|
||||
|
||||
conf.where.items = {
|
||||
item: conf.where?.items,
|
||||
}
|
||||
|
||||
if (original.quantity) {
|
||||
conf.where.items.quantity = original.quantity
|
||||
delete conf.where.items.item.quantity
|
||||
}
|
||||
|
||||
if (original.detail) {
|
||||
conf.where.items = {
|
||||
...original.detail,
|
||||
...conf.where.items,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conf
|
||||
}
|
||||
Reference in New Issue
Block a user