From 62ef82f4979f74778d148a6142a83f10124be59c Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Mon, 27 May 2024 09:49:33 -0300 Subject: [PATCH] feat(order): fulfillment workflow (#7385) FIXES: CORE-2162 CORE-2167 CORE-2041 --- .../workflows/create-fulfillment.spec.ts | 397 ++++++++++++++++ .../order/workflows/create-return.spec.ts | 14 +- .../order/workflows/create-shipment.spec.ts | 423 ++++++++++++++++++ .../steps/create-return-fulfillment.ts | 29 ++ .../core-flows/src/fulfillment/steps/index.ts | 5 +- .../workflows/create-fulfillment.ts | 22 +- .../workflows/create-return-fulfillment.ts | 16 + .../src/fulfillment/workflows/index.ts | 1 + .../steps/adjust-inventory-levels.ts | 47 ++ .../core-flows/src/inventory/steps/index.ts | 11 +- .../src/order/steps/create-return.ts | 12 +- .../core/core-flows/src/order/steps/index.ts | 2 + .../src/order/steps/register-fulfillment.ts | 32 ++ .../src/order/steps/register-shipment.ts | 29 ++ .../src/order/utils/order-validation.ts | 32 ++ .../src/order/workflows/create-fulfillment.ts | 278 ++++++++++++ .../src/order/workflows/create-return.ts | 77 ++-- .../src/order/workflows/create-shipment.ts | 109 +++++ .../core-flows/src/order/workflows/index.ts | 2 + .../src/fulfillment/mutations/fulfillment.ts | 10 +- .../core/types/src/fulfillment/provider.ts | 2 +- .../inventory/mutations/inventory-level.ts | 16 + .../core/types/src/inventory/service-next.ts | 14 +- packages/core/types/src/order/mutations.ts | 2 +- .../fulfillment/create-fulfillment.ts | 7 +- .../workflow/fulfillment/create-shipment.ts | 2 +- .../src/workflow/order/create-fulfillment.ts | 15 + .../src/workflow/order/create-shipment.ts | 17 + .../core/types/src/workflow/order/index.ts | 2 + .../api/admin/orders/[id]/archive/route.ts | 7 +- .../src/api/admin/orders/[id]/cancel/route.ts | 7 +- .../api/admin/orders/[id]/complete/route.ts | 7 +- .../[fulfillment_id]/cancel/route.ts | 9 +- .../[fulfillment_id]/shipment/route.ts | 44 ++ .../admin/orders/[id]/fulfillments/route.ts | 42 ++ .../medusa/src/api/admin/orders/[id]/route.ts | 9 +- .../api/admin/orders/fulfillments/route.ts | 25 -- .../src/api/admin/orders/middlewares.ts | 3 +- .../medusa/src/api/admin/orders/validators.ts | 33 ++ .../src/services/fulfillment-provider.ts | 9 +- .../inventory-next/src/services/inventory.ts | 78 +++- .../__tests__/create-order.ts | 1 - .../src/services/order-module-service.ts | 26 +- 43 files changed, 1730 insertions(+), 195 deletions(-) create mode 100644 integration-tests/modules/__tests__/order/workflows/create-fulfillment.spec.ts create mode 100644 integration-tests/modules/__tests__/order/workflows/create-shipment.spec.ts create mode 100644 packages/core/core-flows/src/fulfillment/steps/create-return-fulfillment.ts create mode 100644 packages/core/core-flows/src/fulfillment/workflows/create-return-fulfillment.ts create mode 100644 packages/core/core-flows/src/inventory/steps/adjust-inventory-levels.ts create mode 100644 packages/core/core-flows/src/order/steps/register-fulfillment.ts create mode 100644 packages/core/core-flows/src/order/steps/register-shipment.ts create mode 100644 packages/core/core-flows/src/order/utils/order-validation.ts create mode 100644 packages/core/core-flows/src/order/workflows/create-fulfillment.ts create mode 100644 packages/core/core-flows/src/order/workflows/create-shipment.ts create mode 100644 packages/core/types/src/workflow/order/create-fulfillment.ts create mode 100644 packages/core/types/src/workflow/order/create-shipment.ts rename packages/medusa/src/api/admin/orders/{ => [id]}/fulfillments/[fulfillment_id]/cancel/route.ts (68%) create mode 100644 packages/medusa/src/api/admin/orders/[id]/fulfillments/[fulfillment_id]/shipment/route.ts create mode 100644 packages/medusa/src/api/admin/orders/[id]/fulfillments/route.ts delete mode 100644 packages/medusa/src/api/admin/orders/fulfillments/route.ts diff --git a/integration-tests/modules/__tests__/order/workflows/create-fulfillment.spec.ts b/integration-tests/modules/__tests__/order/workflows/create-fulfillment.spec.ts new file mode 100644 index 0000000000..092010fbfa --- /dev/null +++ b/integration-tests/modules/__tests__/order/workflows/create-fulfillment.spec.ts @@ -0,0 +1,397 @@ +import { + createOrderFulfillmentWorkflow, + createShippingOptionsWorkflow, +} from "@medusajs/core-flows" +import { ModuleRegistrationName, Modules } from "@medusajs/modules-sdk" +import { + FulfillmentWorkflow, + IOrderModuleService, + IRegionModuleService, + IStockLocationServiceNext, + OrderWorkflow, + ProductDTO, + RegionDTO, + ShippingOptionDTO, + StockLocationDTO, +} from "@medusajs/types" +import { + ContainerRegistrationKeys, + RuleOperator, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { medusaIntegrationTestRunner } from "medusa-test-utils/dist" + +jest.setTimeout(500000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const providerId = "manual_test-provider" +let inventoryItem + +async function prepareDataFixtures({ container }) { + const fulfillmentService = container.resolve( + ModuleRegistrationName.FULFILLMENT + ) + const salesChannelService = container.resolve( + ModuleRegistrationName.SALES_CHANNEL + ) + const stockLocationModule: IStockLocationServiceNext = container.resolve( + ModuleRegistrationName.STOCK_LOCATION + ) + const productModule = container.resolve(ModuleRegistrationName.PRODUCT) + const inventoryModule = container.resolve(ModuleRegistrationName.INVENTORY) + + const shippingProfile = await fulfillmentService.createShippingProfiles({ + name: "test", + type: "default", + }) + + const fulfillmentSet = await fulfillmentService.create({ + name: "Test fulfillment set", + type: "manual_test", + }) + + const serviceZone = await fulfillmentService.createServiceZones({ + name: "Test service zone", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: "country", + country_code: "US", + }, + ], + }) + + const regionService = container.resolve( + ModuleRegistrationName.REGION + ) as IRegionModuleService + + const [region] = await regionService.create([ + { + name: "Test region", + currency_code: "eur", + countries: ["fr"], + }, + ]) + + const salesChannel = await salesChannelService.create({ + name: "Webshop", + }) + + const location: StockLocationDTO = await stockLocationModule.create({ + name: "Warehouse", + address: { + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + phone: "12345", + }, + }) + + const [product] = await productModule.create([ + { + title: "Test product", + variants: [ + { + title: "Test variant", + sku: "test-variant", + }, + ], + }, + ]) + + inventoryItem = await inventoryModule.create({ + sku: "inv-1234", + }) + + await inventoryModule.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + stocked_quantity: 2, + reserved_quantity: 0, + }, + ]) + + const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + + await remoteLink.create([ + { + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: fulfillmentSet.id, + }, + }, + { + [Modules.SALES_CHANNEL]: { + sales_channel_id: salesChannel.id, + }, + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + }, + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, + ]) + + const shippingOptionData: FulfillmentWorkflow.CreateShippingOptionsWorkflowInput = + { + name: "Return shipping option", + price_type: "flat", + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + provider_id: providerId, + type: { + code: "manual-type", + label: "Manual Type", + description: "Manual Type Description", + }, + prices: [ + { + currency_code: "usd", + amount: 10, + }, + { + region_id: region.id, + amount: 100, + }, + ], + rules: [ + { + attribute: "is_return", + operator: RuleOperator.EQ, + value: '"true"', + }, + ], + } + + const { result } = await createShippingOptionsWorkflow(container).run({ + input: [shippingOptionData], + }) + + const remoteQueryObject = remoteQueryObjectFromString({ + entryPoint: "shipping_option", + variables: { + id: result[0].id, + }, + fields: [ + "id", + "name", + "price_type", + "service_zone_id", + "shipping_profile_id", + "provider_id", + "data", + "metadata", + "type.*", + "created_at", + "updated_at", + "deleted_at", + "shipping_option_type_id", + "prices.*", + ], + }) + + const remoteQuery = container.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const [createdShippingOption] = await remoteQuery(remoteQueryObject) + return { + shippingOption: createdShippingOption, + region, + salesChannel, + location, + product, + } +} + +async function createOrderFixture({ container, product, location }) { + const orderService: IOrderModuleService = container.resolve( + ModuleRegistrationName.ORDER + ) + let order = await orderService.create({ + region_id: "test_region_idclear", + email: "foo@bar.com", + items: [ + { + title: "Custom Item 2", + variant_sku: product.variants[0].sku, + variant_title: product.variants[0].title, + quantity: 1, + unit_price: 50, + adjustments: [ + { + code: "VIP_25 ETH", + amount: "0.000000000000000005", + description: "VIP discount", + promotion_id: "prom_123", + provider_id: "coupon_kings", + }, + ], + } as any, + ], + transactions: [ + { + amount: 50, + currency_code: "usd", + }, + ], + sales_channel_id: "test", + shipping_address: { + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + phone: "12345", + }, + billing_address: { + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + }, + shipping_methods: [ + { + name: "Test shipping method", + amount: 10, + data: {}, + tax_lines: [ + { + description: "shipping Tax 1", + tax_rate_id: "tax_usa_shipping", + code: "code", + rate: 10, + }, + ], + adjustments: [ + { + code: "VIP_10", + amount: 1, + description: "VIP discount", + promotion_id: "prom_123", + }, + ], + }, + ], + currency_code: "usd", + customer_id: "joe", + }) + + const inventoryModule = container.resolve(ModuleRegistrationName.INVENTORY) + const reservation = await inventoryModule.createReservationItems([ + { + line_item_id: order.items![0].id, + inventory_item_id: inventoryItem.id, + location_id: location.id, + quantity: order.items![0].quantity, + }, + ]) + + order = await orderService.retrieve(order.id, { + relations: ["items"], + }) + + return order +} + +medusaIntegrationTestRunner({ + env, + testSuite: ({ getContainer }) => { + let container + + beforeAll(() => { + container = getContainer() + }) + + describe("Create order fulfillment workflow", () => { + let shippingOption: ShippingOptionDTO + let region: RegionDTO + let location: StockLocationDTO + let product: ProductDTO + + let orderService: IOrderModuleService + + beforeEach(async () => { + const fixtures = await prepareDataFixtures({ + container, + }) + + shippingOption = fixtures.shippingOption + region = fixtures.region + location = fixtures.location + product = fixtures.product + + orderService = container.resolve(ModuleRegistrationName.ORDER) + }) + + it("should create a order fulfillment", async () => { + const order = await createOrderFixture({ container, product, location }) + const createReturnOrderData: OrderWorkflow.CreateOrderFulfillmentWorkflowInput = + { + order_id: order.id, + created_by: "user_1", + items: [ + { + id: order.items![0].id, + quantity: 1, + }, + ], + no_notification: false, + location_id: undefined, + } + + await createOrderFulfillmentWorkflow(container).run({ + input: createReturnOrderData, + }) + + const remoteQuery = container.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) + const remoteQueryObject = remoteQueryObjectFromString({ + entryPoint: "order", + variables: { + id: order.id, + }, + fields: [ + "*", + "items.*", + "shipping_methods.*", + "total", + "item_total", + "fulfillments.*", + ], + }) + + const [orderFulfill] = await remoteQuery(remoteQueryObject) + + expect(orderFulfill.fulfillments).toHaveLength(1) + expect(orderFulfill.items[0].detail.fulfilled_quantity).toEqual(1) + + const inventoryModule = container.resolve( + ModuleRegistrationName.INVENTORY + ) + const reservation = await inventoryModule.listReservationItems({ + line_item_id: order.items![0].id, + }) + expect(reservation).toHaveLength(0) + + const stockAvailability = await inventoryModule.retrieveStockedQuantity( + inventoryItem.id, + [location.id] + ) + expect(stockAvailability).toEqual(1) + }) + }) + }, +}) diff --git a/integration-tests/modules/__tests__/order/workflows/create-return.spec.ts b/integration-tests/modules/__tests__/order/workflows/create-return.spec.ts index db83bc3cc0..3c4901ccaa 100644 --- a/integration-tests/modules/__tests__/order/workflows/create-return.spec.ts +++ b/integration-tests/modules/__tests__/order/workflows/create-return.spec.ts @@ -1,3 +1,7 @@ +import { + createReturnOrderWorkflow, + createShippingOptionsWorkflow, +} from "@medusajs/core-flows" import { ModuleRegistrationName, Modules, @@ -15,16 +19,12 @@ import { ShippingOptionDTO, StockLocationDTO, } from "@medusajs/types" -import { medusaIntegrationTestRunner } from "medusa-test-utils/dist" -import { - createReturnOrderWorkflow, - createShippingOptionsWorkflow, -} from "@medusajs/core-flows" import { ContainerRegistrationKeys, - remoteQueryObjectFromString, RuleOperator, + remoteQueryObjectFromString, } from "@medusajs/utils" +import { medusaIntegrationTestRunner } from "medusa-test-utils/dist" jest.setTimeout(500000) @@ -394,7 +394,7 @@ medusaIntegrationTestRunner({ await createReturnOrderWorkflow(container).run({ input: createReturnOrderData, - throwOnError: false, + throwOnError: true, }) const remoteQuery = container.resolve( diff --git a/integration-tests/modules/__tests__/order/workflows/create-shipment.spec.ts b/integration-tests/modules/__tests__/order/workflows/create-shipment.spec.ts new file mode 100644 index 0000000000..87e99fb282 --- /dev/null +++ b/integration-tests/modules/__tests__/order/workflows/create-shipment.spec.ts @@ -0,0 +1,423 @@ +import { + createOrderFulfillmentWorkflow, + createOrderShipmentWorkflow, + createShippingOptionsWorkflow, +} from "@medusajs/core-flows" +import { ModuleRegistrationName, Modules } from "@medusajs/modules-sdk" +import { + FulfillmentWorkflow, + IOrderModuleService, + IRegionModuleService, + IStockLocationServiceNext, + OrderWorkflow, + ProductDTO, + RegionDTO, + ShippingOptionDTO, + StockLocationDTO, +} from "@medusajs/types" +import { + ContainerRegistrationKeys, + RuleOperator, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { medusaIntegrationTestRunner } from "medusa-test-utils/dist" + +jest.setTimeout(500000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const providerId = "manual_test-provider" +let inventoryItem + +async function prepareDataFixtures({ container }) { + const fulfillmentService = container.resolve( + ModuleRegistrationName.FULFILLMENT + ) + const salesChannelService = container.resolve( + ModuleRegistrationName.SALES_CHANNEL + ) + const stockLocationModule: IStockLocationServiceNext = container.resolve( + ModuleRegistrationName.STOCK_LOCATION + ) + const productModule = container.resolve(ModuleRegistrationName.PRODUCT) + const inventoryModule = container.resolve(ModuleRegistrationName.INVENTORY) + + const shippingProfile = await fulfillmentService.createShippingProfiles({ + name: "test", + type: "default", + }) + + const fulfillmentSet = await fulfillmentService.create({ + name: "Test fulfillment set", + type: "manual_test", + }) + + const serviceZone = await fulfillmentService.createServiceZones({ + name: "Test service zone", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: "country", + country_code: "US", + }, + ], + }) + + const regionService = container.resolve( + ModuleRegistrationName.REGION + ) as IRegionModuleService + + const [region] = await regionService.create([ + { + name: "Test region", + currency_code: "eur", + countries: ["fr"], + }, + ]) + + const salesChannel = await salesChannelService.create({ + name: "Webshop", + }) + + const location: StockLocationDTO = await stockLocationModule.create({ + name: "Warehouse", + address: { + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + phone: "12345", + }, + }) + + const [product] = await productModule.create([ + { + title: "Test product", + variants: [ + { + title: "Test variant", + sku: "test-variant", + }, + ], + }, + ]) + + inventoryItem = await inventoryModule.create({ + sku: "inv-1234", + }) + + await inventoryModule.createInventoryLevels([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + stocked_quantity: 2, + reserved_quantity: 0, + }, + ]) + + const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK) + + await remoteLink.create([ + { + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: fulfillmentSet.id, + }, + }, + { + [Modules.SALES_CHANNEL]: { + sales_channel_id: salesChannel.id, + }, + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + }, + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, + ]) + + const shippingOptionData: FulfillmentWorkflow.CreateShippingOptionsWorkflowInput = + { + name: "Return shipping option", + price_type: "flat", + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + provider_id: providerId, + type: { + code: "manual-type", + label: "Manual Type", + description: "Manual Type Description", + }, + prices: [ + { + currency_code: "usd", + amount: 10, + }, + { + region_id: region.id, + amount: 100, + }, + ], + rules: [ + { + attribute: "is_return", + operator: RuleOperator.EQ, + value: '"true"', + }, + ], + } + + const { result } = await createShippingOptionsWorkflow(container).run({ + input: [shippingOptionData], + }) + + const remoteQueryObject = remoteQueryObjectFromString({ + entryPoint: "shipping_option", + variables: { + id: result[0].id, + }, + fields: [ + "id", + "name", + "price_type", + "service_zone_id", + "shipping_profile_id", + "provider_id", + "data", + "metadata", + "type.*", + "created_at", + "updated_at", + "deleted_at", + "shipping_option_type_id", + "prices.*", + ], + }) + + const remoteQuery = container.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const [createdShippingOption] = await remoteQuery(remoteQueryObject) + return { + shippingOption: createdShippingOption, + region, + salesChannel, + location, + product, + } +} + +async function createOrderFixture({ container, product, location }) { + const orderService: IOrderModuleService = container.resolve( + ModuleRegistrationName.ORDER + ) + let order = await orderService.create({ + region_id: "test_region_idclear", + email: "foo@bar.com", + items: [ + { + title: "Custom Item 2", + variant_sku: product.variants[0].sku, + variant_title: product.variants[0].title, + quantity: 1, + unit_price: 50, + adjustments: [ + { + code: "VIP_25 ETH", + amount: "0.000000000000000005", + description: "VIP discount", + promotion_id: "prom_123", + provider_id: "coupon_kings", + }, + ], + } as any, + ], + transactions: [ + { + amount: 50, + currency_code: "usd", + }, + ], + sales_channel_id: "test", + shipping_address: { + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + phone: "12345", + }, + billing_address: { + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + }, + shipping_methods: [ + { + name: "Test shipping method", + amount: 10, + data: {}, + tax_lines: [ + { + description: "shipping Tax 1", + tax_rate_id: "tax_usa_shipping", + code: "code", + rate: 10, + }, + ], + adjustments: [ + { + code: "VIP_10", + amount: 1, + description: "VIP discount", + promotion_id: "prom_123", + }, + ], + }, + ], + currency_code: "usd", + customer_id: "joe", + }) + + const inventoryModule = container.resolve(ModuleRegistrationName.INVENTORY) + const reservation = await inventoryModule.createReservationItems([ + { + line_item_id: order.items![0].id, + inventory_item_id: inventoryItem.id, + location_id: location.id, + quantity: order.items![0].quantity, + }, + ]) + + order = await orderService.retrieve(order.id, { + relations: ["items"], + }) + + return order +} + +medusaIntegrationTestRunner({ + env, + testSuite: ({ getContainer }) => { + let container + + beforeAll(() => { + container = getContainer() + }) + + describe("Create order fulfillment workflow", () => { + let shippingOption: ShippingOptionDTO + let region: RegionDTO + let location: StockLocationDTO + let product: ProductDTO + + let orderService: IOrderModuleService + + beforeEach(async () => { + const fixtures = await prepareDataFixtures({ + container, + }) + + shippingOption = fixtures.shippingOption + region = fixtures.region + location = fixtures.location + product = fixtures.product + + orderService = container.resolve(ModuleRegistrationName.ORDER) + }) + + it("should create a order fulfillment", async () => { + const order = await createOrderFixture({ container, product, location }) + const createReturnOrderData: OrderWorkflow.CreateOrderFulfillmentWorkflowInput = + { + order_id: order.id, + created_by: "user_1", + items: [ + { + id: order.items![0].id, + quantity: 1, + }, + ], + no_notification: false, + location_id: undefined, + } + + const { result: fulfillment } = await createOrderFulfillmentWorkflow( + container + ).run({ + input: createReturnOrderData, + }) + + const createShipmentData: OrderWorkflow.CreateOrderShipmentWorkflowInput = + { + order_id: order.id, + fulfillment_id: fulfillment.id, + items: [ + { + id: order.items![0].id, + quantity: 1, + }, + ], + labels: [ + { + tracking_number: "123456", + tracking_url: "abcdef-xpress.com/track/123456", + label_url: "http://abcdef-xpress.com/label/123456", + }, + ], + } + + await createOrderShipmentWorkflow(container).run({ + input: createShipmentData, + }) + + const remoteQuery = container.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) + const remoteQueryObject = remoteQueryObjectFromString({ + entryPoint: "order", + variables: { + id: order.id, + }, + fields: [ + "*", + "items.*", + "shipping_methods.*", + "total", + "item_total", + "fulfillments.*", + ], + }) + + const [orderFulfill] = await remoteQuery(remoteQueryObject) + + expect(orderFulfill.fulfillments).toHaveLength(1) + expect(orderFulfill.items[0].detail.fulfilled_quantity).toEqual(1) + + const inventoryModule = container.resolve( + ModuleRegistrationName.INVENTORY + ) + const reservation = await inventoryModule.listReservationItems({ + line_item_id: order.items![0].id, + }) + expect(reservation).toHaveLength(0) + + const stockAvailability = await inventoryModule.retrieveStockedQuantity( + inventoryItem.id, + [location.id] + ) + expect(stockAvailability).toEqual(1) + }) + }) + }, +}) diff --git a/packages/core/core-flows/src/fulfillment/steps/create-return-fulfillment.ts b/packages/core/core-flows/src/fulfillment/steps/create-return-fulfillment.ts new file mode 100644 index 0000000000..c478bdb59d --- /dev/null +++ b/packages/core/core-flows/src/fulfillment/steps/create-return-fulfillment.ts @@ -0,0 +1,29 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { FulfillmentTypes, IFulfillmentModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +export const createReturnFulfillmentStepId = "create-return-fulfillment" +export const createReturnFulfillmentStep = createStep( + createReturnFulfillmentStepId, + async (data: FulfillmentTypes.CreateFulfillmentDTO, { container }) => { + const service = container.resolve( + ModuleRegistrationName.FULFILLMENT + ) + + const fulfillment = await service.createReturnFulfillment(data) + + return new StepResponse(fulfillment, fulfillment.id) + }, + async (id, { container }) => { + if (!id) { + return + } + + const service = container.resolve( + ModuleRegistrationName.FULFILLMENT + ) + + // await service.cancelReturnFulfillment(id) // TODO: Implement cancelReturnFulfillment + await service.cancelFulfillment(id) + } +) diff --git a/packages/core/core-flows/src/fulfillment/steps/index.ts b/packages/core/core-flows/src/fulfillment/steps/index.ts index 270e5c7b45..29161d709a 100644 --- a/packages/core/core-flows/src/fulfillment/steps/index.ts +++ b/packages/core/core-flows/src/fulfillment/steps/index.ts @@ -1,14 +1,15 @@ -export * from "./create-shipping-option-rules" export * from "./add-shipping-options-prices" export * from "./cancel-fulfillment" export * from "./create-fulfillment" export * from "./create-fulfillment-set" +export * from "./create-return-fulfillment" export * from "./create-service-zones" +export * from "./create-shipping-option-rules" export * from "./create-shipping-profiles" export * from "./delete-fulfillment-sets" export * from "./delete-service-zones" -export * from "./delete-shipping-options" export * from "./delete-shipping-option-rules" +export * from "./delete-shipping-options" export * from "./set-shipping-options-prices" export * from "./update-fulfillment" export * from "./upsert-shipping-options" diff --git a/packages/core/core-flows/src/fulfillment/workflows/create-fulfillment.ts b/packages/core/core-flows/src/fulfillment/workflows/create-fulfillment.ts index 47951d64a5..543e7b4509 100644 --- a/packages/core/core-flows/src/fulfillment/workflows/create-fulfillment.ts +++ b/packages/core/core-flows/src/fulfillment/workflows/create-fulfillment.ts @@ -1,12 +1,6 @@ import { FulfillmentDTO, FulfillmentWorkflow } from "@medusajs/types" -import { - WorkflowData, - createWorkflow, - transform, -} from "@medusajs/workflows-sdk" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" import { createFulfillmentStep } from "../steps" -import { Modules } from "@medusajs/utils" -import { createLinkStep } from "../../common" export const createFulfillmentWorkflowId = "create-fulfillment-workflow" export const createFulfillmentWorkflow = createWorkflow( @@ -16,20 +10,6 @@ export const createFulfillmentWorkflow = createWorkflow( ): WorkflowData => { const fulfillment = createFulfillmentStep(input) - const link = transform( - { order_id: input.order_id, fulfillment }, - (data) => { - return [ - { - [Modules.ORDER]: { order_id: data.order_id }, - [Modules.FULFILLMENT]: { fulfillment_id: data.fulfillment.id }, - }, - ] - } - ) - - createLinkStep(link) - return fulfillment } ) diff --git a/packages/core/core-flows/src/fulfillment/workflows/create-return-fulfillment.ts b/packages/core/core-flows/src/fulfillment/workflows/create-return-fulfillment.ts new file mode 100644 index 0000000000..a0c4e56d11 --- /dev/null +++ b/packages/core/core-flows/src/fulfillment/workflows/create-return-fulfillment.ts @@ -0,0 +1,16 @@ +import { FulfillmentDTO, FulfillmentWorkflow } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { createReturnFulfillmentStep } from "../steps" + +export const createReturnFulfillmentWorkflowId = + "create-return-fulfillment-workflow" +export const createReturnFulfillmentWorkflow = createWorkflow( + createReturnFulfillmentWorkflowId, + ( + input: WorkflowData + ): WorkflowData => { + const fulfillment = createReturnFulfillmentStep(input) + + return fulfillment + } +) diff --git a/packages/core/core-flows/src/fulfillment/workflows/index.ts b/packages/core/core-flows/src/fulfillment/workflows/index.ts index 33dff39285..7148ccd64c 100644 --- a/packages/core/core-flows/src/fulfillment/workflows/index.ts +++ b/packages/core/core-flows/src/fulfillment/workflows/index.ts @@ -1,6 +1,7 @@ export * from "./batch-shipping-option-rules" export * from "./cancel-fulfillment" export * from "./create-fulfillment" +export * from "./create-return-fulfillment" export * from "./create-service-zones" export * from "./create-shipment" export * from "./create-shipping-options" diff --git a/packages/core/core-flows/src/inventory/steps/adjust-inventory-levels.ts b/packages/core/core-flows/src/inventory/steps/adjust-inventory-levels.ts new file mode 100644 index 0000000000..497aee6ba6 --- /dev/null +++ b/packages/core/core-flows/src/inventory/steps/adjust-inventory-levels.ts @@ -0,0 +1,47 @@ +import { IInventoryServiceNext, InventoryNext } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +export const adjustInventoryLevelsStepId = "adjust-inventory-levels-step" +export const adjustInventoryLevelsStep = createStep( + adjustInventoryLevelsStepId, + async ( + input: InventoryNext.BulkAdjustInventoryLevelInput[], + { container } + ) => { + const inventoryService: IInventoryServiceNext = container.resolve( + ModuleRegistrationName.INVENTORY + ) + + const adjustedLevels: InventoryNext.InventoryLevelDTO[] = + await inventoryService.adjustInventory( + input.map((item) => { + return { + inventoryItemId: item.inventory_item_id, + locationId: item.location_id, + adjustment: item.adjustment, + } + }) + ) + + return new StepResponse( + adjustedLevels, + input.map((item) => { + return { + ...item, + adjustment: item.adjustment * -1, + } + }) + ) + }, + async (adjustedLevels, { container }) => { + if (!adjustedLevels) { + return + } + + const inventoryService = container.resolve(ModuleRegistrationName.INVENTORY) + + await inventoryService.adjustInventory(adjustedLevels) + } +) diff --git a/packages/core/core-flows/src/inventory/steps/index.ts b/packages/core/core-flows/src/inventory/steps/index.ts index 03a832caad..03f2fc3ade 100644 --- a/packages/core/core-flows/src/inventory/steps/index.ts +++ b/packages/core/core-flows/src/inventory/steps/index.ts @@ -1,10 +1,11 @@ -export * from "./delete-inventory-items" +export * from "./adjust-inventory-levels" export * from "./attach-inventory-items" export * from "./create-inventory-items" -export * from "./validate-singular-inventory-items-for-tags" export * from "./create-inventory-levels" -export * from "./validate-inventory-locations" -export * from "./update-inventory-items" +export * from "./delete-inventory-items" export * from "./delete-inventory-levels" -export * from "./update-inventory-levels" export * from "./delete-levels-by-item-and-location" +export * from "./update-inventory-items" +export * from "./update-inventory-levels" +export * from "./validate-inventory-locations" +export * from "./validate-singular-inventory-items-for-tags" diff --git a/packages/core/core-flows/src/order/steps/create-return.ts b/packages/core/core-flows/src/order/steps/create-return.ts index 3d021256af..24143418bd 100644 --- a/packages/core/core-flows/src/order/steps/create-return.ts +++ b/packages/core/core-flows/src/order/steps/create-return.ts @@ -1,6 +1,6 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { CreateOrderReturnDTO, IOrderModuleService } from "@medusajs/types" -import { createStep, StepResponse } from "@medusajs/workflows-sdk" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" type CreateReturnStepInput = CreateOrderReturnDTO @@ -12,11 +12,11 @@ export const createReturnStep = createStep( ModuleRegistrationName.ORDER ) - const created = await service.createReturn(data) - return new StepResponse(created, created) + await service.createReturn(data) + return new StepResponse(void 0, data.order_id) }, - async (createdId, { container }) => { - if (!createdId) { + async (orderId, { container }) => { + if (!orderId) { return } @@ -24,6 +24,6 @@ export const createReturnStep = createStep( ModuleRegistrationName.ORDER ) - // TODO: delete return + await service.revertLastVersion(orderId) } ) diff --git a/packages/core/core-flows/src/order/steps/index.ts b/packages/core/core-flows/src/order/steps/index.ts index 77bb6c9e78..164466f85d 100644 --- a/packages/core/core-flows/src/order/steps/index.ts +++ b/packages/core/core-flows/src/order/steps/index.ts @@ -3,5 +3,7 @@ export * from "./complete-orders" export * from "./create-orders" export * from "./get-item-tax-lines" export * from "./link-order-payment-collection" +export * from "./register-fulfillment" +export * from "./register-shipment" export * from "./set-tax-lines-for-items" export * from "./update-tax-lines" diff --git a/packages/core/core-flows/src/order/steps/register-fulfillment.ts b/packages/core/core-flows/src/order/steps/register-fulfillment.ts new file mode 100644 index 0000000000..f1da06618d --- /dev/null +++ b/packages/core/core-flows/src/order/steps/register-fulfillment.ts @@ -0,0 +1,32 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + IOrderModuleService, + RegisterOrderFulfillmentDTO, +} from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type RegisterOrderFulfillmentStepInput = RegisterOrderFulfillmentDTO + +export const registerOrderFulfillmentStepId = "register-order-fullfillment" +export const registerOrderFulfillmentStep = createStep( + registerOrderFulfillmentStepId, + async (data: RegisterOrderFulfillmentStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.registerFulfillment(data) + return new StepResponse(void 0, data.order_id) + }, + async (orderId, { container }) => { + if (!orderId) { + return + } + + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.revertLastVersion(orderId) + } +) diff --git a/packages/core/core-flows/src/order/steps/register-shipment.ts b/packages/core/core-flows/src/order/steps/register-shipment.ts new file mode 100644 index 0000000000..9d266d4728 --- /dev/null +++ b/packages/core/core-flows/src/order/steps/register-shipment.ts @@ -0,0 +1,29 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IOrderModuleService, RegisterOrderShipmentDTO } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type RegisterOrderShipmentStepInput = RegisterOrderShipmentDTO + +export const registerOrderShipmentStepId = "register-order-shipment" +export const registerOrderShipmentStep = createStep( + registerOrderShipmentStepId, + async (data: RegisterOrderShipmentStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.registerShipment(data) + return new StepResponse(void 0, data.order_id) + }, + async (orderId, { container }) => { + if (!orderId) { + return + } + + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.revertLastVersion(orderId) + } +) diff --git a/packages/core/core-flows/src/order/utils/order-validation.ts b/packages/core/core-flows/src/order/utils/order-validation.ts new file mode 100644 index 0000000000..84e8ea417c --- /dev/null +++ b/packages/core/core-flows/src/order/utils/order-validation.ts @@ -0,0 +1,32 @@ +import { OrderDTO, OrderWorkflow } from "@medusajs/types" +import { MedusaError, OrderStatus, arrayDifference } from "@medusajs/utils" + +export function throwIfOrderIsCancelled({ order }: { order: OrderDTO }) { + if (order.status === OrderStatus.CANCELED) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Order with id ${order.id} has been cancelled.` + ) + } +} + +export function throwIfItemsDoesNotExistsInOrder({ + order, + inputItems, +}: { + order: Pick + inputItems: OrderWorkflow.CreateOrderFulfillmentWorkflowInput["items"] +}) { + const orderItemIds = order.items?.map((i) => i.id) ?? [] + const inputItemIds = inputItems.map((i) => i.id) + const diff = arrayDifference(inputItemIds, orderItemIds) + + if (diff.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Items with ids ${diff.join(", ")} does not exist in order with id ${ + order.id + }.` + ) + } +} diff --git a/packages/core/core-flows/src/order/workflows/create-fulfillment.ts b/packages/core/core-flows/src/order/workflows/create-fulfillment.ts new file mode 100644 index 0000000000..baf1a0f16a --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/create-fulfillment.ts @@ -0,0 +1,278 @@ +import { Modules } from "@medusajs/modules-sdk" +import { + FulfillmentDTO, + FulfillmentWorkflow, + OrderDTO, + OrderWorkflow, +} from "@medusajs/types" +import { MedusaError } from "@medusajs/utils" +import { + WorkflowData, + createStep, + createWorkflow, + parallelize, + transform, +} from "@medusajs/workflows-sdk" +import { createLinkStep, useRemoteQueryStep } from "../../common" +import { createFulfillmentWorkflow } from "../../fulfillment" +import { adjustInventoryLevelsStep } from "../../inventory" +import { + deleteReservationsStep, + updateReservationsStep, +} from "../../reservation" +import { registerOrderFulfillmentStep } from "../steps" +import { + throwIfItemsDoesNotExistsInOrder, + throwIfOrderIsCancelled, +} from "../utils/order-validation" + +const validateOrder = createStep( + "validate-order", + ( + { + order, + inputItems, + }: { + order: OrderDTO + inputItems: OrderWorkflow.CreateOrderFulfillmentWorkflowInput["items"] + }, + context + ) => { + throwIfOrderIsCancelled({ order }) + throwIfItemsDoesNotExistsInOrder({ order, inputItems }) + } +) + +function prepareRegisterOrderFulfillmentData({ + order, + fulfillment, + input, +}: { + order: OrderDTO + fulfillment: FulfillmentDTO + input: OrderWorkflow.CreateOrderFulfillmentWorkflowInput +}) { + return { + order_id: order.id, + reference: Modules.FULFILLMENT, + reference_id: fulfillment.id, + created_by: input.created_by, + items: order.items!.map((i) => { + return { + id: i.id, + quantity: i.quantity, + } + }), + } +} + +function prepareFulfillmentData({ + order, + input, + shippingOption, +}: { + order: OrderDTO + input: OrderWorkflow.CreateOrderFulfillmentWorkflowInput + shippingOption: { + id: string + provider_id: string + service_zone: { fulfillment_set: { location?: { id: string } } } + } +}) { + const inputItems = input.items + const orderItemsMap = new Map["items"][0]>( + order.items!.map((i) => [i.id, i]) + ) + const fulfillmentItems = inputItems.map((i) => { + const orderItem = orderItemsMap.get(i.id)! + return { + line_item_id: i.id, + quantity: i.quantity, + title: orderItem.variant_title ?? orderItem.title, + sku: orderItem.variant_sku || "", + barcode: orderItem.variant_barcode || "", + } as FulfillmentWorkflow.CreateFulfillmentItemWorkflowDTO + }) + + let locationId: string | undefined = input.location_id + if (!locationId) { + locationId = shippingOption.service_zone.fulfillment_set.location?.id + } + + if (!locationId) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Cannot create fulfillment without stock location, either provide a location or you should link the shipping option ${shippingOption.id} to a stock location.` + ) + } + + return { + input: { + location_id: locationId, + provider_id: shippingOption.provider_id, + shipping_option_id: shippingOption.id, + items: fulfillmentItems, + labels: [] as FulfillmentWorkflow.CreateFulfillmentLabelWorkflowDTO[], // TODO: shipping labels + delivery_address: order.shipping_address ?? ({} as any), + }, + } +} + +function prepareInventoryReservations({ reservations, order, input }) { + if (!reservations || !reservations.length) { + throw new Error( + `No stock reservation found for items ${input.items.map((i) => i.id)}` + ) + } + + const reservationMap = reservations.reduce((acc, reservation) => { + acc[reservation.line_item_id as string] = reservation + return acc + }, {}) + + const inputItemsMap = input.items.reduce((acc, item) => { + acc[item.id] = item + return acc + }, {}) + + const toDelete: string[] = [] + const toUpdate: { + id: string + quantity: number // TODO: BigNumberInput + location_id: string + }[] = [] + const inventoryAdjustment: { + inventory_item_id: string + location_id: string + adjustment: number // TODO: BigNumberInput + }[] = [] + + for (const item of order.items) { + const reservation = reservationMap[item.id] + const inputQuantity = inputItemsMap[item.id]?.quantity ?? item.quantity + + const quantity = reservation.quantity - inputQuantity + + inventoryAdjustment.push({ + inventory_item_id: reservation.inventory_item_id, + location_id: input.location_id ?? reservation.location_id, + adjustment: -item.quantity, // TODO: MathBN.mul(-1, item.quantity) + }) + + if (quantity === 0) { + toDelete.push(reservation.id) + } else { + toUpdate.push({ + id: reservation.id, + quantity: quantity, + location_id: input.location_id ?? reservation.location_id, + }) + } + } + + return { + toDelete, + toUpdate, + inventoryAdjustment, + } +} + +export const createOrderFulfillmentWorkflowId = "create-order-fulfillment" +export const createOrderFulfillmentWorkflow = createWorkflow( + createOrderFulfillmentWorkflowId, + ( + input: WorkflowData + ): WorkflowData => { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: [ + "id", + "status", + "region_id", + "currency_code", + "items.*", + "shipping_address.*", + "shipping_methods.shipping_option_id", // TODO: which shipping method to use when multiple? + ], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }) + + validateOrder({ order, inputItems: input.items }) + + const shippingOptionId = transform(order, (data) => { + return data.shipping_methods?.[0]?.shipping_option_id + }) + + const shippingOption = useRemoteQueryStep({ + entry_point: "shipping_options", + fields: ["id", "provider_id", "service_zone.fulfillment_set.location.id"], + variables: { + id: shippingOptionId, + }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "get-shipping-option" }) + + const fulfillmentData = transform( + { order, input, shippingOption }, + prepareFulfillmentData + ) + + const fulfillment = createFulfillmentWorkflow.runAsStep(fulfillmentData) + + const registerOrderFulfillmentData = transform( + { order, fulfillment, input }, + prepareRegisterOrderFulfillmentData + ) + + registerOrderFulfillmentStep(registerOrderFulfillmentData) + + const link = transform( + { order_id: input.order_id, fulfillment }, + (data) => { + return [ + { + [Modules.ORDER]: { order_id: data.order_id }, + [Modules.FULFILLMENT]: { fulfillment_id: data.fulfillment.id }, + }, + ] + } + ) + createLinkStep(link) + + const lineItemIds = transform({ order }, ({ order }) => { + return order.items?.map((i) => i.id) + }) + const reservations = useRemoteQueryStep({ + entry_point: "reservations", + fields: [ + "id", + "line_item_id", + "quantity", + "inventory_item_id", + "location_id", + ], + variables: { + filter: { + line_item_id: lineItemIds, + }, + }, + }).config({ name: "get-reservations" }) + + const { toDelete, toUpdate, inventoryAdjustment } = transform( + { order, reservations, input }, + prepareInventoryReservations + ) + + parallelize( + updateReservationsStep(toUpdate), + deleteReservationsStep(toDelete), + adjustInventoryLevelsStep(inventoryAdjustment) + ) + + // trigger event OrderModuleService.Events.FULFILLMENT_CREATED + return fulfillment + } +) diff --git a/packages/core/core-flows/src/order/workflows/create-return.ts b/packages/core/core-flows/src/order/workflows/create-return.ts index 1ddf9b2360..5f90213b2d 100644 --- a/packages/core/core-flows/src/order/workflows/create-return.ts +++ b/packages/core/core-flows/src/order/workflows/create-return.ts @@ -1,3 +1,4 @@ +import { Modules } from "@medusajs/modules-sdk" import { CreateOrderShippingMethodDTO, FulfillmentWorkflow, @@ -7,55 +8,27 @@ import { WithCalculatedPrice, } from "@medusajs/types" import { + ContainerRegistrationKeys, + MathBN, + MedusaError, + arrayDifference, + isDefined, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { + WorkflowData, createStep, createWorkflow, transform, - WorkflowData, } from "@medusajs/workflows-sdk" import { createLinkStep, useRemoteQueryStep } from "../../common" -import { - arrayDifference, - ContainerRegistrationKeys, - isDefined, - MathBN, - MedusaError, - Modules, - remoteQueryObjectFromString, -} from "@medusajs/utils" +import { createReturnFulfillmentWorkflow } from "../../fulfillment" import { updateOrderTaxLinesStep } from "../steps" import { createReturnStep } from "../steps/create-return" -import { createFulfillmentWorkflow } from "../../fulfillment" - -function throwIfOrderIsCancelled({ order }: { order: OrderDTO }) { - // TODO: need work, check canceled - if (false /*order.canceled_at*/) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Order with id ${order.id} has been cancelled.` - ) - } -} - -function throwIfItemsDoesNotExistsInOrder({ - order, - inputItems, -}: { - order: Pick - inputItems: OrderWorkflow.CreateOrderReturnWorkflowInput["items"] -}) { - const orderItemIds = order.items?.map((i) => i.id) ?? [] - const inputItemIds = inputItems.map((i) => i.id) - const diff = arrayDifference(inputItemIds, orderItemIds) - - if (diff.length) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Items with ids ${diff.join(", ")} does not exist in order with id ${ - order.id - }.` - ) - } -} +import { + throwIfItemsDoesNotExistsInOrder, + throwIfOrderIsCancelled, +} from "../utils/order-validation" async function validateReturnReasons( { @@ -214,8 +187,7 @@ function prepareFulfillmentData({ items: fulfillmentItems, labels: [] as FulfillmentWorkflow.CreateFulfillmentLabelWorkflowDTO[], delivery_address: order.shipping_address ?? ({} as any), // TODO: should it be the stock location address? - order: {} as FulfillmentWorkflow.CreateFulfillmentOrderWorkflowDTO, // TODO see what todo here, is that even necessary? - order_id: input.order_id, + order: order, }, } } @@ -280,6 +252,7 @@ export const createReturnOrderWorkflow = createWorkflow( entry_point: "orders", fields: [ "id", + "status", "region_id", "currency_code", "total", @@ -339,8 +312,20 @@ export const createReturnOrderWorkflow = createWorkflow( prepareFulfillmentData ) - createFulfillmentWorkflow.runAsStep(fulfillmentData) + const returnFulfillment = + createReturnFulfillmentWorkflow.runAsStep(fulfillmentData) - // TODO call the createReturn from the fulfillment provider + const link = transform( + { order_id: input.order_id, fulfillment: returnFulfillment }, + (data) => { + return [ + { + [Modules.ORDER]: { order_id: data.order_id }, + [Modules.FULFILLMENT]: { fulfillment_id: data.fulfillment.id }, + }, + ] + } + ) + createLinkStep(link) } ) diff --git a/packages/core/core-flows/src/order/workflows/create-shipment.ts b/packages/core/core-flows/src/order/workflows/create-shipment.ts new file mode 100644 index 0000000000..f3f1885b92 --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/create-shipment.ts @@ -0,0 +1,109 @@ +import { FulfillmentDTO, OrderDTO, OrderWorkflow } from "@medusajs/types" +import { Modules } from "@medusajs/utils" +import { + WorkflowData, + createStep, + createWorkflow, + transform, +} from "@medusajs/workflows-sdk" +import { useRemoteQueryStep } from "../../common" +import { createShipmentWorkflow } from "../../fulfillment" +import { registerOrderShipmentStep } from "../steps" +import { + throwIfItemsDoesNotExistsInOrder, + throwIfOrderIsCancelled, +} from "../utils/order-validation" + +const validateOrder = createStep( + "validate-order", + ({ + order, + input, + }: { + order: OrderDTO + input: OrderWorkflow.CreateOrderShipmentWorkflowInput + }) => { + const inputItems = input.items + + throwIfOrderIsCancelled({ order }) + throwIfItemsDoesNotExistsInOrder({ order, inputItems }) + + const order_ = order as OrderDTO & { fulfillments: FulfillmentDTO[] } + const fulfillment = order_.fulfillments.find( + (f) => f.id === input.fulfillment_id + ) + if (!fulfillment) { + throw new Error( + `Fulfillment with id ${input.fulfillment_id} not found in the order` + ) + } + } +) + +function prepareRegisterShipmentData({ + order, + input, +}: { + order: OrderDTO + input: OrderWorkflow.CreateOrderShipmentWorkflowInput +}) { + const fulfillId = input.fulfillment_id + const order_ = order as OrderDTO & { fulfillments: FulfillmentDTO[] } + const fulfillment = order_.fulfillments.find((f) => f.id === fulfillId)! + + return { + order_id: order.id, + reference: Modules.FULFILLMENT, + reference_id: fulfillment.id, + created_by: input.created_by, + items: order.items!.map((i) => { + return { + id: i.id, + quantity: i.quantity, + } + }), + } +} + +export const createOrderShipmentWorkflowId = "create-order-shipment" +export const createOrderShipmentWorkflow = createWorkflow( + createOrderShipmentWorkflowId, + ( + input: WorkflowData + ): WorkflowData => { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: [ + "id", + "status", + "region_id", + "currency_code", + "items.*", + "fulfillments.*", + ], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }) + + validateOrder({ order, input }) + + const fulfillmentData = transform({ input }, ({ input }) => { + return { + id: input.fulfillment_id, + labels: input.labels, + } + }) + + createShipmentWorkflow.runAsStep({ + input: fulfillmentData, + }) + + const shipmentData = transform( + { order, input }, + prepareRegisterShipmentData + ) + + registerOrderShipmentStep(shipmentData) + } +) diff --git a/packages/core/core-flows/src/order/workflows/index.ts b/packages/core/core-flows/src/order/workflows/index.ts index 3a848ef72f..9b0cabf445 100644 --- a/packages/core/core-flows/src/order/workflows/index.ts +++ b/packages/core/core-flows/src/order/workflows/index.ts @@ -1,5 +1,7 @@ export * from "./archive-orders" export * from "./complete-orders" +export * from "./create-fulfillment" export * from "./create-orders" export * from "./create-return" +export * from "./create-shipment" export * from "./update-tax-lines" diff --git a/packages/core/types/src/fulfillment/mutations/fulfillment.ts b/packages/core/types/src/fulfillment/mutations/fulfillment.ts index 1dfcea6cba..350e6693b7 100644 --- a/packages/core/types/src/fulfillment/mutations/fulfillment.ts +++ b/packages/core/types/src/fulfillment/mutations/fulfillment.ts @@ -1,12 +1,8 @@ +import { OrderDTO } from "../../order" import { CreateFulfillmentAddressDTO } from "./fulfillment-address" import { CreateFulfillmentItemDTO } from "./fulfillment-item" import { CreateFulfillmentLabelDTO } from "./fulfillment-label" -/** - * The fulfillment order to be created. - */ -export interface CreateFulfillmentOrderDTO {} - /** * The fulfillment to be created. */ @@ -72,9 +68,9 @@ export interface CreateFulfillmentDTO { labels: Omit[] /** - * The associated fulfillment order. + * The associated order to be sent to the provider. */ - order: CreateFulfillmentOrderDTO + order?: Partial } /** diff --git a/packages/core/types/src/fulfillment/provider.ts b/packages/core/types/src/fulfillment/provider.ts index 63fdb31a39..122383bb67 100644 --- a/packages/core/types/src/fulfillment/provider.ts +++ b/packages/core/types/src/fulfillment/provider.ts @@ -51,7 +51,7 @@ export interface IFulfillmentProvider { createFulfillment( data: object, items: object[], - order: object, + order: object | undefined, fulfillment: Record ): Promise> /** diff --git a/packages/core/types/src/inventory/mutations/inventory-level.ts b/packages/core/types/src/inventory/mutations/inventory-level.ts index b2f6830a27..22f9dcbf7b 100644 --- a/packages/core/types/src/inventory/mutations/inventory-level.ts +++ b/packages/core/types/src/inventory/mutations/inventory-level.ts @@ -56,3 +56,19 @@ export type BulkUpdateInventoryLevelInput = { */ location_id: string } & UpdateInventoryLevelInput + +export type BulkAdjustInventoryLevelInput = { + /** + * The ID of the associated inventory level. + */ + inventory_item_id: string + /** + * The ID of the associated location. + */ + location_id: string + + /** + * The quantity to adjust the inventory level by. + */ + adjustment: number // TODO: BigNumberInput +} & UpdateInventoryLevelInput diff --git a/packages/core/types/src/inventory/service-next.ts b/packages/core/types/src/inventory/service-next.ts index 841be8192e..4509eef829 100644 --- a/packages/core/types/src/inventory/service-next.ts +++ b/packages/core/types/src/inventory/service-next.ts @@ -1,9 +1,9 @@ import { RestoreReturn, SoftDeleteReturn } from "../dal" -import { Context } from "../shared-context" +import { InventoryNext } from "." import { FindConfig } from "../common" import { IModuleService } from "../modules-sdk" -import { InventoryNext } from "." +import { Context } from "../shared-context" /** * The main service interface for the Inventory Module. @@ -994,6 +994,16 @@ export interface IInventoryServiceNext extends IModuleService { * -5 * ) */ + + adjustInventory( + data: { + inventoryItemId: string + locationId: string + adjustment: number + }[], + context?: Context + ): Promise + adjustInventory( inventoryItemId: string, locationId: string, diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts index b8e0a70c3a..4e881833c3 100644 --- a/packages/core/types/src/order/mutations.ts +++ b/packages/core/types/src/order/mutations.ts @@ -389,8 +389,8 @@ export interface RegisterOrderShipmentDTO { description?: string internal_note?: string reference?: string + reference_id?: string created_by?: string - shipping_method: Omit | string items: { id: string quantity: BigNumberInput diff --git a/packages/core/types/src/workflow/fulfillment/create-fulfillment.ts b/packages/core/types/src/workflow/fulfillment/create-fulfillment.ts index bae310ccf1..65d50cc2bd 100644 --- a/packages/core/types/src/workflow/fulfillment/create-fulfillment.ts +++ b/packages/core/types/src/workflow/fulfillment/create-fulfillment.ts @@ -177,10 +177,7 @@ export type CreateFulfillmentWorkflowInput = { labels: CreateFulfillmentLabelWorkflowDTO[] /** - * The associated fulfillment order. + * The associated fulfillment order to be sent to the provider. */ - order: CreateFulfillmentOrderWorkflowDTO - - // TODO: revisit - either remove `order_id` or `order` - order_id: string + order?: CreateFulfillmentOrderWorkflowDTO } diff --git a/packages/core/types/src/workflow/fulfillment/create-shipment.ts b/packages/core/types/src/workflow/fulfillment/create-shipment.ts index a571002421..9389c58b1a 100644 --- a/packages/core/types/src/workflow/fulfillment/create-shipment.ts +++ b/packages/core/types/src/workflow/fulfillment/create-shipment.ts @@ -12,5 +12,5 @@ export interface CreateShipmentWorkflowInput { /** * The labels associated with the fulfillment. */ - labels?: CreateFulfillmentLabelWorkflowDTO[] + labels: CreateFulfillmentLabelWorkflowDTO[] } diff --git a/packages/core/types/src/workflow/order/create-fulfillment.ts b/packages/core/types/src/workflow/order/create-fulfillment.ts new file mode 100644 index 0000000000..b3b78727fa --- /dev/null +++ b/packages/core/types/src/workflow/order/create-fulfillment.ts @@ -0,0 +1,15 @@ +import { BigNumberInput } from "../../totals" + +interface CreateOrderFulfillmentItem { + id: string + quantity: BigNumberInput +} + +export interface CreateOrderFulfillmentWorkflowInput { + order_id: string + created_by?: string // The id of the authenticated user + items: CreateOrderFulfillmentItem[] + no_notification?: boolean + location_id?: string + metadata?: Record +} diff --git a/packages/core/types/src/workflow/order/create-shipment.ts b/packages/core/types/src/workflow/order/create-shipment.ts new file mode 100644 index 0000000000..dc65cf85bc --- /dev/null +++ b/packages/core/types/src/workflow/order/create-shipment.ts @@ -0,0 +1,17 @@ +import { BigNumberInput } from "../../totals" +import { CreateFulfillmentLabelWorkflowDTO } from "../fulfillment" + +interface CreateOrderShipmentItem { + id: string + quantity: BigNumberInput +} + +export interface CreateOrderShipmentWorkflowInput { + order_id: string + fulfillment_id: string + created_by?: string // The id of the authenticated user + items: CreateOrderShipmentItem[] + labels: CreateFulfillmentLabelWorkflowDTO[] + no_notification?: boolean + metadata?: Record +} diff --git a/packages/core/types/src/workflow/order/index.ts b/packages/core/types/src/workflow/order/index.ts index 29b175eae9..9a2fb3a6ce 100644 --- a/packages/core/types/src/workflow/order/index.ts +++ b/packages/core/types/src/workflow/order/index.ts @@ -1 +1,3 @@ +export * from "./create-fulfillment" export * from "./create-return-order" +export * from "./create-shipment" diff --git a/packages/medusa/src/api/admin/orders/[id]/archive/route.ts b/packages/medusa/src/api/admin/orders/[id]/archive/route.ts index 606b0f8720..ef6e618f04 100644 --- a/packages/medusa/src/api/admin/orders/[id]/archive/route.ts +++ b/packages/medusa/src/api/admin/orders/[id]/archive/route.ts @@ -1,5 +1,8 @@ import { archiveOrderWorkflow } from "@medusajs/core-flows" -import { remoteQueryObjectFromString } from "@medusajs/utils" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" import { AuthenticatedMedusaRequest, MedusaResponse, @@ -10,7 +13,7 @@ export const POST = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const remoteQuery = req.scope.resolve("remoteQuery") + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) const { id } = req.params await archiveOrderWorkflow(req.scope).run({ diff --git a/packages/medusa/src/api/admin/orders/[id]/cancel/route.ts b/packages/medusa/src/api/admin/orders/[id]/cancel/route.ts index 2fee833355..2ceb3fd77f 100644 --- a/packages/medusa/src/api/admin/orders/[id]/cancel/route.ts +++ b/packages/medusa/src/api/admin/orders/[id]/cancel/route.ts @@ -1,4 +1,7 @@ -import { remoteQueryObjectFromString } from "@medusajs/utils" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" import { AuthenticatedMedusaRequest, MedusaResponse, @@ -8,7 +11,7 @@ export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const remoteQuery = req.scope.resolve("remoteQuery") + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) const variables = { id: req.params.id } diff --git a/packages/medusa/src/api/admin/orders/[id]/complete/route.ts b/packages/medusa/src/api/admin/orders/[id]/complete/route.ts index 8c5cb1dcaf..f340b293be 100644 --- a/packages/medusa/src/api/admin/orders/[id]/complete/route.ts +++ b/packages/medusa/src/api/admin/orders/[id]/complete/route.ts @@ -1,5 +1,8 @@ import { completeOrderWorkflow } from "@medusajs/core-flows" -import { remoteQueryObjectFromString } from "@medusajs/utils" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" import { AuthenticatedMedusaRequest, MedusaResponse, @@ -10,7 +13,7 @@ export const POST = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const remoteQuery = req.scope.resolve("remoteQuery") + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) const { id } = req.params await completeOrderWorkflow(req.scope).run({ diff --git a/packages/medusa/src/api/admin/orders/fulfillments/[fulfillment_id]/cancel/route.ts b/packages/medusa/src/api/admin/orders/[id]/fulfillments/[fulfillment_id]/cancel/route.ts similarity index 68% rename from packages/medusa/src/api/admin/orders/fulfillments/[fulfillment_id]/cancel/route.ts rename to packages/medusa/src/api/admin/orders/[id]/fulfillments/[fulfillment_id]/cancel/route.ts index 2070238e48..2079e093d7 100644 --- a/packages/medusa/src/api/admin/orders/fulfillments/[fulfillment_id]/cancel/route.ts +++ b/packages/medusa/src/api/admin/orders/[id]/fulfillments/[fulfillment_id]/cancel/route.ts @@ -1,14 +1,17 @@ -import { remoteQueryObjectFromString } from "@medusajs/utils" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" import { AuthenticatedMedusaRequest, MedusaResponse, -} from "../../../../../../types/routing" +} from "../../../../../../../types/routing" export const POST = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const remoteQuery = req.scope.resolve("remoteQuery") + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) const variables = { id: req.params.id } diff --git a/packages/medusa/src/api/admin/orders/[id]/fulfillments/[fulfillment_id]/shipment/route.ts b/packages/medusa/src/api/admin/orders/[id]/fulfillments/[fulfillment_id]/shipment/route.ts new file mode 100644 index 0000000000..27499d8b08 --- /dev/null +++ b/packages/medusa/src/api/admin/orders/[id]/fulfillments/[fulfillment_id]/shipment/route.ts @@ -0,0 +1,44 @@ +import { createOrderShipmentWorkflow } from "@medusajs/core-flows" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../../../../types/routing" +import { AdminOrderCreateShipmentType } from "../../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const variables = { id: req.params.id } + + const input = { + ...req.validatedBody, + order_id: req.params.id, + fulfillment_id: req.params.fulfillment_id, + labels: req.validatedBody.labels ?? [], + } + + const { errors } = await createOrderShipmentWorkflow(req.scope).run({ + input, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "order", + variables, + fields: req.remoteQueryConfig.fields, + }) + + const [order] = await remoteQuery(queryObject) + res.status(200).json({ order }) +} diff --git a/packages/medusa/src/api/admin/orders/[id]/fulfillments/route.ts b/packages/medusa/src/api/admin/orders/[id]/fulfillments/route.ts new file mode 100644 index 0000000000..67dece10e5 --- /dev/null +++ b/packages/medusa/src/api/admin/orders/[id]/fulfillments/route.ts @@ -0,0 +1,42 @@ +import { createOrderFulfillmentWorkflow } from "@medusajs/core-flows" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../../types/routing" +import { AdminOrderCreateFulfillmentType } from "../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const variables = { id: req.params.id } + + const input = { + ...req.validatedBody, + order_id: req.params.id, + } + + const { errors } = await createOrderFulfillmentWorkflow(req.scope).run({ + input, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "order", + variables, + fields: req.remoteQueryConfig.fields, + }) + + const [order] = await remoteQuery(queryObject) + res.status(200).json({ order }) +} diff --git a/packages/medusa/src/api/admin/orders/[id]/route.ts b/packages/medusa/src/api/admin/orders/[id]/route.ts index 5a02fdea48..6dec01842d 100644 --- a/packages/medusa/src/api/admin/orders/[id]/route.ts +++ b/packages/medusa/src/api/admin/orders/[id]/route.ts @@ -1,4 +1,7 @@ -import { remoteQueryObjectFromString } from "@medusajs/utils" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" import { AuthenticatedMedusaRequest, MedusaResponse, @@ -8,7 +11,7 @@ export const GET = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const remoteQuery = req.scope.resolve("remoteQuery") + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) const variables = { id: req.params.id } @@ -26,7 +29,7 @@ export const POST = async ( req: AuthenticatedMedusaRequest, res: MedusaResponse ) => { - const remoteQuery = req.scope.resolve("remoteQuery") + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) const variables = { id: req.params.id } diff --git a/packages/medusa/src/api/admin/orders/fulfillments/route.ts b/packages/medusa/src/api/admin/orders/fulfillments/route.ts deleted file mode 100644 index d291c62313..0000000000 --- a/packages/medusa/src/api/admin/orders/fulfillments/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { remoteQueryObjectFromString } from "@medusajs/utils" -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "../../../../types/routing" - -export const POST = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - const remoteQuery = req.scope.resolve("remoteQuery") - - const variables = { id: req.params.id } - - // TODO: Workflow fulfill items, create fulfillments - v1.x - packages/medusa/src/api/routes/admin/orders/create-fulfillment.ts - - const queryObject = remoteQueryObjectFromString({ - entryPoint: "order", - variables, - fields: req.remoteQueryConfig.fields, - }) - - const [order] = await remoteQuery(queryObject) - res.status(200).json({ order }) -} diff --git a/packages/medusa/src/api/admin/orders/middlewares.ts b/packages/medusa/src/api/admin/orders/middlewares.ts index 6cf836f578..00703ee1ee 100644 --- a/packages/medusa/src/api/admin/orders/middlewares.ts +++ b/packages/medusa/src/api/admin/orders/middlewares.ts @@ -8,6 +8,7 @@ import { AdminCompleteOrder, AdminGetOrdersOrderParams, AdminGetOrdersParams, + AdminOrderCreateFulfillment, } from "./validators" export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [ @@ -74,7 +75,7 @@ export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [ method: ["POST"], matcher: "/admin/orders/:id/fulfillments", middlewares: [ - // validateAndTransformBody(), + validateAndTransformBody(AdminOrderCreateFulfillment), validateAndTransformQuery( AdminGetOrdersOrderParams, QueryConfig.retrieveTransformQueryConfig diff --git a/packages/medusa/src/api/admin/orders/validators.ts b/packages/medusa/src/api/admin/orders/validators.ts index ad7cb489a1..57fe6c8018 100644 --- a/packages/medusa/src/api/admin/orders/validators.ts +++ b/packages/medusa/src/api/admin/orders/validators.ts @@ -46,3 +46,36 @@ export const AdminCompleteOrder = z.object({ order_id: z.string(), }) export type AdminCompleteOrderType = z.infer + +const Item = z.object({ + id: z.string(), + quantity: z.number(), +}) + +export const AdminOrderCreateFulfillment = z.object({ + items: z.array(Item), + location_id: z.string().optional(), + no_notification: z.boolean().optional(), + metadata: z.record(z.unknown()).optional(), +}) + +export type AdminOrderCreateFulfillmentType = z.infer< + typeof AdminOrderCreateFulfillment +> + +const Label = z.object({ + tracking_number: z.string(), + tracking_url: z.string(), + label_url: z.string(), +}) + +export const AdminOrderCreateShipment = z.object({ + items: z.array(Item), + labels: z.array(Label).optional(), + no_notification: z.boolean().optional(), + metadata: z.record(z.unknown()).optional(), +}) + +export type AdminOrderCreateShipmentType = z.infer< + typeof AdminOrderCreateShipment +> diff --git a/packages/modules/fulfillment/src/services/fulfillment-provider.ts b/packages/modules/fulfillment/src/services/fulfillment-provider.ts index bf58792b87..e617268529 100644 --- a/packages/modules/fulfillment/src/services/fulfillment-provider.ts +++ b/packages/modules/fulfillment/src/services/fulfillment-provider.ts @@ -4,7 +4,7 @@ import { FulfillmentTypes, IFulfillmentProvider, } from "@medusajs/types" -import { ModulesSdkUtils, promiseAll, MedusaError } from "@medusajs/utils" +import { MedusaError, ModulesSdkUtils, promiseAll } from "@medusajs/utils" import { FulfillmentProvider } from "@models" type InjectedDependencies = { @@ -86,7 +86,7 @@ export default class FulfillmentProviderService extends ModulesSdkUtils.internal providerId: string, data: object, items: object[], - order: object, + order: object | undefined, fulfillment: Record ): Promise> { const provider = this.retrieveProviderRegistration(providerId) @@ -101,10 +101,7 @@ export default class FulfillmentProviderService extends ModulesSdkUtils.internal return await provider.cancelFulfillment(fulfillment) } - async createReturn( - providerId: string, - fulfillment: Record, - ) { + async createReturn(providerId: string, fulfillment: Record) { const provider = this.retrieveProviderRegistration(providerId) return await provider.createReturnFulfillment(fulfillment) } diff --git a/packages/modules/inventory-next/src/services/inventory.ts b/packages/modules/inventory-next/src/services/inventory.ts index c3bb6e2855..7d04d3f2c9 100644 --- a/packages/modules/inventory-next/src/services/inventory.ts +++ b/packages/modules/inventory-next/src/services/inventory.ts @@ -19,6 +19,7 @@ import { MedusaError, ModulesSdkUtils, isDefined, + isString, partitionArray, promiseAll, } from "@medusajs/utils" @@ -799,33 +800,68 @@ export default class InventoryModuleService< * @return The updated inventory level * @throws when the inventory level is not found */ - @InjectManager("baseRepository_") - @EmitEvents() - async adjustInventory( + adjustInventory( inventoryItemId: string, locationId: string, adjustment: number, - @MedusaContext() context: Context = {} - ): Promise { - const result = await this.adjustInventory_( - inventoryItemId, - locationId, - adjustment, - context - ) + context: Context + ): Promise - context.messageAggregator?.saveRawMessageData({ - eventName: InventoryEvents.inventory_level_updated, - metadata: { - service: this.constructor.name, - action: CommonEvents.UPDATED, - object: "inventory-level", - }, - data: { id: result.id }, - }) + adjustInventory( + data: { + inventoryItemId: string + locationId: string + adjustment: number + }[], + context: Context + ): Promise + + @InjectManager("baseRepository_") + @EmitEvents() + async adjustInventory( + inventoryItemIdOrData: string | any, + locationId?: string | Context, + adjustment?: number, + @MedusaContext() context: Context = {} + ): Promise< + InventoryNext.InventoryLevelDTO | InventoryNext.InventoryLevelDTO[] + > { + let all: any = inventoryItemIdOrData + + if (isString(inventoryItemIdOrData)) { + all = [ + { + inventoryItemId: inventoryItemIdOrData, + locationId, + adjustment, + }, + ] + } + + const results: TInventoryLevel[] = [] + + for (const data of all) { + const result = await this.adjustInventory_( + data.inventoryItemId, + data.locationId, + data.adjustment, + context + ) + results.push(result) + + context.messageAggregator?.saveRawMessageData({ + eventName: InventoryEvents.inventory_level_updated, + metadata: { + service: this.constructor.name, + action: CommonEvents.UPDATED, + object: "inventory-level", + }, + data: { id: result.id }, + }) + } return await this.baseRepository_.serialize( - result, + Array.isArray(inventoryItemIdOrData) ? results : results[0], { populate: true, } diff --git a/packages/modules/order/integration-tests/__tests__/create-order.ts b/packages/modules/order/integration-tests/__tests__/create-order.ts index d771bc3f6b..fa67752b4a 100644 --- a/packages/modules/order/integration-tests/__tests__/create-order.ts +++ b/packages/modules/order/integration-tests/__tests__/create-order.ts @@ -489,7 +489,6 @@ moduleIntegrationTestRunner({ await service.registerShipment({ order_id: createdOrder.id, reference: Modules.FULFILLMENT, - shipping_method: createdOrder.shipping_methods![0].id, items: createdOrder.items!.map((item) => { return { id: item.id, diff --git a/packages/modules/order/src/services/order-module-service.ts b/packages/modules/order/src/services/order-module-service.ts index 5bc9773384..ff1b8c5426 100644 --- a/packages/modules/order/src/services/order-module-service.ts +++ b/packages/modules/order/src/services/order-module-service.ts @@ -2149,35 +2149,12 @@ export default class OrderModuleService< ): Promise { let shippingMethodId - if (!isString(data.shipping_method)) { - const methods = await this.createShippingMethods( - data.order_id, - data.shipping_method as any, - sharedContext - ) - shippingMethodId = methods[0].id - } else { - shippingMethodId = data.shipping_method - } - - const method = await this.shippingMethodService_.retrieve( - shippingMethodId, - { - relations: ["tax_lines", "adjustments"], - }, - sharedContext - ) - - const calculatedAmount = getShippingMethodsTotals([method as any], {})[ - method.id - ] - const actions: CreateOrderChangeActionDTO[] = data.items.map((item) => { return { action: ChangeActionType.SHIP_ITEM, internal_note: item.internal_note, reference: data.reference, - reference_id: shippingMethodId, + reference_id: data.reference_id, details: { reference_id: item.id, quantity: item.quantity, @@ -2191,7 +2168,6 @@ export default class OrderModuleService< action: ChangeActionType.SHIPPING_ADD, reference: data.reference, reference_id: shippingMethodId, - amount: calculatedAmount.total, }) }