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
This commit is contained in:
William Bouchard
2025-10-22 04:26:05 -04:00
committed by GitHub
parent bad0858348
commit fe4e7481a9
9 changed files with 206 additions and 55 deletions

View File

@@ -0,0 +1,6 @@
---
"@medusajs/order": patch
"@medusajs/dashboard": patch
---
feat(order,dashboard): version order credit lines

View File

@@ -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 = ({
<span className="txt-small text-ui-fg-subtle mx-1">
-
</span>
<Tooltip
content={format(
new Date(creditLine.created_at),
"dd MMM, yyyy, HH:mm:ss"
)}
>
<Text
size="small"
leading="compact"
className="txt-small text-ui-fg-subtle"
>
{format(
new Date(creditLine.created_at),
"dd MMM, yyyy"
)}
</Text>
</Tooltip>
<span className="txt-small text-ui-fg-subtle mx-1">
-
</span>
<Text
size="small"
leading="compact"

View File

@@ -608,6 +608,14 @@ moduleIntegrationTestRunner<IOrderModuleService>({
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<IOrderModuleService>({
"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<IOrderModuleService>({
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<IOrderModuleService>({
"items.detail",
"summary",
"shipping_methods",
"credit_lines",
],
relations: ["items"],
relations: ["items", "credit_lines"],
})
const serializedRevertedOrder = JSON.parse(
@@ -710,6 +729,8 @@ moduleIntegrationTestRunner<IOrderModuleService>({
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({

View File

@@ -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"
}
},

View File

@@ -0,0 +1,42 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20251016182939 extends Migration {
override async up(): Promise<void> {
// 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<void> {
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";`
)
}
}

View File

@@ -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"],

View File

@@ -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),
}
}

View File

@@ -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<typeof OrderItem>[] = []
const creditLinesToCreate: CreateOrderCreditLineDTO[] = []
const creditLinesToUpsert: InferEntityType<typeof OrderCreditLine>[] = []
const shippingMethodsToUpsert: InferEntityType<typeof OrderShippingMethod>[] =
[]
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,

View File

@@ -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