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