From fe4e7481a9ee6e360623d15ecfaf51f3df00f9d7 Mon Sep 17 00:00:00 2001
From: William Bouchard <46496014+willbouch@users.noreply.github.com>
Date: Wed, 22 Oct 2025 04:26:05 -0400
Subject: [PATCH] feat(order,dashboard): version order credit lines (#13766)
* feat(): version order credit lines
* undo last change
* adjust where
* remove date on ui
* Create five-donuts-obey.md
* add test
* nit comment
* woops
---
.changeset/five-donuts-obey.md | 6 ++
.../order-summary-section.tsx | 21 ------
.../__tests__/order-edit.spec.ts | 25 +++++++-
.../migrations/.snapshot-medusa-order.json | 64 +++++++++++++++++--
.../src/migrations/Migration20251016182939.ts | 42 ++++++++++++
.../modules/order/src/models/credit-line.ts | 13 +++-
.../src/services/order-module-service.ts | 42 ++++++++++--
.../order/src/utils/apply-order-changes.ts | 43 +++++++------
.../order/src/utils/base-repository-find.ts | 5 ++
9 files changed, 206 insertions(+), 55 deletions(-)
create mode 100644 .changeset/five-donuts-obey.md
create mode 100644 packages/modules/order/src/migrations/Migration20251016182939.ts
diff --git a/.changeset/five-donuts-obey.md b/.changeset/five-donuts-obey.md
new file mode 100644
index 0000000000..197aed5bdf
--- /dev/null
+++ b/.changeset/five-donuts-obey.md
@@ -0,0 +1,6 @@
+---
+"@medusajs/order": patch
+"@medusajs/dashboard": patch
+---
+
+feat(order,dashboard): version order credit lines
diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
index 2a5da2a2cc..0d588127e1 100644
--- a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
+++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
@@ -38,7 +38,6 @@ import {
} from "@medusajs/ui"
import { AdminReservation } from "@medusajs/types/src/http"
-import { format } from "date-fns"
import { ActionMenu } from "../../../../../components/common/action-menu"
import DisplayId from "../../../../../components/common/display-id/display-id"
import { Thumbnail } from "../../../../../components/common/thumbnail"
@@ -872,26 +871,6 @@ const DiscountAndTotalBreakdown = ({
-
-
-
- {format(
- new Date(creditLine.created_at),
- "dd MMM, yyyy"
- )}
-
-
-
- -
-
({
quantity: 1,
},
},
+ {
+ action: ChangeActionType.CREDIT_LINE_ADD,
+ order_id: createdOrder.id,
+ version: createdOrder.version,
+ reference: "gesture_of_goodwill",
+ reference_id: "refr_123",
+ amount: 10,
+ },
],
})
@@ -627,9 +635,15 @@ moduleIntegrationTestRunner({
"items.detail",
"summary",
"shipping_methods",
+ "credit_lines",
+ "transactions",
+ ],
+ relations: [
+ "items",
+ "shipping_methods",
+ "credit_lines",
"transactions",
],
- relations: ["items", "shipping_methods", "transactions"],
})
const serializedModifiedOrder = JSON.parse(JSON.stringify(modified))
@@ -643,6 +657,10 @@ moduleIntegrationTestRunner({
expect(serializedModifiedOrder.shipping_methods).toHaveLength(1)
expect(serializedModifiedOrder.shipping_methods[0].amount).toEqual(10)
+ expect(serializedModifiedOrder.credit_lines).toHaveLength(1)
+ expect(serializedModifiedOrder.credit_lines[0].amount).toEqual(10)
+ expect(serializedModifiedOrder.credit_lines[0].version).toEqual(2)
+
expect(serializedModifiedOrder.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -694,8 +712,9 @@ moduleIntegrationTestRunner({
"items.detail",
"summary",
"shipping_methods",
+ "credit_lines",
],
- relations: ["items"],
+ relations: ["items", "credit_lines"],
})
const serializedRevertedOrder = JSON.parse(
@@ -710,6 +729,8 @@ moduleIntegrationTestRunner({
expect(serializedRevertedOrder.shipping_methods).toHaveLength(1)
expect(serializedRevertedOrder.shipping_methods[0].amount).toEqual(10)
+ expect(serializedRevertedOrder.credit_lines).toHaveLength(0)
+
expect(serializedRevertedOrder.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
diff --git a/packages/modules/order/src/migrations/.snapshot-medusa-order.json b/packages/modules/order/src/migrations/.snapshot-medusa-order.json
index 3af9c75022..8e9f91fb90 100644
--- a/packages/modules/order/src/migrations/.snapshot-medusa-order.json
+++ b/packages/modules/order/src/migrations/.snapshot-medusa-order.json
@@ -490,6 +490,7 @@
"id"
],
"referencedTableName": "public.order_address",
+ "createForeignKeyConstraint": true,
"deleteRule": "set null",
"updateRule": "cascade"
},
@@ -503,6 +504,7 @@
"id"
],
"referencedTableName": "public.order_address",
+ "createForeignKeyConstraint": true,
"deleteRule": "set null",
"updateRule": "cascade"
}
@@ -843,6 +845,7 @@
"id"
],
"referencedTableName": "public.order",
+ "createForeignKeyConstraint": true,
"updateRule": "cascade"
}
},
@@ -1127,6 +1130,7 @@
"id"
],
"referencedTableName": "public.order_change",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
}
@@ -1144,14 +1148,15 @@
"nullable": false,
"mappedType": "text"
},
- "order_id": {
- "name": "order_id",
- "type": "text",
+ "version": {
+ "name": "version",
+ "type": "integer",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
- "mappedType": "text"
+ "default": "1",
+ "mappedType": "integer"
},
"reference": {
"name": "reference",
@@ -1198,6 +1203,15 @@
"nullable": true,
"mappedType": "json"
},
+ "order_id": {
+ "name": "order_id",
+ "type": "text",
+ "unsigned": false,
+ "autoincrement": false,
+ "primary": false,
+ "nullable": false,
+ "mappedType": "text"
+ },
"created_at": {
"name": "created_at",
"type": "timestamptz",
@@ -1252,6 +1266,15 @@
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_order_credit_line_deleted_at\" ON \"order_credit_line\" (deleted_at) WHERE deleted_at IS NULL"
},
+ {
+ "keyName": "IDX_order_credit_line_order_id_version",
+ "columnNames": [],
+ "composite": false,
+ "constraint": false,
+ "primary": false,
+ "unique": false,
+ "expression": "CREATE INDEX IF NOT EXISTS \"IDX_order_credit_line_order_id_version\" ON \"order_credit_line\" (order_id, version) WHERE deleted_at IS NULL"
+ },
{
"keyName": "IDX_order_credit_line_deleted_at",
"columnNames": [],
@@ -1284,6 +1307,7 @@
"id"
],
"referencedTableName": "public.order",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
}
@@ -1975,6 +1999,7 @@
"id"
],
"referencedTableName": "public.order",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
},
@@ -1988,6 +2013,7 @@
"id"
],
"referencedTableName": "public.order_line_item",
+ "createForeignKeyConstraint": true,
"updateRule": "cascade"
}
},
@@ -2163,6 +2189,7 @@
"id"
],
"referencedTableName": "public.order_line_item",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
}
@@ -2320,6 +2347,7 @@
"id"
],
"referencedTableName": "public.order_line_item",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
}
@@ -2640,6 +2668,7 @@
"id"
],
"referencedTableName": "public.order_shipping_method",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
}
@@ -2797,6 +2826,7 @@
"id"
],
"referencedTableName": "public.order_shipping_method",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
}
@@ -2937,6 +2967,7 @@
"id"
],
"referencedTableName": "public.order",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
}
@@ -3213,6 +3244,7 @@
"id"
],
"referencedTableName": "public.order",
+ "createForeignKeyConstraint": true,
"updateRule": "cascade"
},
"return_exchange_id_foreign": {
@@ -3225,6 +3257,7 @@
"id"
],
"referencedTableName": "public.order_exchange",
+ "createForeignKeyConstraint": true,
"deleteRule": "set null",
"updateRule": "cascade"
},
@@ -3238,6 +3271,7 @@
"id"
],
"referencedTableName": "public.order_claim",
+ "createForeignKeyConstraint": true,
"deleteRule": "set null",
"updateRule": "cascade"
}
@@ -3460,6 +3494,7 @@
"id"
],
"referencedTableName": "public.order",
+ "createForeignKeyConstraint": true,
"updateRule": "cascade"
},
"order_exchange_return_id_foreign": {
@@ -3472,6 +3507,7 @@
"id"
],
"referencedTableName": "public.return",
+ "createForeignKeyConstraint": true,
"deleteRule": "set null",
"updateRule": "cascade"
}
@@ -3638,6 +3674,7 @@
"id"
],
"referencedTableName": "public.order_exchange",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
},
@@ -3651,6 +3688,7 @@
"id"
],
"referencedTableName": "public.order_line_item",
+ "createForeignKeyConstraint": true,
"updateRule": "cascade"
}
},
@@ -3875,6 +3913,7 @@
"id"
],
"referencedTableName": "public.order",
+ "createForeignKeyConstraint": true,
"updateRule": "cascade"
},
"order_claim_return_id_foreign": {
@@ -3887,6 +3926,7 @@
"id"
],
"referencedTableName": "public.return",
+ "createForeignKeyConstraint": true,
"deleteRule": "set null",
"updateRule": "cascade"
}
@@ -4162,6 +4202,7 @@
"id"
],
"referencedTableName": "public.order",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
},
@@ -4175,6 +4216,7 @@
"id"
],
"referencedTableName": "public.return",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
},
@@ -4188,6 +4230,7 @@
"id"
],
"referencedTableName": "public.order_exchange",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
},
@@ -4201,6 +4244,7 @@
"id"
],
"referencedTableName": "public.order_claim",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
}
@@ -4431,6 +4475,7 @@
"id"
],
"referencedTableName": "public.order",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
},
@@ -4444,6 +4489,7 @@
"id"
],
"referencedTableName": "public.return",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
},
@@ -4457,6 +4503,7 @@
"id"
],
"referencedTableName": "public.order_exchange",
+ "createForeignKeyConstraint": true,
"deleteRule": "set null",
"updateRule": "cascade"
},
@@ -4470,6 +4517,7 @@
"id"
],
"referencedTableName": "public.order_claim",
+ "createForeignKeyConstraint": true,
"deleteRule": "set null",
"updateRule": "cascade"
},
@@ -4483,6 +4531,7 @@
"id"
],
"referencedTableName": "public.order_shipping_method",
+ "createForeignKeyConstraint": true,
"updateRule": "cascade"
}
},
@@ -4673,6 +4722,7 @@
"id"
],
"referencedTableName": "public.order_claim",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
},
@@ -4686,6 +4736,7 @@
"id"
],
"referencedTableName": "public.order_line_item",
+ "createForeignKeyConstraint": true,
"updateRule": "cascade"
}
},
@@ -4815,6 +4866,7 @@
"id"
],
"referencedTableName": "public.order_claim_item",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
}
@@ -4972,6 +5024,7 @@
"id"
],
"referencedTableName": "public.return_reason",
+ "createForeignKeyConstraint": true,
"deleteRule": "set null",
"updateRule": "cascade"
}
@@ -5194,6 +5247,7 @@
"id"
],
"referencedTableName": "public.return_reason",
+ "createForeignKeyConstraint": true,
"deleteRule": "set null",
"updateRule": "cascade"
},
@@ -5207,6 +5261,7 @@
"id"
],
"referencedTableName": "public.return",
+ "createForeignKeyConstraint": true,
"deleteRule": "cascade",
"updateRule": "cascade"
},
@@ -5220,6 +5275,7 @@
"id"
],
"referencedTableName": "public.order_line_item",
+ "createForeignKeyConstraint": true,
"updateRule": "cascade"
}
},
diff --git a/packages/modules/order/src/migrations/Migration20251016182939.ts b/packages/modules/order/src/migrations/Migration20251016182939.ts
new file mode 100644
index 0000000000..2f71953bb9
--- /dev/null
+++ b/packages/modules/order/src/migrations/Migration20251016182939.ts
@@ -0,0 +1,42 @@
+import { Migration } from "@mikro-orm/migrations"
+
+export class Migration20251016182939 extends Migration {
+ override async up(): Promise {
+ // Step 1: Add the column
+ this.addSql(`
+ alter table if exists "order_credit_line"
+ add column if not exists "version" integer default null;
+ `)
+
+ // Step 2: Populate the version column from the related order table
+ this.addSql(`
+ update "order_credit_line" ocl
+ set "version" = o."version"
+ from "order" o
+ where ocl."order_id" = o."id";
+ `)
+
+ // Step 3: Set NOT NULL and default AFTER backfilling
+ this.addSql(`
+ alter table if exists "order_credit_line"
+ alter column "version" set not null,
+ alter column "version" set default 1;
+ `)
+
+ // Step 4: Add index for performance
+ this.addSql(`
+ create index if not exists "IDX_order_credit_line_order_id_version"
+ on "order_credit_line" (order_id, version)
+ where deleted_at is null;
+ `)
+ }
+
+ override async down(): Promise {
+ this.addSql(
+ `drop index if exists "IDX_order_credit_line_order_id_version";`
+ )
+ this.addSql(
+ `alter table if exists "order_credit_line" drop column if exists "version";`
+ )
+ }
+}
diff --git a/packages/modules/order/src/models/credit-line.ts b/packages/modules/order/src/models/credit-line.ts
index 288a9aae62..1df7e1c1a5 100644
--- a/packages/modules/order/src/models/credit-line.ts
+++ b/packages/modules/order/src/models/credit-line.ts
@@ -4,14 +4,15 @@ import { Order } from "./order"
const OrderCreditLine_ = model
.define("OrderCreditLine", {
id: model.id({ prefix: "ordcl" }).primaryKey(),
- order: model.belongsTo(() => Order, {
- mappedBy: "credit_lines",
- }),
+ version: model.number().default(1),
reference: model.text().nullable(),
reference_id: model.text().nullable(),
amount: model.bigNumber(),
raw_amount: model.json(),
metadata: model.json().nullable(),
+ order: model.belongsTo(() => Order, {
+ mappedBy: "credit_lines",
+ }),
})
.indexes([
{
@@ -20,6 +21,12 @@ const OrderCreditLine_ = model
unique: false,
where: "deleted_at IS NULL",
},
+ {
+ name: "IDX_order_credit_line_order_id_version",
+ on: ["order_id", "version"],
+ unique: false,
+ where: "deleted_at IS NULL",
+ },
{
name: "IDX_order_credit_line_deleted_at",
on: ["deleted_at"],
diff --git a/packages/modules/order/src/services/order-module-service.ts b/packages/modules/order/src/services/order-module-service.ts
index f60369f075..1476e82f2e 100644
--- a/packages/modules/order/src/services/order-module-service.ts
+++ b/packages/modules/order/src/services/order-module-service.ts
@@ -3116,6 +3116,21 @@ export default class OrderModuleService
this.orderShippingService_.softDelete(orderShippingIds, sharedContext)
)
+ // Order Credit Lines
+ const orderCreditLines = await this.orderCreditLineService_.list(
+ {
+ order_id: order.id,
+ version: currentVersion,
+ },
+ { select: ["id", "version"] },
+ sharedContext
+ )
+ const orderCreditLineIds = orderCreditLines.map((cl) => cl.id)
+
+ updatePromises.push(
+ this.orderCreditLineService_.softDelete(orderCreditLineIds, sharedContext)
+ )
+
// Order
updatePromises.push(
this.orderService_.update(
@@ -3220,6 +3235,21 @@ export default class OrderModuleService
this.orderShippingService_.softDelete(orderShippingIds, sharedContext)
)
+ // Order Credit Lines
+ const orderCreditLines = await this.orderCreditLineService_.list(
+ {
+ order_id: order.id,
+ version: currentVersion,
+ },
+ { select: ["id", "version"] },
+ sharedContext
+ )
+ const orderCreditLineIds = orderCreditLines.map((cl) => cl.id)
+
+ updatePromises.push(
+ this.orderCreditLineService_.softDelete(orderCreditLineIds, sharedContext)
+ )
+
// Order
updatePromises.push(
this.orderService_.update(
@@ -3487,7 +3517,7 @@ export default class OrderModuleService
shippingMethodsToUpsert,
summariesToUpsert,
orderToUpdate,
- creditLinesToCreate,
+ creditLinesToUpsert,
} = await applyChangesToOrder(orders, actionsMap, {
addActionReferenceToObject: true,
includeTaxLinesAndAdjustementsToPreview: async (...args) => {
@@ -3505,7 +3535,7 @@ export default class OrderModuleService
orderItems,
_orderSummaryUpdate,
orderShippingMethods,
- createdOrderCreditLines,
+ orderCreditLines,
] = await promiseAll([
orderToUpdate.length
? this.orderService_.update(orderToUpdate, sharedContext)
@@ -3525,9 +3555,9 @@ export default class OrderModuleService
sharedContext
)
: null,
- creditLinesToCreate.length
- ? this.orderCreditLineService_.create(
- creditLinesToCreate,
+ creditLinesToUpsert.length
+ ? this.orderCreditLineService_.upsert(
+ creditLinesToUpsert,
sharedContext
)
: null,
@@ -3536,7 +3566,7 @@ export default class OrderModuleService
return {
items: orderItems ?? [],
shipping_methods: orderShippingMethods ?? [],
- credit_lines: createdOrderCreditLines ?? ([] as any),
+ credit_lines: orderCreditLines ?? ([] as any),
}
}
diff --git a/packages/modules/order/src/utils/apply-order-changes.ts b/packages/modules/order/src/utils/apply-order-changes.ts
index a21a6c7518..63d61eace9 100644
--- a/packages/modules/order/src/utils/apply-order-changes.ts
+++ b/packages/modules/order/src/utils/apply-order-changes.ts
@@ -1,17 +1,16 @@
import {
- CreateOrderCreditLineDTO,
InferEntityType,
OrderChangeActionDTO,
OrderDTO,
} from "@medusajs/framework/types"
import {
ChangeActionType,
- MathBN,
createRawPropertiesFromBigNumber,
decorateCartTotals,
isDefined,
+ MathBN,
} from "@medusajs/framework/utils"
-import { OrderItem, OrderShippingMethod } from "@models"
+import { OrderCreditLine, OrderItem, OrderShippingMethod } from "@models"
import { calculateOrderChange } from "./calculate-order-change"
export interface ApplyOrderChangeDTO extends OrderChangeActionDTO {
@@ -30,7 +29,7 @@ export async function applyChangesToOrder(
}
) {
const itemsToUpsert: InferEntityType[] = []
- const creditLinesToCreate: CreateOrderCreditLineDTO[] = []
+ const creditLinesToUpsert: InferEntityType[] = []
const shippingMethodsToUpsert: InferEntityType[] =
[]
const summariesToUpsert: any[] = []
@@ -98,23 +97,29 @@ export async function applyChangesToOrder(
itemsToUpsert.push(itemToUpsert)
}
- const creditLines = (calculated.order.credit_lines ?? []).filter(
- (creditLine) => !("id" in creditLine)
- )
+ if (version > order.version) {
+ // Handle credit line versioning
+ for (const creditLine of calculated.order.credit_lines ?? []) {
+ const creditLine_ = creditLine as any
+ if (!creditLine_) {
+ continue
+ }
- for (const creditLine of creditLines) {
- const creditLineToCreate = {
- order_id: order.id,
- amount: creditLine.amount,
- reference: creditLine.reference,
- reference_id: creditLine.reference_id,
- metadata: creditLine.metadata,
+ const upsertCreditLine = {
+ id: creditLine_.version === version ? creditLine_.id : undefined,
+ order_id: order.id,
+ version,
+ reference: creditLine_.reference,
+ reference_id: creditLine_.reference_id,
+ amount: creditLine_.amount,
+ raw_amount: creditLine_.raw_amount,
+ metadata: creditLine_.metadata,
+ } as any
+
+ creditLinesToUpsert.push(upsertCreditLine)
}
- creditLinesToCreate.push(creditLineToCreate)
- }
-
- if (version > order.version) {
+ // Handle shipping method versioning
for (const shippingMethod of calculated.order.shipping_methods ?? []) {
const shippingMethod_ = shippingMethod as any
const isNewShippingMethod = !isDefined(shippingMethod_?.detail)
@@ -190,7 +195,7 @@ export async function applyChangesToOrder(
return {
itemsToUpsert,
- creditLinesToCreate,
+ creditLinesToUpsert,
shippingMethodsToUpsert,
summariesToUpsert,
orderToUpdate,
diff --git a/packages/modules/order/src/utils/base-repository-find.ts b/packages/modules/order/src/utils/base-repository-find.ts
index 2e8fcbf067..b1d5e504fe 100644
--- a/packages/modules/order/src/utils/base-repository-find.ts
+++ b/packages/modules/order/src/utils/base-repository-find.ts
@@ -224,6 +224,11 @@ function configurePopulateWhere(
popWhere.summary.version = version
}
+ if (hasRelation("credit_lines")) {
+ popWhere.credit_lines ??= {}
+ popWhere.credit_lines.version = version
+ }
+
if (hasRelation("items") || hasRelation("order.items")) {
popWhere.items ??= {}
popWhere.items.version = version