Skip to content

Commit

Permalink
feat(medusa,fulfillment): pass stock location data to fulfillment pro…
Browse files Browse the repository at this point in the history
…vider (#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.
  • Loading branch information
srindom authored Sep 28, 2024
1 parent d00afb7 commit 17b2868
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -97,8 +98,10 @@ export async function setupFullDataFulfillmentStructure(
service: IFulfillmentModuleService,
{
providerId,
locationId,
}: {
providerId: string
locationId: string
}
) {
const randomString = Math.random().toString(36).substring(7)
Expand Down Expand Up @@ -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,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -22,15 +26,110 @@ 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 () => {
appContainer = getContainer()
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: "[email protected]" },
},
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: "[email protected]" },
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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -130,19 +230,24 @@ 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({
input: {
id: fulfillment.id,
shipped_at: date,
packed_at: date,
location_id: "new location",
location_id: location.id,
},
throwOnError: false,
})
Expand Down Expand Up @@ -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: [],
})

Expand Down
40 changes: 34 additions & 6 deletions integration-tests/modules/__tests__/fulfillment/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -21,6 +21,7 @@ medusaIntegrationTestRunner({
testSuite: ({ getContainer, api, dbConnection }) => {
let service: IFulfillmentModuleService
let container
let location: StockLocationDTO

beforeAll(() => {
container = getContainer()
Expand All @@ -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: "[email protected]" },
},
metadata: { custom_location: "yes" },
})
})

/**
Expand All @@ -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(
{},
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -138,6 +159,7 @@ medusaIntegrationTestRunner({
)

const data = generateCreateFulfillmentData({
location_id: location.id,
provider_id: providerId,
shipping_option_id: shippingOption.id,
order_id: "order_123",
Expand All @@ -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,
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
@@ -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"
/**
Expand All @@ -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)
}
)
Loading

0 comments on commit 17b2868

Please sign in to comment.