From f441362f4a7304bce25605e2516053314497cd73 Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Wed, 2 Apr 2025 10:23:33 +0200 Subject: [PATCH] feat(medusa,core-flows,types,js-sdk): Draft Order workflows and API endpoints (#11805) --- .../draft-order/admin/draft-order.spec.ts | 13 + .../cart/steps/validate-line-item-prices.ts | 2 +- .../core/core-flows/src/draft-order/index.ts | 2 + ...reate-draft-order-line-item-adjustments.ts | 66 +++ ...draft-order-shipping-method-adjustments.ts | 54 ++ .../get-draft-order-promotion-context.ts | 24 + .../core-flows/src/draft-order/steps/index.ts | 1 + ...emove-draft-order-line-item-adjustments.ts | 46 ++ ...draft-order-shipping-method-adjustments.ts | 43 ++ .../restore-draft-order-shipping-methods.ts | 55 ++ .../steps/update-draft-order-promotions.ts | 106 ++++ .../update-draft-order-shipping-metod.ts | 66 +++ .../steps/validate-draft-order-change.ts | 19 + ...validate-draft-order-remove-action-item.ts | 40 ++ ...date-draft-order-shipping-method-action.ts | 38 ++ ...validate-draft-order-update-action-item.ts | 36 ++ .../draft-order/steps/validate-draft-order.ts | 19 + .../steps/validate-promo-codes-to-add.ts | 23 + .../steps/validate-promo-codes-to-remove.ts | 19 + .../src/draft-order/utils/fields.ts | 38 ++ .../src/draft-order/utils/validation.ts | 66 +++ .../workflows/add-draft-order-items.ts | 128 +++++ .../workflows/add-draft-order-promotions.ts | 103 ++++ .../add-draft-order-shipping-methods.ts | 184 +++++++ .../workflows/begin-draft-order-edit.ts | 39 ++ .../workflows/cancel-draft-order-edit.ts | 149 ++++++ .../workflows/confirm-draft-order-edit.ts | 216 ++++++++ .../workflows/convert-draft-order.ts | 96 ++++ .../src/draft-order/workflows/index.ts | 16 + .../refresh-draft-order-adjustments.ts | 74 +++ .../remove-draft-order-action-item.ts | 103 ++++ ...move-draft-order-action-shipping-method.ts | 112 +++++ .../remove-draft-order-promotions.ts | 104 ++++ .../workflows/request-draft-order-edit.ts | 90 ++++ .../update-draft-order-action-item.ts | 122 +++++ ...date-draft-order-action-shipping-method.ts | 151 ++++++ .../workflows/update-draft-order-item.ts | 128 +++++ .../update-draft-order-shipping-method.ts | 154 ++++++ .../workflows/update-draft-order.ts | 258 ++++++++++ packages/core/core-flows/src/index.ts | 3 +- .../src/order/workflows/add-line-items.ts | 2 +- .../src/product/workflows/batch-products.ts | 1 - packages/core/js-sdk/src/admin/draft-order.ts | 471 ++++++++++++++++++ .../src/http/draft-order/admin/entities.ts | 4 +- .../src/http/draft-order/admin/payloads.ts | 143 ++++++ .../src/http/draft-order/admin/responses.ts | 6 +- .../types/src/http/order/admin/entities.ts | 6 +- packages/core/types/src/http/order/common.ts | 1 + .../types/src/http/promotion/admin/queries.ts | 2 +- packages/core/types/src/order/common.ts | 8 + packages/core/types/src/order/mutations.ts | 11 + .../core/types/src/workflow/order/items.ts | 16 +- .../utils/src/order/order-change-action.ts | 3 + .../src/api/admin/customers/middlewares.ts | 4 +- .../src/api/admin/customers/validators.ts | 4 +- .../[id]/convert-to-order/route.ts | 22 + .../draft-orders/[id]/edit/confirm/route.ts | 24 + .../[id]/edit/items/[action_id]/route.ts | 44 ++ .../[id]/edit/items/item/[item_id]/route.ts | 28 ++ .../draft-orders/[id]/edit/items/route.ts | 22 + .../[id]/edit/promotions/route.ts | 47 ++ .../draft-orders/[id]/edit/request/route.ts | 24 + .../api/admin/draft-orders/[id]/edit/route.ts | 42 ++ .../shipping-methods/[action_id]/route.ts | 48 ++ .../method/[method_id]/route.ts | 27 + .../[id]/edit/shipping-methods/route.ts | 22 + .../src/api/admin/draft-orders/[id]/route.ts | 4 +- .../src/api/admin/draft-orders/middlewares.ts | 62 +++ .../src/api/admin/draft-orders/validators.ts | 95 ++++ .../[id]/items/item/[item_id]/route.ts | 2 +- .../src/api/admin/promotions/validators.ts | 4 +- .../modules/order/src/utils/actions/index.ts | 3 + .../order/src/utils/actions/promotion-add.ts | 16 + .../src/utils/actions/promotion-remove.ts | 16 + .../src/utils/actions/shipping-update.ts | 16 + 75 files changed, 4235 insertions(+), 21 deletions(-) create mode 100644 packages/core/core-flows/src/draft-order/index.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/create-draft-order-line-item-adjustments.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/create-draft-order-shipping-method-adjustments.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/get-draft-order-promotion-context.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/index.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/remove-draft-order-line-item-adjustments.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/remove-draft-order-shipping-method-adjustments.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/restore-draft-order-shipping-methods.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/update-draft-order-promotions.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/update-draft-order-shipping-metod.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/validate-draft-order-change.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/validate-draft-order-remove-action-item.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/validate-draft-order-shipping-method-action.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/validate-draft-order-update-action-item.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/validate-draft-order.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/validate-promo-codes-to-add.ts create mode 100644 packages/core/core-flows/src/draft-order/steps/validate-promo-codes-to-remove.ts create mode 100644 packages/core/core-flows/src/draft-order/utils/fields.ts create mode 100644 packages/core/core-flows/src/draft-order/utils/validation.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/add-draft-order-items.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/add-draft-order-promotions.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/add-draft-order-shipping-methods.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/begin-draft-order-edit.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/cancel-draft-order-edit.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/confirm-draft-order-edit.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/convert-draft-order.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/index.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/refresh-draft-order-adjustments.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/remove-draft-order-action-item.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/remove-draft-order-action-shipping-method.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/remove-draft-order-promotions.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/request-draft-order-edit.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/update-draft-order-action-item.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/update-draft-order-action-shipping-method.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/update-draft-order-item.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/update-draft-order-shipping-method.ts create mode 100644 packages/core/core-flows/src/draft-order/workflows/update-draft-order.ts create mode 100644 packages/medusa/src/api/admin/draft-orders/[id]/convert-to-order/route.ts create mode 100644 packages/medusa/src/api/admin/draft-orders/[id]/edit/confirm/route.ts create mode 100644 packages/medusa/src/api/admin/draft-orders/[id]/edit/items/[action_id]/route.ts create mode 100644 packages/medusa/src/api/admin/draft-orders/[id]/edit/items/item/[item_id]/route.ts create mode 100644 packages/medusa/src/api/admin/draft-orders/[id]/edit/items/route.ts create mode 100644 packages/medusa/src/api/admin/draft-orders/[id]/edit/promotions/route.ts create mode 100644 packages/medusa/src/api/admin/draft-orders/[id]/edit/request/route.ts create mode 100644 packages/medusa/src/api/admin/draft-orders/[id]/edit/route.ts create mode 100644 packages/medusa/src/api/admin/draft-orders/[id]/edit/shipping-methods/[action_id]/route.ts create mode 100644 packages/medusa/src/api/admin/draft-orders/[id]/edit/shipping-methods/method/[method_id]/route.ts create mode 100644 packages/medusa/src/api/admin/draft-orders/[id]/edit/shipping-methods/route.ts create mode 100644 packages/modules/order/src/utils/actions/promotion-add.ts create mode 100644 packages/modules/order/src/utils/actions/promotion-remove.ts create mode 100644 packages/modules/order/src/utils/actions/shipping-update.ts diff --git a/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts b/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts index 8d45aede92..b5523f09cb 100644 --- a/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts +++ b/integration-tests/http/__tests__/draft-order/admin/draft-order.spec.ts @@ -102,5 +102,18 @@ medusaIntegrationTestRunner({ expect(response.data.draft_order.email).toBe("test_new@test.com") }) }) + + describe("POST /draft-orders/:id/convert-to-order", () => { + it("should convert a draft order to an order", async () => { + const response = await api.post( + `/admin/draft-orders/${testDraftOrder.id}/convert-to-order`, + {}, + adminHeaders + ) + + expect(response.status).toBe(200) + expect(response.data.order.status).toBe("pending") + }) + }) }, }) diff --git a/packages/core/core-flows/src/cart/steps/validate-line-item-prices.ts b/packages/core/core-flows/src/cart/steps/validate-line-item-prices.ts index 5741d58c10..9a4788416e 100644 --- a/packages/core/core-flows/src/cart/steps/validate-line-item-prices.ts +++ b/packages/core/core-flows/src/cart/steps/validate-line-item-prices.ts @@ -24,7 +24,7 @@ export const validateLineItemPricesStepId = "validate-line-item-prices" /** * This step validates the specified line item objects to ensure they have prices. * If an item doesn't have a price, the step throws an error. - * + * * @example * const data = validateLineItemPricesStep({ * items: [ diff --git a/packages/core/core-flows/src/draft-order/index.ts b/packages/core/core-flows/src/draft-order/index.ts new file mode 100644 index 0000000000..68de82c9f9 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/index.ts @@ -0,0 +1,2 @@ +export * from "./steps" +export * from "./workflows" diff --git a/packages/core/core-flows/src/draft-order/steps/create-draft-order-line-item-adjustments.ts b/packages/core/core-flows/src/draft-order/steps/create-draft-order-line-item-adjustments.ts new file mode 100644 index 0000000000..f1b7fe05b5 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/create-draft-order-line-item-adjustments.ts @@ -0,0 +1,66 @@ +import { Modules } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { + CreateLineItemAdjustmentDTO, + IOrderModuleService, +} from "@medusajs/types" + +export const createDraftOrderLineItemAdjustmentsStepId = + "create-draft-order-line-item-adjustments" + +interface CreateDraftOrderLineItemAdjustmentsStepInput { + order_id: string + lineItemAdjustmentsToCreate: CreateLineItemAdjustmentDTO[] +} + +export const createDraftOrderLineItemAdjustmentsStep = createStep( + createDraftOrderLineItemAdjustmentsStepId, + async function ( + data: CreateDraftOrderLineItemAdjustmentsStepInput, + { container } + ) { + const { lineItemAdjustmentsToCreate = [], order_id } = data + + if (!lineItemAdjustmentsToCreate?.length) { + return new StepResponse(void 0, []) + } + + const service = container.resolve(Modules.ORDER) + + /** + * If an items quantity has been changed to 0, it will result in an undefined amount. + * In this case, we don't want to create an adjustment, as the item will be removed, + * and trying to create an adjustment will throw an error. + */ + const filteredAdjustments = lineItemAdjustmentsToCreate.filter( + (adjustment) => { + return !!adjustment.amount + } + ) + + const lineItemAdjustments = await service.createOrderLineItemAdjustments( + filteredAdjustments.map((adjustment) => ({ + ...adjustment, + order_id, + })) + ) + + const createdLineItemAdjustments = lineItemAdjustments.map( + (adjustment) => adjustment.id + ) + + return new StepResponse( + createdLineItemAdjustments, + createdLineItemAdjustments + ) + }, + async function (createdLineItemAdjustments, { container }) { + const service = container.resolve(Modules.ORDER) + + if (!createdLineItemAdjustments?.length) { + return + } + + await service.deleteOrderLineItemAdjustments(createdLineItemAdjustments) + } +) diff --git a/packages/core/core-flows/src/draft-order/steps/create-draft-order-shipping-method-adjustments.ts b/packages/core/core-flows/src/draft-order/steps/create-draft-order-shipping-method-adjustments.ts new file mode 100644 index 0000000000..b5daa71b12 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/create-draft-order-shipping-method-adjustments.ts @@ -0,0 +1,54 @@ +import { Modules } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { + CreateShippingMethodAdjustmentDTO, + IOrderModuleService, +} from "@medusajs/types" + +export const createDraftOrderShippingMethodAdjustmentsStepId = + "create-draft-order-shipping-method-adjustments" + +interface CreateDraftOrderShippingMethodAdjustmentsStepInput { + shippingMethodAdjustmentsToCreate: CreateShippingMethodAdjustmentDTO[] +} + +export const createDraftOrderShippingMethodAdjustmentsStep = createStep( + createDraftOrderShippingMethodAdjustmentsStepId, + async function ( + data: CreateDraftOrderShippingMethodAdjustmentsStepInput, + { container } + ) { + const { shippingMethodAdjustmentsToCreate = [] } = data + + if (!shippingMethodAdjustmentsToCreate?.length) { + return new StepResponse(void 0, []) + } + + const service = container.resolve(Modules.ORDER) + + const shippingMethodAdjustments = + await service.createOrderShippingMethodAdjustments( + shippingMethodAdjustmentsToCreate + ) + + const createdShippingMethodAdjustments = shippingMethodAdjustments.map( + (adjustment) => adjustment.id + ) + + return new StepResponse( + createdShippingMethodAdjustments, + createdShippingMethodAdjustments + ) + }, + async function (createdShippingMethodAdjustments, { container }) { + const service = container.resolve(Modules.ORDER) + + if (!createdShippingMethodAdjustments?.length) { + return + } + + await service.deleteOrderShippingMethodAdjustments( + createdShippingMethodAdjustments + ) + } +) diff --git a/packages/core/core-flows/src/draft-order/steps/get-draft-order-promotion-context.ts b/packages/core/core-flows/src/draft-order/steps/get-draft-order-promotion-context.ts new file mode 100644 index 0000000000..361d342295 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/get-draft-order-promotion-context.ts @@ -0,0 +1,24 @@ +import { Modules } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { IOrderModuleService, OrderDTO } from "@medusajs/types" + +interface GetDraftOrderPromotionContextStepInput { + order: OrderDTO +} + +export const getDraftOrderPromotionContextStep = createStep( + "get-draft-order-promotion-context", + async ({ order }: GetDraftOrderPromotionContextStepInput, { container }) => { + const service = container.resolve(Modules.ORDER) + + const preview = await service.previewOrderChange(order.id) + + const orderWithPreviewItemsAndAShipping: OrderDTO = { + ...order, + items: preview.items, + shipping_methods: preview.shipping_methods, + } + + return new StepResponse(orderWithPreviewItemsAndAShipping) + } +) diff --git a/packages/core/core-flows/src/draft-order/steps/index.ts b/packages/core/core-flows/src/draft-order/steps/index.ts new file mode 100644 index 0000000000..cffc22c6a9 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/index.ts @@ -0,0 +1 @@ +export * from "./validate-draft-order" diff --git a/packages/core/core-flows/src/draft-order/steps/remove-draft-order-line-item-adjustments.ts b/packages/core/core-flows/src/draft-order/steps/remove-draft-order-line-item-adjustments.ts new file mode 100644 index 0000000000..4037d3f381 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/remove-draft-order-line-item-adjustments.ts @@ -0,0 +1,46 @@ +import { Modules } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { IOrderModuleService } from "@medusajs/types" +export const removeDraftOrderLineItemAdjustmentsStepId = + "remove-draft-order-line-item-adjustments" + +interface RemoveDraftOrderLineItemAdjustmentsStepInput { + lineItemAdjustmentIdsToRemove: string[] +} + +export const removeDraftOrderLineItemAdjustmentsStep = createStep( + removeDraftOrderLineItemAdjustmentsStepId, + async function ( + data: RemoveDraftOrderLineItemAdjustmentsStepInput, + { container } + ) { + const { lineItemAdjustmentIdsToRemove = [] } = data + + if (!lineItemAdjustmentIdsToRemove?.length) { + return new StepResponse(void 0, []) + } + + const draftOrderModuleService = container.resolve( + Modules.ORDER + ) + + await draftOrderModuleService.deleteOrderLineItemAdjustments( + lineItemAdjustmentIdsToRemove + ) + + return new StepResponse(void 0, lineItemAdjustmentIdsToRemove) + }, + async function (lineItemAdjustmentIdsToRemove, { container }) { + const draftOrderModuleService = container.resolve( + Modules.ORDER + ) + + if (!lineItemAdjustmentIdsToRemove?.length) { + return + } + + await draftOrderModuleService.restoreOrderLineItemAdjustments( + lineItemAdjustmentIdsToRemove + ) + } +) diff --git a/packages/core/core-flows/src/draft-order/steps/remove-draft-order-shipping-method-adjustments.ts b/packages/core/core-flows/src/draft-order/steps/remove-draft-order-shipping-method-adjustments.ts new file mode 100644 index 0000000000..5a054caba2 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/remove-draft-order-shipping-method-adjustments.ts @@ -0,0 +1,43 @@ +import { Modules } from "@medusajs/framework/utils" +import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" +import { IOrderModuleService } from "@medusajs/types" + +export const removeDraftOrderShippingMethodAdjustmentsStepId = + "remove-draft-order-shipping-method-adjustments" + +interface RemoveDraftOrderShippingMethodAdjustmentsStepInput { + shippingMethodAdjustmentIdsToRemove: string[] +} + +export const removeDraftOrderShippingMethodAdjustmentsStep = createStep( + removeDraftOrderShippingMethodAdjustmentsStepId, + async function ( + data: RemoveDraftOrderShippingMethodAdjustmentsStepInput, + { container } + ) { + const { shippingMethodAdjustmentIdsToRemove = [] } = data + + if (!shippingMethodAdjustmentIdsToRemove?.length) { + return new StepResponse(void 0, []) + } + + const service = container.resolve(Modules.ORDER) + + await service.deleteOrderShippingMethodAdjustments( + shippingMethodAdjustmentIdsToRemove + ) + + return new StepResponse(void 0, shippingMethodAdjustmentIdsToRemove) + }, + async function (shippingMethodAdjustmentIdsToRemove, { container }) { + const service = container.resolve(Modules.ORDER) + + if (!shippingMethodAdjustmentIdsToRemove?.length) { + return + } + + await service.restoreOrderShippingMethodAdjustments( + shippingMethodAdjustmentIdsToRemove + ) + } +) diff --git a/packages/core/core-flows/src/draft-order/steps/restore-draft-order-shipping-methods.ts b/packages/core/core-flows/src/draft-order/steps/restore-draft-order-shipping-methods.ts new file mode 100644 index 0000000000..9241dd914d --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/restore-draft-order-shipping-methods.ts @@ -0,0 +1,55 @@ +import { Modules } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { BigNumberInput, IOrderModuleService } from "@medusajs/types" + +export const restoreDraftOrderShippingMethodsStepId = + "restore-draft-order-shipping-methods" + +interface RestoreDraftOrderShippingMethodsStepInput { + shippingMethods: { + id: string + before: { + shipping_option_id: string + amount: BigNumberInput + } + after: { + shipping_option_id: string + amount: BigNumberInput + } + }[] +} + +export const restoreDraftOrderShippingMethodsStep = createStep( + restoreDraftOrderShippingMethodsStepId, + async function ( + input: RestoreDraftOrderShippingMethodsStepInput, + { container } + ) { + const service = container.resolve(Modules.ORDER) + + await service.updateOrderShippingMethods( + input.shippingMethods.map(({ id, before }) => ({ + id, + shipping_option_id: before.shipping_option_id, + amount: before.amount, + })) + ) + + return new StepResponse(void 0, input.shippingMethods) + }, + async (input, { container }) => { + const service = container.resolve(Modules.ORDER) + + if (!input) { + return + } + + await service.updateOrderShippingMethods( + input.map(({ id, after }) => ({ + id, + shipping_option_id: after.shipping_option_id, + amount: after.amount, + })) + ) + } +) diff --git a/packages/core/core-flows/src/draft-order/steps/update-draft-order-promotions.ts b/packages/core/core-flows/src/draft-order/steps/update-draft-order-promotions.ts new file mode 100644 index 0000000000..e341931d6b --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/update-draft-order-promotions.ts @@ -0,0 +1,106 @@ +import { + ContainerRegistrationKeys, + Modules, + PromotionActions, +} from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { IPromotionModuleService } from "@medusajs/types" + +export const updateDraftOrderPromotionsStepId = "update-draft-order-promotions" + +interface UpdateDraftOrderPromotionsStepInput { + id: string + promo_codes: string[] + action?: PromotionActions +} + +export const updateDraftOrderPromotionsStep = createStep( + updateDraftOrderPromotionsStepId, + async function (data: UpdateDraftOrderPromotionsStepInput, { container }) { + const { id, promo_codes = [], action = PromotionActions.ADD } = data + + const remoteLink = container.resolve(ContainerRegistrationKeys.LINK) + const remoteQuery = container.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) + const promotionService = container.resolve( + Modules.PROMOTION + ) + + const existingDraftOrderPromotionLinks = await remoteQuery({ + entryPoint: "order_promotion", + fields: ["order_id", "promotion_id"], + variables: { order_id: [id] }, + }) + + const promotionLinkMap = new Map( + existingDraftOrderPromotionLinks.map((link) => [link.promotion_id, link]) + ) + + const linksToCreate: any[] = [] + const linksToDismiss: any[] = [] + + if (promo_codes?.length) { + const promotions = await promotionService.listPromotions( + { code: promo_codes }, + { select: ["id"] } + ) + + for (const promotion of promotions) { + const linkObject = { + [Modules.ORDER]: { order_id: id }, + [Modules.PROMOTION]: { promotion_id: promotion.id }, + } + + if ([PromotionActions.ADD, PromotionActions.REPLACE].includes(action)) { + linksToCreate.push(linkObject) + } + + if (action === PromotionActions.REMOVE) { + const link = promotionLinkMap.get(promotion.id) + + if (link) { + linksToDismiss.push(linkObject) + } + } + } + } + + if (action === PromotionActions.REPLACE) { + for (const link of existingDraftOrderPromotionLinks) { + linksToDismiss.push({ + [Modules.ORDER]: { order_id: link.order_id }, + [Modules.PROMOTION]: { promotion_id: link.promotion_id }, + }) + } + } + + if (linksToDismiss.length) { + await remoteLink.dismiss(linksToDismiss) + } + + const createdLinks = linksToCreate.length + ? await remoteLink.create(linksToCreate) + : [] + + return new StepResponse(null, { + // @ts-expect-error + createdLinkIds: createdLinks.map((link) => link.id), + dismissedLinks: linksToDismiss, + }) + }, + async function (revertData, { container }) { + const { dismissedLinks, createdLinkIds } = revertData ?? {} + + const remoteLink = container.resolve(ContainerRegistrationKeys.LINK) + + if (dismissedLinks?.length) { + await remoteLink.create(dismissedLinks) + } + + if (createdLinkIds?.length) { + // @ts-expect-error + await remoteLink.delete(createdLinkIds) + } + } +) diff --git a/packages/core/core-flows/src/draft-order/steps/update-draft-order-shipping-metod.ts b/packages/core/core-flows/src/draft-order/steps/update-draft-order-shipping-metod.ts new file mode 100644 index 0000000000..54ca15cd00 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/update-draft-order-shipping-metod.ts @@ -0,0 +1,66 @@ +import { MedusaError, Modules } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { BigNumberInput, IOrderModuleService } from "@medusajs/types" + +export const updateDraftOrderShippingMethodStepId = + "update-draft-order-shipping-method" + +interface UpdateDraftOrderShippingMethodStepInput { + order_id: string + shipping_method_id: string + shipping_option_id?: string + amount?: BigNumberInput + metadata?: Record | null +} + +export const updateDraftOrderShippingMethodStep = createStep( + updateDraftOrderShippingMethodStepId, + async function ( + input: UpdateDraftOrderShippingMethodStepInput, + { container } + ) { + const service = container.resolve(Modules.ORDER) + + const [beforeUpdate] = await service.listOrderShippingMethods( + { + id: input.shipping_method_id, + }, + { + take: 1, + select: ["id", "shipping_option_id", "amount"], + } + ) + + if (!beforeUpdate) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `A shipping method with id ${input.shipping_method_id} was not found` + ) + } + + const [updatedMethod] = await service.updateOrderShippingMethods([ + { + id: input.shipping_method_id, + shipping_option_id: input.shipping_option_id, + amount: input.amount, + }, + ]) + + return new StepResponse( + { + before: beforeUpdate, + after: updatedMethod, + }, + beforeUpdate + ) + }, + (input, { container }) => { + const service = container.resolve(Modules.ORDER) + + if (!input) { + return + } + + service.updateOrderShippingMethods([input]) + } +) diff --git a/packages/core/core-flows/src/draft-order/steps/validate-draft-order-change.ts b/packages/core/core-flows/src/draft-order/steps/validate-draft-order-change.ts new file mode 100644 index 0000000000..71fb9f626b --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/validate-draft-order-change.ts @@ -0,0 +1,19 @@ +import { createStep } from "@medusajs/framework/workflows-sdk" +import { OrderChangeDTO, OrderDTO } from "@medusajs/types" +import { throwIfOrderChangeIsNotActive } from "../../order/utils/order-validation" +import { throwIfNotDraftOrder } from "../utils/validation" + +interface ValidateDraftOrderChangeStepInput { + order: OrderDTO + orderChange: OrderChangeDTO +} + +export const validateDraftOrderChangeStepId = "validate-draft-order-change" + +export const validateDraftOrderChangeStep = createStep( + validateDraftOrderChangeStepId, + async function ({ order, orderChange }: ValidateDraftOrderChangeStepInput) { + throwIfNotDraftOrder({ order }) + throwIfOrderChangeIsNotActive({ orderChange }) + } +) diff --git a/packages/core/core-flows/src/draft-order/steps/validate-draft-order-remove-action-item.ts b/packages/core/core-flows/src/draft-order/steps/validate-draft-order-remove-action-item.ts new file mode 100644 index 0000000000..94ac749e23 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/validate-draft-order-remove-action-item.ts @@ -0,0 +1,40 @@ +import { OrderChangeActionDTO } from "@medusajs/types" + +import { ChangeActionType, MedusaError } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { OrderChangeDTO, OrderWorkflow } from "@medusajs/types" + +export interface ValidateDraftOrderUpdateActionItemStepInput { + input: OrderWorkflow.DeleteOrderEditItemActionWorkflowInput + orderChange: OrderChangeDTO +} + +export const validateDraftOrderRemoveActionItemStep = createStep( + "validate-draft-order-remove-action-item", + async function ({ + input, + orderChange, + }: ValidateDraftOrderUpdateActionItemStepInput) { + const associatedAction = (orderChange.actions ?? []).find( + (a) => a.id === input.action_id + ) as OrderChangeActionDTO + + if (!associatedAction) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `No item found for order ${input.order_id} in order change ${orderChange.id}` + ) + } + + if ( + ![ChangeActionType.ITEM_ADD, ChangeActionType.ITEM_UPDATE].includes( + associatedAction.action as ChangeActionType + ) + ) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Action ${associatedAction.id} is not adding or updating an item` + ) + } + } +) diff --git a/packages/core/core-flows/src/draft-order/steps/validate-draft-order-shipping-method-action.ts b/packages/core/core-flows/src/draft-order/steps/validate-draft-order-shipping-method-action.ts new file mode 100644 index 0000000000..f41c0b0c43 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/validate-draft-order-shipping-method-action.ts @@ -0,0 +1,38 @@ +import { ChangeActionType, MedusaError } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { + OrderChangeActionDTO, + OrderChangeDTO, + OrderWorkflow, +} from "@medusajs/types" + +export interface ValidateDraftOrderShippingMethodActionStepInput { + input: OrderWorkflow.DeleteOrderEditShippingMethodWorkflowInput + orderChange: OrderChangeDTO +} + +export const validateDraftOrderShippingMethodActionStep = createStep( + "validate-draft-order-shipping-method-action", + async function ({ + input, + orderChange, + }: ValidateDraftOrderShippingMethodActionStepInput) { + const associatedAction = (orderChange.actions ?? []).find( + (a) => a.id === input.action_id + ) as OrderChangeActionDTO + + if (!associatedAction) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `No shipping method found for order ${input.order_id} in order change ${orderChange.id}` + ) + } + + if (associatedAction.action !== ChangeActionType.SHIPPING_ADD) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Action ${associatedAction.id} is not adding a shipping method` + ) + } + } +) diff --git a/packages/core/core-flows/src/draft-order/steps/validate-draft-order-update-action-item.ts b/packages/core/core-flows/src/draft-order/steps/validate-draft-order-update-action-item.ts new file mode 100644 index 0000000000..fce40bf7d2 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/validate-draft-order-update-action-item.ts @@ -0,0 +1,36 @@ +import { OrderChangeActionDTO } from "@medusajs/types" + +import { ChangeActionType, MedusaError } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { OrderChangeDTO, OrderWorkflow } from "@medusajs/types" + +export interface ValidateDraftOrderUpdateActionItemStepInput { + input: OrderWorkflow.UpdateOrderEditAddNewItemWorkflowInput + orderChange: OrderChangeDTO +} + +export const validateDraftOrderUpdateActionItemStep = createStep( + "validate-draft-order-update-action-item", + async function ({ + input, + orderChange, + }: ValidateDraftOrderUpdateActionItemStepInput) { + const associatedAction = (orderChange.actions ?? []).find( + (a) => a.id === input.action_id + ) as OrderChangeActionDTO + + if (!associatedAction) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `No request to add item for order ${input.order_id} in order change ${orderChange.id}` + ) + } + + if (associatedAction.action !== ChangeActionType.ITEM_ADD) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Action ${associatedAction.id} is not adding an item` + ) + } + } +) diff --git a/packages/core/core-flows/src/draft-order/steps/validate-draft-order.ts b/packages/core/core-flows/src/draft-order/steps/validate-draft-order.ts new file mode 100644 index 0000000000..f555bcced3 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/validate-draft-order.ts @@ -0,0 +1,19 @@ +import { MedusaError, OrderStatus } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" +import { OrderDTO } from "@medusajs/types" + +interface ValidateDraftOrderStepInput { + order: OrderDTO +} + +export const validateDraftOrderStep = createStep( + "validate-draft-order", + async function ({ order }: ValidateDraftOrderStepInput) { + if (order.status !== OrderStatus.DRAFT && !order.is_draft_order) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Order ${order.id} is not a draft order` + ) + } + } +) diff --git a/packages/core/core-flows/src/draft-order/steps/validate-promo-codes-to-add.ts b/packages/core/core-flows/src/draft-order/steps/validate-promo-codes-to-add.ts new file mode 100644 index 0000000000..ac12471b23 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/validate-promo-codes-to-add.ts @@ -0,0 +1,23 @@ +import { createStep } from "@medusajs/framework/workflows-sdk" +import { PromotionDTO } from "@medusajs/types" +import { + throwIfCodesAreInactive, + throwIfCodesAreMissing, +} from "../utils/validation" + +export const validatePromoCodesToAddId = "validate-promo-codes-to-add" + +interface ValidatePromoCodesToAddStepInput { + promo_codes: string[] + promotions: PromotionDTO[] +} + +export const validatePromoCodesToAddStep = createStep( + validatePromoCodesToAddId, + async function (input: ValidatePromoCodesToAddStepInput) { + const { promo_codes, promotions } = input + + throwIfCodesAreMissing(promo_codes, promotions) + throwIfCodesAreInactive(promo_codes, promotions) + } +) diff --git a/packages/core/core-flows/src/draft-order/steps/validate-promo-codes-to-remove.ts b/packages/core/core-flows/src/draft-order/steps/validate-promo-codes-to-remove.ts new file mode 100644 index 0000000000..71190608d8 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/steps/validate-promo-codes-to-remove.ts @@ -0,0 +1,19 @@ +import { createStep } from "@medusajs/framework/workflows-sdk" +import { PromotionDTO } from "@medusajs/types" +import { throwIfCodesAreMissing } from "../utils/validation" + +export const validatePromoCodesToRemoveId = "validate-promo-codes-to-remove" + +interface ValidatePromoCodesToRemoveStepInput { + promo_codes: string[] + promotions: PromotionDTO[] +} + +export const validatePromoCodesToRemoveStep = createStep( + validatePromoCodesToRemoveId, + async function (input: ValidatePromoCodesToRemoveStepInput) { + const { promo_codes, promotions } = input + + throwIfCodesAreMissing(promo_codes, promotions) + } +) diff --git a/packages/core/core-flows/src/draft-order/utils/fields.ts b/packages/core/core-flows/src/draft-order/utils/fields.ts new file mode 100644 index 0000000000..664eaaa508 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/utils/fields.ts @@ -0,0 +1,38 @@ +export const draftOrderFieldsForRefreshSteps = [ + "id", + "is_draft_order", + "status", + "currency_code", + "metadata", + "sales_channel_id", + "region_id", + "region.*", + "items.*", + "items.product.id", + "items.product.collection_id", + "items.product.categories.id", + "items.product.tags.id", + "items.variant.id", + "items.variant.product.id", + "items.variant.weight", + "items.variant.length", + "items.variant.height", + "items.variant.width", + "items.variant.material", + "items.adjustments.*", + "items.tax_lines.*", + "shipping_address.*", + "shipping_methods.*", + "shipping_methods.adjustments.*", + "shipping_methods.tax_lines.*", + "customer.*", + "customer.groups.*", + "promotion_link.*", + "promotion_link.promotion", + "promotion_link.promotion.id", + "promotion_link.promotion.code", + "subtotal", + "item_total", + "total", + "item_subtotal", +] diff --git a/packages/core/core-flows/src/draft-order/utils/validation.ts b/packages/core/core-flows/src/draft-order/utils/validation.ts new file mode 100644 index 0000000000..d1d8286651 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/utils/validation.ts @@ -0,0 +1,66 @@ +import { + MedusaError, + OrderStatus, + PromotionStatus, +} from "@medusajs/framework/utils" +import { OrderDTO, PromotionDTO } from "@medusajs/types" + +interface ThrowIfNotDraftOrderInput { + order: OrderDTO +} + +export function throwIfNotDraftOrder({ order }: ThrowIfNotDraftOrderInput) { + if (order.status !== OrderStatus.DRAFT && !order.is_draft_order) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Order is not a draft" + ) + } +} + +function getMessageByCount(count: number, singular: string, plural: string) { + return count === 1 ? singular : plural +} + +export function throwIfCodesAreMissing( + promo_codes: string[], + promotions: PromotionDTO[] +) { + const missingPromoCodes = promo_codes.filter( + (code) => !promotions.some((promotion) => promotion.code === code) + ) + + if (missingPromoCodes.length > 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + getMessageByCount( + missingPromoCodes.length, + `Promotion code "${missingPromoCodes[0]}" not found`, + `Promotion codes "${missingPromoCodes.join('", "')}" not found` + ) + ) + } +} + +export function throwIfCodesAreInactive( + promo_codes: string[], + promotions: PromotionDTO[] +) { + const inactivePromoCodes = promo_codes.filter((code) => + promotions.some( + (promotion) => + promotion.code === code && promotion.status !== PromotionStatus.ACTIVE + ) + ) + + if (inactivePromoCodes.length > 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + getMessageByCount( + inactivePromoCodes.length, + `Promotion code "${inactivePromoCodes[0]}" is not active`, + `Promotion codes "${inactivePromoCodes.join('", "')}" are not active` + ) + ) + } +} diff --git a/packages/core/core-flows/src/draft-order/workflows/add-draft-order-items.ts b/packages/core/core-flows/src/draft-order/workflows/add-draft-order-items.ts new file mode 100644 index 0000000000..c5b4ef37ec --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/add-draft-order-items.ts @@ -0,0 +1,128 @@ +import { + ChangeActionType, + OrderChangeStatus, + PromotionActions, +} from "@medusajs/framework/utils" +import { + createWorkflow, + transform, + when, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { OrderChangeDTO, OrderDTO, OrderWorkflow } from "@medusajs/types" +import { useRemoteQueryStep } from "../../common" +import { + addOrderLineItemsWorkflow, + createOrderChangeActionsWorkflow, + previewOrderChangeStep, + updateOrderTaxLinesWorkflow, +} from "../../order" +import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-change" +import { draftOrderFieldsForRefreshSteps } from "../utils/fields" +import { refreshDraftOrderAdjustmentsWorkflow } from "./refresh-draft-order-adjustments" + +export const addDraftOrderItemsWorkflowId = "add-draft-order-items" + +export const addDraftOrderItemsWorkflow = createWorkflow( + addDraftOrderItemsWorkflowId, + function ( + input: WorkflowData + ) { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: draftOrderFieldsForRefreshSteps, + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: ["id", "status"], + variables: { + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], + }, + }, + list: false, + }).config({ name: "order-change-query" }) + + validateDraftOrderChangeStep({ order, orderChange }) + + const lineItems = addOrderLineItemsWorkflow.runAsStep({ + input: { + order_id: order.id, + items: input.items, + }, + }) + + const lineItemIds = transform(lineItems, (lineItems) => { + return lineItems.map((item) => item.id) + }) + + updateOrderTaxLinesWorkflow.runAsStep({ + input: { + order_id: order.id, + item_ids: lineItemIds, + }, + }) + + const appliedPromoCodes: string[] = transform(order, (order) => { + const promotionLink = (order as any).promotion_link + + if (!promotionLink) { + return [] + } + + if (Array.isArray(promotionLink)) { + return promotionLink.map((promo) => promo.promotion.code) + } + + return [promotionLink.promotion.code] + }) + + // If any the order has any promo codes, then we need to refresh the adjustments. + when( + appliedPromoCodes, + (appliedPromoCodes) => appliedPromoCodes.length > 0 + ).then(() => { + refreshDraftOrderAdjustmentsWorkflow.runAsStep({ + input: { + order, + promo_codes: appliedPromoCodes, + action: PromotionActions.REPLACE, + }, + }) + }) + + const orderChangeActionInput = transform( + { order, orderChange, items: input.items, lineItems }, + ({ order, orderChange, items, lineItems }) => { + return items.map((item, index) => ({ + order_change_id: orderChange.id, + order_id: order.id, + version: orderChange.version, + action: ChangeActionType.ITEM_ADD, + internal_note: item.internal_note, + details: { + reference_id: lineItems[index].id, + quantity: item.quantity, + unit_price: item.unit_price ?? lineItems[index].unit_price, + compare_at_unit_price: + item.compare_at_unit_price ?? + lineItems[index].compare_at_unit_price, + metadata: item.metadata, + }, + })) + } + ) + + createOrderChangeActionsWorkflow.runAsStep({ + input: orderChangeActionInput, + }) + + return new WorkflowResponse(previewOrderChangeStep(input.order_id)) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/add-draft-order-promotions.ts b/packages/core/core-flows/src/draft-order/workflows/add-draft-order-promotions.ts new file mode 100644 index 0000000000..0c1c1d1ddf --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/add-draft-order-promotions.ts @@ -0,0 +1,103 @@ +import { + ChangeActionType, + OrderChangeStatus, + PromotionActions, +} from "@medusajs/framework/utils" +import { + createWorkflow, + transform, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { OrderChangeDTO, OrderDTO, PromotionDTO } from "@medusajs/types" +import { useRemoteQueryStep } from "../../common" +import { + createOrderChangeActionsWorkflow, + previewOrderChangeStep, +} from "../../order" +import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-change" +import { validatePromoCodesToAddStep } from "../steps/validate-promo-codes-to-add" +import { draftOrderFieldsForRefreshSteps } from "../utils/fields" +import { refreshDraftOrderAdjustmentsWorkflow } from "./refresh-draft-order-adjustments" + +export const addDraftOrderPromotionWorkflowId = "add-draft-order-promotion" + +interface AddDraftOrderPromotionWorkflowInput { + order_id: string + promo_codes: string[] +} + +export const addDraftOrderPromotionWorkflow = createWorkflow( + addDraftOrderPromotionWorkflowId, + function (input: WorkflowData) { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: draftOrderFieldsForRefreshSteps, + variables: { + id: input.order_id, + }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: ["id", "status"], + variables: { + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], + }, + }, + list: false, + }).config({ name: "order-change-query" }) + + validateDraftOrderChangeStep({ order, orderChange }) + + const promotions: PromotionDTO[] = useRemoteQueryStep({ + entry_point: "promotion", + fields: ["id", "code", "status"], + variables: { + filters: { + code: input.promo_codes, + }, + }, + list: true, + }).config({ name: "promotions-query" }) + + validatePromoCodesToAddStep({ + promo_codes: input.promo_codes, + promotions, + }) + + refreshDraftOrderAdjustmentsWorkflow.runAsStep({ + input: { + order, + promo_codes: input.promo_codes, + action: PromotionActions.ADD, + }, + }) + + const orderChangeActionInput = transform( + { order, orderChange, promotions }, + ({ order, orderChange, promotions }) => { + return promotions.map((promotion) => ({ + action: ChangeActionType.PROMOTION_ADD, + reference: "order_promotion", + order_change_id: orderChange.id, + reference_id: promotion.id, + order_id: order.id, + details: { + added_code: promotion.code, + }, + })) + } + ) + + createOrderChangeActionsWorkflow.runAsStep({ + input: orderChangeActionInput, + }) + + return new WorkflowResponse(previewOrderChangeStep(input.order_id)) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/add-draft-order-shipping-methods.ts b/packages/core/core-flows/src/draft-order/workflows/add-draft-order-shipping-methods.ts new file mode 100644 index 0000000000..5b47f6e5c3 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/add-draft-order-shipping-methods.ts @@ -0,0 +1,184 @@ +import { + ChangeActionType, + OrderChangeStatus, + PromotionActions, +} from "@medusajs/framework/utils" +import { + createWorkflow, + transform, + when, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { BigNumberInput, OrderChangeDTO, OrderDTO } from "@medusajs/types" +import { useRemoteQueryStep } from "../../common" +import { + createOrderChangeActionsWorkflow, + previewOrderChangeStep, + updateOrderTaxLinesWorkflow, +} from "../../order" +import { createOrderShippingMethods } from "../../order/steps/create-order-shipping-methods" +import { prepareShippingMethod } from "../../order/utils/prepare-shipping-method" +import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-change" +import { draftOrderFieldsForRefreshSteps } from "../utils/fields" +import { refreshDraftOrderAdjustmentsWorkflow } from "./refresh-draft-order-adjustments" + +export const addDraftOrderShippingMethodsWorkflowId = + "add-draft-order-shipping-methods" + +interface AddDraftOrderShippingMethodsWorkflowInput { + /** + * The ID of the draft order to add the shipping methods to. + */ + order_id: string + /** + * The ID of the shipping option to add the shipping methods from. + */ + shipping_option_id: string + /** + * The custom amount to add the shipping methods with. + */ + custom_amount?: BigNumberInput | null +} + +export const addDraftOrderShippingMethodsWorkflow = createWorkflow( + addDraftOrderShippingMethodsWorkflowId, + function (input: WorkflowData) { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: draftOrderFieldsForRefreshSteps, + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: ["id", "status", "version"], + variables: { + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], + }, + }, + list: false, + }).config({ name: "order-change-query" }) + + validateDraftOrderChangeStep({ order, orderChange }) + + const shippingOptions = useRemoteQueryStep({ + entry_point: "shipping_option", + fields: [ + "id", + "name", + "calculated_price.calculated_amount", + "calculated_price.is_calculated_price_tax_inclusive", + ], + variables: { + id: input.shipping_option_id, + calculated_price: { + context: { currency_code: order.currency_code }, + }, + }, + }).config({ name: "fetch-shipping-option" }) + + const shippingMethodInput = transform( + { + relatedEntity: { order_id: order.id }, + shippingOptions, + customPrice: input.custom_amount as any, // Need to cast this to any otherwise the type becomes to complex. + orderChange, + input, + }, + prepareShippingMethod() + ) + + const createdMethods = createOrderShippingMethods({ + shipping_methods: [shippingMethodInput], + }) + + const shippingMethodIds = transform(createdMethods, (createdMethods) => { + return createdMethods.map((item) => item.id) + }) + + updateOrderTaxLinesWorkflow.runAsStep({ + input: { + order_id: order.id, + shipping_method_ids: shippingMethodIds, + }, + }) + + const appliedPromoCodes = transform(order, (order) => { + const promotionLink = (order as any).promotion_link + + if (!promotionLink) { + return [] + } + + if (Array.isArray(promotionLink)) { + return promotionLink.map((promo) => promo.promotion.code) + } + + return [promotionLink.promotion.code] + }) + + // If any the order has any promo codes, then we need to refresh the adjustments. + when( + appliedPromoCodes, + (appliedPromoCodes) => appliedPromoCodes.length > 0 + ).then(() => { + const refetchedOrder = useRemoteQueryStep({ + entry_point: "orders", + fields: draftOrderFieldsForRefreshSteps, + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "refetched-order-query" }) + + refreshDraftOrderAdjustmentsWorkflow.runAsStep({ + input: { + order: refetchedOrder, + promo_codes: appliedPromoCodes, + action: PromotionActions.REPLACE, + }, + }) + }) + + const orderChangeActionInput = transform( + { + order, + shippingOptions, + createdMethods, + customPrice: input.custom_amount as any, // Need to cast this to any otherwise the type becomes to complex. + orderChange, + }, + ({ + shippingOptions, + order, + createdMethods, + customPrice, + orderChange, + }) => { + const shippingOption = shippingOptions[0] + const createdMethod = createdMethods[0] + const methodPrice = + customPrice ?? shippingOption.calculated_price.calculated_amount + + return { + action: ChangeActionType.SHIPPING_ADD, + reference: "order_shipping_method", + order_change_id: orderChange.id, + reference_id: createdMethod.id, + amount: methodPrice, + order_id: order.id, + } + } + ) + + createOrderChangeActionsWorkflow.runAsStep({ + input: [orderChangeActionInput], + }) + + return new WorkflowResponse(previewOrderChangeStep(order.id)) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/begin-draft-order-edit.ts b/packages/core/core-flows/src/draft-order/workflows/begin-draft-order-edit.ts new file mode 100644 index 0000000000..31ed8cd0e3 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/begin-draft-order-edit.ts @@ -0,0 +1,39 @@ +import { + createWorkflow, + transform, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { OrderDTO, OrderWorkflow } from "@medusajs/types" +import { useRemoteQueryStep } from "../../common" +import { createOrderChangeStep } from "../../order" +import { validateDraftOrderStep } from "../steps" + +export const beginDraftOrderEditWorkflowId = "begin-draft-order-edit" + +export const beginDraftOrderEditWorkflow = createWorkflow( + beginDraftOrderEditWorkflowId, + function (input: WorkflowData) { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: ["id", "status", "is_draft_order"], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + validateDraftOrderStep({ order }) + + const orderChangeInput = transform({ input }, ({ input }) => { + return { + change_type: "edit" as const, + order_id: input.order_id, + created_by: input.created_by, + description: input.description, + internal_note: input.internal_note, + } + }) + + return new WorkflowResponse(createOrderChangeStep(orderChangeInput)) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/cancel-draft-order-edit.ts b/packages/core/core-flows/src/draft-order/workflows/cancel-draft-order-edit.ts new file mode 100644 index 0000000000..981e658deb --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/cancel-draft-order-edit.ts @@ -0,0 +1,149 @@ +import { + ChangeActionType, + OrderChangeStatus, + PromotionActions, +} from "@medusajs/framework/utils" +import { + createWorkflow, + parallelize, + transform, + when, + WorkflowData, +} from "@medusajs/framework/workflows-sdk" +import { OrderChangeDTO, OrderDTO } from "@medusajs/types" +import { useRemoteQueryStep } from "../../common" +import { deleteOrderChangesStep, deleteOrderShippingMethods } from "../../order" +import { restoreDraftOrderShippingMethodsStep } from "../steps/restore-draft-order-shipping-methods" +import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-change" +import { draftOrderFieldsForRefreshSteps } from "../utils/fields" +import { refreshDraftOrderAdjustmentsWorkflow } from "./refresh-draft-order-adjustments" + +export const cancelDraftOrderEditWorkflowId = "cancel-draft-order-edit" + +export interface CancelDraftOrderEditWorkflowInput { + order_id: string +} + +export const cancelDraftOrderEditWorkflow = createWorkflow( + cancelDraftOrderEditWorkflowId, + function (input: WorkflowData) { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: ["version", ...draftOrderFieldsForRefreshSteps], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: ["id", "status", "version", "actions.*"], + variables: { + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], + }, + }, + list: false, + }).config({ name: "order-change-query" }) + + validateDraftOrderChangeStep({ order, orderChange }) + + const shippingToRemove = transform( + { orderChange, input }, + ({ orderChange }) => { + return (orderChange.actions ?? []) + .filter((a) => a.action === ChangeActionType.SHIPPING_ADD) + .map(({ id }) => id) + } + ) + + const shippingToRestore = transform( + { orderChange, input }, + ({ orderChange }) => { + return (orderChange.actions ?? []) + .filter((a) => a.action === ChangeActionType.SHIPPING_UPDATE) + .map(({ reference_id, details }) => ({ + id: reference_id, + before: { + shipping_option_id: details?.old_shipping_option_id, + amount: details?.old_amount, + }, + after: { + shipping_option_id: details?.new_shipping_option_id, + amount: details?.new_amount, + }, + })) + } + ) + + const promotionsToRemove = transform( + { orderChange, input }, + ({ orderChange }) => { + return (orderChange.actions ?? []) + .filter((a) => a.action === ChangeActionType.PROMOTION_ADD) + .map(({ details }) => details?.added_code) + .filter(Boolean) as string[] + } + ) + + const promotionsToRestore = transform( + { orderChange, input }, + ({ orderChange }) => { + return (orderChange.actions ?? []) + .filter((a) => a.action === ChangeActionType.PROMOTION_REMOVE) + .map(({ details }) => details?.removed_code) + .filter(Boolean) as string[] + } + ) + + const promotionsToRefresh = transform( + { order, promotionsToRemove, promotionsToRestore }, + ({ order, promotionsToRemove, promotionsToRestore }) => { + const promotionLink = (order as any).promotion_link + const codes: Set = new Set() + + if (promotionLink) { + if (Array.isArray(promotionLink)) { + promotionLink.forEach((promo) => { + codes.add(promo.promotion.code) + }) + } else { + codes.add(promotionLink.promotion.code) + } + } + + for (const code of promotionsToRemove) { + codes.delete(code) + } + + for (const code of promotionsToRestore) { + codes.add(code) + } + + return Array.from(codes) + } + ) + + parallelize( + deleteOrderChangesStep({ ids: [orderChange.id] }), + deleteOrderShippingMethods({ ids: shippingToRemove }) + ) + + refreshDraftOrderAdjustmentsWorkflow.runAsStep({ + input: { + order, + promo_codes: promotionsToRefresh, + action: PromotionActions.REPLACE, + }, + }) + + when(shippingToRestore, (methods) => { + return !!methods?.length + }).then(() => { + restoreDraftOrderShippingMethodsStep({ + shippingMethods: shippingToRestore as any, + }) + }) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/confirm-draft-order-edit.ts b/packages/core/core-flows/src/draft-order/workflows/confirm-draft-order-edit.ts new file mode 100644 index 0000000000..2a74e810ab --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/confirm-draft-order-edit.ts @@ -0,0 +1,216 @@ +import { + ChangeActionType, + MathBN, + OrderChangeStatus, +} from "@medusajs/framework/utils" +import { + createWorkflow, + transform, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { BigNumberInput, OrderChangeDTO, OrderDTO } from "@medusajs/types" +import { reserveInventoryStep } from "../../cart" +import { prepareConfirmInventoryInput } from "../../cart/utils/prepare-confirm-inventory-input" +import { useRemoteQueryStep } from "../../common" +import { + createOrUpdateOrderPaymentCollectionWorkflow, + previewOrderChangeStep, +} from "../../order" +import { confirmOrderChanges } from "../../order/steps/confirm-order-changes" +import { deleteReservationsByLineItemsStep } from "../../reservation" +import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-change" + +export const confirmDraftOrderEditWorkflowId = "confirm-draft-order-edit" + +export interface ConfirmDraftOrderEditWorkflowInput { + /** + * The ID of the draft order to confirm the edit for. + */ + order_id: string + /** + * The ID of the user confirming the edit. + */ + confirmed_by: string +} + +export const confirmDraftOrderEditWorkflow = createWorkflow( + confirmDraftOrderEditWorkflowId, + function (input: ConfirmDraftOrderEditWorkflowInput) { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: [ + "id", + "status", + "is_draft_order", + "version", + "canceled_at", + "items.id", + "items.title", + "items.variant_title", + "items.variant_sku", + "items.variant_barcode", + "shipping_address.*", + ], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: [ + "id", + "status", + "actions.id", + "actions.order_id", + "actions.return_id", + "actions.action", + "actions.details", + "actions.reference", + "actions.reference_id", + "actions.internal_note", + ], + variables: { + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], + }, + }, + list: false, + }).config({ name: "order-change-query" }) + + validateDraftOrderChangeStep({ + order, + orderChange, + }) + + const orderPreview = previewOrderChangeStep(order.id) + + confirmOrderChanges({ + changes: [orderChange], + orderId: order.id, + confirmed_by: input.confirmed_by, + }) + + const orderItems = useRemoteQueryStep({ + entry_point: "order", + fields: [ + "id", + "version", + "canceled_at", + "sales_channel_id", + "items.*", + "items.variant.manage_inventory", + "items.variant.allow_backorder", + "items.variant.inventory_items.inventory_item_id", + "items.variant.inventory_items.required_quantity", + "items.variant.inventory_items.inventory.location_levels.stock_locations.id", + "items.variant.inventory_items.inventory.location_levels.stock_locations.name", + "items.variant.inventory_items.inventory.location_levels.stock_locations.sales_channels.id", + "items.variant.inventory_items.inventory.location_levels.stock_locations.sales_channels.name", + ], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-items-query" }) + + const lineItemIds = transform( + { orderItems, previousOrderItems: order.items }, + + (data) => { + const previousItemIds = (data.previousOrderItems || []).map( + ({ id }) => id + ) // items that have been removed with the change + const newItemIds = data.orderItems.items.map(({ id }) => id) + return [...new Set([...previousItemIds, ...newItemIds])] + } + ) + + deleteReservationsByLineItemsStep(lineItemIds) + + const { variants, items } = transform( + { orderItems, orderPreview }, + ({ orderItems, orderPreview }) => { + const allItems: any[] = [] + const allVariants: any[] = [] + orderItems.items.forEach((ordItem) => { + const itemAction = orderPreview.items?.find( + (item) => + item.id === ordItem.id && + item.actions?.find( + (a) => + a.action === ChangeActionType.ITEM_ADD || + a.action === ChangeActionType.ITEM_UPDATE + ) + ) + + if (!itemAction) { + return + } + + const unitPrice: BigNumberInput = + itemAction.raw_unit_price ?? itemAction.unit_price + + const compareAtUnitPrice: BigNumberInput | undefined = + itemAction.raw_compare_at_unit_price ?? + itemAction.compare_at_unit_price + + const updateAction = itemAction.actions!.find( + (a) => a.action === ChangeActionType.ITEM_UPDATE + ) + + const quantity: BigNumberInput = + itemAction.raw_quantity ?? itemAction.quantity + + const newQuantity = updateAction + ? MathBN.sub(quantity, ordItem.raw_quantity) + : quantity + + if (MathBN.lte(newQuantity, 0)) { + return + } + + const reservationQuantity = MathBN.sub( + newQuantity, + ordItem.raw_fulfilled_quantity + ) + + allItems.push({ + id: ordItem.id, + variant_id: ordItem.variant_id, + quantity: reservationQuantity, + unit_price: unitPrice, + compare_at_unit_price: compareAtUnitPrice, + }) + allVariants.push(ordItem.variant) + }) + + return { + variants: allVariants, + items: allItems, + } + } + ) + + const formatedInventoryItems = transform( + { + input: { + sales_channel_id: (orderItems as any).sales_channel_id, + variants, + items, + }, + }, + prepareConfirmInventoryInput + ) + + reserveInventoryStep(formatedInventoryItems) + + createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({ + input: { + order_id: order.id, + }, + }) + + return new WorkflowResponse(orderPreview) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/convert-draft-order.ts b/packages/core/core-flows/src/draft-order/workflows/convert-draft-order.ts new file mode 100644 index 0000000000..b9cda1b4fa --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/convert-draft-order.ts @@ -0,0 +1,96 @@ +import { Modules, OrderStatus } from "@medusajs/framework/utils" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { IOrderModuleService, OrderDTO } from "@medusajs/types" +import { useRemoteQueryStep } from "../../common" +import { validateDraftOrderStep } from "../steps/validate-draft-order" + +const convertDraftOrderWorkflowId = "convert-draft-order" + +interface ConvertDraftOrderWorkflowInput { + id: string +} + +interface ConvertDraftOrderStepInput { + id: string +} + +/** + * This step converts a draft order to a pending order. + * + * @example + * ```typescript + * const order = await convertDraftOrderStep({ id: "order_123" }) + * ``` + */ +const convertDraftOrderStep = createStep( + "convert-draft-order", + async function ({ id }: ConvertDraftOrderStepInput, { container }) { + const service = container.resolve(Modules.ORDER) + + const response = await service.updateOrders([ + { + id, + status: OrderStatus.PENDING, + is_draft_order: false, + }, + ]) + + const order = response[0] + + return new StepResponse(order, { + id, + }) + }, + async function (prevData, { container }) { + if (!prevData) { + return + } + + const service = container.resolve(Modules.ORDER) + + await service.updateOrders([ + { + id: prevData.id, + status: OrderStatus.DRAFT, + is_draft_order: true, + }, + ]) + } +) + +/** + * This workflow converts a draft order to a pending order. + * + * @example + * ```typescript + * const order = await convertDraftOrderWorkflow({ id: "order_123" }) + * ``` + */ +export const convertDraftOrderWorkflow = createWorkflow( + convertDraftOrderWorkflowId, + function ( + input: WorkflowData + ): WorkflowResponse { + const order = useRemoteQueryStep({ + entry_point: "orders", + fields: ["id", "status", "is_draft_order"], + variables: { + id: input.id, + }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + validateDraftOrderStep({ order }) + + const updatedOrder = convertDraftOrderStep({ id: input.id }) + + return new WorkflowResponse(updatedOrder) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/index.ts b/packages/core/core-flows/src/draft-order/workflows/index.ts new file mode 100644 index 0000000000..8040f0f300 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/index.ts @@ -0,0 +1,16 @@ +export * from "./add-draft-order-items" +export * from "./add-draft-order-promotions" +export * from "./add-draft-order-shipping-methods" +export * from "./begin-draft-order-edit" +export * from "./cancel-draft-order-edit" +export * from "./confirm-draft-order-edit" +export * from "./convert-draft-order" +export * from "./remove-draft-order-action-item" +export * from "./remove-draft-order-action-shipping-method" +export * from "./remove-draft-order-promotions" +export * from "./request-draft-order-edit" +export * from "./update-draft-order" +export * from "./update-draft-order-action-item" +export * from "./update-draft-order-action-shipping-method" +export * from "./update-draft-order-item" +export * from "./update-draft-order-shipping-method" diff --git a/packages/core/core-flows/src/draft-order/workflows/refresh-draft-order-adjustments.ts b/packages/core/core-flows/src/draft-order/workflows/refresh-draft-order-adjustments.ts new file mode 100644 index 0000000000..a0bd3fb832 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/refresh-draft-order-adjustments.ts @@ -0,0 +1,74 @@ +import { PromotionActions } from "@medusajs/framework/utils" +import { + createWorkflow, + parallelize, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { OrderDTO } from "@medusajs/types" +import { + getActionsToComputeFromPromotionsStep, + getPromotionCodesToApply, + prepareAdjustmentsFromPromotionActionsStep, +} from "../../cart" +import { createDraftOrderLineItemAdjustmentsStep } from "../steps/create-draft-order-line-item-adjustments" +import { createDraftOrderShippingMethodAdjustmentsStep } from "../steps/create-draft-order-shipping-method-adjustments" +import { removeDraftOrderLineItemAdjustmentsStep } from "../steps/remove-draft-order-line-item-adjustments" +import { removeDraftOrderShippingMethodAdjustmentsStep } from "../steps/remove-draft-order-shipping-method-adjustments" +import { updateDraftOrderPromotionsStep } from "../steps/update-draft-order-promotions" + +export const refreshDraftOrderAdjustmentsWorkflowId = + "refresh-draft-order-adjustments" + +interface RefreshDraftOrderAdjustmentsWorkflowInput { + order: OrderDTO + promo_codes: string[] + action: PromotionActions +} + +export const refreshDraftOrderAdjustmentsWorkflow = createWorkflow( + refreshDraftOrderAdjustmentsWorkflowId, + function (input: WorkflowData) { + const promotionCodesToApply = getPromotionCodesToApply({ + cart: input.order, + promo_codes: input.promo_codes, + action: input.action, + }) + + const actions = getActionsToComputeFromPromotionsStep({ + cart: input.order as any, + promotionCodesToApply, + }) + + const { + lineItemAdjustmentsToCreate, + lineItemAdjustmentIdsToRemove, + shippingMethodAdjustmentsToCreate, + shippingMethodAdjustmentIdsToRemove, + } = prepareAdjustmentsFromPromotionActionsStep({ actions }) + + parallelize( + removeDraftOrderLineItemAdjustmentsStep({ + lineItemAdjustmentIdsToRemove: lineItemAdjustmentIdsToRemove, + }), + removeDraftOrderShippingMethodAdjustmentsStep({ + shippingMethodAdjustmentIdsToRemove: + shippingMethodAdjustmentIdsToRemove, + }), + createDraftOrderLineItemAdjustmentsStep({ + lineItemAdjustmentsToCreate: lineItemAdjustmentsToCreate, + order_id: input.order.id, + }), + createDraftOrderShippingMethodAdjustmentsStep({ + shippingMethodAdjustmentsToCreate: shippingMethodAdjustmentsToCreate, + }), + updateDraftOrderPromotionsStep({ + id: input.order.id, + promo_codes: input.promo_codes, + action: input.action, + }) + ) + + return new WorkflowResponse(void 0) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/remove-draft-order-action-item.ts b/packages/core/core-flows/src/draft-order/workflows/remove-draft-order-action-item.ts new file mode 100644 index 0000000000..b2e722038b --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/remove-draft-order-action-item.ts @@ -0,0 +1,103 @@ +import { OrderChangeStatus, PromotionActions } from "@medusajs/framework/utils" +import { + createWorkflow, + transform, + when, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + OrderChangeDTO, + OrderDTO, + OrderPreviewDTO, + OrderWorkflow, +} from "@medusajs/types" +import { useRemoteQueryStep } from "../../common" +import { + deleteOrderChangeActionsStep, + previewOrderChangeStep, +} from "../../order" +import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-change" +import { validateDraftOrderRemoveActionItemStep } from "../steps/validate-draft-order-remove-action-item" +import { draftOrderFieldsForRefreshSteps } from "../utils/fields" +import { refreshDraftOrderAdjustmentsWorkflow } from "./refresh-draft-order-adjustments" + +export const removeDraftOrderActionItemWorkflowId = + "remove-draft-order-action-item" + +export const removeDraftOrderActionItemWorkflow = createWorkflow( + removeDraftOrderActionItemWorkflowId, + function ( + input: WorkflowData + ): WorkflowResponse { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: ["id", "status", "is_draft_order", "canceled_at", "items.*"], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: ["id", "status", "version", "actions.*"], + variables: { + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], + }, + }, + list: false, + }).config({ name: "order-change-query" }) + + validateDraftOrderChangeStep({ order, orderChange }) + + validateDraftOrderRemoveActionItemStep({ + input, + orderChange, + }) + + deleteOrderChangeActionsStep({ ids: [input.action_id] }) + + const refetchedOrder = useRemoteQueryStep({ + entry_point: "orders", + fields: draftOrderFieldsForRefreshSteps, + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "refetched-order-query" }) + + const appliedPromoCodes: string[] = transform( + refetchedOrder, + (refetchedOrder) => { + const promotionLink = (refetchedOrder as any).promotion_link + + if (!promotionLink) { + return [] + } + + if (Array.isArray(promotionLink)) { + return promotionLink.map((promo) => promo.promotion.code) + } + + return [promotionLink.promotion.code] + } + ) + + // If any the order has any promo codes, then we need to refresh the adjustments. + when( + appliedPromoCodes, + (appliedPromoCodes) => appliedPromoCodes.length > 0 + ).then(() => { + refreshDraftOrderAdjustmentsWorkflow.runAsStep({ + input: { + order: refetchedOrder, + promo_codes: appliedPromoCodes, + action: PromotionActions.REPLACE, + }, + }) + }) + + return new WorkflowResponse(previewOrderChangeStep(input.order_id)) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/remove-draft-order-action-shipping-method.ts b/packages/core/core-flows/src/draft-order/workflows/remove-draft-order-action-shipping-method.ts new file mode 100644 index 0000000000..945c46e835 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/remove-draft-order-action-shipping-method.ts @@ -0,0 +1,112 @@ +import { OrderChangeStatus, PromotionActions } from "@medusajs/framework/utils" +import { + createWorkflow, + parallelize, + transform, + when, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + OrderChangeActionDTO, + OrderChangeDTO, + OrderDTO, + OrderPreviewDTO, + OrderWorkflow, +} from "@medusajs/types" +import { useRemoteQueryStep } from "../../common" +import { + deleteOrderChangeActionsStep, + deleteOrderShippingMethods, + previewOrderChangeStep, +} from "../../order" +import { getDraftOrderPromotionContextStep } from "../steps/get-draft-order-promotion-context" +import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-change" +import { validateDraftOrderShippingMethodActionStep } from "../steps/validate-draft-order-shipping-method-action" +import { draftOrderFieldsForRefreshSteps } from "../utils/fields" +import { refreshDraftOrderAdjustmentsWorkflow } from "./refresh-draft-order-adjustments" + +export const removeDraftOrderActionShippingMethodWorkflowId = + "remove-draft-order-action-shipping-method" + +export const removeDraftOrderActionShippingMethodWorkflow = createWorkflow( + removeDraftOrderActionShippingMethodWorkflowId, + function ( + input: WorkflowData + ): WorkflowResponse { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: draftOrderFieldsForRefreshSteps, + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: ["id", "status", "version", "actions.*"], + variables: { + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], + }, + }, + list: false, + }).config({ name: "order-change-query" }) + + validateDraftOrderChangeStep({ order, orderChange }) + validateDraftOrderShippingMethodActionStep({ orderChange, input }) + + const dataToRemove = transform( + { orderChange, input }, + ({ orderChange, input }) => { + const associatedAction = (orderChange.actions ?? []).find( + (a) => a.id === input.action_id + ) as OrderChangeActionDTO + + return { + actionId: associatedAction.id, + shippingMethodId: associatedAction.reference_id, + } + } + ) + + parallelize( + deleteOrderChangeActionsStep({ ids: [dataToRemove.actionId] }), + deleteOrderShippingMethods({ ids: [dataToRemove.shippingMethodId] }) + ) + + const context = getDraftOrderPromotionContextStep({ + order, + }) + + const appliedPromoCodes = transform(context, (context) => { + const promotionLink = (context as any).promotion_link + + if (!promotionLink) { + return [] + } + + if (Array.isArray(promotionLink)) { + return promotionLink.map((promo) => promo.promotion.code) + } + + return [promotionLink.promotion.code] + }) + + when( + appliedPromoCodes, + (appliedPromoCodes) => appliedPromoCodes.length > 0 + ).then(() => { + refreshDraftOrderAdjustmentsWorkflow.runAsStep({ + input: { + order, + promo_codes: appliedPromoCodes, + action: PromotionActions.REPLACE, + }, + }) + }) + + return new WorkflowResponse(previewOrderChangeStep(input.order_id)) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/remove-draft-order-promotions.ts b/packages/core/core-flows/src/draft-order/workflows/remove-draft-order-promotions.ts new file mode 100644 index 0000000000..a2602e50c6 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/remove-draft-order-promotions.ts @@ -0,0 +1,104 @@ +import { + ChangeActionType, + OrderChangeStatus, + PromotionActions, +} from "@medusajs/framework/utils" +import { + createWorkflow, + transform, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { OrderChangeDTO, OrderDTO, PromotionDTO } from "@medusajs/types" +import { useRemoteQueryStep } from "../../common" +import { + createOrderChangeActionsWorkflow, + previewOrderChangeStep, +} from "../../order" +import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-change" +import { validatePromoCodesToRemoveStep } from "../steps/validate-promo-codes-to-remove" +import { draftOrderFieldsForRefreshSteps } from "../utils/fields" +import { refreshDraftOrderAdjustmentsWorkflow } from "./refresh-draft-order-adjustments" + +export const removeDraftOrderPromotionsWorkflowId = + "remove-draft-order-promotions" + +interface RemoveDraftOrderPromotionsWorkflowInput { + order_id: string + promo_codes: string[] +} + +export const removeDraftOrderPromotionsWorkflow = createWorkflow( + removeDraftOrderPromotionsWorkflowId, + function (input: WorkflowData) { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: draftOrderFieldsForRefreshSteps, + variables: { + id: input.order_id, + }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: ["id", "status"], + variables: { + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], + }, + }, + list: false, + }).config({ name: "order-change-query" }) + + validateDraftOrderChangeStep({ order, orderChange }) + + const promotions: PromotionDTO[] = useRemoteQueryStep({ + entry_point: "promotion", + fields: ["id", "code", "status"], + variables: { + filters: { + code: input.promo_codes, + }, + }, + list: true, + }).config({ name: "promotions-query" }) + + validatePromoCodesToRemoveStep({ + promo_codes: input.promo_codes, + promotions, + }) + + refreshDraftOrderAdjustmentsWorkflow.runAsStep({ + input: { + order, + promo_codes: input.promo_codes, + action: PromotionActions.REMOVE, + }, + }) + + const orderChangeActionInput = transform( + { order, orderChange, promotions }, + ({ order, orderChange, promotions }) => { + return promotions.map((promotion) => ({ + action: ChangeActionType.PROMOTION_REMOVE, + reference: "order_promotion", + order_change_id: orderChange.id, + reference_id: promotion.id, + order_id: order.id, + details: { + removed_code: promotion.code, + }, + })) + } + ) + + createOrderChangeActionsWorkflow.runAsStep({ + input: orderChangeActionInput, + }) + + return new WorkflowResponse(previewOrderChangeStep(input.order_id)) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/request-draft-order-edit.ts b/packages/core/core-flows/src/draft-order/workflows/request-draft-order-edit.ts new file mode 100644 index 0000000000..ad304ce4fd --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/request-draft-order-edit.ts @@ -0,0 +1,90 @@ +import { OrderChangeStatus } from "@medusajs/framework/utils" +import { + createWorkflow, + transform, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { OrderChangeDTO, OrderDTO } from "@medusajs/types" +import { useRemoteQueryStep } from "../../common" +import { + createOrUpdateOrderPaymentCollectionWorkflow, + previewOrderChangeStep, + updateOrderChangesStep, +} from "../../order" +import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-change" + +export const requestDraftOrderEditId = "request-draft-order-edit" + +function getOrderChangesData({ + input, + orderChange, +}: { + input: { requested_by?: string } + orderChange: { id: string } +}) { + return transform({ input, orderChange }, ({ input, orderChange }) => { + return [ + { + id: orderChange.id, + status: OrderChangeStatus.REQUESTED, + requested_at: new Date(), + requested_by: input.requested_by, + }, + ] + }) +} + +/** + * The data to request a draft order edit. + */ +export type RequestDraftOrderEditWorkflowInput = { + /** + * The ID of the draft order to request the edit for. + */ + order_id: string + /** + * The ID of the user requesting the edit. + */ + requested_by?: string +} + +export const requestDraftOrderEditWorkflow = createWorkflow( + requestDraftOrderEditId, + function (input: RequestDraftOrderEditWorkflowInput) { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: ["id", "version", "status", "is_draft_order", "canceled_at"], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: ["id", "canceled_at"], + variables: { + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], + }, + }, + list: false, + }).config({ name: "order-change-query" }) + + validateDraftOrderChangeStep({ + order, + orderChange, + }) + + const updateOrderChangesData = getOrderChangesData({ input, orderChange }) + updateOrderChangesStep(updateOrderChangesData) + + createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({ + input: { + order_id: order.id, + }, + }) + + return new WorkflowResponse(previewOrderChangeStep(order.id)) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/update-draft-order-action-item.ts b/packages/core/core-flows/src/draft-order/workflows/update-draft-order-action-item.ts new file mode 100644 index 0000000000..ba6bc77a8d --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/update-draft-order-action-item.ts @@ -0,0 +1,122 @@ +import { OrderChangeStatus, PromotionActions } from "@medusajs/framework/utils" +import { + createWorkflow, + transform, + when, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + OrderChangeActionDTO, + OrderChangeDTO, + OrderDTO, + OrderWorkflow, +} from "@medusajs/types" +import { useRemoteQueryStep } from "../../common" +import { + previewOrderChangeStep, + updateOrderChangeActionsStep, +} from "../../order" +import { getDraftOrderPromotionContextStep } from "../steps/get-draft-order-promotion-context" +import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-change" +import { validateDraftOrderUpdateActionItemStep } from "../steps/validate-draft-order-update-action-item" +import { draftOrderFieldsForRefreshSteps } from "../utils/fields" +import { refreshDraftOrderAdjustmentsWorkflow } from "./refresh-draft-order-adjustments" + +export const updateDraftOrderActionItemId = "update-draft-order-action-item" + +export const updateDraftOrderActionItemWorkflow = createWorkflow( + updateDraftOrderActionItemId, + function ( + input: WorkflowData + ) { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: draftOrderFieldsForRefreshSteps, + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: ["id", "status", "version", "actions.*"], + variables: { + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], + }, + }, + list: false, + }).config({ name: "order-change-query" }) + + validateDraftOrderChangeStep({ + order, + orderChange, + }) + + validateDraftOrderUpdateActionItemStep({ + input, + orderChange, + }) + + const updateData = transform( + { orderChange, input }, + ({ input, orderChange }) => { + const originalAction = (orderChange.actions ?? []).find( + (a) => a.id === input.action_id + ) as OrderChangeActionDTO + + const data = input.data + + return { + id: input.action_id, + details: { + quantity: data.quantity ?? originalAction.details?.quantity, + unit_price: data.unit_price ?? originalAction.details?.unit_price, + compare_at_unit_price: + data.compare_at_unit_price ?? + originalAction.details?.compare_at_unit_price, + }, + internal_note: data.internal_note, + } + } + ) + + updateOrderChangeActionsStep([updateData]) + + const context = getDraftOrderPromotionContextStep({ + order, + }) + + const appliedPromoCodes: string[] = transform(context, (context) => { + const promotionLink = (context as any).promotion_link + + if (!promotionLink) { + return [] + } + + if (Array.isArray(promotionLink)) { + return promotionLink.map((promo) => promo.promotion.code) + } + + return [promotionLink.promotion.code] + }) + + // If any the order has any promo codes, then we need to refresh the adjustments. + when( + appliedPromoCodes, + (appliedPromoCodes) => appliedPromoCodes.length > 0 + ).then(() => { + refreshDraftOrderAdjustmentsWorkflow.runAsStep({ + input: { + order: context, + promo_codes: appliedPromoCodes, + action: PromotionActions.REPLACE, + }, + }) + }) + + return new WorkflowResponse(previewOrderChangeStep(input.order_id)) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/update-draft-order-action-shipping-method.ts b/packages/core/core-flows/src/draft-order/workflows/update-draft-order-action-shipping-method.ts new file mode 100644 index 0000000000..bb9fe8a083 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/update-draft-order-action-shipping-method.ts @@ -0,0 +1,151 @@ +import { OrderChangeStatus, PromotionActions } from "@medusajs/framework/utils" +import { + createWorkflow, + parallelize, + transform, + when, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + OrderChangeActionDTO, + OrderChangeDTO, + OrderDTO, + OrderPreviewDTO, + OrderWorkflow, +} from "@medusajs/types" +import { useRemoteQueryStep } from "../../common" +import { + previewOrderChangeStep, + updateOrderChangeActionsStep, + updateOrderShippingMethodsStep, +} from "../../order" +import { prepareShippingMethodUpdate } from "../../order/utils/prepare-shipping-method" +import { getDraftOrderPromotionContextStep } from "../steps/get-draft-order-promotion-context" +import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-change" +import { validateDraftOrderShippingMethodActionStep } from "../steps/validate-draft-order-shipping-method-action" +import { draftOrderFieldsForRefreshSteps } from "../utils/fields" +import { refreshDraftOrderAdjustmentsWorkflow } from "./refresh-draft-order-adjustments" + +export const updateDraftOrderActionShippingMethodWorkflowId = + "update-draft-order-action-shipping-method" + +export const updateDraftOrderActionShippingMethodWorkflow = createWorkflow( + updateDraftOrderActionShippingMethodWorkflowId, + function ( + input: WorkflowData + ): WorkflowResponse { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: draftOrderFieldsForRefreshSteps, + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: ["id", "status", "version", "actions.*"], + variables: { + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], + }, + }, + list: false, + }).config({ name: "order-change-query" }) + + validateDraftOrderChangeStep({ order, orderChange }) + + const shippingOptions = when({ input }, ({ input }) => { + return input.data?.custom_amount === null + }).then(() => { + const action = transform( + { orderChange, input, order }, + ({ orderChange, input, order }) => { + const originalAction = (orderChange.actions ?? []).find( + (a) => a.id === input.action_id + ) as OrderChangeActionDTO + + return { + shipping_method_id: originalAction.reference_id, + currency_code: order.currency_code, + } + } + ) + + const shippingMethod = useRemoteQueryStep({ + entry_point: "order_shipping_method", + fields: ["id", "shipping_option_id"], + variables: { + id: action.shipping_method_id, + }, + list: false, + }).config({ name: "fetch-shipping-method" }) + + return useRemoteQueryStep({ + entry_point: "shipping_option", + fields: [ + "id", + "name", + "calculated_price.calculated_amount", + "calculated_price.is_calculated_price_tax_inclusive", + ], + variables: { + id: shippingMethod.shipping_option_id, + calculated_price: { + context: { currency_code: action.currency_code }, + }, + }, + }).config({ name: "fetch-shipping-option" }) + }) + + validateDraftOrderShippingMethodActionStep({ + orderChange, + input, + }) + + const updateData = transform( + { orderChange, input, shippingOptions }, + prepareShippingMethodUpdate + ) + + parallelize( + updateOrderChangeActionsStep([updateData.action]), + updateOrderShippingMethodsStep([updateData.shippingMethod!]) + ) + + const context = getDraftOrderPromotionContextStep({ + order, + }) + + const appliedPromoCodes = transform(context, (context) => { + const promotionLink = (context as any).promotion_link + + if (!promotionLink) { + return [] + } + + if (Array.isArray(promotionLink)) { + return promotionLink.map((promo) => promo.promotion.code) + } + + return [promotionLink.promotion.code] + }) + + when( + appliedPromoCodes, + (appliedPromoCodes) => appliedPromoCodes.length > 0 + ).then(() => { + refreshDraftOrderAdjustmentsWorkflow.runAsStep({ + input: { + order, + promo_codes: appliedPromoCodes, + action: PromotionActions.REPLACE, + }, + }) + }) + + return new WorkflowResponse(previewOrderChangeStep(input.order_id)) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/update-draft-order-item.ts b/packages/core/core-flows/src/draft-order/workflows/update-draft-order-item.ts new file mode 100644 index 0000000000..ad826ea8d3 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/update-draft-order-item.ts @@ -0,0 +1,128 @@ +import { + BigNumber, + ChangeActionType, + MathBN, + OrderChangeStatus, + PromotionActions, +} from "@medusajs/framework/utils" +import { + createWorkflow, + transform, + when, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + OrderChangeDTO, + OrderDTO, + OrderPreviewDTO, + OrderWorkflow, +} from "@medusajs/types" +import { useRemoteQueryStep } from "../../common" +import { + createOrderChangeActionsWorkflow, + previewOrderChangeStep, +} from "../../order" +import { getDraftOrderPromotionContextStep } from "../steps/get-draft-order-promotion-context" +import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-change" +import { draftOrderFieldsForRefreshSteps } from "../utils/fields" +import { refreshDraftOrderAdjustmentsWorkflow } from "./refresh-draft-order-adjustments" + +export const updateDraftOrderItemWorkflowId = "update-draft-order-item" + +export const updateDraftOrderItemWorkflow = createWorkflow( + updateDraftOrderItemWorkflowId, + function ( + input: WorkflowData + ): WorkflowResponse { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: draftOrderFieldsForRefreshSteps, + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: ["id", "status"], + variables: { + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], + }, + }, + list: false, + }).config({ name: "order-change-query" }) + + validateDraftOrderChangeStep({ order, orderChange }) + + const orderChangeActionInput = transform( + { order, orderChange, items: input.items }, + ({ order, orderChange, items }) => { + return items.map((item) => { + const existing = order?.items?.find( + (exItem) => exItem.id === item.id + )! + + const quantityDiff = new BigNumber( + MathBN.sub(item.quantity, existing.quantity) + ) + + return { + order_change_id: orderChange.id, + order_id: order.id, + version: orderChange.version, + action: ChangeActionType.ITEM_UPDATE, + internal_note: item.internal_note, + details: { + reference_id: item.id, + quantity: item.quantity, + unit_price: item.unit_price, + compare_at_unit_price: item.compare_at_unit_price, + quantity_diff: quantityDiff, + }, + } + }) + } + ) + + createOrderChangeActionsWorkflow.runAsStep({ + input: orderChangeActionInput, + }) + + const context = getDraftOrderPromotionContextStep({ + order, + }) + + const appliedPromoCodes: string[] = transform(context, (context) => { + const promotionLink = (context as any).promotion_link + + if (!promotionLink) { + return [] + } + + if (Array.isArray(promotionLink)) { + return promotionLink.map((promo) => promo.promotion.code) + } + + return [promotionLink.promotion.code] + }) + + // If any the order has any promo codes, then we need to refresh the adjustments. + when( + appliedPromoCodes, + (appliedPromoCodes) => appliedPromoCodes.length > 0 + ).then(() => { + refreshDraftOrderAdjustmentsWorkflow.runAsStep({ + input: { + order: context, + promo_codes: appliedPromoCodes, + action: PromotionActions.REPLACE, + }, + }) + }) + + return new WorkflowResponse(previewOrderChangeStep(input.order_id)) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/update-draft-order-shipping-method.ts b/packages/core/core-flows/src/draft-order/workflows/update-draft-order-shipping-method.ts new file mode 100644 index 0000000000..253515d423 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/update-draft-order-shipping-method.ts @@ -0,0 +1,154 @@ +import { + ChangeActionType, + OrderChangeStatus, + PromotionActions, +} from "@medusajs/framework/utils" +import { + createWorkflow, + transform, + when, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { BigNumberInput, OrderChangeDTO, OrderDTO } from "@medusajs/types" +import { useRemoteQueryStep } from "../../common" +import { + createOrderChangeActionsWorkflow, + previewOrderChangeStep, + updateOrderTaxLinesWorkflow, +} from "../../order" +import { updateDraftOrderShippingMethodStep } from "../steps/update-draft-order-shipping-metod" +import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-change" +import { draftOrderFieldsForRefreshSteps } from "../utils/fields" +import { refreshDraftOrderAdjustmentsWorkflow } from "./refresh-draft-order-adjustments" + +export const updateDraftOrderShippingMethodWorkflowId = + "update-draft-order-shipping-method" + +export interface UpdateDraftOrderShippingMethodWorkflowInput { + /** + * The ID of the order to update the shipping method in its edit. + */ + order_id: string + data: { + /** + * The ID of the shipping method to update. + */ + shipping_method_id: string + /** + * The ID of the shipping option to associate with the shipping method. + */ + shipping_option_id?: string + /** + * Set a custom amount for the shipping method. + */ + custom_amount?: BigNumberInput + /** + * A note viewed by admins only related to the shipping method. + */ + internal_note?: string | null + } +} + +export const updateDraftOrderShippingMethodWorkflow = createWorkflow( + updateDraftOrderShippingMethodWorkflowId, + function (input: WorkflowData) { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: ["id", "status", "is_draft_order"], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: ["id", "status", "version", "actions.*"], + variables: { + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], + }, + }, + list: false, + }).config({ name: "order-change-query" }) + + validateDraftOrderChangeStep({ order, orderChange }) + + const { before, after } = updateDraftOrderShippingMethodStep({ + order_id: input.order_id, + shipping_method_id: input.data.shipping_method_id, + shipping_option_id: input.data.shipping_option_id, + amount: input.data.custom_amount, + }) + + updateOrderTaxLinesWorkflow.runAsStep({ + input: { + order_id: order.id, + shipping_method_ids: [input.data.shipping_method_id], + }, + }) + + const refetchedOrder = useRemoteQueryStep({ + entry_point: "orders", + fields: draftOrderFieldsForRefreshSteps, + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "refetched-order-query" }) + + const appliedPromoCodes = transform(refetchedOrder, (refetchedOrder) => { + const promotionLink = (refetchedOrder as any).promotion_link + + if (!promotionLink) { + return [] + } + + if (Array.isArray(promotionLink)) { + return promotionLink.map((promo) => promo.promotion.code) + } + + return [promotionLink.promotion.code] + }) + + when( + appliedPromoCodes, + (appliedPromoCodes) => appliedPromoCodes.length > 0 + ).then(() => { + refreshDraftOrderAdjustmentsWorkflow.runAsStep({ + input: { + order: refetchedOrder, + promo_codes: appliedPromoCodes, + action: PromotionActions.REPLACE, + }, + }) + }) + + const orderChangeActionInput = transform( + { order, orderChange, data: input.data, before, after }, + ({ order, orderChange, data, before, after }) => { + return { + order_change_id: orderChange.id, + reference: "order_shipping_method", + reference_id: data.shipping_method_id, + order_id: order.id, + version: orderChange.version, + action: ChangeActionType.SHIPPING_UPDATE, + internal_note: data.internal_note, + details: { + old_shipping_option_id: before.shipping_option_id, + new_shipping_option_id: after.shipping_option_id, + old_amount: before.amount, + new_amount: after.amount, + }, + } + } + ) + + createOrderChangeActionsWorkflow.runAsStep({ + input: [orderChangeActionInput as any], + }) + + return new WorkflowResponse(previewOrderChangeStep(input.order_id)) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/update-draft-order.ts b/packages/core/core-flows/src/draft-order/workflows/update-draft-order.ts new file mode 100644 index 0000000000..df87add151 --- /dev/null +++ b/packages/core/core-flows/src/draft-order/workflows/update-draft-order.ts @@ -0,0 +1,258 @@ +import { Modules, OrderWorkflowEvents } from "@medusajs/framework/utils" +import { + createStep, + createWorkflow, + StepResponse, + transform, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + IOrderModuleService, + OrderDTO, + RegisterOrderChangeDTO, + UpdateOrderDTO, + UpsertOrderAddressDTO, +} from "@medusajs/types" +import { emitEventStep, useRemoteQueryStep } from "../../common" +import { previewOrderChangeStep, registerOrderChangesStep } from "../../order" +import { validateDraftOrderStep } from "../steps/validate-draft-order" + +export const updateDraftOrderWorkflowId = "update-draft-order" + +export interface UpdateDraftOrderWorkflowInput { + /** + * The ID of the draft order to update. + */ + id: string + /** + * The ID of the user updating the draft order. + */ + user_id: string + /** + * Create or update the shipping address of the draft order. + */ + shipping_address?: UpsertOrderAddressDTO + /** + * Create or update the billing address of the draft order. + */ + billing_address?: UpsertOrderAddressDTO + /** + * The ID of the customer to associate the draft order with. + */ + customer_id?: string + /** + * The new email of the draft order. + */ + email?: string + /** + * The ID of the sales channel to associate the draft order with. + */ + sales_channel_id?: string + /** + * The new metadata of the draft order. + */ + metadata?: Record | null +} + +interface UpdateDraftOrderStepInput { + order: OrderDTO + input: UpdateOrderDTO +} + +const updateDraftOrderStep = createStep( + "update-draft-order", + async ({ order, input }: UpdateDraftOrderStepInput, { container }) => { + const service = container.resolve(Modules.ORDER) + + const updatedOrders = await service.updateOrders([ + { + id: order.id, + ...input, + }, + ]) + + const updatedOrder = updatedOrders[0] + + return new StepResponse(updatedOrder, order) + }, + async function (prevData, { container }) { + if (!prevData) { + return + } + + const service = container.resolve(Modules.ORDER) + + await service.updateOrders([prevData as UpdateOrderDTO]) + } +) + +export const updateDraftOrderWorkflow = createWorkflow( + updateDraftOrderWorkflowId, + function (input: WorkflowData) { + const order = useRemoteQueryStep({ + entry_point: "orders", + fields: [ + "id", + "customer_id", + "status", + "is_draft_order", + "sales_channel_id", + "email", + "customer_id", + "shipping_address.*", + "billing_address.*", + "metadata", + ], + variables: { + id: input.id, + }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "order-query" }) + + validateDraftOrderStep({ order }) + + const updateInput = transform( + { input, order }, + ({ + input, + order, + }: { + input: UpdateDraftOrderWorkflowInput + order: OrderDTO + }) => { + const update: UpdateOrderDTO = {} + + if (input.shipping_address) { + const address = { + ...order.shipping_address, + ...input.shipping_address, + } + delete address.id + update.shipping_address = address + } + + if (input.billing_address) { + const address = { + ...order.billing_address, + ...input.billing_address, + } + delete address.id + update.billing_address = address + } + + return { ...input, ...update } + } + ) + + const updatedOrder = updateDraftOrderStep({ + order, + input: updateInput, + }) + + const orderChangeInput = transform( + { input, updatedOrder, order }, + ({ input, updatedOrder, order }) => { + const changes: RegisterOrderChangeDTO[] = [] + + if (input.shipping_address) { + changes.push({ + change_type: "update_order" as const, + order_id: input.id, + created_by: input.user_id, + confirmed_by: input.user_id, + details: { + type: "shipping_address", + old: order.shipping_address, + new: updatedOrder.shipping_address, + }, + }) + } + + if (input.billing_address) { + changes.push({ + change_type: "update_order" as const, + order_id: input.id, + created_by: input.user_id, + confirmed_by: input.user_id, + details: { + type: "billing_address", + old: order.billing_address, + new: updatedOrder.billing_address, + }, + }) + } + + if (input.customer_id) { + changes.push({ + change_type: "update_order" as const, + order_id: input.id, + created_by: input.user_id, + confirmed_by: input.user_id, + details: { + type: "customer_id", + old: order.customer_id, + new: updatedOrder.customer_id, + }, + }) + } + + if (input.email) { + changes.push({ + change_type: "update_order" as const, + order_id: input.id, + created_by: input.user_id, + confirmed_by: input.user_id, + details: { + type: "email", + old: order.email, + new: updatedOrder.email, + }, + }) + } + + if (input.sales_channel_id) { + changes.push({ + change_type: "update_order" as const, + order_id: input.id, + created_by: input.user_id, + confirmed_by: input.user_id, + details: { + type: "sales_channel_id", + old: order.sales_channel_id, + new: updatedOrder.sales_channel_id, + }, + }) + } + + if (input.metadata) { + changes.push({ + change_type: "update_order" as const, + order_id: input.id, + created_by: input.user_id, + confirmed_by: input.user_id, + details: { + type: "metadata", + old: order.metadata, + new: updatedOrder.metadata, + }, + }) + } + + return changes + } + ) + + registerOrderChangesStep(orderChangeInput) + + emitEventStep({ + eventName: OrderWorkflowEvents.UPDATED, + data: { id: input.id }, + }) + + const preview = previewOrderChangeStep(input.id) + + return new WorkflowResponse(preview) + } +) diff --git a/packages/core/core-flows/src/index.ts b/packages/core/core-flows/src/index.ts index 91f8726db3..f28e17b9e7 100644 --- a/packages/core/core-flows/src/index.ts +++ b/packages/core/core-flows/src/index.ts @@ -5,11 +5,13 @@ export * from "./common" export * from "./customer" export * from "./customer-group" export * from "./defaults" +export * from "./draft-order" export * from "./file" export * from "./fulfillment" export * from "./inventory" export * from "./invite" export * from "./line-item" +export * from "./notification" export * from "./order" export * from "./payment" export * from "./payment-collection" @@ -28,4 +30,3 @@ export * from "./stock-location" export * from "./store" export * from "./tax" export * from "./user" -export * from "./notification" \ No newline at end of file diff --git a/packages/core/core-flows/src/order/workflows/add-line-items.ts b/packages/core/core-flows/src/order/workflows/add-line-items.ts index 9c97518bf3..3b7d20f8be 100644 --- a/packages/core/core-flows/src/order/workflows/add-line-items.ts +++ b/packages/core/core-flows/src/order/workflows/add-line-items.ts @@ -30,7 +30,7 @@ import { pricingContextResult } from "../../cart/utils/schemas" function prepareLineItems(data) { const items = (data.input.items ?? []).map((item) => { - const variant = data.variants.find((v) => v.id === item.variant_id)! + const variant = data.variants?.find((v) => v.id === item.variant_id)! const input: PrepareLineItemDataInput = { item, diff --git a/packages/core/core-flows/src/product/workflows/batch-products.ts b/packages/core/core-flows/src/product/workflows/batch-products.ts index 2954cde1c4..fea00eb548 100644 --- a/packages/core/core-flows/src/product/workflows/batch-products.ts +++ b/packages/core/core-flows/src/product/workflows/batch-products.ts @@ -41,7 +41,6 @@ const conditionallyDeleteProducts = (input: BatchProductWorkflowInput) => deleteProductsWorkflow.runAsStep({ input: { ids: input.delete! } }) ) - export const batchProductsWorkflowId = "batch-products" /** * This workflow creates, updates, or deletes products. It's used by the diff --git a/packages/core/js-sdk/src/admin/draft-order.ts b/packages/core/js-sdk/src/admin/draft-order.ts index 7fc081f0dc..ccb6de4f17 100644 --- a/packages/core/js-sdk/src/admin/draft-order.ts +++ b/packages/core/js-sdk/src/admin/draft-order.ts @@ -202,4 +202,475 @@ export class DraftOrder { } ) } + + /** + * This method converts a draft order to an order. It sends a request to the + * [Convert Draft Order to Order](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersidconvert-to-order) API route. + * + * @param id - The draft order's ID. + * @param query - Configure the fields to retrieve in the order. + * @param headers - Headers to pass in the request. + * + * @example + * To convert a draft order to an order: + * + * ```ts + * sdk.admin.draftOrder.convertToOrder("draft_order_123") + * .then(({ order }) => { + * console.log(order) + * }) + */ + async convertToOrder( + id: string, + query?: HttpTypes.AdminDraftOrderParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/draft-orders/${id}/convert-to-order`, + { + method: "POST", + query, + headers, + } + ) + } + + /** + * This method adds items to a draft order. It sends a request to the + * [Add Draft Order Items](https://docs.medusajs.com/api/admin#draft-orders_postordereditsiditems) API route. + * + * @param id - The draft order's ID. + * @param body - The data to add the items to the draft order. + * @param headers - Headers to pass in the request. + * + * @example + * To add items to a draft order: + * + * ```ts + * sdk.admin.draftOrder.addItems("order_123", { + * items: [ + * { + * variant_id: "variant_123", + * quantity: 1, + * }, + * ], + * }) + * .then(({ draft_order_preview }) => { + * console.log(draft_order_preview) + * }) + * ``` + */ + async addItems( + id: string, + body: HttpTypes.AdminAddDraftOrderItems, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/draft-orders/${id}/edit/items`, + { + method: "POST", + body, + headers, + } + ) + } + + /** + * This method updates an item that is part of an action in a draft order. It sends a request to the + * [Update Draft Order Item](https://docs.medusajs.com/api/admin#draft-orders_postordereditsiditemsaction_id) API route. + * + * @param id - The draft order's ID. + * @param actionId - The action ID. + * @param body - The data to update the item. + * @param headers - Headers to pass in the request. + * + * @example + * To update an item that is part of an action in a draft order: + * + * ```ts + * sdk.admin.draftOrder.updateActionItem("order_123", "action_123", { + * quantity: 2, + * }) + * .then(({ draft_order_preview }) => { + * console.log(draft_order_preview) + * }) + * ``` + */ + async updateActionItem( + id: string, + actionId: string, + body: HttpTypes.AdminUpdateDraftOrderItem, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/draft-orders/${id}/edit/items/${actionId}`, + { + method: "POST", + body, + headers, + } + ) + } + + /** + * This method removes an item that is part of an action in a draft order. It sends a request to the + * [Remove Draft Order Item](https://docs.medusajs.com/api/admin#draft-orders_deleteordereditsiditemsaction_id) API route. + * + * @param id - The draft order's ID. + * @param actionId - The action ID. + * @param headers - Headers to pass in the request. + * + * @example + * To remove an item that is part of an action in a draft order: + * + * ```ts + * sdk.admin.draftOrder.removeActionItem("order_123", "action_123") + * .then(({ draft_order_preview }) => { + * console.log(draft_order_preview) + * }) + * ``` + */ + async removeActionItem( + id: string, + actionId: string, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/draft-orders/${id}/edit/items/${actionId}`, + { + method: "DELETE", + headers, + } + ) + } + + /** + * This method updates an item in a draft order. It sends a request to the + * [Update Draft Order Item](https://docs.medusajs.com/api/admin#draft-orders_postordereditsiditemsitem_id) API route. + * + * @param id - The draft order's ID. + * @param itemId - The item ID. + * @param body - The data to update the item. + * @param headers - Headers to pass in the request. + * + * @example + * To update an item in a draft order: + * + * ```ts + * sdk.admin.draftOrder.updateItem("order_123", "item_123", { + * quantity: 2, + * }) + * .then(({ draft_order_preview }) => { + * console.log(draft_order_preview) + * }) + * ``` + */ + async updateItem( + id: string, + itemId: string, + body: HttpTypes.AdminUpdateDraftOrderItem, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/draft-orders/${id}/edit/items/item/${itemId}`, + { + method: "POST", + body, + headers, + } + ) + } + + /** + * This method adds promotions to a draft order. It sends a request to the + * [Add Draft Order Promotions](https://docs.medusajs.com/api/admin#draft-orders_postordereditsidpromotions) API route. + * + * @param id - The draft order's ID. + * @param body - The data to add the promotions to the draft order. + * @param headers - Headers to pass in the request. + * + * @example + * To add promotions to a draft order: + * + * ```ts + * sdk.admin.draftOrder.addPromotions("order_123", { + * promo_codes: ["PROMO_CODE_1", "PROMO_CODE_2"], + * }) + * .then(({ draft_order_preview }) => { + * console.log(draft_order_preview) + * }) + * ``` + */ + async addPromotions( + id: string, + body: HttpTypes.AdminAddDraftOrderPromotions, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/draft-orders/${id}/edit/promotions`, + { + method: "POST", + body, + headers, + } + ) + } + + /** + * This method removes promotions from a draft order. It sends a request to the + * [Remove Draft Order Promotions](https://docs.medusajs.com/api/admin#draft-orders_deleteordereditsidpromotions) API route. + * + * @param id - The draft order's ID. + * @param body - The data to remove the promotions from the draft order. + * @param headers - Headers to pass in the request. + * + * @example + * To remove promotions from a draft order: + * + * ```ts + * sdk.admin.draftOrder.removePromotions("order_123", { + * promo_codes: ["PROMO_CODE_1", "PROMO_CODE_2"], + * }) + * ``` + */ + async removePromotions( + id: string, + body: HttpTypes.AdminRemoveDraftOrderPromotions, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/draft-orders/${id}/edit/promotions`, + { + method: "DELETE", + body, + headers, + } + ) + } + + /** + * This method adds a shipping method to a draft order. It sends a request to the + * [Add Draft Order Shipping Method](https://docs.medusajs.com/api/admin#draft-orders_postordereditsidshipping-methods) API route. + * + * @param id - The draft order's ID. + * @param body - The data to add the shipping method to the draft order. + * @param headers - Headers to pass in the request. + * + * @example + * To add a shipping method to a draft order: + * + * ```ts + * sdk.admin.draftOrder.addShippingMethod("order_123", { + * shipping_option_id: "shipping_option_123", + * }) + * .then(({ draft_order_preview }) => { + * console.log(draft_order_preview) + * }) + * ``` + */ + async addShippingMethod( + id: string, + body: HttpTypes.AdminAddDraftOrderShippingMethod, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/draft-orders/${id}/edit/shipping-methods`, + { + method: "POST", + body, + headers, + } + ) + } + + /** + * This method updates a shipping method in a draft order. It sends a request to the + * [Update Draft Order Shipping Method](https://docs.medusajs.com/api/admin#draft-orders_postordereditsidshipping-methodsaction_id) API route. + * + * @param id - The draft order's ID. + * @param actionId - The action ID. + * @param body - The data to update the shipping method. + * @param headers - Headers to pass in the request. + * + * @example + * To update a shipping method in a draft order: + * + * ```ts + * sdk.admin.draftOrder.updateShippingMethod("order_123", "action_123", { + * shipping_option_id: "shipping_option_123", + * }) + * .then(({ draft_order_preview }) => { + * console.log(draft_order_preview) + * }) + * ``` + */ + async updateActionShippingMethod( + id: string, + actionId: string, + body: HttpTypes.AdminUpdateDraftOrderActionShippingMethod, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/draft-orders/${id}/edit/shipping-methods/${actionId}`, + { + method: "POST", + body, + headers, + } + ) + } + + /** + * This method removes a shipping method from a draft order. It sends a request to the + * [Remove Draft Order Shipping Method](https://docs.medusajs.com/api/admin#draft-orders_deleteordereditsidshipping-methodsaction_id) API route. + * + * @param id - The draft order's ID. + * @param actionId - The action ID. + * @param headers - Headers to pass in the request. + * + * @example + * To remove a shipping method from a draft order: + * + * ```ts + * sdk.admin.draftOrder.removeShippingMethod("order_123", "action_123") + * .then(({ draft_order_preview }) => { + * console.log(draft_order_preview) + * }) + * ``` + */ + async removeActionShippingMethod( + id: string, + actionId: string, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/draft-orders/${id}/edit/shipping-methods/${actionId}`, + { + method: "DELETE", + headers, + } + ) + } + + async updateShippingMethod( + id: string, + methodId: string, + body: HttpTypes.AdminUpdateDraftOrderShippingMethod, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/draft-orders/${id}/edit/shipping-methods/method/${methodId}`, + { + method: "POST", + body, + headers, + } + ) + } + /** + * This method begins an edit to a draft order. It sends a request to the + * [Begin Draft Order Edit](https://docs.medusajs.com/api/admin#draft-orders_postordereditsid) API route. + * + * @param id - The draft order's ID. + * @param headers - Headers to pass in the request. + * + * @example + * To begin an edit to a draft order: + * + * ```ts + * sdk.admin.draftOrder.beginEdit("order_123") + * .then(({ draft_order_preview }) => { + * console.log(draft_order_preview) + * }) + * ``` + */ + async beginEdit(id: string, headers?: ClientHeaders) { + return await this.client.fetch( + `/admin/draft-orders/${id}/edit`, + { + method: "POST", + headers, + } + ) + } + + /** + * This method cancels an edit to a draft order. It sends a request to the + * [Cancel Draft Order Edit](https://docs.medusajs.com/api/admin#draft-orders_deleteordereditsid) API route. + * + * @param id - The draft order's ID. + * @param headers - Headers to pass in the request. + * + * @example + * To cancel an edit to a draft order: + * + * ```ts + * sdk.admin.draftOrder.cancelEdit("order_123") + * .then(({ id, object, deleted }) => { + * console.log(id, object, deleted) + * }) + * ``` + */ + async cancelEdit(id: string, headers?: ClientHeaders) { + return await this.client.fetch< + HttpTypes.DeleteResponse<"draft-order-edit"> + >(`/admin/draft-orders/${id}/edit`, { + method: "DELETE", + headers, + }) + } + + /** + * This method requests an edit to a draft order. It sends a request to the + * [Request Draft Order Edit](https://docs.medusajs.com/api/admin#draft-orders_postordereditsidrequest) API route. + * + * @param id - The draft order's ID. + * @param headers - Headers to pass in the request. + * + * @example + * To request an edit to a draft order: + * + * ```ts + * sdk.admin.draftOrder.requestEdit("order_123") + * .then(({ draft_order_preview }) => { + * console.log(draft_order_preview) + * }) + * ``` + */ + async requestEdit(id: string, headers?: ClientHeaders) { + return await this.client.fetch( + `/admin/draft-orders/${id}/edit/request`, + { + method: "POST", + headers, + } + ) + } + + /** + * This method confirms an edit to a draft order. It sends a request to the + * [Confirm Draft Order Edit](https://docs.medusajs.com/api/admin#draft-orders_postordereditsidconfirm) API route. + * + * @param id - The draft order's ID. + * @param headers - Headers to pass in the request. + * + * @example + * To confirm an edit to a draft order: + * + * ```ts + * sdk.admin.draftOrder.confirmEdit("order_123") + * .then(({ draft_order_preview }) => { + * console.log(draft_order_preview) + * }) + * ``` + */ + async confirmEdit(id: string, headers?: ClientHeaders) { + return await this.client.fetch( + `/admin/draft-orders/${id}/edit/confirm`, + { + method: "POST", + headers, + } + ) + } } diff --git a/packages/core/types/src/http/draft-order/admin/entities.ts b/packages/core/types/src/http/draft-order/admin/entities.ts index e26058ffb7..8184889b6f 100644 --- a/packages/core/types/src/http/draft-order/admin/entities.ts +++ b/packages/core/types/src/http/draft-order/admin/entities.ts @@ -1,3 +1,5 @@ -import { AdminOrder } from "../../order" +import { AdminOrder, AdminOrderPreview } from "../../order" export interface AdminDraftOrder extends AdminOrder {} + +export interface AdminDraftOrderPreview extends AdminOrderPreview {} diff --git a/packages/core/types/src/http/draft-order/admin/payloads.ts b/packages/core/types/src/http/draft-order/admin/payloads.ts index b36ba365cb..f25e598971 100644 --- a/packages/core/types/src/http/draft-order/admin/payloads.ts +++ b/packages/core/types/src/http/draft-order/admin/payloads.ts @@ -51,6 +51,10 @@ export interface AdminCreateDraftOrder { * Either customer_id or email must be provided. */ customer_id?: string | null + /** + * The ID of the sales channel to associate the draft order with. + */ + sales_channel_id: string /** * The ID of the region to associate the draft order with. */ @@ -96,6 +100,14 @@ export interface AdminUpdateDraftOrder { * The draft order's email. */ email?: string + /** + * The ID of the customer to associate the draft order with. + */ + customer_id?: string + /** + * The ID of the sales channel to associate the draft order with. + */ + sales_channel_id?: string /** * The draft order's shipping address. */ @@ -109,3 +121,134 @@ export interface AdminUpdateDraftOrder { */ metadata?: Record | null } + +export interface AdminUpdateDraftOrderItem { + /** + * The item's quantity. + */ + quantity: number + /** + * The item's unit price. + */ + unit_price?: number | null + /** + * The item's compare at unit price. + */ + compare_at_unit_price?: number | null + /** + * The item's internal note. + */ + internal_note?: string | null + /** + * The item's metadata. + */ + metadata?: Record | null +} + +export interface AdminAddDraftOrderItem { + /** + * The item's variant ID. + * + * Either variant_id or title must be provided. + */ + variant_id?: string + /** + * The item's title. + * + * Either variant_id or title must be provided. + */ + title?: string + /** + * The item's quantity. + */ + quantity: number + /** + * The item's unit price. + */ + unit_price?: number | null + /** + * The item's compare at unit price. + */ + compare_at_unit_price?: number | null + /** + * The item's internal note. + */ + internal_note?: string | null + /** + * The item's metadata. + */ + metadata?: Record | null +} + +export interface AdminAddDraftOrderItems { + items: AdminAddDraftOrderItem[] +} + +export interface AdminAddDraftOrderPromotions { + promo_codes: string[] +} + +export interface AdminRemoveDraftOrderPromotions { + promo_codes: string[] +} + +export interface AdminAddDraftOrderShippingMethod { + /** + * ID of the shipping option to associate with the shipping method. + */ + shipping_option_id: string + /** + * Custom amount for the shipping method. + */ + custom_amount?: number + /** + * Description of the shipping method. + */ + description?: string + /** + * Internal note for the shipping method. + */ + internal_note?: string + /** + * Metadata for the shipping method. + */ + metadata?: Record +} + +export interface AdminUpdateDraftOrderActionShippingMethod { + /** + * ID of the shipping option to associate with the shipping method. + */ + shipping_option_id: string + /** + * Custom amount for the shipping method. + */ + custom_amount?: number | null + /** + * Description of the shipping method. + */ + description?: string | null + /** + * Internal note for the shipping method. + */ + internal_note?: string | null + /** + * Metadata for the shipping method. + */ + metadata?: Record | null +} + +export interface AdminUpdateDraftOrderShippingMethod { + /** + * ID of the shipping option to associate with the shipping method. + */ + shipping_option_id?: string + /** + * Custom amount for the shipping method. + */ + custom_amount?: number + /** + * Internal note for the shipping method. + */ + internal_note?: string | null +} diff --git a/packages/core/types/src/http/draft-order/admin/responses.ts b/packages/core/types/src/http/draft-order/admin/responses.ts index c2d30c4628..7d97b57080 100644 --- a/packages/core/types/src/http/draft-order/admin/responses.ts +++ b/packages/core/types/src/http/draft-order/admin/responses.ts @@ -1,5 +1,5 @@ import { PaginatedResponse } from "../../common" -import { AdminDraftOrder } from "./entities" +import { AdminDraftOrder, AdminDraftOrderPreview } from "./entities" export interface AdminDraftOrderResponse { draft_order: AdminDraftOrder @@ -9,3 +9,7 @@ export interface AdminDraftOrderListResponse extends PaginatedResponse<{ draft_orders: AdminDraftOrder[] }> {} + +export interface AdminDraftOrderPreviewResponse { + draft_order_preview: AdminDraftOrderPreview +} diff --git a/packages/core/types/src/http/order/admin/entities.ts b/packages/core/types/src/http/order/admin/entities.ts index dcd43b67ab..020330baf3 100644 --- a/packages/core/types/src/http/order/admin/entities.ts +++ b/packages/core/types/src/http/order/admin/entities.ts @@ -4,7 +4,7 @@ import { AdminCustomer } from "../../customer" import { AdminExchange } from "../../exchange" import { AdminPaymentCollection } from "../../payment/admin" import { AdminProduct, AdminProductVariant } from "../../product" -import { AdminRegionCountry } from "../../region" +import { AdminRegion, AdminRegionCountry } from "../../region" import { AdminReturn } from "../../return" import { AdminSalesChannel } from "../../sales-channel" import { @@ -30,6 +30,10 @@ export interface AdminOrder extends Omit { * The associated sales channel's details. */ sales_channel?: AdminSalesChannel + /** + * The order's region. + */ + region?: AdminRegion /** * The details of the customer that placed the order. */ diff --git a/packages/core/types/src/http/order/common.ts b/packages/core/types/src/http/order/common.ts index 0cc8d5e9b3..dfd0811d2d 100644 --- a/packages/core/types/src/http/order/common.ts +++ b/packages/core/types/src/http/order/common.ts @@ -939,6 +939,7 @@ export interface BaseOrderChange { | "edit" | "return_request" | "transfer" + | "update_order" /** * The ID of the associated order diff --git a/packages/core/types/src/http/promotion/admin/queries.ts b/packages/core/types/src/http/promotion/admin/queries.ts index b2a849bca1..4109adcdfc 100644 --- a/packages/core/types/src/http/promotion/admin/queries.ts +++ b/packages/core/types/src/http/promotion/admin/queries.ts @@ -13,7 +13,7 @@ export interface AdminGetPromotionsParams /** * Filter by promotion code. */ - code?: string | string[] + code?: string | string[] | OperatorMap /** * Filter by campaign ID to retrieve promotions by campaign. */ diff --git a/packages/core/types/src/order/common.ts b/packages/core/types/src/order/common.ts index 36cb115a5a..ea3573a1cf 100644 --- a/packages/core/types/src/order/common.ts +++ b/packages/core/types/src/order/common.ts @@ -21,12 +21,15 @@ export type ChangeActionType = | "RETURN_ITEM" | "SHIPPING_ADD" | "SHIPPING_REMOVE" + | "SHIPPING_UPDATE" | "SHIP_ITEM" | "WRITE_OFF_ITEM" | "REINSTATE_ITEM" | "TRANSFER_CUSTOMER" | "UPDATE_ORDER_PROPERTIES" | "CREDIT_LINE_ADD" + | "PROMOTION_ADD" + | "PROMOTION_REMOVE" export type OrderChangeStatus = | "confirmed" @@ -1119,6 +1122,11 @@ export interface OrderDTO { */ summary?: OrderSummaryDTO + /** + * Whether the order is a draft order. + */ + is_draft_order?: boolean + /** * Holds custom data in key-value pairs. */ diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts index ff54f30ac1..bf761978f2 100644 --- a/packages/core/types/src/order/mutations.ts +++ b/packages/core/types/src/order/mutations.ts @@ -6,6 +6,7 @@ import { OrderItemDTO, OrderLineItemDTO, OrderReturnReasonDTO, + OrderStatus, OrderTransactionDTO, ReturnDTO, } from "./common" @@ -223,6 +224,16 @@ export interface UpdateOrderDTO { */ sales_channel_id?: string + /** + * The status of the order. + */ + status?: OrderStatus + + /** + * Whether the order is a draft order. + */ + is_draft_order?: boolean + /** * The items of the order. */ diff --git a/packages/core/types/src/workflow/order/items.ts b/packages/core/types/src/workflow/order/items.ts index 9d599509aa..4ab1b2fd40 100644 --- a/packages/core/types/src/workflow/order/items.ts +++ b/packages/core/types/src/workflow/order/items.ts @@ -8,7 +8,11 @@ interface NewItem { /** * The ID of the variant to add to the order. */ - variant_id: string + variant_id?: string + /** + * The title of the item to add to the order. + */ + title?: string /** * The quantity of the item to add to the order. */ @@ -211,8 +215,8 @@ export interface UpdateClaimAddNewItemWorkflowInput { claim_id: string /** * The ID of the action associated with the item to update. - * Every item has an `actions` property, whose value is an array of actions. - * You can find the action with the name `ITEM_ADD` using the item's `action` property, + * Every item has an `actions` property, whose value is an array of actions. + * You can find the action with the name `ITEM_ADD` using the item's `action` property, * and use the value of the action's `id` property. */ action_id: string @@ -256,7 +260,7 @@ export interface OrderClaimItemWorkflowInput { /** * The items to add to the claim. */ - items: (ExistingItem & { + items: (ExistingItem & { /** * The reason for adding the item to the claim. */ @@ -274,8 +278,8 @@ export interface UpdateClaimItemWorkflowInput { claim_id: string /** * The ID of the action associated with the item to update. - * Every item has an `actions` property, whose value is an array of actions. - * You can find the action with the name `WRITE_OFF_ITEM` using its `action` property, + * Every item has an `actions` property, whose value is an array of actions. + * You can find the action with the name `WRITE_OFF_ITEM` using its `action` property, * and use the value of its `id` property. */ action_id: string diff --git a/packages/core/utils/src/order/order-change-action.ts b/packages/core/utils/src/order/order-change-action.ts index 484b9f0a94..67c8ea57f4 100644 --- a/packages/core/utils/src/order/order-change-action.ts +++ b/packages/core/utils/src/order/order-change-action.ts @@ -11,10 +11,13 @@ export enum ChangeActionType { CANCEL_RETURN_ITEM = "CANCEL_RETURN_ITEM", SHIPPING_ADD = "SHIPPING_ADD", SHIPPING_REMOVE = "SHIPPING_REMOVE", + SHIPPING_UPDATE = "SHIPPING_UPDATE", SHIP_ITEM = "SHIP_ITEM", WRITE_OFF_ITEM = "WRITE_OFF_ITEM", REINSTATE_ITEM = "REINSTATE_ITEM", TRANSFER_CUSTOMER = "TRANSFER_CUSTOMER", UPDATE_ORDER_PROPERTIES = "UPDATE_ORDER_PROPERTIES", CREDIT_LINE_ADD = "CREDIT_LINE_ADD", + PROMOTION_ADD = "PROMOTION_ADD", + PROMOTION_REMOVE = "PROMOTION_REMOVE", } diff --git a/packages/medusa/src/api/admin/customers/middlewares.ts b/packages/medusa/src/api/admin/customers/middlewares.ts index 0dde4c4eb0..d0ce98be6d 100644 --- a/packages/medusa/src/api/admin/customers/middlewares.ts +++ b/packages/medusa/src/api/admin/customers/middlewares.ts @@ -78,7 +78,7 @@ export const adminCustomerRoutesMiddlewares: MiddlewareRoute[] = [ middlewares: [ validateAndTransformQuery( AdminCustomerAddressParams, - QueryConfig.retrieveTransformQueryConfig + QueryConfig.retrieveAddressTransformQueryConfig ), ], }, @@ -89,7 +89,7 @@ export const adminCustomerRoutesMiddlewares: MiddlewareRoute[] = [ validateAndTransformBody(AdminUpdateCustomerAddress), validateAndTransformQuery( AdminCustomerParams, - QueryConfig.retrieveAddressTransformQueryConfig + QueryConfig.retrieveTransformQueryConfig ), ], }, diff --git a/packages/medusa/src/api/admin/customers/validators.ts b/packages/medusa/src/api/admin/customers/validators.ts index a7121a9ba2..9a103cc742 100644 --- a/packages/medusa/src/api/admin/customers/validators.ts +++ b/packages/medusa/src/api/admin/customers/validators.ts @@ -22,7 +22,9 @@ export const AdminCustomerGroupInCustomerParams = z.object({ export const AdminCustomersParamsFields = z.object({ q: z.string().optional(), - id: z.union([z.string(), z.array(z.string())]).optional(), + id: z + .union([z.string(), z.array(z.string()), createOperatorMap()]) + .optional(), email: z.union([z.string(), z.array(z.string())]).optional(), groups: z .union([ diff --git a/packages/medusa/src/api/admin/draft-orders/[id]/convert-to-order/route.ts b/packages/medusa/src/api/admin/draft-orders/[id]/convert-to-order/route.ts new file mode 100644 index 0000000000..2f5140c0fb --- /dev/null +++ b/packages/medusa/src/api/admin/draft-orders/[id]/convert-to-order/route.ts @@ -0,0 +1,22 @@ +import { convertDraftOrderWorkflow } from "@medusajs/core-flows" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +import { HttpTypes } from "@medusajs/types" + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + + await convertDraftOrderWorkflow(req.scope).run({ + input: { + id: req.params.id, + }, + }) + + const result = await query.graph({ + entity: "orders", + filters: { id: req.params.id }, + fields: req.queryConfig.fields, + }) + + res.status(200).json({ order: result.data[0] as HttpTypes.AdminOrder }) +} diff --git a/packages/medusa/src/api/admin/draft-orders/[id]/edit/confirm/route.ts b/packages/medusa/src/api/admin/draft-orders/[id]/edit/confirm/route.ts new file mode 100644 index 0000000000..dab182d210 --- /dev/null +++ b/packages/medusa/src/api/admin/draft-orders/[id]/edit/confirm/route.ts @@ -0,0 +1,24 @@ +import { confirmDraftOrderEditWorkflow } from "@medusajs/core-flows" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { HttpTypes } from "@medusajs/types" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + + const { result } = await confirmDraftOrderEditWorkflow(req.scope).run({ + input: { + order_id: id, + confirmed_by: req.auth_context.actor_id, + }, + }) + + res.json({ + draft_order_preview: result as unknown as HttpTypes.AdminOrderPreview, + }) +} diff --git a/packages/medusa/src/api/admin/draft-orders/[id]/edit/items/[action_id]/route.ts b/packages/medusa/src/api/admin/draft-orders/[id]/edit/items/[action_id]/route.ts new file mode 100644 index 0000000000..dcb8723374 --- /dev/null +++ b/packages/medusa/src/api/admin/draft-orders/[id]/edit/items/[action_id]/route.ts @@ -0,0 +1,44 @@ +import { + removeDraftOrderActionItemWorkflow, + updateDraftOrderActionItemWorkflow, +} from "@medusajs/core-flows" +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework" +import { HttpTypes } from "@medusajs/types" +import { AdminUpdateDraftOrderItemType } from "../../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id, action_id } = req.params + + const { result } = await updateDraftOrderActionItemWorkflow(req.scope).run({ + input: { + data: req.validatedBody, + order_id: id, + action_id, + }, + }) + + res.json({ + draft_order_preview: result as unknown as HttpTypes.AdminOrderPreview, + }) +} + +export const DELETE = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id, action_id } = req.params + + const { result } = await removeDraftOrderActionItemWorkflow(req.scope).run({ + input: { + order_id: id, + action_id, + }, + }) + + res.json({ + draft_order_preview: result as unknown as HttpTypes.AdminOrderPreview, + }) +} diff --git a/packages/medusa/src/api/admin/draft-orders/[id]/edit/items/item/[item_id]/route.ts b/packages/medusa/src/api/admin/draft-orders/[id]/edit/items/item/[item_id]/route.ts new file mode 100644 index 0000000000..1146ef7891 --- /dev/null +++ b/packages/medusa/src/api/admin/draft-orders/[id]/edit/items/item/[item_id]/route.ts @@ -0,0 +1,28 @@ +import { updateDraftOrderItemWorkflow } from "@medusajs/core-flows" +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework" +import { HttpTypes } from "@medusajs/types" +import { AdminUpdateDraftOrderItemType } from "../../../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id, item_id } = req.params + + const { result } = await updateDraftOrderItemWorkflow(req.scope).run({ + input: { + ...req.validatedBody, + order_id: id, + items: [ + { + ...req.validatedBody, + id: item_id, + }, + ], + }, + }) + + res.json({ + draft_order_preview: result as unknown as HttpTypes.AdminOrderPreview, + }) +} diff --git a/packages/medusa/src/api/admin/draft-orders/[id]/edit/items/route.ts b/packages/medusa/src/api/admin/draft-orders/[id]/edit/items/route.ts new file mode 100644 index 0000000000..6838627790 --- /dev/null +++ b/packages/medusa/src/api/admin/draft-orders/[id]/edit/items/route.ts @@ -0,0 +1,22 @@ +import { addDraftOrderItemsWorkflow } from "@medusajs/core-flows" +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework" +import { HttpTypes } from "@medusajs/types" +import { AdminAddDraftOrderItemsType } from "../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + + const { result } = await addDraftOrderItemsWorkflow(req.scope).run({ + input: { + ...req.validatedBody, + order_id: id, + }, + }) + + res.json({ + draft_order_preview: result as unknown as HttpTypes.AdminOrderPreview, + }) +} diff --git a/packages/medusa/src/api/admin/draft-orders/[id]/edit/promotions/route.ts b/packages/medusa/src/api/admin/draft-orders/[id]/edit/promotions/route.ts new file mode 100644 index 0000000000..7d68d2c2e3 --- /dev/null +++ b/packages/medusa/src/api/admin/draft-orders/[id]/edit/promotions/route.ts @@ -0,0 +1,47 @@ +import { + addDraftOrderPromotionWorkflow, + removeDraftOrderPromotionsWorkflow, +} from "@medusajs/core-flows" +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework" +import { HttpTypes } from "@medusajs/types" +import { + AdminAddDraftOrderPromotionsType, + AdminRemoveDraftOrderPromotionsType, +} from "../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + + const { result } = await addDraftOrderPromotionWorkflow(req.scope).run({ + input: { + ...req.validatedBody, + order_id: id, + }, + }) + + res.json({ + draft_order_preview: result as unknown as HttpTypes.AdminOrderPreview, + }) +} + +export const DELETE = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + + + const { result } = await removeDraftOrderPromotionsWorkflow(req.scope).run({ + input: { + ...req.validatedBody, + order_id: id, + }, + }) + + res.json({ + draft_order_preview: result as unknown as HttpTypes.AdminOrderPreview, + }) +} diff --git a/packages/medusa/src/api/admin/draft-orders/[id]/edit/request/route.ts b/packages/medusa/src/api/admin/draft-orders/[id]/edit/request/route.ts new file mode 100644 index 0000000000..4935ed4308 --- /dev/null +++ b/packages/medusa/src/api/admin/draft-orders/[id]/edit/request/route.ts @@ -0,0 +1,24 @@ +import { requestDraftOrderEditWorkflow } from "@medusajs/core-flows" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { HttpTypes } from "@medusajs/types" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + + const { result } = await requestDraftOrderEditWorkflow(req.scope).run({ + input: { + order_id: id, + requested_by: req.auth_context.actor_id, + }, + }) + + res.json({ + draft_order_preview: result as unknown as HttpTypes.AdminOrderPreview, + }) +} diff --git a/packages/medusa/src/api/admin/draft-orders/[id]/edit/route.ts b/packages/medusa/src/api/admin/draft-orders/[id]/edit/route.ts new file mode 100644 index 0000000000..f603d6bdaf --- /dev/null +++ b/packages/medusa/src/api/admin/draft-orders/[id]/edit/route.ts @@ -0,0 +1,42 @@ +import { + beginDraftOrderEditWorkflow, + cancelDraftOrderEditWorkflow, +} from "@medusajs/core-flows" +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework" +import { HttpTypes } from "@medusajs/types" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + + const { result } = await beginDraftOrderEditWorkflow(req.scope).run({ + input: { + order_id: id, + }, + }) + + res.json({ + draft_order_preview: result as unknown as HttpTypes.AdminDraftOrderPreview, + }) +} + +export const DELETE = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + + await cancelDraftOrderEditWorkflow(req.scope).run({ + input: { + order_id: id, + }, + }) + + res.status(200).json({ + id, + object: "draft-order-edit", + deleted: true, + }) +} diff --git a/packages/medusa/src/api/admin/draft-orders/[id]/edit/shipping-methods/[action_id]/route.ts b/packages/medusa/src/api/admin/draft-orders/[id]/edit/shipping-methods/[action_id]/route.ts new file mode 100644 index 0000000000..943f16153b --- /dev/null +++ b/packages/medusa/src/api/admin/draft-orders/[id]/edit/shipping-methods/[action_id]/route.ts @@ -0,0 +1,48 @@ +import { + removeDraftOrderActionShippingMethodWorkflow, + updateDraftOrderActionShippingMethodWorkflow, +} from "@medusajs/core-flows" +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework" +import { HttpTypes } from "@medusajs/types" +import { AdminUpdateDraftOrderActionShippingMethodType } from "../../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id, action_id } = req.params + + const { result } = await updateDraftOrderActionShippingMethodWorkflow( + req.scope + ).run({ + input: { + data: { ...req.validatedBody }, + order_id: id, + action_id, + }, + }) + + res.json({ + draft_order_preview: result as unknown as HttpTypes.AdminDraftOrderPreview, + }) +} + +export const DELETE = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id, action_id } = req.params + + const { result } = await removeDraftOrderActionShippingMethodWorkflow( + req.scope + ).run({ + input: { + order_id: id, + action_id, + }, + }) + + res.json({ + draft_order_preview: result as unknown as HttpTypes.AdminDraftOrderPreview, + }) +} diff --git a/packages/medusa/src/api/admin/draft-orders/[id]/edit/shipping-methods/method/[method_id]/route.ts b/packages/medusa/src/api/admin/draft-orders/[id]/edit/shipping-methods/method/[method_id]/route.ts new file mode 100644 index 0000000000..1633c6c176 --- /dev/null +++ b/packages/medusa/src/api/admin/draft-orders/[id]/edit/shipping-methods/method/[method_id]/route.ts @@ -0,0 +1,27 @@ +import { updateDraftOrderShippingMethodWorkflow } from "@medusajs/core-flows" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { HttpTypes } from "@medusajs/types" +import { AdminUpdateDraftOrderShippingMethodType } from "../../../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id, method_id } = req.params + + const { result } = await updateDraftOrderShippingMethodWorkflow( + req.scope + ).run({ + input: { + data: { shipping_method_id: method_id, ...req.validatedBody }, + order_id: id, + }, + }) + + res.json({ + draft_order_preview: result as unknown as HttpTypes.AdminDraftOrderPreview, + }) +} diff --git a/packages/medusa/src/api/admin/draft-orders/[id]/edit/shipping-methods/route.ts b/packages/medusa/src/api/admin/draft-orders/[id]/edit/shipping-methods/route.ts new file mode 100644 index 0000000000..edd0e90e31 --- /dev/null +++ b/packages/medusa/src/api/admin/draft-orders/[id]/edit/shipping-methods/route.ts @@ -0,0 +1,22 @@ +import { addDraftOrderShippingMethodsWorkflow } from "@medusajs/core-flows" +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework" +import { HttpTypes } from "@medusajs/types" +import { AdminAddDraftOrderShippingMethodType } from "../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + + const { result } = await addDraftOrderShippingMethodsWorkflow(req.scope).run({ + input: { + order_id: id, + ...req.validatedBody, + }, + }) + + res.json({ + draft_order_preview: result as unknown as HttpTypes.AdminOrderPreview, + }) +} diff --git a/packages/medusa/src/api/admin/draft-orders/[id]/route.ts b/packages/medusa/src/api/admin/draft-orders/[id]/route.ts index fdc1b7ba32..cf03681e1b 100644 --- a/packages/medusa/src/api/admin/draft-orders/[id]/route.ts +++ b/packages/medusa/src/api/admin/draft-orders/[id]/route.ts @@ -1,6 +1,6 @@ import { getOrderDetailWorkflow, - updateOrderWorkflow, + updateDraftOrderWorkflow, } from "@medusajs/core-flows" import { AuthenticatedMedusaRequest, @@ -36,7 +36,7 @@ export const POST = async ( ) => { const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - await updateOrderWorkflow(req.scope).run({ + await updateDraftOrderWorkflow(req.scope).run({ input: { ...req.validatedBody, user_id: req.auth_context.actor_id, diff --git a/packages/medusa/src/api/admin/draft-orders/middlewares.ts b/packages/medusa/src/api/admin/draft-orders/middlewares.ts index 58ba7198d6..2d3785d690 100644 --- a/packages/medusa/src/api/admin/draft-orders/middlewares.ts +++ b/packages/medusa/src/api/admin/draft-orders/middlewares.ts @@ -5,10 +5,18 @@ import { import { MiddlewareRoute } from "@medusajs/framework/http" import * as QueryConfig from "./query-config" import { + AdminAddDraftOrderItems, + AdminAddDraftOrderPromotions, + AdminAddDraftOrderShippingMethod, AdminCreateDraftOrder, AdminGetDraftOrderParams, AdminGetDraftOrdersParams, + AdminRemoveDraftOrderPromotions, AdminUpdateDraftOrder, + AdminUpdateDraftOrderActionItem, + AdminUpdateDraftOrderActionShippingMethod, + AdminUpdateDraftOrderItem, + AdminUpdateDraftOrderShippingMethod, } from "./validators" export const adminDraftOrderRoutesMiddlewares: MiddlewareRoute[] = [ @@ -54,4 +62,58 @@ export const adminDraftOrderRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["POST"], + matcher: "/admin/draft-orders/:id/convert-to-order", + middlewares: [ + validateAndTransformQuery( + AdminGetDraftOrderParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, + { + method: ["POST"], + matcher: "/admin/draft-orders/:id/edit/items", + middlewares: [validateAndTransformBody(AdminAddDraftOrderItems)], + }, + { + method: ["POST"], + matcher: "/admin/draft-orders/:id/edit/items/item/:item_id", + middlewares: [validateAndTransformBody(AdminUpdateDraftOrderItem)], + }, + { + method: ["POST"], + matcher: "/admin/draft-orders/:id/edit/items/:action_id", + middlewares: [validateAndTransformBody(AdminUpdateDraftOrderActionItem)], + }, + { + method: ["POST"], + matcher: "/admin/draft-orders/:id/edit/promotions", + middlewares: [validateAndTransformBody(AdminAddDraftOrderPromotions)], + }, + { + method: ["DELETE"], + matcher: "/admin/draft-orders/:id/edit/promotions", + middlewares: [validateAndTransformBody(AdminRemoveDraftOrderPromotions)], + }, + { + method: ["POST"], + matcher: "/admin/draft-orders/:id/edit/shipping-methods", + middlewares: [validateAndTransformBody(AdminAddDraftOrderShippingMethod)], + }, + { + method: ["POST"], + matcher: "/admin/draft-orders/:id/edit/shipping-methods/method/:method_id", + middlewares: [ + validateAndTransformBody(AdminUpdateDraftOrderShippingMethod), + ], + }, + { + method: ["POST"], + matcher: "/admin/draft-orders/:id/edit/shipping-methods/:action_id", + middlewares: [ + validateAndTransformBody(AdminUpdateDraftOrderActionShippingMethod), + ], + }, ] diff --git a/packages/medusa/src/api/admin/draft-orders/validators.ts b/packages/medusa/src/api/admin/draft-orders/validators.ts index 9799ded9e1..b02e140d33 100644 --- a/packages/medusa/src/api/admin/draft-orders/validators.ts +++ b/packages/medusa/src/api/admin/draft-orders/validators.ts @@ -23,6 +23,7 @@ const AdminGetDraftOrdersParamsFields = z.object({ q: z.string().optional(), region_id: z.union([z.string(), z.array(z.string())]).optional(), sales_channel_id: z.array(z.string()).optional(), + customer_id: z.union([z.string(), z.array(z.string())]).optional(), }) export type AdminGetDraftOrdersParamsType = z.infer< @@ -105,7 +106,101 @@ export const AdminCreateDraftOrder = WithAdditionalData( export type AdminUpdateDraftOrderType = z.infer export const AdminUpdateDraftOrder = z.object({ email: z.string().optional(), + customer_id: z.string().optional(), + sales_channel_id: z.string().optional(), shipping_address: AddressPayload.optional(), billing_address: AddressPayload.optional(), metadata: z.record(z.unknown()).nullish(), }) + +export type AdminAddDraftOrderPromotionsType = z.infer< + typeof AdminAddDraftOrderPromotions +> +export const AdminAddDraftOrderPromotions = z.object({ + promo_codes: z.array(z.string()), +}) + +export type AdminRemoveDraftOrderPromotionsType = z.infer< + typeof AdminRemoveDraftOrderPromotions +> +export const AdminRemoveDraftOrderPromotions = z.object({ + promo_codes: z.array(z.string()), +}) + +export type AdminUpdateDraftOrderItemType = z.infer< + typeof AdminUpdateDraftOrderItem +> +export const AdminUpdateDraftOrderItem = z.object({ + quantity: z.number(), + unit_price: z.number().nullish(), + compare_at_unit_price: z.number().nullish(), + internal_note: z.string().optional(), +}) + +export type AdminUpdateDraftOrderActionItemType = z.infer< + typeof AdminUpdateDraftOrderActionItem +> +export const AdminUpdateDraftOrderActionItem = z.object({ + quantity: z.number(), + unit_price: z.number().nullish(), + compare_at_unit_price: z.number().nullish(), + internal_note: z.string().optional(), +}) + +export const AdminAddDraftOrderItems = z.object({ + items: z + .array( + z.object({ + variant_id: z.string().optional(), + title: z.string().optional(), + quantity: z.number(), + unit_price: z.number().nullish(), + compare_at_unit_price: z.number().nullish(), + internal_note: z.string().nullish(), + allow_backorder: z.boolean().optional(), + metadata: z.record(z.unknown()).optional(), + }) + ) + .refine( + (items) => { + return items.every((item) => item.variant_id || item.title) + }, + { + message: "Items must have either a variant_id or a title", + } + ), +}) +export type AdminAddDraftOrderItemsType = z.infer< + typeof AdminAddDraftOrderItems +> + +export const AdminAddDraftOrderShippingMethod = z.object({ + shipping_option_id: z.string(), + custom_amount: z.number().optional(), + description: z.string().optional(), + internal_note: z.string().optional(), + metadata: z.record(z.unknown()).optional(), +}) +export type AdminAddDraftOrderShippingMethodType = z.infer< + typeof AdminAddDraftOrderShippingMethod +> + +export const AdminUpdateDraftOrderActionShippingMethod = z.object({ + shipping_option_id: z.string(), + custom_amount: z.number().nullish(), + description: z.string().nullish(), + internal_note: z.string().nullish(), + metadata: z.record(z.unknown()).nullish(), +}) +export type AdminUpdateDraftOrderActionShippingMethodType = z.infer< + typeof AdminUpdateDraftOrderActionShippingMethod +> + +export const AdminUpdateDraftOrderShippingMethod = z.object({ + shipping_option_id: z.string().optional(), + custom_amount: z.number().optional(), + internal_note: z.string().nullish(), +}) +export type AdminUpdateDraftOrderShippingMethodType = z.infer< + typeof AdminUpdateDraftOrderShippingMethod +> diff --git a/packages/medusa/src/api/admin/order-edits/[id]/items/item/[item_id]/route.ts b/packages/medusa/src/api/admin/order-edits/[id]/items/item/[item_id]/route.ts index 7a8087205c..f42c292f2b 100644 --- a/packages/medusa/src/api/admin/order-edits/[id]/items/item/[item_id]/route.ts +++ b/packages/medusa/src/api/admin/order-edits/[id]/items/item/[item_id]/route.ts @@ -1,9 +1,9 @@ import { orderEditUpdateItemQuantityWorkflow } from "@medusajs/core-flows" -import { HttpTypes } from "@medusajs/framework/types" import { AuthenticatedMedusaRequest, MedusaResponse, } from "@medusajs/framework/http" +import { HttpTypes } from "@medusajs/framework/types" import { AdminPostOrderEditsUpdateItemQuantityReqSchemaType } from "../../../../validators" export const POST = async ( diff --git a/packages/medusa/src/api/admin/promotions/validators.ts b/packages/medusa/src/api/admin/promotions/validators.ts index 09067929db..28057c24b6 100644 --- a/packages/medusa/src/api/admin/promotions/validators.ts +++ b/packages/medusa/src/api/admin/promotions/validators.ts @@ -23,7 +23,9 @@ export const AdminGetPromotionParams = createSelectParams() export const AdminGetPromotionsParamsFields = z.object({ q: z.string().optional(), - code: z.union([z.string(), z.array(z.string())]).optional(), + code: z + .union([z.string(), z.array(z.string()), createOperatorMap()]) + .optional(), campaign_id: z.union([z.string(), z.array(z.string())]).optional(), application_method: z .object({ diff --git a/packages/modules/order/src/utils/actions/index.ts b/packages/modules/order/src/utils/actions/index.ts index 204d8c5cb5..fe9b0ddaa9 100644 --- a/packages/modules/order/src/utils/actions/index.ts +++ b/packages/modules/order/src/utils/actions/index.ts @@ -7,6 +7,8 @@ export * from "./fulfill-item" export * from "./item-add" export * from "./item-remove" export * from "./item-update" +export * from "./promotion-add" +export * from "./promotion-remove" export * from "./receive-damaged-return-item" export * from "./receive-return-item" export * from "./reinstate-item" @@ -14,5 +16,6 @@ export * from "./return-item" export * from "./ship-item" export * from "./shipping-add" export * from "./shipping-remove" +export * from "./shipping-update" export * from "./transfer-customer" export * from "./write-off-item" diff --git a/packages/modules/order/src/utils/actions/promotion-add.ts b/packages/modules/order/src/utils/actions/promotion-add.ts new file mode 100644 index 0000000000..23f8f5de7f --- /dev/null +++ b/packages/modules/order/src/utils/actions/promotion-add.ts @@ -0,0 +1,16 @@ +import { ChangeActionType, MedusaError } from "@medusajs/framework/utils" +import { OrderChangeProcessing } from "../calculate-order-change" + +OrderChangeProcessing.registerActionType(ChangeActionType.PROMOTION_ADD, { + operation({ action, currentOrder, options }) { + // no-op + }, + validate({ action }) { + if (!action.reference_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Reference ID is required." + ) + } + }, +}) diff --git a/packages/modules/order/src/utils/actions/promotion-remove.ts b/packages/modules/order/src/utils/actions/promotion-remove.ts new file mode 100644 index 0000000000..f8b28ffc70 --- /dev/null +++ b/packages/modules/order/src/utils/actions/promotion-remove.ts @@ -0,0 +1,16 @@ +import { ChangeActionType, MedusaError } from "@medusajs/framework/utils" +import { OrderChangeProcessing } from "../calculate-order-change" + +OrderChangeProcessing.registerActionType(ChangeActionType.PROMOTION_REMOVE, { + operation({ action, currentOrder, options }) { + // no-op + }, + validate({ action }) { + if (!action.reference_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Reference ID is required." + ) + } + }, +}) diff --git a/packages/modules/order/src/utils/actions/shipping-update.ts b/packages/modules/order/src/utils/actions/shipping-update.ts new file mode 100644 index 0000000000..6ab80669b4 --- /dev/null +++ b/packages/modules/order/src/utils/actions/shipping-update.ts @@ -0,0 +1,16 @@ +import { ChangeActionType, MedusaError } from "@medusajs/framework/utils" +import { OrderChangeProcessing } from "../calculate-order-change" + +OrderChangeProcessing.registerActionType(ChangeActionType.SHIPPING_UPDATE, { + operation({ action, currentOrder, options }) { + // no-op + }, + validate({ action }) { + if (!action.reference_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Reference ID is required." + ) + } + }, +})