From 62b9dcc6c1ce46aadb7944215006c12da3c9f619 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Thu, 18 Apr 2024 13:00:01 +0200 Subject: [PATCH] feat(core-flows,medusa,types): cancel fulfillments API (#7095) what: - adds POST cancel fulfillments endpoint RESOLVES CORE-1972 --- .changeset/large-rockets-hug.md | 7 +++ .../__tests__/fulfillment/index.spec.ts | 49 ++++++++++++++++++- .../fulfillment/steps/cancel-fulfillment.ts | 17 +++++++ .../core-flows/src/fulfillment/steps/index.ts | 7 +-- .../workflows/cancel-fulfillment.ts | 10 ++++ .../src/fulfillment/workflows/index.ts | 3 +- .../admin/fulfillments/[id]/cancel/route.ts | 30 ++++++++++++ .../src/api-v2/admin/fulfillments/helpers.ts | 24 +++++++++ .../api-v2/admin/fulfillments/middlewares.ts | 25 ++++++++++ .../api-v2/admin/fulfillments/query-config.ts | 24 +++++++++ .../api-v2/admin/fulfillments/validators.ts | 7 +++ packages/medusa/src/api-v2/middlewares.ts | 4 +- .../src/http/fulfillment/admin/fulfillment.ts | 4 +- 13 files changed, 202 insertions(+), 9 deletions(-) create mode 100644 .changeset/large-rockets-hug.md create mode 100644 packages/core-flows/src/fulfillment/steps/cancel-fulfillment.ts create mode 100644 packages/core-flows/src/fulfillment/workflows/cancel-fulfillment.ts create mode 100644 packages/medusa/src/api-v2/admin/fulfillments/[id]/cancel/route.ts create mode 100644 packages/medusa/src/api-v2/admin/fulfillments/helpers.ts create mode 100644 packages/medusa/src/api-v2/admin/fulfillments/middlewares.ts create mode 100644 packages/medusa/src/api-v2/admin/fulfillments/query-config.ts create mode 100644 packages/medusa/src/api-v2/admin/fulfillments/validators.ts diff --git a/.changeset/large-rockets-hug.md b/.changeset/large-rockets-hug.md new file mode 100644 index 0000000000..4e7a204b2f --- /dev/null +++ b/.changeset/large-rockets-hug.md @@ -0,0 +1,7 @@ +--- +"@medusajs/core-flows": patch +"@medusajs/medusa": patch +"@medusajs/types": patch +--- + +feat(core-flows,medusa,types): cancel fulfillments API diff --git a/integration-tests/modules/__tests__/fulfillment/index.spec.ts b/integration-tests/modules/__tests__/fulfillment/index.spec.ts index f03e44a298..144661647f 100644 --- a/integration-tests/modules/__tests__/fulfillment/index.spec.ts +++ b/integration-tests/modules/__tests__/fulfillment/index.spec.ts @@ -1,22 +1,31 @@ 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" jest.setTimeout(100000) const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} medusaIntegrationTestRunner({ env, - testSuite: ({ getContainer }) => { + testSuite: ({ getContainer, api, dbConnection }) => { let service: IFulfillmentModuleService + let container beforeAll(() => { - const container = getContainer() + container = getContainer() service = container.resolve(ModuleRegistrationName.FULFILLMENT) }) + beforeEach(async () => { + await createAdminUser(dbConnection, adminHeaders, container) + }) + /** * The test runner run both the medusa migrations as well as the modules * migrations. In order to ensure the backward compatibility @@ -65,5 +74,41 @@ medusaIntegrationTestRunner({ expect(fulfillment.items).toHaveLength(1) }) }) + + describe("POST /admin/fulfillments/:id/cancel", () => { + it("should throw an error when id is not found", async () => { + const error = await api + .post(`/admin/fulfillments/does-not-exist/cancel`, {}, 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 cancel a fulfillment", async () => { + await setupFullDataFulfillmentStructure(service, { + providerId: `manual_test-provider`, + }) + + const [fulfillment] = await service.listFulfillments() + + const response = await api.post( + `/admin/fulfillments/${fulfillment.id}/cancel`, + {}, + adminHeaders + ) + + expect(response.status).toEqual(200) + + const canceledFulfillment = await service.retrieveFulfillment( + fulfillment.id + ) + + expect(canceledFulfillment.canceled_at).toBeTruthy() + }) + }) }, }) diff --git a/packages/core-flows/src/fulfillment/steps/cancel-fulfillment.ts b/packages/core-flows/src/fulfillment/steps/cancel-fulfillment.ts new file mode 100644 index 0000000000..a3980467ce --- /dev/null +++ b/packages/core-flows/src/fulfillment/steps/cancel-fulfillment.ts @@ -0,0 +1,17 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IFulfillmentModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +export const cancelFulfillmentStepId = "cancel-fulfillment" +export const cancelFulfillmentStep = createStep( + cancelFulfillmentStepId, + async (id: string, { container }) => { + const service = container.resolve( + ModuleRegistrationName.FULFILLMENT + ) + + await service.cancelFulfillment(id) + + return new StepResponse(void 0, id) + } +) diff --git a/packages/core-flows/src/fulfillment/steps/index.ts b/packages/core-flows/src/fulfillment/steps/index.ts index fe9e711cb2..77f4f72609 100644 --- a/packages/core-flows/src/fulfillment/steps/index.ts +++ b/packages/core-flows/src/fulfillment/steps/index.ts @@ -1,11 +1,12 @@ export * from "./add-rules-to-fulfillment-shipping-option" export * from "./add-shipping-options-prices" +export * from "./cancel-fulfillment" export * from "./create-fulfillment-set" export * from "./create-service-zones" -export * from "./upsert-shipping-options" +export * from "./create-shipping-profiles" +export * from "./delete-fulfillment-sets" export * from "./delete-service-zones" export * from "./delete-shipping-options" -export * from "./delete-fulfillment-sets" -export * from "./create-shipping-profiles" export * from "./remove-rules-from-fulfillment-shipping-option" export * from "./set-shipping-options-prices" +export * from "./upsert-shipping-options" diff --git a/packages/core-flows/src/fulfillment/workflows/cancel-fulfillment.ts b/packages/core-flows/src/fulfillment/workflows/cancel-fulfillment.ts new file mode 100644 index 0000000000..be903244d2 --- /dev/null +++ b/packages/core-flows/src/fulfillment/workflows/cancel-fulfillment.ts @@ -0,0 +1,10 @@ +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { cancelFulfillmentStep } from "../steps" + +export const cancelFulfillmentWorkflowId = "cancel-fulfillment-workflow" +export const cancelFulfillmentWorkflow = createWorkflow( + cancelFulfillmentWorkflowId, + (input: WorkflowData<{ id: string }>) => { + cancelFulfillmentStep(input.id) + } +) diff --git a/packages/core-flows/src/fulfillment/workflows/index.ts b/packages/core-flows/src/fulfillment/workflows/index.ts index 6f51ddb82a..3da92f0658 100644 --- a/packages/core-flows/src/fulfillment/workflows/index.ts +++ b/packages/core-flows/src/fulfillment/workflows/index.ts @@ -1,10 +1,11 @@ export * from "./add-rules-to-fulfillment-shipping-option" +export * from "./cancel-fulfillment" export * from "./create-service-zones" export * from "./create-shipping-options" export * from "./create-shipping-profiles" +export * from "./delete-fulfillment-sets" export * from "./delete-service-zones" export * from "./delete-shipping-options" -export * from "./delete-fulfillment-sets" export * from "./remove-rules-from-fulfillment-shipping-option" export * from "./update-service-zones" export * from "./update-shipping-options" diff --git a/packages/medusa/src/api-v2/admin/fulfillments/[id]/cancel/route.ts b/packages/medusa/src/api-v2/admin/fulfillments/[id]/cancel/route.ts new file mode 100644 index 0000000000..0de2fd96b1 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/fulfillments/[id]/cancel/route.ts @@ -0,0 +1,30 @@ +import { cancelFulfillmentWorkflow } from "@medusajs/core-flows" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../../types/routing" +import { refetchFulfillment } from "../../helpers" +import { AdminCancelFulfillmentType } from "../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + const { errors } = await cancelFulfillmentWorkflow(req.scope).run({ + input: { id }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const fulfillment = await refetchFulfillment( + id, + req.scope, + req.remoteQueryConfig.fields + ) + + res.status(200).json({ fulfillment }) +} diff --git a/packages/medusa/src/api-v2/admin/fulfillments/helpers.ts b/packages/medusa/src/api-v2/admin/fulfillments/helpers.ts new file mode 100644 index 0000000000..16e512f6e8 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/fulfillments/helpers.ts @@ -0,0 +1,24 @@ +import { MedusaContainer } from "@medusajs/types" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" + +export const refetchFulfillment = async ( + fulfillmentId: string, + scope: MedusaContainer, + fields: string[] +) => { + const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + const queryObject = remoteQueryObjectFromString({ + entryPoint: "fulfillments", + variables: { + filters: { id: fulfillmentId }, + }, + fields: fields, + }) + + const [fulfillment] = await remoteQuery(queryObject) + + return fulfillment +} diff --git a/packages/medusa/src/api-v2/admin/fulfillments/middlewares.ts b/packages/medusa/src/api-v2/admin/fulfillments/middlewares.ts new file mode 100644 index 0000000000..5a00d82742 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/fulfillments/middlewares.ts @@ -0,0 +1,25 @@ +import { MiddlewareRoute } from "../../../types/middlewares" +import { authenticate } from "../../../utils/authenticate-middleware" +import { validateAndTransformBody } from "../../utils/validate-body" +import { validateAndTransformQuery } from "../../utils/validate-query" +import * as QueryConfig from "./query-config" +import { AdminCancelFulfillment, AdminFulfillmentParams } from "./validators" + +export const adminFulfillmentsRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: "ALL", + matcher: "/admin/fulfillments*", + middlewares: [authenticate("admin", ["session", "bearer", "api-key"])], + }, + { + method: ["POST"], + matcher: "/admin/fulfillments/:id/cancel", + middlewares: [ + validateAndTransformBody(AdminCancelFulfillment), + validateAndTransformQuery( + AdminFulfillmentParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, +] diff --git a/packages/medusa/src/api-v2/admin/fulfillments/query-config.ts b/packages/medusa/src/api-v2/admin/fulfillments/query-config.ts new file mode 100644 index 0000000000..777f99bd72 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/fulfillments/query-config.ts @@ -0,0 +1,24 @@ +export const defaultAdminFulfillmentsFields = [ + "id", + "location_id", + "packed_at", + "shipped_at", + "delivered_at", + "canceled_at", + "data", + "provider_id", + "shipping_option_id", + "metadata", + "created_at", + "updated_at", +] + +export const retrieveTransformQueryConfig = { + defaults: defaultAdminFulfillmentsFields, + isList: false, +} + +export const listTransformQueryConfig = { + ...retrieveTransformQueryConfig, + isList: true, +} diff --git a/packages/medusa/src/api-v2/admin/fulfillments/validators.ts b/packages/medusa/src/api-v2/admin/fulfillments/validators.ts new file mode 100644 index 0000000000..ba7afae8c9 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/fulfillments/validators.ts @@ -0,0 +1,7 @@ +import { z } from "zod" +import { createSelectParams } from "../../utils/validators" + +export const AdminFulfillmentParams = createSelectParams() + +export const AdminCancelFulfillment = z.object({}) +export type AdminCancelFulfillmentType = z.infer diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index 4687fc1344..edf19cfcfe 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -7,14 +7,15 @@ import { adminCustomerGroupRoutesMiddlewares } from "./admin/customer-groups/mid import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares" import { adminDraftOrderRoutesMiddlewares } from "./admin/draft-orders/middlewares" import { adminFulfillmentSetsRoutesMiddlewares } from "./admin/fulfillment-sets/middlewares" +import { adminFulfillmentsRoutesMiddlewares } from "./admin/fulfillments/middlewares" import { adminInventoryRoutesMiddlewares } from "./admin/inventory-items/middlewares" import { adminInviteRoutesMiddlewares } from "./admin/invites/middlewares" import { adminPaymentRoutesMiddlewares } from "./admin/payments/middlewares" import { adminPriceListsRoutesMiddlewares } from "./admin/price-lists/middlewares" import { adminPricingRoutesMiddlewares } from "./admin/pricing/middlewares" import { adminProductCategoryRoutesMiddlewares } from "./admin/product-categories/middlewares" -import { adminProductRoutesMiddlewares } from "./admin/products/middlewares" import { adminProductTypeRoutesMiddlewares } from "./admin/product-types/middlewares" +import { adminProductRoutesMiddlewares } from "./admin/products/middlewares" import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares" import { adminRegionRoutesMiddlewares } from "./admin/regions/middlewares" import { adminReservationRoutesMiddlewares } from "./admin/reservations/middlewares" @@ -74,5 +75,6 @@ export const config: MiddlewaresConfig = { ...adminProductCategoryRoutesMiddlewares, ...adminReservationRoutesMiddlewares, ...adminShippingProfilesMiddlewares, + ...adminFulfillmentsRoutesMiddlewares, ], } diff --git a/packages/types/src/http/fulfillment/admin/fulfillment.ts b/packages/types/src/http/fulfillment/admin/fulfillment.ts index 6b8a044182..40f7904f01 100644 --- a/packages/types/src/http/fulfillment/admin/fulfillment.ts +++ b/packages/types/src/http/fulfillment/admin/fulfillment.ts @@ -1,8 +1,8 @@ +import { DeleteResponse } from "../../../common" import { AdminFulfillmentAddressResponse } from "./fulfillment-address" -import { AdminFulfillmentProviderResponse } from "./fulfillment-provider" import { AdminFulfillmentItemResponse } from "./fulfillment-item" import { AdminFulfillmentLabelResponse } from "./fulfillment-label" -import { DeleteResponse } from "../../../common" +import { AdminFulfillmentProviderResponse } from "./fulfillment-provider" /** * @experimental