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:
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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: [],
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user