diff --git a/packages/core/types/src/order/common.ts b/packages/core/types/src/order/common.ts index 0951ade7c2..a123555836 100644 --- a/packages/core/types/src/order/common.ts +++ b/packages/core/types/src/order/common.ts @@ -15,6 +15,7 @@ export type ChangeActionType = | "CANCEL_ITEM_FULFILLMENT" | "ITEM_ADD" | "ITEM_REMOVE" + | "ITEM_UPDATE" | "RECEIVE_DAMAGED_RETURN_ITEM" | "RECEIVE_RETURN_ITEM" | "RETURN_ITEM" diff --git a/packages/core/utils/src/order/order-change-action.ts b/packages/core/utils/src/order/order-change-action.ts index dec489e7e2..006e70e468 100644 --- a/packages/core/utils/src/order/order-change-action.ts +++ b/packages/core/utils/src/order/order-change-action.ts @@ -3,6 +3,7 @@ export enum ChangeActionType { CANCEL_ITEM_FULFILLMENT = "CANCEL_ITEM_FULFILLMENT", ITEM_ADD = "ITEM_ADD", ITEM_REMOVE = "ITEM_REMOVE", + ITEM_UPDATE = "ITEM_UPDATE", RECEIVE_DAMAGED_RETURN_ITEM = "RECEIVE_DAMAGED_RETURN_ITEM", RECEIVE_RETURN_ITEM = "RECEIVE_RETURN_ITEM", RETURN_ITEM = "RETURN_ITEM", diff --git a/packages/core/utils/src/totals/big-number.ts b/packages/core/utils/src/totals/big-number.ts index 1defd1b2c8..214ef6ab4f 100644 --- a/packages/core/utils/src/totals/big-number.ts +++ b/packages/core/utils/src/totals/big-number.ts @@ -121,4 +121,12 @@ export class BigNumber implements IBigNumber { valueOf(): number { return this.numeric_ } + + [Symbol.toPrimitive](hint) { + if (hint === "string") { + return this.raw?.value + } + + return this.numeric_ + } } diff --git a/packages/modules/order/integration-tests/__tests__/order-edit.ts b/packages/modules/order/integration-tests/__tests__/order-edit.ts index 02010d0326..1366c953d2 100644 --- a/packages/modules/order/integration-tests/__tests__/order-edit.ts +++ b/packages/modules/order/integration-tests/__tests__/order-edit.ts @@ -342,6 +342,165 @@ moduleIntegrationTestRunner({ ) }) + it("should create an order change, add actions to it and confirm the changes.", async function () { + const createdOrder = await service.createOrders(input) + createdOrder.items = createdOrder.items!.sort((a, b) => + a.title.localeCompare(b.title) + ) + + const orderChange = await service.createOrderChange({ + order_id: createdOrder.id, + actions: [ + { + action: ChangeActionType.FULFILL_ITEM, + details: { + reference_id: createdOrder.items![1].id, + quantity: 2, + }, + }, + { + action: ChangeActionType.ITEM_UPDATE, + details: { + reference_id: createdOrder.items![1].id, + quantity: 1, + }, + }, + ], + }) + + await expect( + service.confirmOrderChange({ + id: orderChange.id, + }) + ).rejects.toThrow( + `Item ${ + createdOrder.items![1].id + } has already been fulfilled and quantity cannot be lower than 2.` + ) + await service.deleteOrderChanges([orderChange.id]) + + // Create Order Change + const orderChange2 = await service.createOrderChange({ + order_id: createdOrder.id, + actions: [ + { + action: ChangeActionType.FULFILL_ITEM, + details: { + reference_id: createdOrder.items![1].id, + quantity: 1, + }, + }, + { + action: ChangeActionType.ITEM_UPDATE, + details: { + reference_id: createdOrder.items![1].id, + quantity: 4, + }, + }, + ], + }) + + await service.confirmOrderChange({ + id: orderChange2.id, + }) + + const changedOrder = await service.retrieveOrder(createdOrder.id, { + select: ["total", "items.detail", "summary"], + relations: ["items"], + }) + + expect( + JSON.parse( + JSON.stringify( + changedOrder.items?.find( + (i) => i.id === createdOrder.items![1].id + )?.detail + ) + ) + ).toEqual( + expect.objectContaining({ + quantity: 4, + fulfilled_quantity: 1, + }) + ) + + // Create Order Change + const orderChange3 = await service.createOrderChange({ + order_id: createdOrder.id, + actions: [ + { + action: ChangeActionType.FULFILL_ITEM, + details: { + reference_id: createdOrder.items![1].id, + quantity: 3, + }, + }, + { + action: ChangeActionType.ITEM_UPDATE, + details: { + reference_id: createdOrder.items![1].id, + quantity: 3, + }, + }, + ], + }) + + await expect( + service.confirmOrderChange({ + id: orderChange3.id, + }) + ).rejects.toThrow( + `Item ${ + createdOrder.items![1].id + } has already been fulfilled and quantity cannot be lower than 4.` + ) + await service.deleteOrderChanges([orderChange3.id]) + + // Create Order Change + const orderChange4 = await service.createOrderChange({ + order_id: createdOrder.id, + actions: [ + { + action: ChangeActionType.FULFILL_ITEM, + details: { + reference_id: createdOrder.items![1].id, + quantity: 1, + }, + }, + { + action: ChangeActionType.ITEM_UPDATE, + details: { + reference_id: createdOrder.items![1].id, + quantity: 3, + }, + }, + ], + }) + + await service.confirmOrderChange({ + id: orderChange4.id, + }) + + const modified = await service.retrieveOrder(createdOrder.id, { + select: ["total", "items.detail", "summary"], + relations: ["items"], + }) + + expect( + JSON.parse( + JSON.stringify( + modified.items?.find((i) => i.id === createdOrder.items![1].id) + ?.detail + ) + ) + ).toEqual( + expect.objectContaining({ + quantity: 3, + fulfilled_quantity: 2, + }) + ) + }) + it("should create an order change, add actions to it, confirm the changes, revert all the changes and restore the changes again.", async function () { const createdOrder = await service.createOrders(input) createdOrder.items = createdOrder.items!.sort((a, b) => diff --git a/packages/modules/order/src/utils/actions/index.ts b/packages/modules/order/src/utils/actions/index.ts index 97e77d48c7..fe1ea6d71d 100644 --- a/packages/modules/order/src/utils/actions/index.ts +++ b/packages/modules/order/src/utils/actions/index.ts @@ -3,6 +3,7 @@ export * from "./cancel-return" export * from "./fulfill-item" export * from "./item-add" export * from "./item-remove" +export * from "./item-update" export * from "./receive-damaged-return-item" export * from "./receive-return-item" export * from "./reinstate-item" diff --git a/packages/modules/order/src/utils/actions/item-update.ts b/packages/modules/order/src/utils/actions/item-update.ts new file mode 100644 index 0000000000..d3aac24afe --- /dev/null +++ b/packages/modules/order/src/utils/actions/item-update.ts @@ -0,0 +1,69 @@ +import { ChangeActionType, MathBN, MedusaError } from "@medusajs/utils" +import { OrderChangeProcessing } from "../calculate-order-change" +import { setActionReference } from "../set-action-reference" + +OrderChangeProcessing.registerActionType(ChangeActionType.ITEM_UPDATE, { + operation({ action, currentOrder, options }) { + const existingIndex = currentOrder.items.findIndex( + (item) => item.id === action.details.reference_id + ) + + const existing = currentOrder.items[existingIndex] + + existing.detail.quantity ??= 0 + + const quantityDiff = MathBN.sub( + action.details.quantity, + existing.detail.quantity + ) + + existing.quantity = action.details.quantity + existing.detail.quantity = action.details.quantity + + setActionReference(existing, action, options) + + if (MathBN.lte(existing.quantity, 0)) { + currentOrder.items.splice(existingIndex, 1) + } + + return MathBN.mult(existing.unit_price, quantityDiff) + }, + validate({ action, currentOrder }) { + const refId = action.details?.reference_id + if (refId == null) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Reference ID is required." + ) + } + + const existing = currentOrder.items.find((item) => item.id === refId) + if (!existing) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Item ID "${refId}" not found.` + ) + } + + if (action.details?.quantity == null) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Quantity of item ${refId} is required.` + ) + } + + action.details.quantity ??= 0 + + const lower = MathBN.lt( + action.details.quantity, + existing.detail?.fulfilled_quantity + ) + + if (lower) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Item ${refId} has already been fulfilled and quantity cannot be lower than ${existing.detail?.fulfilled_quantity}.` + ) + } + }, +})