From 17b2868a505db56d9a8efa7a90e87ad30829d4d0 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Sat, 28 Sep 2024 16:01:48 +0200 Subject: [PATCH] feat(medusa,fulfillment): pass stock location data to fulfillment provider (#9322) **What** - Fetches the stock location's details when creating a fulfillment and return fulfillment. - Passes the data to the fulfillment module, which in turn passes it to the fulfillment provider. **Why** - When creating a fulfillment in a multi-location setup the fulfillment provider will need to know where the package is being sent from (so the shipping service can pick it up). - Previously, we didn't pass anything but the location id to the fulfillment provider. Because the fulfillment provider can't have a dependency on the stock location module this was not sufficient. - This change ensures there is enough data passed to the fulfillment provider to build integrations properly. --- .../__tests__/fixtures/fulfillment/index.ts | 7 +- .../fulfillment/fulfillment.workflows.spec.ts | 118 +++++++++++++++++- .../__tests__/fulfillment/index.spec.ts | 40 +++++- .../workflows/create-fulfillment.ts | 51 +++++++- .../workflows/create-return-fulfillment.ts | 42 ++++++- .../src/fulfillment/mutations/fulfillment.ts | 6 + 6 files changed, 248 insertions(+), 16 deletions(-) diff --git a/integration-tests/modules/__tests__/fixtures/fulfillment/index.ts b/integration-tests/modules/__tests__/fixtures/fulfillment/index.ts index ee670c4e8fa64..4e53d86542b42 100644 --- a/integration-tests/modules/__tests__/fixtures/fulfillment/index.ts +++ b/integration-tests/modules/__tests__/fixtures/fulfillment/index.ts @@ -9,12 +9,13 @@ export function generateCreateFulfillmentData( provider_id: string shipping_option_id: string order_id: string + location_id: string } ) { const randomString = Math.random().toString(36).substring(7) return { - location_id: "test-location", + location_id: data.location_id, packed_at: null, shipped_at: null, delivered_at: null, @@ -97,8 +98,10 @@ export async function setupFullDataFulfillmentStructure( service: IFulfillmentModuleService, { providerId, + locationId, }: { providerId: string + locationId: string } ) { const randomString = Math.random().toString(36).substring(7) @@ -133,6 +136,8 @@ export async function setupFullDataFulfillmentStructure( await service.createFulfillment( generateCreateFulfillmentData({ + order_id: "fake-order", + location_id: locationId, provider_id: providerId, shipping_option_id: shippingOption.id, }) diff --git a/integration-tests/modules/__tests__/fulfillment/fulfillment.workflows.spec.ts b/integration-tests/modules/__tests__/fulfillment/fulfillment.workflows.spec.ts index 163cfab8a5636..e865c1cc1b5d1 100644 --- a/integration-tests/modules/__tests__/fulfillment/fulfillment.workflows.spec.ts +++ b/integration-tests/modules/__tests__/fulfillment/fulfillment.workflows.spec.ts @@ -6,8 +6,12 @@ import { updateFulfillmentWorkflow, updateFulfillmentWorkflowId, } from "@medusajs/core-flows" -import { IFulfillmentModuleService } from "@medusajs/types" -import { Modules } from "@medusajs/utils" +import { + IFulfillmentModuleService, + MedusaContainer, + StockLocationDTO, +} from "@medusajs/types" +import { ContainerRegistrationKeys, Modules } from "@medusajs/utils" import { medusaIntegrationTestRunner } from "medusa-test-utils" import { generateCreateFulfillmentData, @@ -22,7 +26,8 @@ medusaIntegrationTestRunner({ env: { MEDUSA_FF_MEDUSA_V2: true }, testSuite: ({ getContainer }) => { describe("Workflows: Fulfillment", () => { - let appContainer + let location: StockLocationDTO + let appContainer: MedusaContainer let service: IFulfillmentModuleService beforeAll(async () => { @@ -30,7 +35,101 @@ medusaIntegrationTestRunner({ service = appContainer.resolve(Modules.FULFILLMENT) }) + beforeEach(async () => { + const stockLocationService = appContainer.resolve( + Modules.STOCK_LOCATION + ) + + location = await stockLocationService.createStockLocations({ + name: "Test Location", + address: { + address_1: "Test Address", + address_2: "tttest", + city: "Test City", + country_code: "us", + postal_code: "12345", + metadata: { email: "test@mail.com" }, + }, + metadata: { custom_location: "yes" }, + }) + }) + describe("createFulfillmentWorkflow", () => { + describe("invoke", () => { + it("should get stock location", async () => { + const workflow = createFulfillmentWorkflow(appContainer) + + const link = appContainer.resolve( + ContainerRegistrationKeys.REMOTE_LINK + ) + + const shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + + const fulfillmentSet = await service.createFulfillmentSets({ + name: "test", + type: "test-type", + }) + + await link.create({ + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: fulfillmentSet.id, + }, + }) + + const serviceZone = await service.createServiceZones({ + name: "test", + fulfillment_set_id: fulfillmentSet.id, + }) + + const shippingOption = await service.createShippingOptions( + generateCreateShippingOptionsData({ + provider_id: providerId, + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + }) + ) + + const data = generateCreateFulfillmentData({ + provider_id: providerId, + shipping_option_id: shippingOption.id, + order_id: "fake-order", + location_id: location.id, + }) + + const { transaction } = await workflow.run({ + input: data, + throwOnError: true, + }) + + expect( + transaction.context.invoke["get-location"].output.output + ).toEqual({ + id: expect.any(String), + created_at: expect.any(Date), + updated_at: expect.any(Date), + name: "Test Location", + address: { + id: expect.any(String), + address_1: "Test Address", + address_2: "tttest", + city: "Test City", + country_code: "us", + postal_code: "12345", + metadata: { email: "test@mail.com" }, + phone: null, + province: null, + }, + metadata: { custom_location: "yes" }, + }) + }) + }) + describe("compensation", () => { it("should cancel created fulfillment if step following step throws error", async () => { const workflow = createFulfillmentWorkflow(appContainer) @@ -70,6 +169,7 @@ medusaIntegrationTestRunner({ provider_id: providerId, shipping_option_id: shippingOption.id, order_id: "fake-order", + location_id: location.id, }) const { errors } = await workflow.run({ input: data, @@ -130,11 +230,16 @@ medusaIntegrationTestRunner({ ) const data = generateCreateFulfillmentData({ + order_id: "fake-order", provider_id: providerId, shipping_option_id: shippingOption.id, + location_id: location.id, }) - const fulfillment = await service.createFulfillment(data) + const fulfillment = await service.createFulfillment({ + ...data, + location, + }) const date = new Date() const { errors } = await workflow.run({ @@ -142,7 +247,7 @@ medusaIntegrationTestRunner({ id: fulfillment.id, shipped_at: date, packed_at: date, - location_id: "new location", + location_id: location.id, }, throwOnError: false, }) @@ -209,12 +314,15 @@ medusaIntegrationTestRunner({ ) const data = generateCreateFulfillmentData({ + order_id: "fake-order", provider_id: providerId, shipping_option_id: shippingOption.id, + location_id: location.id, }) const fulfillment = await service.createFulfillment({ ...data, + location, labels: [], }) diff --git a/integration-tests/modules/__tests__/fulfillment/index.spec.ts b/integration-tests/modules/__tests__/fulfillment/index.spec.ts index bbc069ebe8942..072a9c0ec277f 100644 --- a/integration-tests/modules/__tests__/fulfillment/index.spec.ts +++ b/integration-tests/modules/__tests__/fulfillment/index.spec.ts @@ -1,4 +1,4 @@ -import { IFulfillmentModuleService } from "@medusajs/types" +import { IFulfillmentModuleService, StockLocationDTO } from "@medusajs/types" import { Modules } from "@medusajs/utils" import { medusaIntegrationTestRunner } from "medusa-test-utils" import { createAdminUser } from "../../../helpers/create-admin-user" @@ -21,6 +21,7 @@ medusaIntegrationTestRunner({ testSuite: ({ getContainer, api, dbConnection }) => { let service: IFulfillmentModuleService let container + let location: StockLocationDTO beforeAll(() => { container = getContainer() @@ -29,6 +30,20 @@ medusaIntegrationTestRunner({ beforeEach(async () => { await createAdminUser(dbConnection, adminHeaders, container) + const stockLocationService = container.resolve(Modules.STOCK_LOCATION) + + location = await stockLocationService.createStockLocations({ + name: "Test Location", + address: { + address_1: "Test Address", + address_2: "tttest", + city: "Test City", + country_code: "us", + postal_code: "12345", + metadata: { email: "test@mail.com" }, + }, + metadata: { custom_location: "yes" }, + }) }) /** @@ -38,7 +53,10 @@ medusaIntegrationTestRunner({ */ describe("Fulfillment module migrations backward compatibility", () => { it("should allow to create a full data structure after the backward compatible migration have run on top of the medusa v1 database", async () => { - await setupFullDataFulfillmentStructure(service, { providerId }) + await setupFullDataFulfillmentStructure(service, { + providerId, + locationId: location.id, + }) const fulfillmentSets = await service.listFulfillmentSets( {}, @@ -92,7 +110,10 @@ medusaIntegrationTestRunner({ }) it("should cancel a fulfillment", async () => { - await setupFullDataFulfillmentStructure(service, { providerId }) + await setupFullDataFulfillmentStructure(service, { + providerId, + locationId: location.id, + }) const [fulfillment] = await service.listFulfillments() @@ -138,6 +159,7 @@ medusaIntegrationTestRunner({ ) const data = generateCreateFulfillmentData({ + location_id: location.id, provider_id: providerId, shipping_option_id: shippingOption.id, order_id: "order_123", @@ -151,7 +173,7 @@ medusaIntegrationTestRunner({ expect(response.data.fulfillment).toEqual( expect.objectContaining({ id: expect.any(String), - location_id: "test-location", + location_id: location.id, packed_at: null, shipped_at: null, delivered_at: null, @@ -218,7 +240,10 @@ medusaIntegrationTestRunner({ }) it("should update a fulfillment to be shipped", async () => { - await setupFullDataFulfillmentStructure(service, { providerId }) + await setupFullDataFulfillmentStructure(service, { + providerId, + locationId: location.id, + }) const [fulfillment] = await service.listFulfillments() @@ -255,7 +280,10 @@ medusaIntegrationTestRunner({ }) it("should throw error when already shipped", async () => { - await setupFullDataFulfillmentStructure(service, { providerId }) + await setupFullDataFulfillmentStructure(service, { + providerId, + locationId: location.id, + }) const [fulfillment] = await service.listFulfillments() 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 2a8834cdfb15c..0be60c4ec3a7e 100644 --- a/packages/core/core-flows/src/fulfillment/workflows/create-fulfillment.ts +++ b/packages/core/core-flows/src/fulfillment/workflows/create-fulfillment.ts @@ -1,10 +1,16 @@ -import { FulfillmentDTO, FulfillmentWorkflow } from "@medusajs/framework/types" +import { + FulfillmentDTO, + FulfillmentWorkflow, + StockLocationDTO, +} from "@medusajs/framework/types" import { WorkflowData, WorkflowResponse, createWorkflow, + transform, } from "@medusajs/framework/workflows-sdk" import { createFulfillmentStep } from "../steps" +import { useRemoteQueryStep } from "../../common" export const createFulfillmentWorkflowId = "create-fulfillment-workflow" /** @@ -15,6 +21,47 @@ export const createFulfillmentWorkflow = createWorkflow( ( input: WorkflowData ): WorkflowResponse => { - return new WorkflowResponse(createFulfillmentStep(input)) + const location: StockLocationDTO = useRemoteQueryStep({ + entry_point: "stock_location", + fields: [ + "id", + "name", + "metadata", + "created_at", + "updated_at", + "address.id", + "address.address_1", + "address.address_2", + "address.city", + "address.country_code", + "address.phone", + "address.province", + "address.postal_code", + "address.metadata", + ], + variables: { id: input.location_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "get-location" }) + + const stepInput = transform({ input, location }, ({ input, location }) => { + return { + ...input, + location, + } + }) + + // When we have support for hooks with a return this would be a great + // place to put a hook for people to collect additional data they would + // like to pass down to the provider. + // + // const providerDataHook = createHook("getProviderData", stepInput) + // + // The collected provider data would be passed to createFulfillment in a + // additional_provider_data: Record field. + + const result = createFulfillmentStep(stepInput) + + return new WorkflowResponse(result) } ) 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 index 6bd94d5deb2ef..0b2b7af6fe24f 100644 --- a/packages/core/core-flows/src/fulfillment/workflows/create-return-fulfillment.ts +++ b/packages/core/core-flows/src/fulfillment/workflows/create-return-fulfillment.ts @@ -1,10 +1,16 @@ -import { FulfillmentDTO, FulfillmentWorkflow } from "@medusajs/framework/types" +import { + FulfillmentDTO, + FulfillmentWorkflow, + StockLocationDTO, +} from "@medusajs/framework/types" import { WorkflowData, WorkflowResponse, createWorkflow, + transform, } from "@medusajs/framework/workflows-sdk" import { createReturnFulfillmentStep } from "../steps" +import { useRemoteQueryStep } from "../../common" export const createReturnFulfillmentWorkflowId = "create-return-fulfillment-workflow" @@ -16,6 +22,38 @@ export const createReturnFulfillmentWorkflow = createWorkflow( ( input: WorkflowData ): WorkflowResponse => { - return new WorkflowResponse(createReturnFulfillmentStep(input)) + const location: StockLocationDTO = useRemoteQueryStep({ + entry_point: "stock_location", + fields: [ + "id", + "name", + "metadata", + "created_at", + "updated_at", + "address.id", + "address.address_1", + "address.address_2", + "address.city", + "address.country_code", + "address.phone", + "address.province", + "address.postal_code", + "address.metadata", + ], + variables: { id: input.location_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "get-location" }) + + const stepInput = transform({ input, location }, ({ input, location }) => { + return { + ...input, + location, + } + }) + + const result = createReturnFulfillmentStep(stepInput) + + return new WorkflowResponse(result) } ) diff --git a/packages/core/types/src/fulfillment/mutations/fulfillment.ts b/packages/core/types/src/fulfillment/mutations/fulfillment.ts index 243b426dc75a8..42d5e816e3b78 100644 --- a/packages/core/types/src/fulfillment/mutations/fulfillment.ts +++ b/packages/core/types/src/fulfillment/mutations/fulfillment.ts @@ -1,4 +1,5 @@ import { OrderDTO } from "../../order" +import { StockLocationDTO } from "../../stock-location" import { CreateFulfillmentAddressDTO } from "./fulfillment-address" import { CreateFulfillmentItemDTO } from "./fulfillment-item" import { CreateFulfillmentLabelDTO } from "./fulfillment-label" @@ -12,6 +13,11 @@ export interface CreateFulfillmentDTO { */ location_id: string + /** + * The associated location's data. + */ + location?: StockLocationDTO + /** * The date the fulfillment was packed. */