fix: Compute "virtual" adjustments for order previews (#13306)

* wip

* add wip

* wip

* reuse action

* finish first draft

* fix tests

* cleanup

* Only compute adjustments when necessary

* Create hot-carrots-look.md

* address comments

* minor tweaks

* fix pay col

* fix test

* wip

* Dwip

* wip

* fix: adjustment typo

* fix: import

* fix: workflow imports

* wip: update test

* feat: upsert versioned  adjustments when previewing order

* fix: revert unique codes change

* fix: order spec test with versioning

* wip: save

* feat: make adjustments work for preview and confirm flow, wip base repo filtering of older version adjustments

* fix: missing populate where

* wip: populate where loading versioned adjustments

* fix: filter out older adjustment versions

* temp: comment adjustments in repo

* test: add adjustment if no version

* wip: configure populate where in order base repository

* fix: rm manual filtering

* fix: revert base repo changes

* fix: revert

* fix: use order item version instead of order version

* fix: rm only in test

* fix: update case spec

* fix: remove sceanrio, wip test with draft promotion

* feat: test correct adjustments when disabling promotion

* feat: complex test case

* feat: test consecutive order edits

* feat: 2 promotions test case with a fixed promo

* feat: migrate existing order line item adjustments to order items latest version

* feat: update dep after merge

* wip: load adjustments separatley

* feat: adjustments collections

* fix: spread result, handle related entity case

* fix: update lock

* feat: make sure version is loaded, refactor, handle related entity case

* fix: check fields

* feat: loading adjustments for list and count

* fix: correct items version field

* fix: rm empty array

* fix: wip order modules spec

* fix: order module specs

* feat: preinit items adjustments

* fix: rm only

* fix: rm only

* chore: cleanup

* fix: migration files

* fix: dont change formatting

* fix: core package build

* chore: more cleanup

* fix: item update util

* fix: duplicate import

* fix: refresh adjustments for exchanges (#13992)

* wip: exchange adjustments

* feat: test - receive items

* feat: finish test case

* fix: casing

* fix(draft-orders, core-flows, orders) refresh adjustments for draft orders (#14025)

* wip: draft orders adjustments refresh

* feat: rewrite to use REPLACE action + test

* fix: rm only

* feat: cleanup old REPLACE actions

* feat: cleanup adjustemnts when 0 promotions

* wip: canceling draft order

* fix: make version arg optional

* fix: restore promotion links

* feat: test reverting on cancelation

* fix: address comments in tests

* wip: fix summary on preview

* fix: get pending diff on preview summary from total

* fix: revert pending diff change

---------

Co-authored-by: fPolic <mainacc.polic@gmail.com>
Co-authored-by: Frane Polić <16856471+fPolic@users.noreply.github.com>
This commit is contained in:
Oli Juhl
2025-11-25 10:41:14 +01:00
committed by GitHub
co-authored by fPolic Frane Polić
parent 0ddd9e36b5
commit 78842af1c3
60 changed files with 3271 additions and 233 deletions
@@ -5,6 +5,7 @@ export * from "./credit-line-add"
export * from "./deliver-item"
export * from "./fulfill-item"
export * from "./item-add"
export * from "./item-adjustments-replace"
export * from "./item-remove"
export * from "./item-update"
export * from "./promotion-add"
@@ -19,3 +20,4 @@ export * from "./shipping-remove"
export * from "./shipping-update"
export * from "./transfer-customer"
export * from "./write-off-item"
@@ -29,7 +29,6 @@ OrderChangeProcessing.registerActionType(ChangeActionType.ITEM_ADD, {
return_id: action.return_id,
claim_id: action.claim_id,
exchange_id: action.exchange_id,
unit_price: action.details.unit_price,
compare_at_unit_price: action.details.compare_at_unit_price,
quantity: action.details.quantity,
@@ -0,0 +1,32 @@
import { ChangeActionType, MedusaError } from "@medusajs/framework/utils"
import { OrderChangeProcessing } from "../calculate-order-change"
import { setActionReference } from "../set-action-reference"
OrderChangeProcessing.registerActionType(
ChangeActionType.ITEM_ADJUSTMENTS_REPLACE,
{
operation({ action, currentOrder, options }) {
let existing = currentOrder.items.find(
(item) => item.id === action.details.reference_id
)
if (!existing) {
return
}
existing.adjustments = action.details.adjustments ?? []
setActionReference(existing, action, options)
},
validate({ action }) {
const refId = action.details?.reference_id
if (!action.details.adjustments) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Adjustments of item ${refId} must exist.`
)
}
},
}
)
@@ -27,6 +27,10 @@ OrderChangeProcessing.registerActionType(ChangeActionType.ITEM_UPDATE, {
existing.quantity = currentQuantity
existing.detail.quantity = currentQuantity
if (action.details.adjustments) {
existing.adjustments = action.details.adjustments
}
if (action.details.unit_price) {
const currentUnitPrice = MathBN.convert(action.details.unit_price)
const originalTotal = MathBN.mult(originalUnitPrice, originalQuantity)
@@ -1,4 +1,5 @@
import {
CreateOrderLineItemAdjustmentDTO,
InferEntityType,
OrderChangeActionDTO,
OrderDTO,
@@ -25,13 +26,14 @@ export async function applyChangesToOrder(
actionsMap: Record<string, any[]>,
options?: {
addActionReferenceToObject?: boolean
includeTaxLinesAndAdjustementsToPreview?: (...args) => void
includeTaxLinesAndAdjustmentsToPreview?: (...args) => void
}
) {
const itemsToUpsert: InferEntityType<typeof OrderItem>[] = []
const creditLinesToUpsert: InferEntityType<typeof OrderCreditLine>[] = []
const shippingMethodsToUpsert: InferEntityType<typeof OrderShippingMethod>[] =
[]
const lineItemAdjustmentsToCreate: CreateOrderLineItemAdjustmentDTO[] = []
const summariesToUpsert: any[] = []
const orderToUpdate: any[] = []
@@ -94,6 +96,20 @@ export async function applyChangesToOrder(
metadata: orderItem.metadata,
} as any
if (version > order.version) {
item.adjustments?.forEach((adjustment) => {
lineItemAdjustmentsToCreate.push({
item_id: itemId,
version,
amount: adjustment.amount,
description: adjustment.description,
promotion_id: adjustment.promotion_id,
code: adjustment.code,
is_tax_inclusive: adjustment.is_tax_inclusive,
})
})
}
itemsToUpsert.push(itemToUpsert)
}
@@ -157,11 +173,12 @@ export async function applyChangesToOrder(
}
// Including tax lines and adjustments for added items and shipping methods
if (options?.includeTaxLinesAndAdjustementsToPreview) {
await options?.includeTaxLinesAndAdjustementsToPreview(
if (options?.includeTaxLinesAndAdjustmentsToPreview) {
await options?.includeTaxLinesAndAdjustmentsToPreview(
calculated.order,
itemsToUpsert,
shippingMethodsToUpsert
shippingMethodsToUpsert,
lineItemAdjustmentsToCreate
)
decorateCartTotals(calculated.order)
}
@@ -194,6 +211,7 @@ export async function applyChangesToOrder(
}
return {
lineItemAdjustmentsToCreate,
itemsToUpsert,
creditLinesToUpsert,
shippingMethodsToUpsert,
@@ -1,7 +1,8 @@
import { Constructor, Context, DAL } from "@medusajs/framework/types"
import { toMikroORMEntity } from "@medusajs/framework/utils"
import { LoadStrategy } from "@medusajs/framework/mikro-orm/core"
import { Order, OrderClaim } from "@models"
import { Order, OrderClaim, OrderLineItemAdjustment } from "@models"
import { mapRepositoryToOrderModel } from "."
export function setFindMethods<T>(klass: Constructor<T>, entity: any) {
@@ -76,13 +77,43 @@ export function setFindMethods<T>(klass: Constructor<T>, entity: any) {
configurePopulateWhere(config, isRelatedEntity, version)
let loadAdjustments = false
if (config.options.populate.includes("items.item.adjustments")) {
// TODO: handle if populate is an object
loadAdjustments = true
config.options.populate.splice(
config.options.populate.indexOf("items.item.adjustments"),
1
)
config.options.populate.push("items")
config.options.populate.push("items.item")
// make sure version is loaded if adjustments are requested
if (config.options.fields?.some((f) => f.includes("items.item."))) {
config.options.fields.push(
isRelatedEntity ? "order.items.version" : "items.version"
)
}
}
if (!config.options.orderBy) {
config.options.orderBy = { id: "ASC" }
}
config.where ??= {}
return await manager.find(this.entity, config.where, config.options)
const result = await manager.find(this.entity, config.where, config.options)
if (loadAdjustments) {
const orders = !isRelatedEntity
? [...result]
: [...result].map((r) => r.order).filter(Boolean)
await loadItemAdjustments(manager, orders)
}
return result
}
klass.prototype.findAndCount = async function findAndCount(
@@ -136,6 +167,25 @@ export function setFindMethods<T>(klass: Constructor<T>, entity: any) {
const version = config.where.version ?? defaultVersion
delete config.where.version
let loadAdjustments = false
if (config.options.populate.includes("items.item.adjustments")) {
loadAdjustments = true
config.options.populate.splice(
config.options.populate.indexOf("items.item.adjustments"),
1
)
config.options.populate.push("items")
config.options.populate.push("items.item")
// make sure version is loaded if adjustments are requested
if (config.options.fields?.some((f) => f.includes("items.item."))) {
config.options.fields.push(
isRelatedEntity ? "order.items.version" : "items.version"
)
}
}
configurePopulateWhere(
config,
isRelatedEntity,
@@ -148,7 +198,57 @@ export function setFindMethods<T>(klass: Constructor<T>, entity: any) {
config.options.orderBy = { id: "ASC" }
}
return await manager.findAndCount(this.entity, config.where, config.options)
const [result, count] = await manager.findAndCount(
this.entity,
config.where,
config.options
)
if (loadAdjustments) {
const orders = !isRelatedEntity
? [...result]
: [...result].map((r) => r.order).filter(Boolean)
await loadItemAdjustments(manager, orders)
}
return [result, count]
}
}
/**
* Load adjustment for the lates items/order version
* @param manager MikroORM manager
* @param orders Orders to load adjustments for
*/
async function loadItemAdjustments(manager, orders) {
const items = orders.flatMap((r) => [...(r.items ?? [])])
const itemsIdMap = new Map<string, any>(items.map((i) => [i.item.id, i.item]))
const params = items.map((i) => {
// preinitialise all items so an empty array is returned for ones without adjustments
if (!i.item.adjustments.isInitialized()) {
i.item.adjustments.initialized = true
}
if (!i.version) {
throw new Error("Item version is required to load adjustments")
}
return {
item_id: i.item.id,
version: i.version,
}
})
const adjustments = await manager.find(OrderLineItemAdjustment, {
$or: params,
})
for (const adjustment of adjustments) {
const item = itemsIdMap.get(adjustment.item_id)
if (item) {
item.adjustments.add(adjustment)
}
}
}
@@ -228,6 +228,8 @@ export class OrderChangeProcessing {
public getSummaryFromOrder(order: OrderDTO): OrderSummaryDTO {
const summary_ = this.summary
const total = order.total
// const pendingDifference = MathBN.sub(total, summary_.transaction_total)
const orderSummary = {
transaction_total: new BigNumber(summary_.transaction_total),
original_order_total: new BigNumber(summary_.original_order_total),