feat(core-flows,medusa,types): fulfillment API: create (#7101)

what:

- adds fulfillment create API

RESOLVES CORE-1962
This commit is contained in:
Riqwan Thamir
2024-04-23 08:35:44 +00:00
committed by GitHub
parent 14a7378375
commit 18f3aacee6
26 changed files with 1037 additions and 14 deletions
@@ -30,7 +30,8 @@ export function generateCreateFulfillmentData(
country_code: "test-country-code_" + randomString,
province: "test-province_" + randomString,
phone: "test-phone_" + randomString,
full_name: "test-full-name_" + randomString,
first_name: "test-first-name_" + randomString,
last_name: "test-last-name_" + randomString,
},
items: data.items ?? [
{
@@ -0,0 +1,261 @@
import {
createFulfillmentWorkflow,
createFulfillmentWorkflowId,
createShipmentWorkflow,
createShipmentWorkflowId,
updateFulfillmentWorkflow,
updateFulfillmentWorkflowId,
} from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IFulfillmentModuleService } from "@medusajs/types"
import { medusaIntegrationTestRunner } from "medusa-test-utils/dist"
import {
generateCreateFulfillmentData,
generateCreateShippingOptionsData,
} from "../fixtures"
jest.setTimeout(50000)
const providerId = "manual_test-provider"
medusaIntegrationTestRunner({
env: { MEDUSA_FF_MEDUSA_V2: true },
testSuite: ({ getContainer }) => {
describe("Workflows: Fulfillment", () => {
let appContainer
let service: IFulfillmentModuleService
beforeAll(async () => {
appContainer = getContainer()
service = appContainer.resolve(ModuleRegistrationName.FULFILLMENT)
})
describe("createFulfillmentWorkflow", () => {
describe("compensation", () => {
it("should cancel created fulfillment if step following step throws error", async () => {
const workflow = createFulfillmentWorkflow(appContainer)
workflow.appendAction("throw", createFulfillmentWorkflowId, {
invoke: async function failStep() {
throw new Error(
`Failed to do something after creating fulfillment`
)
},
})
const shippingProfile = await service.createShippingProfiles({
name: "test",
type: "default",
})
const fulfillmentSet = await service.create({
name: "test",
type: "test-type",
})
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,
})
const { errors } = await workflow.run({
input: data,
throwOnError: false,
})
expect(errors).toEqual([
{
action: "throw",
handlerType: "invoke",
error: expect.objectContaining({
message: `Failed to do something after creating fulfillment`,
}),
},
])
const fulfillments = await service.listFulfillments()
expect(fulfillments.filter((f) => !!f.canceled_at)).toHaveLength(1)
})
})
})
describe("updateFulfillmentWorkflow", () => {
describe("compensation", () => {
it("should rollback updated fulfillment if step following step throws error", async () => {
const workflow = updateFulfillmentWorkflow(appContainer)
workflow.appendAction("throw", updateFulfillmentWorkflowId, {
invoke: async function failStep() {
throw new Error(
`Failed to do something after updating fulfillment`
)
},
})
const shippingProfile = await service.createShippingProfiles({
name: "test",
type: "default",
})
const fulfillmentSet = await service.create({
name: "test",
type: "test-type",
})
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,
})
const fulfillment = await service.createFulfillment(data)
const date = new Date()
const { errors } = await workflow.run({
input: {
id: fulfillment.id,
shipped_at: date,
packed_at: date,
location_id: "new location",
},
throwOnError: false,
})
expect(errors).toEqual([
{
action: "throw",
handlerType: "invoke",
error: expect.objectContaining({
message: `Failed to do something after updating fulfillment`,
}),
},
])
const fulfillmentAfterRollback = await service.retrieveFulfillment(
fulfillment.id
)
expect(fulfillmentAfterRollback).toEqual(
expect.objectContaining({
location_id: data.location_id,
shipped_at: data.shipped_at,
packed_at: data.packed_at,
})
)
})
})
})
describe("createShipmentWorkflow", () => {
describe("compensation", () => {
it("should rollback shipment workflow if following step throws error", async () => {
const workflow = createShipmentWorkflow(appContainer)
workflow.appendAction("throw", createShipmentWorkflowId, {
invoke: async function failStep() {
throw new Error(
`Failed to do something after creating shipment`
)
},
})
const shippingProfile = await service.createShippingProfiles({
name: "test",
type: "default",
})
const fulfillmentSet = await service.create({
name: "test",
type: "test-type",
})
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,
})
const fulfillment = await service.createFulfillment({
...data,
labels: [],
})
const { errors } = await workflow.run({
input: {
id: fulfillment.id,
labels: [
{
tracking_number: "test-tracking-number",
tracking_url: "test-tracking-url",
label_url: "test-label-url",
},
],
},
throwOnError: false,
})
expect(errors).toEqual([
{
action: "throw",
handlerType: "invoke",
error: expect.objectContaining({
message: `Failed to do something after creating shipment`,
}),
},
])
const fulfillmentAfterRollback = await service.retrieveFulfillment(
fulfillment.id,
{ select: ["shipped_at"], relations: ["labels"] }
)
expect(fulfillmentAfterRollback).toEqual(
expect.objectContaining({
shipped_at: null,
// TODO: the revert isn't handling deleting the labels. This needs to be handled uniformly across.
// labels: [],
})
)
})
})
})
})
},
})
@@ -2,7 +2,11 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IFulfillmentModuleService } from "@medusajs/types"
import { medusaIntegrationTestRunner } from "medusa-test-utils/dist"
import { createAdminUser } from "../../../helpers/create-admin-user"
import { setupFullDataFulfillmentStructure } from "../fixtures"
import {
generateCreateFulfillmentData,
generateCreateShippingOptionsData,
setupFullDataFulfillmentStructure,
} from "../fixtures"
jest.setTimeout(100000)
@@ -10,6 +14,7 @@ const env = { MEDUSA_FF_MEDUSA_V2: true }
const adminHeaders = {
headers: { "x-medusa-access-token": "test_token" },
}
const providerId = "manual_test-provider"
medusaIntegrationTestRunner({
env,
@@ -33,9 +38,7 @@ 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: `manual_test-provider`,
})
await setupFullDataFulfillmentStructure(service, { providerId })
const fulfillmentSets = await service.list(
{},
@@ -89,9 +92,7 @@ medusaIntegrationTestRunner({
})
it("should cancel a fulfillment", async () => {
await setupFullDataFulfillmentStructure(service, {
providerId: `manual_test-provider`,
})
await setupFullDataFulfillmentStructure(service, { providerId })
const [fulfillment] = await service.listFulfillments()
@@ -110,5 +111,177 @@ medusaIntegrationTestRunner({
expect(canceledFulfillment.canceled_at).toBeTruthy()
})
})
describe("POST /admin/fulfillments", () => {
it("should create a fulfillment", async () => {
const shippingProfile = await service.createShippingProfiles({
name: "test",
type: "default",
})
const fulfillmentSet = await service.create({
name: "test",
type: "test-type",
})
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,
})
const response = await api
.post(`/admin/fulfillments`, data, adminHeaders)
.catch((e) => e)
expect(response.status).toEqual(200)
expect(response.data.fulfillment).toEqual(
expect.objectContaining({
id: expect.any(String),
location_id: "test-location",
packed_at: null,
shipped_at: null,
delivered_at: null,
canceled_at: null,
provider_id: "manual_test-provider",
delivery_address: expect.objectContaining({
address_1: expect.any(String),
address_2: expect.any(String),
city: expect.any(String),
country_code: expect.any(String),
province: expect.any(String),
postal_code: expect.any(String),
}),
items: [
expect.objectContaining({
id: expect.any(String),
title: expect.any(String),
sku: expect.any(String),
barcode: expect.any(String),
raw_quantity: {
value: "1",
precision: 20,
},
quantity: 1,
}),
],
labels: [
expect.objectContaining({
id: expect.any(String),
tracking_number: expect.any(String),
tracking_url: expect.any(String),
label_url: expect.any(String),
}),
],
})
)
})
})
describe("POST /admin/fulfillments/:id/shipment", () => {
it("should throw an error when id is not found", async () => {
const error = await api
.post(
`/admin/fulfillments/does-not-exist/shipment`,
{
labels: [
{
tracking_number: "test-tracking-number",
tracking_url: "test-tracking-url",
label_url: "test-label-url",
},
],
},
adminHeaders
)
.catch((e) => e)
expect(error.response.status).toEqual(404)
expect(error.response.data).toEqual({
type: "not_found",
message: "Fulfillment with id: does-not-exist was not found",
})
})
it("should update a fulfillment to be shipped", async () => {
await setupFullDataFulfillmentStructure(service, { providerId })
const [fulfillment] = await service.listFulfillments()
const response = await api.post(
`/admin/fulfillments/${fulfillment.id}/shipment`,
{
labels: [
{
tracking_number: "test-tracking-number",
tracking_url: "test-tracking-url",
label_url: "test-label-url",
},
],
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.fulfillment).toEqual(
expect.objectContaining({
id: fulfillment.id,
shipped_at: expect.any(String),
labels: [
expect.objectContaining({
id: expect.any(String),
tracking_number: "test-tracking-number",
tracking_url: "test-tracking-url",
label_url: "test-label-url",
}),
],
})
)
})
it("should throw error when already shipped", async () => {
await setupFullDataFulfillmentStructure(service, { providerId })
const [fulfillment] = await service.listFulfillments()
await service.updateFulfillment(fulfillment.id, {
shipped_at: new Date(),
})
const error = await api
.post(
`/admin/fulfillments/${fulfillment.id}/shipment`,
{
labels: [
{
tracking_number: "test-tracking-number",
tracking_url: "test-tracking-url",
label_url: "test-label-url",
},
],
},
adminHeaders
)
.catch((e) => e)
expect(error.response.status).toEqual(400)
expect(error.response.data).toEqual({
type: "not_allowed",
message: "Shipment has already been created",
})
})
})
},
})