From bc06ad2db48c999023ab823fefc1375196976e9b Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Sat, 6 Apr 2024 18:52:05 +0200 Subject: [PATCH] feat: Clean up shipping options management (#6977) **What** - cleanup existing route on some aspects but not all - add new create shipping options end point and types - add set of integration tests - cleanup existing fulfillment routes - align existing integration tests - transform old type to zod types and validators - fix stock location route --- .changeset/nervous-plants-watch.md | 6 + .../admin/shipping-option-rules.spec.ts | 16 +- .../admin/shipping-options.spec.ts | 207 ++++++++++++++++++ .../api-v2/admin/fulfillment/middlewares.ts | 29 --- .../api-v2/admin/fulfillment/query-config.ts | 29 --- .../[id]/rules/batch/add/route.ts | 45 ---- .../[id]/rules/batch/remove/route.ts | 40 ---- .../api-v2/admin/fulfillment/validators.ts | 36 --- .../src/api-v2/admin/inventory-items/route.ts | 8 +- .../[id]/rules/batch/add/route.ts | 48 ++++ .../[id]/rules/batch/remove/route.ts | 40 ++++ .../admin/shipping-options/middlewares.ts | 54 +++++ .../admin/shipping-options/query-config.ts | 25 +++ .../api-v2/admin/shipping-options/route.ts | 44 ++++ .../admin/shipping-options/validators.ts | 87 ++++++++ packages/medusa/src/api-v2/middlewares.ts | 4 +- .../[id]/payment-sessions/route.ts | 2 +- packages/medusa/src/types/routing.ts | 17 +- .../http/fulfillment/admin/shipping-option.ts | 15 +- packages/types/src/http/index.ts | 1 + packages/types/src/http/pricing/index.ts | 1 + packages/types/src/http/pricing/price.ts | 7 + 22 files changed, 556 insertions(+), 205 deletions(-) create mode 100644 .changeset/nervous-plants-watch.md create mode 100644 integration-tests/modules/__tests__/fulfillment/admin/shipping-options.spec.ts delete mode 100644 packages/medusa/src/api-v2/admin/fulfillment/middlewares.ts delete mode 100644 packages/medusa/src/api-v2/admin/fulfillment/query-config.ts delete mode 100644 packages/medusa/src/api-v2/admin/fulfillment/shipping-options/[id]/rules/batch/add/route.ts delete mode 100644 packages/medusa/src/api-v2/admin/fulfillment/shipping-options/[id]/rules/batch/remove/route.ts delete mode 100644 packages/medusa/src/api-v2/admin/fulfillment/validators.ts create mode 100644 packages/medusa/src/api-v2/admin/shipping-options/[id]/rules/batch/add/route.ts create mode 100644 packages/medusa/src/api-v2/admin/shipping-options/[id]/rules/batch/remove/route.ts create mode 100644 packages/medusa/src/api-v2/admin/shipping-options/middlewares.ts create mode 100644 packages/medusa/src/api-v2/admin/shipping-options/query-config.ts create mode 100644 packages/medusa/src/api-v2/admin/shipping-options/route.ts create mode 100644 packages/medusa/src/api-v2/admin/shipping-options/validators.ts create mode 100644 packages/types/src/http/pricing/index.ts create mode 100644 packages/types/src/http/pricing/price.ts diff --git a/.changeset/nervous-plants-watch.md b/.changeset/nervous-plants-watch.md new file mode 100644 index 0000000000..b00560ee01 --- /dev/null +++ b/.changeset/nervous-plants-watch.md @@ -0,0 +1,6 @@ +--- +"@medusajs/medusa": patch +"@medusajs/types": patch +--- + +Feat/shipping options api 3 diff --git a/integration-tests/modules/__tests__/fulfillment/admin/shipping-option-rules.spec.ts b/integration-tests/modules/__tests__/fulfillment/admin/shipping-option-rules.spec.ts index c27806b04a..fb5fcb47db 100644 --- a/integration-tests/modules/__tests__/fulfillment/admin/shipping-option-rules.spec.ts +++ b/integration-tests/modules/__tests__/fulfillment/admin/shipping-option-rules.spec.ts @@ -63,11 +63,11 @@ medusaIntegrationTestRunner({ }) }) - describe("POST /admin/fulfillment/shipping-options/:id/rules/batch/add", () => { + describe("POST /admin/shipping-options/:id/rules/batch/add", () => { it("should throw error when required params are missing", async () => { const { response } = await api .post( - `/admin/fulfillment/shipping-options/${shippingOption.id}/rules/batch/add`, + `/admin/shipping-options/${shippingOption.id}/rules/batch/add`, { rules: [{ operator: RuleOperator.EQ, value: "new_value" }], }, @@ -86,7 +86,7 @@ medusaIntegrationTestRunner({ it.only("should throw error when shipping option does not exist", async () => { const { response } = await api .post( - `/admin/fulfillment/shipping-options/does-not-exist/rules/batch/add`, + `/admin/shipping-options/does-not-exist/rules/batch/add`, { rules: [ { attribute: "new_attr", operator: "eq", value: "new value" }, @@ -106,7 +106,7 @@ medusaIntegrationTestRunner({ it("should add rules to a shipping option successfully", async () => { const response = await api.post( - `/admin/fulfillment/shipping-options/${shippingOption.id}/rules/batch/add`, + `/admin/shipping-options/${shippingOption.id}/rules/batch/add`, { rules: [ { operator: "eq", attribute: "new_attr", value: "new value" }, @@ -140,11 +140,11 @@ medusaIntegrationTestRunner({ }) }) - describe("POST /admin/fulfillment/shipping-options/:id/rules/batch/remove", () => { + describe("POST /admin/shipping-options/:id/rules/batch/remove", () => { it("should throw error when required params are missing", async () => { const { response } = await api .post( - `/admin/fulfillment/shipping-options/${shippingOption.id}/rules/batch/remove`, + `/admin/shipping-options/${shippingOption.id}/rules/batch/remove`, {}, adminHeaders ) @@ -161,7 +161,7 @@ medusaIntegrationTestRunner({ it("should throw error when shipping option does not exist", async () => { const { response } = await api .post( - `/admin/fulfillment/shipping-options/does-not-exist/rules/batch/remove`, + `/admin/shipping-options/does-not-exist/rules/batch/remove`, { rule_ids: ["test"] }, adminHeaders ) @@ -176,7 +176,7 @@ medusaIntegrationTestRunner({ it("should add rules to a shipping option successfully", async () => { const response = await api.post( - `/admin/fulfillment/shipping-options/${shippingOption.id}/rules/batch/remove`, + `/admin/shipping-options/${shippingOption.id}/rules/batch/remove`, { rule_ids: [shippingOption.rules[0].id], }, diff --git a/integration-tests/modules/__tests__/fulfillment/admin/shipping-options.spec.ts b/integration-tests/modules/__tests__/fulfillment/admin/shipping-options.spec.ts new file mode 100644 index 0000000000..57196ab4d3 --- /dev/null +++ b/integration-tests/modules/__tests__/fulfillment/admin/shipping-options.spec.ts @@ -0,0 +1,207 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + IFulfillmentModuleService, + IRegionModuleService, +} from "@medusajs/types" +import { RuleOperator } from "@medusajs/utils" +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { createAdminUser } from "../../../../helpers/create-admin-user" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } } + +medusaIntegrationTestRunner({ + env, + testSuite: ({ dbConnection, getContainer, api }) => { + describe("Admin: Shipping Option API", () => { + let appContainer + let fulfillmentModule: IFulfillmentModuleService + let regionService: IRegionModuleService + + let shippingProfile + let fulfillmentSet + let region + + const shippingOptionRule = { + operator: RuleOperator.EQ, + attribute: "old_attr", + value: "old value", + } + + beforeAll(async () => { + appContainer = getContainer() + fulfillmentModule = appContainer.resolve( + ModuleRegistrationName.FULFILLMENT + ) + regionService = appContainer.resolve(ModuleRegistrationName.REGION) + }) + + beforeEach(async () => { + await createAdminUser(dbConnection, adminHeaders, appContainer) + + shippingProfile = await fulfillmentModule.createShippingProfiles({ + name: "Test", + type: "default", + }) + + fulfillmentSet = await fulfillmentModule.create({ + name: "Test", + type: "test-type", + service_zones: [ + { + name: "Test", + geo_zones: [{ type: "country", country_code: "us" }], + }, + ], + }) + + region = await regionService.create({ + name: "Test region", + countries: ["FR"], + currency_code: "eur", + }) + }) + + describe("POST /admin/shipping-options", () => { + it("should throw error when required params are missing", async () => { + const shippingOptionPayload = { + name: "Test shipping option", + } + + let err = await api + .post( + `/admin/shipping-options`, + shippingOptionPayload, + adminHeaders + ) + .catch((e) => e.response) + + const errorsFields = [ + { + code: "invalid_type", + expected: "string", + received: "undefined", + path: ["service_zone_id"], + message: "Required", + }, + { + code: "invalid_type", + expected: "string", + received: "undefined", + path: ["shipping_profile_id"], + message: "Required", + }, + { + expected: "'calculated' | 'flat'", + received: "undefined", + code: "invalid_type", + path: ["price_type"], + message: "Required", + }, + { + code: "invalid_type", + expected: "string", + received: "undefined", + path: ["provider_id"], + message: "Required", + }, + { + code: "invalid_type", + expected: "object", + received: "undefined", + path: ["type"], + message: "Required", + }, + { + code: "invalid_type", + expected: "array", + received: "undefined", + path: ["prices"], + message: "Required", + }, + ] + + expect(err.status).toEqual(400) + expect(err.data).toEqual({ + type: "invalid_data", + message: `Invalid request body: ${JSON.stringify(errorsFields)}`, + }) + }) + + it("should create a shipping option successfully", async () => { + const shippingOptionPayload = { + name: "Test shipping option", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + prices: [ + { + currency_code: "usd", + amount: 1000, + }, + { + region_id: region.id, + amount: 1000, + }, + ], + rules: [shippingOptionRule], + } + + const response = await api.post( + `/admin/shipping-options`, + shippingOptionPayload, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.shipping_option).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: shippingOptionPayload.name, + provider: expect.objectContaining({ + id: shippingOptionPayload.provider_id, + }), + price_type: shippingOptionPayload.price_type, + type: expect.objectContaining({ + id: expect.any(String), + label: shippingOptionPayload.type.label, + description: shippingOptionPayload.type.description, + code: shippingOptionPayload.type.code, + }), + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + currency_code: "usd", + amount: 1000, + }), + expect.objectContaining({ + id: expect.any(String), + currency_code: "eur", + amount: 1000, + }), + ]), + rules: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + operator: "eq", + attribute: "old_attr", + value: "old value", + }), + ]), + }) + ) + }) + }) + }) + }, +}) diff --git a/packages/medusa/src/api-v2/admin/fulfillment/middlewares.ts b/packages/medusa/src/api-v2/admin/fulfillment/middlewares.ts deleted file mode 100644 index dee196d8fa..0000000000 --- a/packages/medusa/src/api-v2/admin/fulfillment/middlewares.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { transformBody } from "../../../api/middlewares" -import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" -import { authenticate } from "../../../utils/authenticate-middleware" -import { - AdminPostFulfillmentShippingOptionsRulesBatchAddReq, - AdminPostFulfillmentShippingOptionsRulesBatchRemoveReq, -} from "./validators" - -export const adminFulfillmentRoutesMiddlewares: MiddlewareRoute[] = [ - { - matcher: "/admin/fulfillment*", - middlewares: [authenticate("admin", ["bearer", "session"])], - }, - { - method: ["POST"], - matcher: "/admin/fulfillment/shipping-options/:id/rules/batch/add", - middlewares: [ - transformBody(AdminPostFulfillmentShippingOptionsRulesBatchAddReq), - ], - }, - - { - method: ["POST"], - matcher: "/admin/fulfillment/shipping-options/:id/rules/batch/remove", - middlewares: [ - transformBody(AdminPostFulfillmentShippingOptionsRulesBatchRemoveReq), - ], - }, -] diff --git a/packages/medusa/src/api-v2/admin/fulfillment/query-config.ts b/packages/medusa/src/api-v2/admin/fulfillment/query-config.ts deleted file mode 100644 index 7f687c20e0..0000000000 --- a/packages/medusa/src/api-v2/admin/fulfillment/query-config.ts +++ /dev/null @@ -1,29 +0,0 @@ -export const defaultAdminShippingOptionRelations = ["rules"] -export const allowedAdminShippingOptionRelations = [ - ...defaultAdminShippingOptionRelations, -] -export const defaultAdminShippingOptionFields = [ - "id", - "name", - "price_type", - "data", - "metadata", - "created_at", - "updated_at", - "rules.id", - "rules.attribute", - "rules.operator", - "rules.value", -] - -export const retrieveTransformQueryConfig = { - defaultFields: defaultAdminShippingOptionFields, - defaultRelations: defaultAdminShippingOptionRelations, - allowedRelations: allowedAdminShippingOptionRelations, - isList: false, -} - -export const listTransformQueryConfig = { - ...retrieveTransformQueryConfig, - isList: true, -} diff --git a/packages/medusa/src/api-v2/admin/fulfillment/shipping-options/[id]/rules/batch/add/route.ts b/packages/medusa/src/api-v2/admin/fulfillment/shipping-options/[id]/rules/batch/add/route.ts deleted file mode 100644 index 3a24fe0d19..0000000000 --- a/packages/medusa/src/api-v2/admin/fulfillment/shipping-options/[id]/rules/batch/add/route.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { addRulesToFulfillmentShippingOptionWorkflow } from "@medusajs/core-flows" -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IFulfillmentModuleService } from "@medusajs/types" -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "../../../../../../../../types/routing" -import { - defaultAdminShippingOptionFields, - defaultAdminShippingOptionRelations, -} from "../../../../../query-config" -import { AdminPostFulfillmentShippingOptionsRulesBatchAddReq } from "../../../../../validators" - -export const POST = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - const id = req.params.id - const workflow = addRulesToFulfillmentShippingOptionWorkflow(req.scope) - - const { errors } = await workflow.run({ - input: { - data: req.validatedBody.rules.map((rule) => ({ - ...rule, - shipping_option_id: id, - })), - }, - throwOnError: false, - }) - - if (Array.isArray(errors) && errors[0]) { - throw errors[0].error - } - - const fulfillmentService: IFulfillmentModuleService = req.scope.resolve( - ModuleRegistrationName.FULFILLMENT - ) - - const shippingOption = await fulfillmentService.retrieveShippingOption(id, { - select: defaultAdminShippingOptionFields, - relations: defaultAdminShippingOptionRelations, - }) - - res.status(200).json({ shipping_option: shippingOption }) -} diff --git a/packages/medusa/src/api-v2/admin/fulfillment/shipping-options/[id]/rules/batch/remove/route.ts b/packages/medusa/src/api-v2/admin/fulfillment/shipping-options/[id]/rules/batch/remove/route.ts deleted file mode 100644 index 4c116407af..0000000000 --- a/packages/medusa/src/api-v2/admin/fulfillment/shipping-options/[id]/rules/batch/remove/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { removeRulesFromFulfillmentShippingOptionWorkflow } from "@medusajs/core-flows" -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IFulfillmentModuleService } from "@medusajs/types" -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "../../../../../../../../types/routing" -import { - defaultAdminShippingOptionFields, - defaultAdminShippingOptionRelations, -} from "../../../../../query-config" -import { AdminPostFulfillmentShippingOptionsRulesBatchRemoveReq } from "../../../../../validators" - -export const POST = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - const id = req.params.id - const workflow = removeRulesFromFulfillmentShippingOptionWorkflow(req.scope) - - const { errors } = await workflow.run({ - input: { ids: req.validatedBody.rule_ids }, - throwOnError: false, - }) - - if (Array.isArray(errors) && errors[0]) { - throw errors[0].error - } - - const fulfillmentService: IFulfillmentModuleService = req.scope.resolve( - ModuleRegistrationName.FULFILLMENT - ) - - const shippingOption = await fulfillmentService.retrieveShippingOption(id, { - select: defaultAdminShippingOptionFields, - relations: defaultAdminShippingOptionRelations, - }) - - res.status(200).json({ shipping_option: shippingOption }) -} diff --git a/packages/medusa/src/api-v2/admin/fulfillment/validators.ts b/packages/medusa/src/api-v2/admin/fulfillment/validators.ts deleted file mode 100644 index 17118fd844..0000000000 --- a/packages/medusa/src/api-v2/admin/fulfillment/validators.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { RuleOperator } from "@medusajs/utils" -import { Type } from "class-transformer" -import { - ArrayNotEmpty, - IsArray, - IsEnum, - IsNotEmpty, - IsString, - ValidateNested, -} from "class-validator" -import { IsType } from "../../../utils" - -export class AdminPostFulfillmentShippingOptionsRulesBatchAddReq { - @IsArray() - @ValidateNested({ each: true }) - @Type(() => FulfillmentRuleCreate) - rules: FulfillmentRuleCreate[] -} - -export class AdminPostFulfillmentShippingOptionsRulesBatchRemoveReq { - @ArrayNotEmpty() - @IsString({ each: true }) - rule_ids: string[] -} - -export class FulfillmentRuleCreate { - @IsEnum(RuleOperator) - operator: RuleOperator - - @IsNotEmpty() - @IsString() - attribute: string - - @IsType([String, [String]]) - value: string | string[] -} diff --git a/packages/medusa/src/api-v2/admin/inventory-items/route.ts b/packages/medusa/src/api-v2/admin/inventory-items/route.ts index 0892821f05..ce46d014de 100644 --- a/packages/medusa/src/api-v2/admin/inventory-items/route.ts +++ b/packages/medusa/src/api-v2/admin/inventory-items/route.ts @@ -27,7 +27,7 @@ export const POST = async ( variables: { id: result[0].id, }, - fields: req.retrieveConfig.select as string[], + fields: req.remoteQueryConfig.fields, }) ) @@ -45,11 +45,9 @@ export const GET = async ( entryPoint: "inventory_items", variables: { filters: req.filterableFields, - order: req.listConfig.order, - skip: req.listConfig.skip, - take: req.listConfig.take, + ...req.remoteQueryConfig.pagination, }, - fields: [...(req.listConfig.select as string[])], + fields: req.remoteQueryConfig.fields, }) const { rows: inventory_items, metadata } = await remoteQuery({ diff --git a/packages/medusa/src/api-v2/admin/shipping-options/[id]/rules/batch/add/route.ts b/packages/medusa/src/api-v2/admin/shipping-options/[id]/rules/batch/add/route.ts new file mode 100644 index 0000000000..870793013c --- /dev/null +++ b/packages/medusa/src/api-v2/admin/shipping-options/[id]/rules/batch/add/route.ts @@ -0,0 +1,48 @@ +import { addRulesToFulfillmentShippingOptionWorkflow } from "@medusajs/core-flows" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { AdminShippingOptionRetrieveResponse } from "@medusajs/types" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../../../../types/routing" +import { AdminShippingOptionRulesBatchAddType } from "../../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest< + AdminShippingOptionRulesBatchAddType + >, + res: MedusaResponse +) => { + const id = req.params.id + const workflow = addRulesToFulfillmentShippingOptionWorkflow(req.scope) + + const { errors } = await workflow.run({ + input: { + data: req.validatedBody.rules.map((rule) => ({ + ...rule, + shipping_option_id: id, + })), + }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const query = remoteQueryObjectFromString({ + entryPoint: "shipping_options", + variables: { + id: req.params.id, + }, + fields: req.remoteQueryConfig.fields, + }) + + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + const [shippingOption] = await remoteQuery(query) + + res.status(200).json({ shipping_option: shippingOption }) +} diff --git a/packages/medusa/src/api-v2/admin/shipping-options/[id]/rules/batch/remove/route.ts b/packages/medusa/src/api-v2/admin/shipping-options/[id]/rules/batch/remove/route.ts new file mode 100644 index 0000000000..08bfcd7c49 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/shipping-options/[id]/rules/batch/remove/route.ts @@ -0,0 +1,40 @@ +import { removeRulesFromFulfillmentShippingOptionWorkflow } from "@medusajs/core-flows" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { AdminShippingOptionRetrieveResponse } from "@medusajs/types" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../../../../types/routing" +import { AdminShippingOptionRulesBatchRemoveType } from "../../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const workflow = removeRulesFromFulfillmentShippingOptionWorkflow(req.scope) + + const { errors } = await workflow.run({ + input: { ids: req.validatedBody.rule_ids }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const query = remoteQueryObjectFromString({ + entryPoint: "shipping_options", + variables: { + id: req.params.id, + }, + fields: req.remoteQueryConfig.fields, + }) + + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + const [shippingOption] = await remoteQuery(query) + + res.status(200).json({ shipping_option: shippingOption }) +} diff --git a/packages/medusa/src/api-v2/admin/shipping-options/middlewares.ts b/packages/medusa/src/api-v2/admin/shipping-options/middlewares.ts new file mode 100644 index 0000000000..c6198b6f19 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/shipping-options/middlewares.ts @@ -0,0 +1,54 @@ +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import { authenticate } from "../../../utils/authenticate-middleware" +import { + AdminCreateShippingOption, + AdminGetShippingOptionParams, + AdminShippingOptionRulesBatchAdd, + AdminShippingOptionRulesBatchRemove, +} from "./validators" +import { retrieveTransformQueryConfig } from "./query-config" +import { validateAndTransformBody } from "../../utils/validate-body" +import { validateAndTransformQuery } from "../../utils/validate-query" + +export const adminShippingOptionRoutesMiddlewares: MiddlewareRoute[] = [ + { + matcher: "/admin/shipping-options*", + middlewares: [authenticate("admin", ["bearer", "session"])], + }, + + { + method: ["POST"], + matcher: "/admin/shipping-options", + middlewares: [ + validateAndTransformQuery( + AdminGetShippingOptionParams, + retrieveTransformQueryConfig + ), + validateAndTransformBody(AdminCreateShippingOption), + ], + }, + + { + method: ["POST"], + matcher: "/admin/shipping-options/:id/rules/batch/add", + middlewares: [ + validateAndTransformQuery( + AdminGetShippingOptionParams, + retrieveTransformQueryConfig + ), + validateAndTransformBody(AdminShippingOptionRulesBatchAdd), + ], + }, + + { + method: ["POST"], + matcher: "/admin/shipping-options/:id/rules/batch/remove", + middlewares: [ + validateAndTransformQuery( + AdminGetShippingOptionParams, + retrieveTransformQueryConfig + ), + validateAndTransformBody(AdminShippingOptionRulesBatchRemove), + ], + }, +] diff --git a/packages/medusa/src/api-v2/admin/shipping-options/query-config.ts b/packages/medusa/src/api-v2/admin/shipping-options/query-config.ts new file mode 100644 index 0000000000..b0622b6fda --- /dev/null +++ b/packages/medusa/src/api-v2/admin/shipping-options/query-config.ts @@ -0,0 +1,25 @@ +export const defaultAdminShippingOptionFields = [ + "id", + "name", + "price_type", + "data", + "metadata", + "created_at", + "updated_at", + "*rules", + "*type", + "*prices", + "*service_zone", + "*shipping_profile", + "*provider", +] + +export const retrieveTransformQueryConfig = { + defaults: defaultAdminShippingOptionFields, + isList: false, +} + +export const listTransformQueryConfig = { + ...retrieveTransformQueryConfig, + isList: true, +} diff --git a/packages/medusa/src/api-v2/admin/shipping-options/route.ts b/packages/medusa/src/api-v2/admin/shipping-options/route.ts new file mode 100644 index 0000000000..f548583ca0 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/shipping-options/route.ts @@ -0,0 +1,44 @@ +import { createShippingOptionsWorkflow } from "@medusajs/core-flows" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { AdminShippingOptionRetrieveResponse } from "@medusajs/types" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../types/routing" +import { AdminCreateShippingOptionType } from "./validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const shippingOptionPayload = req.validatedBody + + const workflow = createShippingOptionsWorkflow(req.scope) + + const { result, errors } = await workflow.run({ + input: [shippingOptionPayload], + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const shippingOptionId = result[0].id + + const query = remoteQueryObjectFromString({ + entryPoint: "shipping_options", + variables: { + id: shippingOptionId, + }, + fields: req.remoteQueryConfig.fields, + }) + + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + const [shippingOption] = await remoteQuery(query) + + res.status(200).json({ shipping_option: shippingOption }) +} diff --git a/packages/medusa/src/api-v2/admin/shipping-options/validators.ts b/packages/medusa/src/api-v2/admin/shipping-options/validators.ts new file mode 100644 index 0000000000..19e2626eb2 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/shipping-options/validators.ts @@ -0,0 +1,87 @@ +import { + RuleOperator, + ShippingOptionPriceType as ShippingOptionPriceTypeEnum, +} from "@medusajs/utils" +import { z } from "zod" +import { createSelectParams } from "../../utils/validators" + +export const AdminGetShippingOptionParams = createSelectParams() + +/** + * SHIPPING OPTIONS RULES + */ + +export const AdminCreateShippingOptionRule = z + .object({ + operator: z.nativeEnum(RuleOperator), + attribute: z.string(), + value: z.string().or(z.array(z.string())), + }) + .strict() + +export const AdminShippingOptionRulesBatchAdd = z + .object({ + rules: AdminCreateShippingOptionRule.array(), + }) + .strict() + +export type AdminShippingOptionRulesBatchAddType = z.infer< + typeof AdminShippingOptionRulesBatchAdd +> + +export const AdminShippingOptionRulesBatchRemove = z + .object({ + rule_ids: z.array(z.string()), + }) + .strict() + +export type AdminShippingOptionRulesBatchRemoveType = z.infer< + typeof AdminShippingOptionRulesBatchRemove +> + +/** + * SHIPPING OPTIONS + */ + +export const AdminCreateShippingOptionType = z + .object({ + label: z.string(), + description: z.string(), + code: z.string(), + }) + .strict() + +// eslint-disable-next-line max-len +export const AdminCreateShippingOptionPriceWithCurrency = z + .object({ + currency_code: z.string(), + amount: z.number(), + }) + .strict() + +export const AdminCreateShippingOptionPriceWithRegion = z + .object({ + region_id: z.string(), + amount: z.number(), + }) + .strict() + +export const AdminCreateShippingOption = z + .object({ + name: z.string(), + service_zone_id: z.string(), + shipping_profile_id: z.string(), + data: z.record(z.unknown()).optional(), + price_type: z.nativeEnum(ShippingOptionPriceTypeEnum), + provider_id: z.string(), + type: AdminCreateShippingOptionType, + prices: AdminCreateShippingOptionPriceWithCurrency.or( + AdminCreateShippingOptionPriceWithRegion + ).array(), + rules: AdminCreateShippingOptionRule.array().optional(), + }) + .strict() + +export type AdminCreateShippingOptionType = z.infer< + typeof AdminCreateShippingOption +> diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index 644af58eb9..a5c69ff688 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -5,8 +5,8 @@ import { adminCollectionRoutesMiddlewares } from "./admin/collections/middleware import { adminCurrencyRoutesMiddlewares } from "./admin/currencies/middlewares" import { adminCustomerGroupRoutesMiddlewares } from "./admin/customer-groups/middlewares" import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares" +import { adminShippingOptionRoutesMiddlewares } from "./admin/shipping-options/middlewares" import { adminDraftOrderRoutesMiddlewares } from "./admin/draft-orders/middlewares" -import { adminFulfillmentRoutesMiddlewares } from "./admin/fulfillment/middlewares" import { adminInventoryRoutesMiddlewares } from "./admin/inventory-items/middlewares" import { adminInviteRoutesMiddlewares } from "./admin/invites/middlewares" import { adminPaymentRoutesMiddlewares } from "./admin/payments/middlewares" @@ -59,7 +59,7 @@ export const config: MiddlewaresConfig = { ...adminInventoryRoutesMiddlewares, ...adminCollectionRoutesMiddlewares, ...adminPricingRoutesMiddlewares, - ...adminFulfillmentRoutesMiddlewares, + ...adminShippingOptionRoutesMiddlewares, ...adminDraftOrderRoutesMiddlewares, ...adminSalesChannelRoutesMiddlewares, ...adminStockLocationRoutesMiddlewares, diff --git a/packages/medusa/src/api-v2/store/payment-collections/[id]/payment-sessions/route.ts b/packages/medusa/src/api-v2/store/payment-collections/[id]/payment-sessions/route.ts index c6923fdb94..066c8a3e92 100644 --- a/packages/medusa/src/api-v2/store/payment-collections/[id]/payment-sessions/route.ts +++ b/packages/medusa/src/api-v2/store/payment-collections/[id]/payment-sessions/route.ts @@ -12,7 +12,7 @@ export const POST = async ( res: MedusaResponse ) => { const { id } = req.params - const { context, provider_id, data } = req.body + const { context = {}, provider_id, data } = req.body // If the customer is logged in, we auto-assign them to the payment collection if (req.auth?.actor_id) { diff --git a/packages/medusa/src/types/routing.ts b/packages/medusa/src/types/routing.ts index 098281fa95..6c49652d0a 100644 --- a/packages/medusa/src/types/routing.ts +++ b/packages/medusa/src/types/routing.ts @@ -3,8 +3,10 @@ import type { Customer, User } from "../models" import { MedusaContainer, RequestQueryFields } from "@medusajs/types" import { FindConfig } from "./common" +import * as core from "express-serve-static-core" -export interface MedusaRequest extends Request { +export interface MedusaRequest + extends Request { validatedBody: Body validatedQuery: RequestQueryFields & Record /** @@ -22,7 +24,10 @@ export interface MedusaRequest extends Request { /** * An object containing fields and variables to be used with the remoteQuery */ - remoteQueryConfig: { fields: string[]; pagination: { order?: Record, skip?: number, take?: number } } + remoteQueryConfig: { + fields: string[] + pagination: { order?: Record; skip?: number; take?: number } + } /** * An object containing the fields that are filterable e.g `{ id: Any }` */ @@ -58,12 +63,12 @@ export interface AuthenticatedMedusaRequest } } -export type MedusaResponse = Response +export type MedusaResponse = Response export type MedusaNextFunction = NextFunction -export type MedusaRequestHandler = ( - req: MedusaRequest, - res: MedusaResponse, +export type MedusaRequestHandler = ( + req: MedusaRequest, + res: MedusaResponse, next: MedusaNextFunction ) => Promise | void diff --git a/packages/types/src/http/fulfillment/admin/shipping-option.ts b/packages/types/src/http/fulfillment/admin/shipping-option.ts index 598975c55e..c96e7b8b1f 100644 --- a/packages/types/src/http/fulfillment/admin/shipping-option.ts +++ b/packages/types/src/http/fulfillment/admin/shipping-option.ts @@ -4,12 +4,12 @@ import { AdminShippingOptionTypeResponse } from "./shipping-option-type" import { AdminShippingOptionRuleResponse } from "./shipping-option-rule" import { AdminShippingProfileResponse } from "./shipping-profile" import { AdminFulfillmentProviderResponse } from "./fulfillment-provider" -import { AdminFulfillmentResponse } from "./fulfillment" +import { AdminPriceSetPriceResponse } from "../../pricing" /** * @experimental */ -export interface AdminShippingOptionResponse { +interface AdminShippingOptionResponse { id: string name: string price_type: ShippingOptionPriceType @@ -21,11 +21,18 @@ export interface AdminShippingOptionResponse { metadata: Record | null service_zone: AdminServiceZoneResponse shipping_profile: AdminShippingProfileResponse - fulfillment_provider: AdminFulfillmentProviderResponse + provider: AdminFulfillmentProviderResponse type: AdminShippingOptionTypeResponse rules: AdminShippingOptionRuleResponse[] - fulfillments: AdminFulfillmentResponse[] + prices: AdminPriceSetPriceResponse[] created_at: Date updated_at: Date deleted_at: Date | null } + +/** + * @experimental + */ +export interface AdminShippingOptionRetrieveResponse { + shipping_option: AdminShippingOptionResponse[] +} diff --git a/packages/types/src/http/index.ts b/packages/types/src/http/index.ts index e15e4461e7..2a1fec4ce3 100644 --- a/packages/types/src/http/index.ts +++ b/packages/types/src/http/index.ts @@ -1,4 +1,5 @@ export * from "./api-key" export * from "./fulfillment" +export * from "./pricing" export * from "./sales-channel" export * from "./stock-locations" diff --git a/packages/types/src/http/pricing/index.ts b/packages/types/src/http/pricing/index.ts new file mode 100644 index 0000000000..32d0b05b1e --- /dev/null +++ b/packages/types/src/http/pricing/index.ts @@ -0,0 +1 @@ +export * from "./price" diff --git a/packages/types/src/http/pricing/price.ts b/packages/types/src/http/pricing/price.ts new file mode 100644 index 0000000000..0b11d74193 --- /dev/null +++ b/packages/types/src/http/pricing/price.ts @@ -0,0 +1,7 @@ +export interface AdminPriceSetPriceResponse { + id: string + amount: number + currency_code: string + created_at: string + updated_at: string +}