feat(order, types): Add Credit Line to order module (#10636)

* feat(order, types): Add Credit Line to order module

* chore: add action to inject credit lines
This commit is contained in:
Riqwan Thamir
2024-12-19 10:36:59 +01:00
committed by GitHub
parent fec24aa7eb
commit 3f4d574748
15 changed files with 545 additions and 4 deletions

View File

@@ -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": {

View File

@@ -0,0 +1,25 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20241217162224 extends Migration {
async up(): Promise<void> {
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<void> {
this.addSql('drop table if exists "order_credit_line" cascade;')
}
}

View File

@@ -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<Order>
@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<string, unknown> | 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
}
}

View File

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

View File

@@ -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<Rel<OrderItem>>(this)
@OneToMany(() => OrderCreditLine, (creditLine) => creditLine.order, {
cascade: [Cascade.PERSIST],
})
credit_lines = new Collection<Rel<OrderCreditLine>>(this)
@OneToMany(() => OrderShipping, (shippingMethod) => shippingMethod.order, {
cascade: [Cascade.PERSIST],
})

View File

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

View File

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

View File

@@ -71,6 +71,7 @@ describe("Order Return - Actions", function () {
order_id: "1",
},
],
credit_lines: [],
total: 270,
}

View File

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

View File

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

View File

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

View File

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

View File

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