From b5c658f0719d9cb79c61727d251d0bc94389e69d Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Thu, 11 Jul 2024 09:53:00 -0300 Subject: [PATCH] feat(core-flows): begin returns, claims and exchanges (#8088) * chore(order): begin return * claims and exchanges --- .../order/workflows/begin-order-claim.spec.ts | 393 +++++++++++++++++ .../workflows/begin-order-exchange.spec.ts | 400 ++++++++++++++++++ .../workflows/begin-order-return.spec.ts | 391 +++++++++++++++++ .../src/order/steps/create-claims.ts | 32 ++ ...te-return.ts => create-complete-return.ts} | 10 +- .../src/order/steps/create-exchanges.ts | 32 ++ .../src/order/steps/create-returns.ts | 32 ++ .../core/core-flows/src/order/steps/index.ts | 4 + .../src/order/utils/order-validation.ts | 2 +- .../src/order/workflows/begin-order-claim.ts | 59 +++ .../order/workflows/begin-order-exchange.ts | 58 +++ .../src/order/workflows/begin-return.ts | 58 +++ .../order/workflows/create-complete-return.ts | 11 +- .../core-flows/src/order/workflows/index.ts | 3 + packages/core/types/src/order/common.ts | 28 +- packages/core/types/src/order/mutations.ts | 54 ++- packages/core/types/src/order/service.ts | 128 +++++- .../src/workflow/order/begin-claim-order.ts | 10 + .../workflow/order/begin-exchange-order.ts | 7 + .../src/workflow/order/begin-return-order.ts | 7 + .../core/types/src/workflow/order/index.ts | 3 + .../medusa/src/api/admin/returns/route.ts | 4 +- .../src/api/admin/returns/validators.ts | 38 +- .../src/services/actions/register-shipment.ts | 2 +- .../src/services/order-module-service.ts | 132 +++++- 25 files changed, 1865 insertions(+), 33 deletions(-) create mode 100644 integration-tests/modules/__tests__/order/workflows/begin-order-claim.spec.ts create mode 100644 integration-tests/modules/__tests__/order/workflows/begin-order-exchange.spec.ts create mode 100644 integration-tests/modules/__tests__/order/workflows/begin-order-return.spec.ts create mode 100644 packages/core/core-flows/src/order/steps/create-claims.ts rename packages/core/core-flows/src/order/steps/{create-return.ts => create-complete-return.ts} (70%) create mode 100644 packages/core/core-flows/src/order/steps/create-exchanges.ts create mode 100644 packages/core/core-flows/src/order/steps/create-returns.ts create mode 100644 packages/core/core-flows/src/order/workflows/begin-order-claim.ts create mode 100644 packages/core/core-flows/src/order/workflows/begin-order-exchange.ts create mode 100644 packages/core/core-flows/src/order/workflows/begin-return.ts create mode 100644 packages/core/types/src/workflow/order/begin-claim-order.ts create mode 100644 packages/core/types/src/workflow/order/begin-exchange-order.ts create mode 100644 packages/core/types/src/workflow/order/begin-return-order.ts diff --git a/integration-tests/modules/__tests__/order/workflows/begin-order-claim.spec.ts b/integration-tests/modules/__tests__/order/workflows/begin-order-claim.spec.ts new file mode 100644 index 0000000000..22ae02853d --- /dev/null +++ b/integration-tests/modules/__tests__/order/workflows/begin-order-claim.spec.ts @@ -0,0 +1,393 @@ +import { + beginClaimOrderWorkflow, + createShippingOptionsWorkflow, +} from "@medusajs/core-flows" +import { + FulfillmentWorkflow, + IOrderModuleService, + IRegionModuleService, + IStockLocationService, + OrderWorkflow, + ProductDTO, + StockLocationDTO, +} from "@medusajs/types" +import { + ClaimType, + ContainerRegistrationKeys, + ModuleRegistrationName, + Modules, + RuleOperator, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { medusaIntegrationTestRunner } from "medusa-test-utils" + +jest.setTimeout(500000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const providerId = "manual_test-provider" + +async function prepareDataFixtures({ container }) { + const fulfillmentService = container.resolve( + ModuleRegistrationName.FULFILLMENT + ) + const salesChannelService = container.resolve( + ModuleRegistrationName.SALES_CHANNEL + ) + const stockLocationModule: IStockLocationService = container.resolve( + ModuleRegistrationName.STOCK_LOCATION + ) + const productModule = container.resolve(ModuleRegistrationName.PRODUCT) + const pricingModule = container.resolve(ModuleRegistrationName.PRICING) + const inventoryModule = container.resolve(ModuleRegistrationName.INVENTORY) + + const shippingProfile = await fulfillmentService.createShippingProfiles({ + name: "test", + type: "default", + }) + + const fulfillmentSet = await fulfillmentService.createFulfillmentSets({ + 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.createRegions([ + { + name: "Test region", + currency_code: "eur", + countries: ["fr"], + }, + ]) + + const salesChannel = await salesChannelService.createSalesChannels({ + name: "Webshop", + }) + + const location: StockLocationDTO = + await stockLocationModule.createStockLocations({ + name: "Warehouse", + address: { + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + phone: "12345", + }, + }) + + const [product] = await productModule.createProducts([ + { + title: "Test product", + variants: [ + { + title: "Test variant", + sku: "test-variant", + }, + ], + }, + ]) + + const inventoryItem = await inventoryModule.createInventoryItems({ + 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, + }, + }, + ]) + + await pricingModule.createPricePreferences([ + { + attribute: "currency_code", + value: "usd", + is_tax_inclusive: true, + }, + { + attribute: "region_id", + value: region.id, + is_tax_inclusive: true, + }, + ]) + + 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, + fulfillmentSet, + } +} + +async function createOrderFixture({ container, product }) { + const orderService: IOrderModuleService = container.resolve( + ModuleRegistrationName.ORDER + ) + let order = await orderService.createOrders({ + 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", + }) + + await orderService.addOrderAction([ + { + action: "FULFILL_ITEM", + order_id: order.id, + version: order.version, + reference: "fullfilment", + reference_id: "fulfill_123", + details: { + reference_id: order.items![0].id, + quantity: 1, + }, + }, + { + action: "SHIP_ITEM", + order_id: order.id, + version: order.version, + reference: "fullfilment", + reference_id: "fulfill_123", + details: { + reference_id: order.items![0].id, + quantity: 1, + }, + }, + ]) + + await orderService.applyPendingOrderActions(order.id) + + order = await orderService.retrieveOrder(order.id, { + relations: ["items"], + }) + + return order +} + +medusaIntegrationTestRunner({ + env, + testSuite: ({ getContainer }) => { + let container + + beforeAll(() => { + container = getContainer() + }) + + describe("Begin claim order workflow", () => { + let product: ProductDTO + + beforeEach(async () => { + const fixtures = await prepareDataFixtures({ + container, + }) + + product = fixtures.product + }) + + it("should begin a claim order", async () => { + const order = await createOrderFixture({ container, product }) + + const createClaimOrderData: OrderWorkflow.beginOrderClaimWorkflowInput = + { + type: ClaimType.REFUND, + order_id: order.id, + } + + await beginClaimOrderWorkflow(container).run({ + input: createClaimOrderData, + throwOnError: true, + }) + + const remoteQuery = container.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) + const remoteQueryObject = remoteQueryObjectFromString({ + entryPoint: "order_claim", + variables: { + order_id: createClaimOrderData.order_id, + }, + fields: ["order_id", "id", "type"], + }) + + const [returnOrder] = await remoteQuery(remoteQueryObject) + + expect(returnOrder.order_id).toEqual(order.id) + expect(returnOrder.type).toEqual("refund") + expect(returnOrder.id).toBeDefined() + }) + }) + }, +}) diff --git a/integration-tests/modules/__tests__/order/workflows/begin-order-exchange.spec.ts b/integration-tests/modules/__tests__/order/workflows/begin-order-exchange.spec.ts new file mode 100644 index 0000000000..a79bf41722 --- /dev/null +++ b/integration-tests/modules/__tests__/order/workflows/begin-order-exchange.spec.ts @@ -0,0 +1,400 @@ +import { + beginExchangeOrderWorkflow, + createShippingOptionsWorkflow, +} from "@medusajs/core-flows" +import { + FulfillmentWorkflow, + IOrderModuleService, + IRegionModuleService, + IStockLocationService, + OrderWorkflow, + ProductDTO, + StockLocationDTO, +} from "@medusajs/types" +import { + ContainerRegistrationKeys, + ModuleRegistrationName, + Modules, + RuleOperator, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { medusaIntegrationTestRunner } from "medusa-test-utils" + +jest.setTimeout(500000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const providerId = "manual_test-provider" + +async function prepareDataFixtures({ container }) { + const fulfillmentService = container.resolve( + ModuleRegistrationName.FULFILLMENT + ) + const salesChannelService = container.resolve( + ModuleRegistrationName.SALES_CHANNEL + ) + const stockLocationModule: IStockLocationService = container.resolve( + ModuleRegistrationName.STOCK_LOCATION + ) + const productModule = container.resolve(ModuleRegistrationName.PRODUCT) + const pricingModule = container.resolve(ModuleRegistrationName.PRICING) + const inventoryModule = container.resolve(ModuleRegistrationName.INVENTORY) + + const shippingProfile = await fulfillmentService.createShippingProfiles({ + name: "test", + type: "default", + }) + + const fulfillmentSet = await fulfillmentService.createFulfillmentSets({ + 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.createRegions([ + { + name: "Test region", + currency_code: "eur", + countries: ["fr"], + }, + ]) + + const salesChannel = await salesChannelService.createSalesChannels({ + name: "Webshop", + }) + + const location: StockLocationDTO = + await stockLocationModule.createStockLocations({ + name: "Warehouse", + address: { + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + phone: "12345", + }, + }) + + const [product] = await productModule.createProducts([ + { + title: "Test product", + variants: [ + { + title: "Test variant", + sku: "test-variant", + }, + ], + }, + ]) + + const inventoryItem = await inventoryModule.createInventoryItems({ + 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, + }, + }, + ]) + + await pricingModule.createPricePreferences([ + { + attribute: "currency_code", + value: "usd", + is_tax_inclusive: true, + }, + { + attribute: "region_id", + value: region.id, + is_tax_inclusive: true, + }, + ]) + + 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, + fulfillmentSet, + } +} + +async function createOrderFixture({ container, product }) { + const orderService: IOrderModuleService = container.resolve( + ModuleRegistrationName.ORDER + ) + let order = await orderService.createOrders({ + 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", + }) + + await orderService.addOrderAction([ + { + action: "FULFILL_ITEM", + order_id: order.id, + version: order.version, + reference: "fullfilment", + reference_id: "fulfill_123", + details: { + reference_id: order.items![0].id, + quantity: 1, + }, + }, + { + action: "SHIP_ITEM", + order_id: order.id, + version: order.version, + reference: "fullfilment", + reference_id: "fulfill_123", + details: { + reference_id: order.items![0].id, + quantity: 1, + }, + }, + ]) + + await orderService.applyPendingOrderActions(order.id) + + order = await orderService.retrieveOrder(order.id, { + relations: ["items"], + }) + + return order +} + +medusaIntegrationTestRunner({ + env, + testSuite: ({ getContainer }) => { + let container + + beforeAll(() => { + container = getContainer() + }) + + describe("Begin exchange order workflow", () => { + let product: ProductDTO + + beforeEach(async () => { + const fixtures = await prepareDataFixtures({ + container, + }) + + product = fixtures.product + }) + + it("should begin an exchange order", async () => { + const order = await createOrderFixture({ container, product }) + + const createExchangeOrderData: OrderWorkflow.beginOrderExchangeWorkflowInput = + { + order_id: order.id, + metadata: { + reason: "test", + extra: "data", + value: 1234, + }, + } + + await beginExchangeOrderWorkflow(container).run({ + input: createExchangeOrderData, + throwOnError: true, + }) + + const remoteQuery = container.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) + const remoteQueryObject = remoteQueryObjectFromString({ + entryPoint: "order_exchange", + variables: { + order_id: createExchangeOrderData.order_id, + }, + fields: ["order_id", "id", "metadata"], + }) + + const [returnOrder] = await remoteQuery(remoteQueryObject) + + expect(returnOrder.order_id).toEqual(order.id) + expect(returnOrder.metadata).toEqual({ + reason: "test", + extra: "data", + value: 1234, + }) + expect(returnOrder.id).toBeDefined() + }) + }) + }, +}) diff --git a/integration-tests/modules/__tests__/order/workflows/begin-order-return.spec.ts b/integration-tests/modules/__tests__/order/workflows/begin-order-return.spec.ts new file mode 100644 index 0000000000..cd0b476295 --- /dev/null +++ b/integration-tests/modules/__tests__/order/workflows/begin-order-return.spec.ts @@ -0,0 +1,391 @@ +import { + beginReturnOrderWorkflow, + createShippingOptionsWorkflow, +} from "@medusajs/core-flows" +import { + FulfillmentWorkflow, + IOrderModuleService, + IRegionModuleService, + IStockLocationService, + OrderWorkflow, + ProductDTO, + StockLocationDTO, +} from "@medusajs/types" +import { + ContainerRegistrationKeys, + ModuleRegistrationName, + Modules, + RuleOperator, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { medusaIntegrationTestRunner } from "medusa-test-utils" + +jest.setTimeout(500000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const providerId = "manual_test-provider" + +async function prepareDataFixtures({ container }) { + const fulfillmentService = container.resolve( + ModuleRegistrationName.FULFILLMENT + ) + const salesChannelService = container.resolve( + ModuleRegistrationName.SALES_CHANNEL + ) + const stockLocationModule: IStockLocationService = container.resolve( + ModuleRegistrationName.STOCK_LOCATION + ) + const productModule = container.resolve(ModuleRegistrationName.PRODUCT) + const pricingModule = container.resolve(ModuleRegistrationName.PRICING) + const inventoryModule = container.resolve(ModuleRegistrationName.INVENTORY) + + const shippingProfile = await fulfillmentService.createShippingProfiles({ + name: "test", + type: "default", + }) + + const fulfillmentSet = await fulfillmentService.createFulfillmentSets({ + 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.createRegions([ + { + name: "Test region", + currency_code: "eur", + countries: ["fr"], + }, + ]) + + const salesChannel = await salesChannelService.createSalesChannels({ + name: "Webshop", + }) + + const location: StockLocationDTO = + await stockLocationModule.createStockLocations({ + name: "Warehouse", + address: { + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + phone: "12345", + }, + }) + + const [product] = await productModule.createProducts([ + { + title: "Test product", + variants: [ + { + title: "Test variant", + sku: "test-variant", + }, + ], + }, + ]) + + const inventoryItem = await inventoryModule.createInventoryItems({ + 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, + }, + }, + ]) + + await pricingModule.createPricePreferences([ + { + attribute: "currency_code", + value: "usd", + is_tax_inclusive: true, + }, + { + attribute: "region_id", + value: region.id, + is_tax_inclusive: true, + }, + ]) + + 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, + fulfillmentSet, + } +} + +async function createOrderFixture({ container, product }) { + const orderService: IOrderModuleService = container.resolve( + ModuleRegistrationName.ORDER + ) + let order = await orderService.createOrders({ + 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", + }) + + await orderService.addOrderAction([ + { + action: "FULFILL_ITEM", + order_id: order.id, + version: order.version, + reference: "fullfilment", + reference_id: "fulfill_123", + details: { + reference_id: order.items![0].id, + quantity: 1, + }, + }, + { + action: "SHIP_ITEM", + order_id: order.id, + version: order.version, + reference: "fullfilment", + reference_id: "fulfill_123", + details: { + reference_id: order.items![0].id, + quantity: 1, + }, + }, + ]) + + await orderService.applyPendingOrderActions(order.id) + + order = await orderService.retrieveOrder(order.id, { + relations: ["items"], + }) + + return order +} + +medusaIntegrationTestRunner({ + env, + testSuite: ({ getContainer }) => { + let container + + beforeAll(() => { + container = getContainer() + }) + + describe("Begin return order workflow", () => { + let product: ProductDTO + + beforeEach(async () => { + const fixtures = await prepareDataFixtures({ + container, + }) + + product = fixtures.product + }) + + it("should begin a return order", async () => { + const order = await createOrderFixture({ container, product }) + + const createReturnOrderData: OrderWorkflow.beginOrderReturnWorkflowInput = + { + order_id: order.id, + } + + await beginReturnOrderWorkflow(container).run({ + input: createReturnOrderData, + throwOnError: true, + }) + + const remoteQuery = container.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) + const remoteQueryObject = remoteQueryObjectFromString({ + entryPoint: "return", + variables: { + order_id: createReturnOrderData.order_id, + }, + fields: ["order_id", "id", "status"], + }) + + const [returnOrder] = await remoteQuery(remoteQueryObject) + + expect(returnOrder.order_id).toEqual(order.id) + expect(returnOrder.status).toEqual("requested") + expect(returnOrder.id).toBeDefined() + }) + }) + }, +}) diff --git a/packages/core/core-flows/src/order/steps/create-claims.ts b/packages/core/core-flows/src/order/steps/create-claims.ts new file mode 100644 index 0000000000..c7458a6cde --- /dev/null +++ b/packages/core/core-flows/src/order/steps/create-claims.ts @@ -0,0 +1,32 @@ +import { CreateOrderClaimDTO, IOrderModuleService } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type CreateOrderClaimsStepInput = CreateOrderClaimDTO[] + +export const createOrderClaimsStepId = "create-order-claims" +export const createOrderClaimsStep = createStep( + createOrderClaimsStepId, + async (data: CreateOrderClaimsStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + const orderClaims = await service.createOrderClaims(data) + + const claimIds = orderClaims.map((claim) => claim.id) + + return new StepResponse(orderClaims, claimIds) + }, + async (claimIds, { container }) => { + if (!claimIds) { + return + } + + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.deleteOrderClaims(claimIds) + } +) diff --git a/packages/core/core-flows/src/order/steps/create-return.ts b/packages/core/core-flows/src/order/steps/create-complete-return.ts similarity index 70% rename from packages/core/core-flows/src/order/steps/create-return.ts rename to packages/core/core-flows/src/order/steps/create-complete-return.ts index eef5c2b3ec..4ca9f58821 100644 --- a/packages/core/core-flows/src/order/steps/create-return.ts +++ b/packages/core/core-flows/src/order/steps/create-complete-return.ts @@ -2,12 +2,12 @@ import { CreateOrderReturnDTO, IOrderModuleService } from "@medusajs/types" import { ModuleRegistrationName } from "@medusajs/utils" import { StepResponse, createStep } from "@medusajs/workflows-sdk" -type CreateReturnStepInput = CreateOrderReturnDTO +type CreateCompleteReturnStepInput = CreateOrderReturnDTO -export const createReturnStepId = "create-return" -export const createReturnStep = createStep( - createReturnStepId, - async (data: CreateReturnStepInput, { container }) => { +export const createCompleteReturnStepId = "create-complete-return" +export const createCompleteReturnStep = createStep( + createCompleteReturnStepId, + async (data: CreateCompleteReturnStepInput, { container }) => { const service = container.resolve( ModuleRegistrationName.ORDER ) diff --git a/packages/core/core-flows/src/order/steps/create-exchanges.ts b/packages/core/core-flows/src/order/steps/create-exchanges.ts new file mode 100644 index 0000000000..eac3c8ae37 --- /dev/null +++ b/packages/core/core-flows/src/order/steps/create-exchanges.ts @@ -0,0 +1,32 @@ +import { CreateOrderExchangeDTO, IOrderModuleService } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type CreateOrderExchangesStepInput = CreateOrderExchangeDTO[] + +export const createOrderExchangesStepId = "create-order-exchanges" +export const createOrderExchangesStep = createStep( + createOrderExchangesStepId, + async (data: CreateOrderExchangesStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + const orderExchanges = await service.createOrderExchanges(data) + + const exchangeIds = orderExchanges.map((exchange) => exchange.id) + + return new StepResponse(orderExchanges, exchangeIds) + }, + async (exchangeIds, { container }) => { + if (!exchangeIds) { + return + } + + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.deleteOrderExchanges(exchangeIds) + } +) diff --git a/packages/core/core-flows/src/order/steps/create-returns.ts b/packages/core/core-flows/src/order/steps/create-returns.ts new file mode 100644 index 0000000000..4d54c0c14a --- /dev/null +++ b/packages/core/core-flows/src/order/steps/create-returns.ts @@ -0,0 +1,32 @@ +import { CreateOrderReturnDTO, IOrderModuleService } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type CreateReturnsStepInput = CreateOrderReturnDTO[] + +export const createReturnsStepId = "create-returns" +export const createReturnsStep = createStep( + createReturnsStepId, + async (data: CreateReturnsStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + const orderReturns = await service.createReturns(data) + + const returnIds = orderReturns.map((ret) => ret.id) + + return new StepResponse(orderReturns, returnIds) + }, + async (returnIds, { container }) => { + if (!returnIds) { + return + } + + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.deleteReturns(returnIds) + } +) diff --git a/packages/core/core-flows/src/order/steps/index.ts b/packages/core/core-flows/src/order/steps/index.ts index d7e3260aad..74c13e4911 100644 --- a/packages/core/core-flows/src/order/steps/index.ts +++ b/packages/core/core-flows/src/order/steps/index.ts @@ -5,9 +5,13 @@ export * from "./cancel-order-change" export * from "./cancel-orders" export * from "./cancel-return" export * from "./complete-orders" +export * from "./create-claims" +export * from "./create-complete-return" +export * from "./create-exchanges" export * from "./create-order-change" export * from "./create-order-change-actions" export * from "./create-orders" +export * from "./create-returns" export * from "./decline-order-change" export * from "./delete-order-change" export * from "./delete-order-change-actions" diff --git a/packages/core/core-flows/src/order/utils/order-validation.ts b/packages/core/core-flows/src/order/utils/order-validation.ts index 2df92b8c95..d227d478cc 100644 --- a/packages/core/core-flows/src/order/utils/order-validation.ts +++ b/packages/core/core-flows/src/order/utils/order-validation.ts @@ -18,7 +18,7 @@ export function throwIfItemsDoesNotExistsInOrder({ inputItems: OrderWorkflow.CreateOrderFulfillmentWorkflowInput["items"] }) { const orderItemIds = order.items?.map((i) => i.id) ?? [] - const inputItemIds = inputItems.map((i) => i.id) + const inputItemIds = inputItems?.map((i) => i.id) const diff = arrayDifference(inputItemIds, orderItemIds) if (diff.length) { diff --git a/packages/core/core-flows/src/order/workflows/begin-order-claim.ts b/packages/core/core-flows/src/order/workflows/begin-order-claim.ts new file mode 100644 index 0000000000..c02813f4fc --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/begin-order-claim.ts @@ -0,0 +1,59 @@ +import { OrderChangeDTO, OrderDTO, OrderWorkflow } from "@medusajs/types" +import { + WorkflowData, + createStep, + createWorkflow, + transform, +} from "@medusajs/workflows-sdk" +import { useRemoteQueryStep } from "../../common" +import { createOrderClaimsStep } from "../steps/create-claims" +import { createOrderChangeStep } from "../steps/create-order-change" +import { throwIfOrderIsCancelled } from "../utils/order-validation" + +const validationStep = createStep( + "begin-claim-order-validation", + async function ({ order }: { order: OrderDTO }) { + throwIfOrderIsCancelled({ order }) + } +) + +export const beginClaimOrderWorkflowId = "begin-claim-order" +export const beginClaimOrderWorkflow = createWorkflow( + beginClaimOrderWorkflowId, + function ( + input: WorkflowData + ): WorkflowData { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: ["id", "status"], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }) + + validationStep({ order }) + + const created = createOrderClaimsStep([ + { + type: input.type, + order_id: input.order_id, + metadata: input.metadata, + }, + ]) + + const orderChangeInput = transform( + { created, input }, + ({ created, input }) => { + return { + change_type: "claim" as const, + order_id: input.order_id, + claim_id: created[0].id, + created_by: input.created_by, + description: input.description, + internal_note: input.internal_note, + } + } + ) + return createOrderChangeStep(orderChangeInput) + } +) diff --git a/packages/core/core-flows/src/order/workflows/begin-order-exchange.ts b/packages/core/core-flows/src/order/workflows/begin-order-exchange.ts new file mode 100644 index 0000000000..45183b1523 --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/begin-order-exchange.ts @@ -0,0 +1,58 @@ +import { OrderChangeDTO, OrderDTO, OrderWorkflow } from "@medusajs/types" +import { + WorkflowData, + createStep, + createWorkflow, + transform, +} from "@medusajs/workflows-sdk" +import { useRemoteQueryStep } from "../../common" +import { createOrderExchangesStep } from "../steps/create-exchanges" +import { createOrderChangeStep } from "../steps/create-order-change" +import { throwIfOrderIsCancelled } from "../utils/order-validation" + +const validationStep = createStep( + "begin-exchange-order-validation", + async function ({ order }: { order: OrderDTO }) { + throwIfOrderIsCancelled({ order }) + } +) + +export const beginExchangeOrderWorkflowId = "begin-exchange-order" +export const beginExchangeOrderWorkflow = createWorkflow( + beginExchangeOrderWorkflowId, + function ( + input: WorkflowData + ): WorkflowData { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: ["id", "status"], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }) + + validationStep({ order }) + + const created = createOrderExchangesStep([ + { + order_id: input.order_id, + metadata: input.metadata, + }, + ]) + + const orderChangeInput = transform( + { created, input }, + ({ created, input }) => { + return { + change_type: "exchange" as const, + order_id: input.order_id, + exchange_id: created[0].id, + created_by: input.created_by, + description: input.description, + internal_note: input.internal_note, + } + } + ) + return createOrderChangeStep(orderChangeInput) + } +) diff --git a/packages/core/core-flows/src/order/workflows/begin-return.ts b/packages/core/core-flows/src/order/workflows/begin-return.ts new file mode 100644 index 0000000000..3d4629bee3 --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/begin-return.ts @@ -0,0 +1,58 @@ +import { OrderChangeDTO, OrderDTO, OrderWorkflow } from "@medusajs/types" +import { + WorkflowData, + createStep, + createWorkflow, + transform, +} from "@medusajs/workflows-sdk" +import { useRemoteQueryStep } from "../../common" +import { createOrderChangeStep } from "../steps/create-order-change" +import { createReturnsStep } from "../steps/create-returns" +import { throwIfOrderIsCancelled } from "../utils/order-validation" + +const validationStep = createStep( + "begin-return-order-validation", + async function ({ order }: { order: OrderDTO }) { + throwIfOrderIsCancelled({ order }) + } +) + +export const beginReturnOrderWorkflowId = "begin-return-order" +export const beginReturnOrderWorkflow = createWorkflow( + beginReturnOrderWorkflowId, + function ( + input: WorkflowData + ): WorkflowData { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: ["id", "status"], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }) + + validationStep({ order }) + + const created = createReturnsStep([ + { + order_id: input.order_id, + metadata: input.metadata, + }, + ]) + + const orderChangeInput = transform( + { created, input }, + ({ created, input }) => { + return { + change_type: "return" as const, + order_id: input.order_id, + return_id: created[0].id, + created_by: input.created_by, + description: input.description, + internal_note: input.internal_note, + } + } + ) + return createOrderChangeStep(orderChangeInput) + } +) diff --git a/packages/core/core-flows/src/order/workflows/create-complete-return.ts b/packages/core/core-flows/src/order/workflows/create-complete-return.ts index 23ff38e5fc..a421fbe5d0 100644 --- a/packages/core/core-flows/src/order/workflows/create-complete-return.ts +++ b/packages/core/core-flows/src/order/workflows/create-complete-return.ts @@ -25,7 +25,7 @@ import { } from "@medusajs/workflows-sdk" import { createRemoteLinkStep, useRemoteQueryStep } from "../../common" import { createReturnFulfillmentWorkflow } from "../../fulfillment" -import { createReturnStep } from "../steps/create-return" +import { createCompleteReturnStep } from "../steps/create-complete-return" import { receiveReturnStep } from "../steps/receive-return" import { throwIfItemsDoesNotExistsInOrder, @@ -273,6 +273,13 @@ const validationStep = createStep( }, context ) { + if (!input.items) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Items are required to create a return.` + ) + } + throwIfOrderIsCancelled({ order }) throwIfItemsDoesNotExistsInOrder({ order, inputItems: input.items }) await validateReturnReasons( @@ -359,7 +366,7 @@ export const createAndCompleteReturnOrderWorkflow = createWorkflow( ) const [returnCreated] = parallelize( - createReturnStep({ + createCompleteReturnStep({ order_id: input.order_id, items: input.items, shipping_method: shippingMethodData, diff --git a/packages/core/core-flows/src/order/workflows/index.ts b/packages/core/core-flows/src/order/workflows/index.ts index 229924348d..d0b59a91bd 100644 --- a/packages/core/core-flows/src/order/workflows/index.ts +++ b/packages/core/core-flows/src/order/workflows/index.ts @@ -1,4 +1,7 @@ export * from "./archive-orders" +export * from "./begin-order-claim" +export * from "./begin-order-exchange" +export * from "./begin-return" export * from "./cancel-order" export * from "./cancel-order-change" export * from "./cancel-order-fulfillment" diff --git a/packages/core/types/src/order/common.ts b/packages/core/types/src/order/common.ts index 6337c6547d..4b156b7ef9 100644 --- a/packages/core/types/src/order/common.ts +++ b/packages/core/types/src/order/common.ts @@ -1609,13 +1609,39 @@ export interface FilterableOrderItemProps item_id?: string | string[] | OperatorMap } -export interface FilterableOrderReturnReasonProps { +export interface FilterableOrderReturnReasonProps + extends BaseFilterable { id?: string | string[] value?: string | string[] label?: string description?: string } +export interface FilterableReturnProps + extends BaseFilterable { + id?: string | string[] + order_id?: string | string[] + claim_id?: string | string[] + exchange_id?: string | string[] + status?: string | string[] + refund_amount?: string | string[] +} + +export interface FilterableOrderClaimProps + extends BaseFilterable { + id?: string | string[] + order_id?: string | string[] + return_id?: string | string[] +} + +export interface FilterableOrderExchangeProps + extends BaseFilterable { + id?: string | string[] + order_id?: string | string[] + return_id?: string | string[] + allow_backorder?: boolean +} + export interface OrderChangeReturn { items: { item_id: string diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts index 75dc6ddbc0..a37be7348e 100644 --- a/packages/core/types/src/order/mutations.ts +++ b/packages/core/types/src/order/mutations.ts @@ -1,10 +1,13 @@ import { BigNumberInput } from "../totals" import { ChangeActionType, + OrderClaimDTO, + OrderExchangeDTO, OrderItemDTO, OrderLineItemDTO, OrderReturnReasonDTO, OrderTransactionDTO, + ReturnDTO, } from "./common" /** ADDRESS START */ @@ -256,6 +259,7 @@ export interface CreateOrderChangeDTO { return_id?: string claim_id?: string exchange_id?: string + change_type?: "return" | "exchange" | "claim" | "edit" description?: string internal_note?: string | null requested_by?: string @@ -412,24 +416,64 @@ export interface CancelOrderFulfillmentDTO extends BaseOrderBundledActionsDTO { } export interface RegisterOrderShipmentDTO extends BaseOrderBundledActionsDTO { - items: BaseOrderBundledItemActionsDTO[] + items?: BaseOrderBundledItemActionsDTO[] no_notification?: boolean } export interface CreateOrderReturnDTO extends BaseOrderBundledActionsDTO { - items: { + items?: { id: string quantity: BigNumberInput internal_note?: string | null note?: string | null reason_id?: string | null - metadata?: Record + metadata?: Record | null }[] shipping_method?: Omit | string refund_amount?: BigNumberInput no_notification?: boolean + claim_id?: string + exchange_id?: string } +export interface UpdateOrderReturnDTO { + refund_amount?: BigNumberInput + no_notification?: boolean + claim_id?: string + exchange_id?: string + metadata?: Record | null +} + +export interface UpdateOrderClaimDTO { + refund_amount?: BigNumberInput + no_notification?: boolean + return_id?: string + type?: OrderClaimType + metadata?: Record | null +} + +export interface UpdateOrderExchangeDTO { + difference_due?: BigNumberInput + no_notification?: boolean + return_id?: string + allow_backorder?: boolean + metadata?: Record | null +} + +export interface UpdateOrderReturnWithSelectorDTO { + selector: Partial + data: Partial +} + +export interface UpdateOrderClaimWithSelectorDTO { + selector: Partial + data: Partial +} + +export interface UpdateOrderExchangeWithSelectorDTO { + selector: Partial + data: Partial +} export interface CancelOrderReturnDTO extends Omit { return_id: string @@ -443,11 +487,11 @@ export type ClaimReason = | "other" export interface CreateOrderClaimDTO extends BaseOrderBundledActionsDTO { type: OrderClaimType - claim_items: (BaseOrderBundledItemActionsDTO & { + claim_items?: (BaseOrderBundledItemActionsDTO & { reason: ClaimReason images?: { url: string - metadata?: Record + metadata?: Record | null }[] })[] additional_items?: BaseOrderBundledItemActionsDTO[] diff --git a/packages/core/types/src/order/service.ts b/packages/core/types/src/order/service.ts index 5aae2327f6..793e462f88 100644 --- a/packages/core/types/src/order/service.ts +++ b/packages/core/types/src/order/service.ts @@ -5,6 +5,8 @@ import { Context } from "../shared-context" import { FilterableOrderAddressProps, FilterableOrderChangeActionProps, + FilterableOrderClaimProps, + FilterableOrderExchangeProps, FilterableOrderLineItemAdjustmentProps, FilterableOrderLineItemProps, FilterableOrderLineItemTaxLineProps, @@ -14,6 +16,7 @@ import { FilterableOrderShippingMethodProps, FilterableOrderShippingMethodTaxLineProps, FilterableOrderTransactionProps, + FilterableReturnProps, OrderAddressDTO, OrderChangeActionDTO, OrderChangeDTO, @@ -62,14 +65,20 @@ import { UpdateOrderAddressDTO, UpdateOrderChangeActionDTO, UpdateOrderChangeDTO, + UpdateOrderClaimDTO, + UpdateOrderClaimWithSelectorDTO, UpdateOrderDTO, + UpdateOrderExchangeDTO, + UpdateOrderExchangeWithSelectorDTO, UpdateOrderItemDTO, UpdateOrderItemWithSelectorDTO, UpdateOrderLineItemDTO, UpdateOrderLineItemTaxLineDTO, UpdateOrderLineItemWithSelectorDTO, + UpdateOrderReturnDTO, UpdateOrderReturnReasonDTO, UpdateOrderReturnReasonWithSelectorDTO, + UpdateOrderReturnWithSelectorDTO, UpdateOrderShippingMethodAdjustmentDTO, UpdateOrderShippingMethodTaxLineDTO, UpsertOrderLineItemAdjustmentDTO, @@ -1865,13 +1874,128 @@ export interface IOrderModuleService extends IModuleService { ): Promise softDeleteReturnReasons( - storeIds: string[], + ids: string[], config?: SoftDeleteReturn, sharedContext?: Context ): Promise | void> restoreReturnReasons( - storeIds: string[], + ids: string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> + + createReturns( + data: CreateOrderReturnDTO, + sharedContext?: Context + ): Promise + + createReturns( + data: CreateOrderReturnDTO[], + sharedContext?: Context + ): Promise + + updateReturns(data: UpdateOrderReturnWithSelectorDTO[]): Promise + + updateReturns( + selector: Partial, + data: Partial, + sharedContext?: Context + ): Promise + updateReturns( + id: string, + data: Partial, + sharedContext?: Context + ): Promise + + deleteReturns(ids: string[], sharedContext?: Context): Promise + + softDeleteReturns( + ids: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + restoreReturns( + ids: string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> + + createOrderClaims( + data: CreateOrderClaimDTO, + sharedContext?: Context + ): Promise + + createOrderClaims( + data: CreateOrderClaimDTO[], + sharedContext?: Context + ): Promise + + updateOrderClaims( + data: UpdateOrderClaimWithSelectorDTO[] + ): Promise + + updateOrderClaims( + selector: Partial, + data: Partial, + sharedContext?: Context + ): Promise + updateOrderClaims( + id: string, + data: Partial, + sharedContext?: Context + ): Promise + + deleteOrderClaims(ids: string[], sharedContext?: Context): Promise + + softDeleteOrderClaims( + ids: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + restoreOrderClaims( + ids: string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> + + createOrderExchanges( + data: CreateOrderExchangeDTO, + sharedContext?: Context + ): Promise + + createOrderExchanges( + data: CreateOrderExchangeDTO[], + sharedContext?: Context + ): Promise + + updateOrderExchanges( + data: UpdateOrderExchangeWithSelectorDTO[] + ): Promise + + updateOrderExchanges( + selector: Partial, + data: Partial, + sharedContext?: Context + ): Promise + updateOrderExchanges( + id: string, + data: Partial, + sharedContext?: Context + ): Promise + + deleteOrderExchanges(ids: string[], sharedContext?: Context): Promise + + softDeleteOrderExchanges( + ids: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + restoreOrderExchanges( + ids: string[], config?: RestoreReturn, sharedContext?: Context ): Promise | void> diff --git a/packages/core/types/src/workflow/order/begin-claim-order.ts b/packages/core/types/src/workflow/order/begin-claim-order.ts new file mode 100644 index 0000000000..f5c0721488 --- /dev/null +++ b/packages/core/types/src/workflow/order/begin-claim-order.ts @@ -0,0 +1,10 @@ +import { OrderClaimType } from "../../order/mutations" + +export interface beginOrderClaimWorkflowInput { + type: OrderClaimType + order_id: string + created_by?: string + internal_note?: string + description?: string + metadata?: Record | null +} diff --git a/packages/core/types/src/workflow/order/begin-exchange-order.ts b/packages/core/types/src/workflow/order/begin-exchange-order.ts new file mode 100644 index 0000000000..aa03e074c6 --- /dev/null +++ b/packages/core/types/src/workflow/order/begin-exchange-order.ts @@ -0,0 +1,7 @@ +export interface beginOrderExchangeWorkflowInput { + order_id: string + created_by?: string + internal_note?: string + description?: string + metadata?: Record | null +} diff --git a/packages/core/types/src/workflow/order/begin-return-order.ts b/packages/core/types/src/workflow/order/begin-return-order.ts new file mode 100644 index 0000000000..45b3aaecf9 --- /dev/null +++ b/packages/core/types/src/workflow/order/begin-return-order.ts @@ -0,0 +1,7 @@ +export interface beginOrderReturnWorkflowInput { + order_id: string + created_by?: string + internal_note?: string + description?: string + metadata?: Record | null +} diff --git a/packages/core/types/src/workflow/order/index.ts b/packages/core/types/src/workflow/order/index.ts index 1783ad2855..6249498429 100644 --- a/packages/core/types/src/workflow/order/index.ts +++ b/packages/core/types/src/workflow/order/index.ts @@ -1,3 +1,6 @@ +export * from "./begin-claim-order" +export * from "./begin-exchange-order" +export * from "./begin-return-order" export * from "./cancel-claim" export * from "./cancel-exchange" export * from "./cancel-fulfillment" diff --git a/packages/medusa/src/api/admin/returns/route.ts b/packages/medusa/src/api/admin/returns/route.ts index f07ae8f763..1c707cfca9 100644 --- a/packages/medusa/src/api/admin/returns/route.ts +++ b/packages/medusa/src/api/admin/returns/route.ts @@ -1,4 +1,4 @@ -import { createAndCompleteReturnOrderWorkflow } from "@medusajs/core-flows" +import { beginReturnOrderWorkflow } from "@medusajs/core-flows" import { ContainerRegistrationKeys, remoteQueryObjectFromString, @@ -42,7 +42,7 @@ export const POST = async ( ) => { const input = req.validatedBody as AdminPostReturnsReqSchemaType - const workflow = createAndCompleteReturnOrderWorkflow(req.scope) + const workflow = beginReturnOrderWorkflow(req.scope) const { result } = await workflow.run({ input, }) diff --git a/packages/medusa/src/api/admin/returns/validators.ts b/packages/medusa/src/api/admin/returns/validators.ts index a57166ae28..576784701d 100644 --- a/packages/medusa/src/api/admin/returns/validators.ts +++ b/packages/medusa/src/api/admin/returns/validators.ts @@ -1,3 +1,4 @@ +import { ClaimType } from "@medusajs/utils" import { z } from "zod" import { createFindParams, @@ -37,31 +38,44 @@ export const AdminGetOrdersParams = createFindParams({ export type AdminGetOrdersParamsType = z.infer -const ReturnShippingSchema = z.object({ - option_id: z.string(), - price: z.number().optional(), -}) - const ItemSchema = z.object({ id: z.string(), quantity: z.number().min(1), reason_id: z.string().nullish(), - note: z.string().nullish(), + note: z.string().optional(), }) export const AdminPostReturnsReqSchema = z.object({ order_id: z.string(), - items: z.array(ItemSchema), - return_shipping: ReturnShippingSchema.optional(), - internal_note: z.string().nullish(), - receive_now: z.boolean().optional(), - refund_amount: z.number().optional(), - location_id: z.string().nullish(), + description: z.string().optional(), + internal_note: z.string().optional(), + metadata: z.record(z.unknown()).nullish(), }) export type AdminPostReturnsReqSchemaType = z.infer< typeof AdminPostReturnsReqSchema > +export const AdminPostOrderClaimsReqSchema = z.object({ + type: z.nativeEnum(ClaimType), + order_id: z.string(), + description: z.string().optional(), + internal_note: z.string().optional(), + metadata: z.record(z.unknown()).nullish(), +}) +export type AdminPostOrderClaimsReqSchemaType = z.infer< + typeof AdminPostOrderClaimsReqSchema +> + +export const AdminPostOrderExchangesReqSchema = z.object({ + order_id: z.string(), + description: z.string().optional(), + internal_note: z.string().optional(), + metadata: z.record(z.unknown()).nullish(), +}) +export type AdminPostOrderExchangesReqSchemaType = z.infer< + typeof AdminPostOrderExchangesReqSchema +> + export const AdminPostReceiveReturnsReqSchema = z.object({ return_id: z.string(), items: z.array(ItemSchema), diff --git a/packages/modules/order/src/services/actions/register-shipment.ts b/packages/modules/order/src/services/actions/register-shipment.ts index 27de517052..68175aeeea 100644 --- a/packages/modules/order/src/services/actions/register-shipment.ts +++ b/packages/modules/order/src/services/actions/register-shipment.ts @@ -12,7 +12,7 @@ export async function registerShipment( ): Promise { let shippingMethodId - const actions: CreateOrderChangeActionDTO[] = data.items.map((item) => { + const actions: CreateOrderChangeActionDTO[] = data.items!.map((item) => { return { action: ChangeActionType.SHIP_ITEM, internal_note: item.internal_note, diff --git a/packages/modules/order/src/services/order-module-service.ts b/packages/modules/order/src/services/order-module-service.ts index 350f797afb..e128dfc97d 100644 --- a/packages/modules/order/src/services/order-module-service.ts +++ b/packages/modules/order/src/services/order-module-service.ts @@ -1718,6 +1718,134 @@ export default class OrderModuleService< }) } + // @ts-ignore + async createReturns( + data: OrderTypes.CreateOrderReturnDTO, + sharedContext?: Context + ): Promise + + async createReturns( + data: OrderTypes.CreateOrderReturnDTO[], + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + async createReturns( + data: OrderTypes.CreateOrderReturnDTO | OrderTypes.CreateOrderReturnDTO[], + @MedusaContext() sharedContext?: Context + ): Promise { + const created = await this.createOrderRelatedEntity_( + data, + this.returnService_, + sharedContext + ) + + return await this.baseRepository_.serialize( + !Array.isArray(data) ? created[0] : created, + { + populate: true, + } + ) + } + + // @ts-ignore + async createOrderClaims( + data: OrderTypes.CreateOrderClaimDTO, + sharedContext?: Context + ): Promise + + async createOrderClaims( + data: OrderTypes.CreateOrderClaimDTO[], + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + async createOrderClaims( + data: OrderTypes.CreateOrderClaimDTO | OrderTypes.CreateOrderClaimDTO[], + @MedusaContext() sharedContext?: Context + ): Promise { + const created = await this.createOrderRelatedEntity_( + data, + this.orderClaimService_, + sharedContext + ) + + return await this.baseRepository_.serialize( + !Array.isArray(data) ? created[0] : created, + { + populate: true, + } + ) + } + + // @ts-ignore + async createOrderExchanges( + data: OrderTypes.CreateOrderExchangeDTO, + sharedContext?: Context + ): Promise + + async createOrderExchanges( + data: OrderTypes.CreateOrderExchangeDTO[], + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + async createOrderExchanges( + data: + | OrderTypes.CreateOrderExchangeDTO + | OrderTypes.CreateOrderExchangeDTO[], + @MedusaContext() sharedContext?: Context + ): Promise { + const created = await this.createOrderRelatedEntity_( + data, + this.orderExchangeService_, + sharedContext + ) + + return await this.baseRepository_.serialize( + !Array.isArray(data) ? created[0] : created, + { + populate: true, + } + ) + } + + @InjectTransactionManager("baseRepository_") + private async createOrderRelatedEntity_( + data: any, + service: any, + sharedContext?: Context + ) { + const data_ = Array.isArray(data) ? data : [data] + + const inputDataMap = data_.reduce((acc, curr) => { + acc[curr.order_id] = curr + return acc + }, {}) + + const orderIds = data_.map((d) => d.order_id) + const orders = await this.orderService_.list( + { id: orderIds }, + { select: ["id", "version"] }, + sharedContext + ) + + if (orders.length !== orderIds.length) { + const foundOrders = orders.map((o) => o.id) + const missing = orderIds.filter((id) => !foundOrders.includes(id)) + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Order could not be found: ${missing.join(", ")}` + ) + } + + for (const order of orders) { + inputDataMap[order.id].order_version = order.version + } + + return await service.create(data_, sharedContext) + } + async createOrderChange( data: CreateOrderChangeDTO, sharedContext?: Context @@ -1770,7 +1898,7 @@ export default class OrderModuleService< dataMap[change.order_id] = change } - const orders = await this.listOrders( + const orders = await this.orderService_.list( { id: orderIds }, { select: ["id", "version"] }, sharedContext @@ -2013,7 +2141,7 @@ export default class OrderModuleService< ): Promise { const orderIds = Array.isArray(orderId) ? orderId : [orderId] - const orders = await this.listOrders( + const orders = await this.orderService_.list( { id: orderIds }, { select: ["id", "version"],