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.
This commit is contained in:
Sebastian Rindom
2024-09-28 16:01:48 +02:00
committed by GitHub
parent d00afb76a6
commit 17b2868a50
6 changed files with 248 additions and 16 deletions

View File

@@ -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,
})

View File

@@ -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: [],
})

View File

@@ -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()

View File

@@ -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<FulfillmentWorkflow.CreateFulfillmentWorkflowInput>
): WorkflowResponse<FulfillmentDTO> => {
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<string, unknown> field.
const result = createFulfillmentStep(stepInput)
return new WorkflowResponse(result)
}
)

View File

@@ -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<FulfillmentWorkflow.CreateFulfillmentWorkflowInput>
): WorkflowResponse<FulfillmentDTO> => {
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)
}
)

View File

@@ -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.
*/