Files
medusa-store/packages/modules/order/src/utils/calculate-order-change.ts
Carlos R. L. Rodrigues 9563ee446f fix(utils,core-flows): subtotal calculation and returns location (#13497)
* fix(utils,core-flows): subtotal calculation and returns location

* changeset

* fix test

* var

* rm extra field from test

* fix original total

* fix partial refunds and pending difference

* fix test

* fix test

* test

* extract to util

* original total and update payment when receive return

* original_subtotal

* default fields

* test

* calculate pending difference

* revert claims test

* pending difference

* creadit line fix

* if
2025-09-18 17:50:40 +02:00

278 lines
7.6 KiB
TypeScript

import {
BigNumberInput,
OrderDTO,
OrderSummaryDTO,
} from "@medusajs/framework/types"
import {
BigNumber,
ChangeActionType,
MathBN,
isPresent,
transformPropertiesToBigNumber,
} from "@medusajs/framework/utils"
import {
ActionTypeDefinition,
EVENT_STATUS,
InternalOrderChangeEvent,
OrderChangeEvent,
OrderSummaryCalculated,
OrderTransaction,
VirtualOrder,
} from "@types"
interface ProcessOptions {
addActionReferenceToObject?: boolean
[key: string]: any
}
export class OrderChangeProcessing {
private static typeDefinition: { [key: string]: ActionTypeDefinition } = {}
private static defaultConfig = {
isDeduction: false,
}
private order: VirtualOrder
private transactions: OrderTransaction[]
private actions: InternalOrderChangeEvent[]
private options: ProcessOptions = {}
private actionsProcessed: { [key: string]: InternalOrderChangeEvent[] } = {}
private groupTotal: Record<string, BigNumberInput> = {}
private summary: OrderSummaryCalculated
public static registerActionType(key: string, type: ActionTypeDefinition) {
OrderChangeProcessing.typeDefinition[key] = type
}
constructor({
order,
transactions,
actions,
options,
}: {
order: VirtualOrder
transactions: OrderTransaction[]
actions: InternalOrderChangeEvent[]
options: ProcessOptions
}) {
this.order = JSON.parse(JSON.stringify(order))
this.transactions = JSON.parse(JSON.stringify(transactions ?? []))
this.actions = JSON.parse(JSON.stringify(actions ?? []))
this.options = options
let paid = MathBN.convert(0)
let refunded = MathBN.convert(0)
let transactionTotal = MathBN.convert(0)
let creditLineTotal = (this.order.credit_lines || []).reduce(
(acc, creditLine) => MathBN.add(acc, creditLine.amount),
MathBN.convert(0)
)
for (const tr of transactions) {
if (MathBN.lt(tr.amount, 0)) {
refunded = MathBN.add(refunded, MathBN.abs(tr.amount))
} else {
paid = MathBN.add(paid, tr.amount)
}
transactionTotal = MathBN.add(transactionTotal, tr.amount)
}
transformPropertiesToBigNumber(this.order.metadata)
this.summary = {
pending_difference: 0,
current_order_total: this.order.total ?? 0,
original_order_total: this.order.total ?? 0,
transaction_total: transactionTotal,
paid_total: paid,
refunded_total: refunded,
credit_line_total: creditLineTotal,
accounting_total: MathBN.sub(this.order.total ?? 0, creditLineTotal),
}
}
private isEventActive(action: InternalOrderChangeEvent): boolean {
const status = action.status
return (
status === undefined ||
status === EVENT_STATUS.PENDING ||
status === EVENT_STATUS.DONE
)
}
public processActions() {
let newCreditLineTotal = (this.order.credit_lines ?? [])
.filter((cl) => !("id" in cl))
.reduce(
(acc, creditLine) => MathBN.add(acc, creditLine.amount),
MathBN.convert(0)
)
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 = MathBN.mult(action.amount!, type.isDeduction ? -1 : 1)
if (action.change_id) {
this.groupTotal[action.change_id] ??= 0
this.groupTotal[action.change_id] = MathBN.add(
this.groupTotal[action.change_id],
amount
)
}
if (action.action === ChangeActionType.CREDIT_LINE_ADD) {
newCreditLineTotal = MathBN.add(newCreditLineTotal, amount)
summary.current_order_total = MathBN.sub(
summary.current_order_total,
amount
)
} else {
summary.current_order_total = MathBN.add(
summary.current_order_total,
amount
)
}
}
summary.credit_line_total = newCreditLineTotal
summary.accounting_total = summary.current_order_total
summary.transaction_total = MathBN.sum(
...this.transactions.map((tr) => tr.amount)
)
summary.pending_difference = MathBN.sub(
summary.current_order_total,
summary.transaction_total
)
}
private processAction_(
action: InternalOrderChangeEvent,
isReplay = false
): BigNumberInput | void {
const definedType = OrderChangeProcessing.typeDefinition[action.action]
if (!isPresent(definedType)) {
throw new Error(`Action type ${action.action} is not defined`)
}
const type = {
...OrderChangeProcessing.defaultConfig,
...definedType,
}
this.actionsProcessed[action.action] ??= []
if (!isReplay) {
this.actionsProcessed[action.action].push(action)
}
let calculatedAmount = action.amount ?? 0
const params = {
actions: this.actions,
action,
currentOrder: this.order,
summary: this.summary,
transactions: this.transactions,
type,
options: this.options,
}
if (typeof type.validate === "function") {
type.validate(params)
}
if (typeof type.operation === "function") {
calculatedAmount = type.operation(params) as BigNumberInput
// the action.amount has priority over the calculated amount
if (action.amount == undefined) {
action.amount = calculatedAmount ?? 0
}
}
return calculatedAmount
}
public getSummary(): OrderSummaryDTO {
const summary = this.summary
const orderSummary = {
transaction_total: new BigNumber(summary.transaction_total),
original_order_total: new BigNumber(summary.original_order_total),
current_order_total: new BigNumber(summary.current_order_total),
pending_difference: new BigNumber(summary.pending_difference),
paid_total: new BigNumber(summary.paid_total),
refunded_total: new BigNumber(summary.refunded_total),
credit_line_total: new BigNumber(summary.credit_line_total),
accounting_total: new BigNumber(summary.accounting_total),
} as unknown as OrderSummaryDTO
return orderSummary
}
// Returns the order summary from a calculated order including taxes
public getSummaryFromOrder(order: OrderDTO): OrderSummaryDTO {
const summary_ = this.summary
const total = order.total
const orderSummary = {
transaction_total: new BigNumber(summary_.transaction_total),
original_order_total: new BigNumber(summary_.original_order_total),
current_order_total: new BigNumber(total),
pending_difference: new BigNumber(summary_.pending_difference),
paid_total: new BigNumber(summary_.paid_total),
refunded_total: new BigNumber(summary_.refunded_total),
credit_line_total: new BigNumber(summary_.credit_line_total),
accounting_total: new BigNumber(summary_.accounting_total),
} as any
orderSummary.accounting_total = orderSummary.current_order_total
return orderSummary
}
public getCurrentOrder(): VirtualOrder {
return this.order
}
}
export function calculateOrderChange({
order,
transactions = [],
actions = [],
options = {},
}: {
order: VirtualOrder
transactions?: OrderTransaction[]
actions?: OrderChangeEvent[]
options?: ProcessOptions
}) {
const calc = new OrderChangeProcessing({
order,
transactions,
actions,
options,
})
calc.processActions()
return {
instance: calc,
summary: calc.getSummary(),
getSummaryFromOrder: (order: OrderDTO) => calc.getSummaryFromOrder(order),
order: calc.getCurrentOrder(),
}
}