diff --git a/packages/core/types/src/order/common.ts b/packages/core/types/src/order/common.ts index 2144dafea3..4d0403b751 100644 --- a/packages/core/types/src/order/common.ts +++ b/packages/core/types/src/order/common.ts @@ -26,6 +26,7 @@ export type ChangeActionType = | "REINSTATE_ITEM" | "TRANSFER_CUSTOMER" | "UPDATE_ORDER_PROPERTIES" + | "CREDIT_LINE_ADD" export type OrderChangeStatus = | "confirmed" @@ -1168,6 +1169,13 @@ export interface OrderDTO { */ transactions?: OrderTransactionDTO[] + /** + * The credit lines for an order + * + * @expandable + */ + credit_lines?: OrderCreditLineDTO[] + /** * The summary of the order totals. * @@ -3023,3 +3031,50 @@ export interface OrderPreviewDTO })[] return_requested_total: number } + +/** + * The order credit line details. + */ +export interface OrderCreditLineDTO { + /** + * The ID of the order credit line. + */ + id: string + + /** + * The ID of the order that the credit line belongs to. + */ + order_id: string + + /** + * The associated order + * + * @expandable + */ + order: OrderDTO + + /** + * The reference model name that the credit line is generated from + */ + reference: string | null + + /** + * The reference model id that the credit line is generated from + */ + reference_id: string | null + + /** + * The metadata of the order detail + */ + metadata: Record | null + + /** + * The date when the order credit line was created. + */ + created_at: Date + + /** + * The date when the order credit line was last updated. + */ + updated_at: Date +} diff --git a/packages/core/utils/src/order/order-change-action.ts b/packages/core/utils/src/order/order-change-action.ts index 8aea4fff06..484b9f0a94 100644 --- a/packages/core/utils/src/order/order-change-action.ts +++ b/packages/core/utils/src/order/order-change-action.ts @@ -16,4 +16,5 @@ export enum ChangeActionType { REINSTATE_ITEM = "REINSTATE_ITEM", TRANSFER_CUSTOMER = "TRANSFER_CUSTOMER", UPDATE_ORDER_PROPERTIES = "UPDATE_ORDER_PROPERTIES", + CREDIT_LINE_ADD = "CREDIT_LINE_ADD", } diff --git a/packages/modules/order/src/migrations/.snapshot-medusa-order.json b/packages/modules/order/src/migrations/.snapshot-medusa-order.json index deb6066344..499109f0d3 100644 --- a/packages/modules/order/src/migrations/.snapshot-medusa-order.json +++ b/packages/modules/order/src/migrations/.snapshot-medusa-order.json @@ -487,6 +487,153 @@ } } }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "order_id": { + "name": "order_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "reference": { + "name": "reference", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "amount": { + "name": "amount", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "decimal" + }, + "raw_amount": { + "name": "raw_amount", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "json" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "order_credit_line", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_order_credit_line_order_id", + "columnNames": [ + "order_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_order_credit_line_order_id\" ON \"order_credit_line\" (order_id) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "IDX_order_credit_line_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_order_credit_line_deleted_at\" ON \"order_credit_line\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, + { + "keyName": "order_credit_line_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "order_credit_line_order_id_foreign": { + "constraintName": "order_credit_line_order_id_foreign", + "columnNames": [ + "order_id" + ], + "localTableName": "public.order_credit_line", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.order", + "updateRule": "cascade" + } + } + }, { "columns": { "id": { diff --git a/packages/modules/order/src/migrations/Migration20241217162224.ts b/packages/modules/order/src/migrations/Migration20241217162224.ts new file mode 100644 index 0000000000..0ed45eff84 --- /dev/null +++ b/packages/modules/order/src/migrations/Migration20241217162224.ts @@ -0,0 +1,25 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20241217162224 extends Migration { + async up(): Promise { + this.addSql( + 'create table if not exists "order_credit_line" ("id" text not null, "order_id" text not null, "reference" text null, "reference_id" text null, "amount" numeric not null, "raw_amount" jsonb not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "order_credit_line_pkey" primary key ("id"));' + ) + + this.addSql( + 'CREATE INDEX IF NOT EXISTS "IDX_order_credit_line_order_id" ON "order_credit_line" (order_id) WHERE deleted_at IS NOT NULL;' + ) + + this.addSql( + 'CREATE INDEX IF NOT EXISTS "IDX_order_credit_line_deleted_at" ON "order_credit_line" (deleted_at) WHERE deleted_at IS NOT NULL;' + ) + + this.addSql( + 'alter table if exists "order_credit_line" add constraint "order_credit_line_order_id_foreign" foreign key ("order_id") references "order" ("id") on update cascade;' + ) + } + + async down(): Promise { + this.addSql('drop table if exists "order_credit_line" cascade;') + } +} diff --git a/packages/modules/order/src/models/credit-line.ts b/packages/modules/order/src/models/credit-line.ts new file mode 100644 index 0000000000..e4e8952d9f --- /dev/null +++ b/packages/modules/order/src/models/credit-line.ts @@ -0,0 +1,107 @@ +import { BigNumberRawValue, DAL } from "@medusajs/framework/types" +import { + BigNumber, + createPsqlIndexStatementHelper, + generateEntityId, + MikroOrmBigNumberProperty, +} from "@medusajs/framework/utils" +import { + BeforeCreate, + Entity, + ManyToOne, + OnInit, + OptionalProps, + PrimaryKey, + Property, + Rel, +} from "@mikro-orm/core" +import Order from "./order" + +type OptionalLineItemProps = DAL.ModelDateColumns + +const tableName = "order_credit_line" +const OrderIdIndex = createPsqlIndexStatementHelper({ + tableName, + columns: ["order_id"], + where: "deleted_at IS NOT NULL", +}) + +const DeletedAtIndex = createPsqlIndexStatementHelper({ + tableName, + columns: "deleted_at", + where: "deleted_at IS NOT NULL", +}) + +@Entity({ tableName }) +export default class OrderCreditLine { + [OptionalProps]?: OptionalLineItemProps + + @PrimaryKey({ columnType: "text" }) + id: string + + @ManyToOne({ + entity: () => Order, + mapToPk: true, + fieldName: "order_id", + columnType: "text", + }) + @OrderIdIndex.MikroORMIndex() + order_id: string + + @ManyToOne(() => Order, { + persist: false, + }) + order: Rel + + @Property({ + columnType: "text", + nullable: true, + }) + reference: string | null = null + + @Property({ + columnType: "text", + nullable: true, + }) + reference_id: string | null = null + + @MikroOrmBigNumberProperty() + amount: BigNumber | number + + @Property({ columnType: "jsonb" }) + raw_amount: BigNumberRawValue + + @Property({ columnType: "jsonb", nullable: true }) + metadata: Record | null = null + + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @Property({ columnType: "timestamptz", nullable: true }) + @DeletedAtIndex.MikroORMIndex() + deleted_at: Date | null = null + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "ordcl") + this.order_id ??= this.order?.id + } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "ordcl") + this.order_id ??= this.order?.id + } +} diff --git a/packages/modules/order/src/models/index.ts b/packages/modules/order/src/models/index.ts index 8262fb98d1..7a5b02030a 100644 --- a/packages/modules/order/src/models/index.ts +++ b/packages/modules/order/src/models/index.ts @@ -2,6 +2,7 @@ export { default as OrderAddress } from "./address" export { default as OrderClaim } from "./claim" export { default as OrderClaimItem } from "./claim-item" export { default as OrderClaimItemImage } from "./claim-item-image" +export { default as OrderCreditLine } from "./credit-line" export { default as OrderExchange } from "./exchange" export { default as OrderExchangeItem } from "./exchange-item" export { default as OrderLineItem } from "./line-item" diff --git a/packages/modules/order/src/models/order.ts b/packages/modules/order/src/models/order.ts index 283e60fcec..69af343f9f 100644 --- a/packages/modules/order/src/models/order.ts +++ b/packages/modules/order/src/models/order.ts @@ -20,6 +20,7 @@ import { Rel, } from "@mikro-orm/core" import OrderAddress from "./address" +import OrderCreditLine from "./credit-line" import OrderItem from "./order-item" import OrderShipping from "./order-shipping-method" import OrderSummary from "./order-summary" @@ -180,6 +181,11 @@ export default class Order { }) items = new Collection>(this) + @OneToMany(() => OrderCreditLine, (creditLine) => creditLine.order, { + cascade: [Cascade.PERSIST], + }) + credit_lines = new Collection>(this) + @OneToMany(() => OrderShipping, (shippingMethod) => shippingMethod.order, { cascade: [Cascade.PERSIST], }) diff --git a/packages/modules/order/src/services/__tests__/util/actions/credit-line-add.spec.ts b/packages/modules/order/src/services/__tests__/util/actions/credit-line-add.spec.ts new file mode 100644 index 0000000000..c1cb1854e2 --- /dev/null +++ b/packages/modules/order/src/services/__tests__/util/actions/credit-line-add.spec.ts @@ -0,0 +1,134 @@ +import { ChangeActionType } from "@medusajs/framework/utils" +import { VirtualOrder } from "@types" +import { calculateOrderChange } from "../../../../utils" + +describe("Action: Credit Line Add", function () { + const originalOrder: VirtualOrder = { + id: "order_1", + items: [ + { + id: "item_1", + quantity: 1, + unit_price: 10, + compare_at_unit_price: null, + order_id: "1", + + detail: { + quantity: 1, + order_id: "1", + delivered_quantity: 1, + shipped_quantity: 1, + fulfilled_quantity: 1, + return_requested_quantity: 0, + return_received_quantity: 0, + return_dismissed_quantity: 0, + written_off_quantity: 0, + }, + }, + ], + shipping_methods: [ + { + id: "shipping_1", + amount: 20, + order_id: "1", + }, + ], + credit_lines: [], + total: 30, + } + + /* + We have an original order with a total of 30, the summary would then be the following: + + { + "transaction_total": 0, + "original_order_total": 30, + "current_order_total": 30, + "pending_difference": 30, + "difference_sum": 0, + "paid_total": 0, + "refunded_total": 0, + "credit_line_total": 0 + } + + Upon adding a credit line, the order total and the pending difference will increase making it possible for the merchant + to request the customer for a payment for an arbitrary reason, or prepare the order balance sheet to then allow + the merchant to provide a refund. + + { + "transaction_total": 0, + "original_order_total": 30, + "current_order_total": 60, + "pending_difference": 60, + "difference_sum": 30, + "paid_total": 0, + "refunded_total": 0, + "credit_line_total": 30 + } + */ + it("should add credit lines", function () { + const actions = [ + { + action: ChangeActionType.CREDIT_LINE_ADD, + reference: "payment", + reference_id: "payment_1", + amount: 30, + }, + ] + + const changes = calculateOrderChange({ + order: originalOrder, + actions: actions, + options: { addActionReferenceToObject: true }, + }) + + const sumToJSON = JSON.parse(JSON.stringify(changes.summary)) + + expect(sumToJSON).toEqual({ + transaction_total: 0, + original_order_total: 30, + current_order_total: 60, + pending_difference: 60, + difference_sum: 30, + paid_total: 0, + refunded_total: 0, + credit_line_total: 30, + }) + + originalOrder.credit_lines.push({ + id: "credit_line_1", + order_id: "order_1", + reference: "payment", + reference_id: "payment_1", + amount: 10, + }) + + const actionsSecond = [ + { + action: ChangeActionType.CREDIT_LINE_ADD, + reference: "payment", + reference_id: "payment_2", + amount: 30, + }, + ] + + const changesSecond = calculateOrderChange({ + order: originalOrder, + actions: actionsSecond, + options: { addActionReferenceToObject: true }, + }) + + const sumToJSONSecond = JSON.parse(JSON.stringify(changesSecond.summary)) + + expect(sumToJSONSecond).toEqual({ + transaction_total: 0, + original_order_total: 30, + current_order_total: 70, + pending_difference: 70, + difference_sum: 30, + paid_total: 0, + refunded_total: 0, + credit_line_total: 40, + }) + }) +}) diff --git a/packages/modules/order/src/services/__tests__/util/actions/exchanges.ts b/packages/modules/order/src/services/__tests__/util/actions/exchanges.ts index 0a87c01d72..09739ef4ad 100644 --- a/packages/modules/order/src/services/__tests__/util/actions/exchanges.ts +++ b/packages/modules/order/src/services/__tests__/util/actions/exchanges.ts @@ -71,6 +71,7 @@ describe("Order Exchange - Actions", function () { order_id: "1", }, ], + credit_lines: [], total: 270, } @@ -121,6 +122,7 @@ describe("Order Exchange - Actions", function () { difference_sum: 42.5, paid_total: 0, refunded_total: 0, + credit_line_total: 0, }) const toJson = JSON.parse(JSON.stringify(changes.order.items)) diff --git a/packages/modules/order/src/services/__tests__/util/actions/returns.ts b/packages/modules/order/src/services/__tests__/util/actions/returns.ts index 3be35a4314..cb31b053a5 100644 --- a/packages/modules/order/src/services/__tests__/util/actions/returns.ts +++ b/packages/modules/order/src/services/__tests__/util/actions/returns.ts @@ -71,6 +71,7 @@ describe("Order Return - Actions", function () { order_id: "1", }, ], + credit_lines: [], total: 270, } diff --git a/packages/modules/order/src/services/order-module-service.ts b/packages/modules/order/src/services/order-module-service.ts index 3a15c3cd7f..edb0c5a02b 100644 --- a/packages/modules/order/src/services/order-module-service.ts +++ b/packages/modules/order/src/services/order-module-service.ts @@ -47,6 +47,7 @@ import { OrderClaim, OrderClaimItem, OrderClaimItemImage, + OrderCreditLine, OrderExchange, OrderExchangeItem, OrderItem, @@ -132,6 +133,7 @@ const generateMethodForModels = { OrderClaimItemImage, OrderExchange, OrderExchangeItem, + OrderCreditLine, } // TODO: rm template args here, keep it for later to not collide with carlos work at least as little as possible @@ -157,7 +159,8 @@ export default class OrderModuleService< TClaimItem extends OrderClaimItem = OrderClaimItem, TClaimItemImage extends OrderClaimItemImage = OrderClaimItemImage, TExchange extends OrderExchange = OrderExchange, - TExchangeItem extends OrderExchangeItem = OrderExchangeItem + TExchangeItem extends OrderExchangeItem = OrderExchangeItem, + TCreditLine extends OrderCreditLine = OrderCreditLine > extends ModulesSdkUtils.MedusaService<{ Order: { dto: OrderTypes.OrderDTO } @@ -186,6 +189,7 @@ export default class OrderModuleService< OrderClaimItemImage: { dto: OrderTypes.OrderClaimItemImageDTO } OrderExchange: { dto: OrderTypes.OrderExchangeDTO } OrderExchangeItem: { dto: OrderTypes.OrderExchangeItemDTO } + OrderCreditLine: { dto: OrderTypes.OrderCreditLineDTO } }>(generateMethodForModels) implements IOrderModuleService { diff --git a/packages/modules/order/src/types/utils/index.ts b/packages/modules/order/src/types/utils/index.ts index 399106897f..2299eff8e9 100644 --- a/packages/modules/order/src/types/utils/index.ts +++ b/packages/modules/order/src/types/utils/index.ts @@ -54,6 +54,14 @@ export type VirtualOrder = { amount: BigNumberInput }[] + credit_lines: { + id: string + order_id: string + reference_id?: string + reference?: string + amount: BigNumberInput + }[] + total: BigNumberInput customer_id?: string @@ -75,6 +83,7 @@ export interface OrderSummaryCalculated { difference_sum: BigNumberInput paid_total: BigNumberInput refunded_total: BigNumberInput + credit_line_total: BigNumberInput } export interface OrderTransaction { diff --git a/packages/modules/order/src/utils/actions/credit-line-add.ts b/packages/modules/order/src/utils/actions/credit-line-add.ts new file mode 100644 index 0000000000..27d6f33c4e --- /dev/null +++ b/packages/modules/order/src/utils/actions/credit-line-add.ts @@ -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.CREDIT_LINE_ADD, { + operation({ action, currentOrder, options }) { + const creditLines = currentOrder.credit_lines ?? [] + let existing = creditLines.find((cl) => cl.id === action.reference_id) + + if (!existing) { + existing = { + id: action.reference_id!, + order_id: currentOrder.id, + amount: action.amount as number, + } + + creditLines.push(existing) + } + + setActionReference(existing, action, options) + + currentOrder.credit_lines = creditLines + }, + validate({ action }) { + if (action.amount == null) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Amount is required." + ) + } + }, +}) diff --git a/packages/modules/order/src/utils/actions/index.ts b/packages/modules/order/src/utils/actions/index.ts index 4b21479706..204d8c5cb5 100644 --- a/packages/modules/order/src/utils/actions/index.ts +++ b/packages/modules/order/src/utils/actions/index.ts @@ -1,5 +1,7 @@ export * from "./cancel-item-fulfillment" export * from "./cancel-return" +export * from "./change-shipping-address" +export * from "./credit-line-add" export * from "./deliver-item" export * from "./fulfill-item" export * from "./item-add" @@ -12,6 +14,5 @@ export * from "./return-item" export * from "./ship-item" export * from "./shipping-add" export * from "./shipping-remove" -export * from "./write-off-item" export * from "./transfer-customer" -export * from "./change-shipping-address" +export * from "./write-off-item" diff --git a/packages/modules/order/src/utils/calculate-order-change.ts b/packages/modules/order/src/utils/calculate-order-change.ts index b1e1800122..69edd698b7 100644 --- a/packages/modules/order/src/utils/calculate-order-change.ts +++ b/packages/modules/order/src/utils/calculate-order-change.ts @@ -58,6 +58,12 @@ export class OrderChangeProcessing { 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) + ) + + const currentOrderTotal = MathBN.add(this.order.total ?? 0, creditLineTotal) for (const tr of transactions) { if (MathBN.lt(tr.amount, 0)) { @@ -73,11 +79,12 @@ export class OrderChangeProcessing { this.summary = { pending_difference: 0, difference_sum: 0, - current_order_total: this.order.total ?? 0, + current_order_total: currentOrderTotal, original_order_total: this.order.total ?? 0, transaction_total: transactionTotal, paid_total: paid, refunded_total: refunded, + credit_line_total: creditLineTotal, } } @@ -100,6 +107,7 @@ export class OrderChangeProcessing { } const summary = this.summary + for (const action of this.actions) { if (!this.isEventActive(action)) { continue @@ -123,6 +131,13 @@ export class OrderChangeProcessing { if (!this.isEventDone(action) && !action.change_id) { summary.difference_sum = MathBN.add(summary.difference_sum, amount) } + + const creditLineTotal = (this.order.credit_lines || []).reduce( + (acc, creditLine) => MathBN.add(acc, creditLine.amount), + MathBN.convert(0) + ) + + summary.credit_line_total = creditLineTotal summary.current_order_total = MathBN.add( summary.current_order_total, amount @@ -200,6 +215,7 @@ export class OrderChangeProcessing { difference_sum: new BigNumber(summary.difference_sum), paid_total: new BigNumber(summary.paid_total), refunded_total: new BigNumber(summary.refunded_total), + credit_line_total: new BigNumber(summary.credit_line_total), } as unknown as OrderSummaryDTO return orderSummary