From a24d6e6c97c52266d2574e9b9d6ba5fbf65e9bba Mon Sep 17 00:00:00 2001 From: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Sun, 7 Apr 2024 17:30:55 +0200 Subject: [PATCH] feat: Update service zone (#6990) --- .../__tests__/admin/fulfillment-sets.spec.ts | 167 ++++++++++++------ .../fulfillment/steps/update-service-zones.ts | 43 +++++ .../src/fulfillment/workflows/index.ts | 3 +- .../workflows/update-service-zones.ts | 15 ++ .../service-zone.spec.ts | 113 ++++++------ .../services/fulfillment-module-service.ts | 133 ++++++++++++-- .../[id]/service-zones/[zone_id]/route.ts | 82 ++++++++- .../admin/fulfillment-sets/middlewares.ts | 12 ++ .../admin/fulfillment-sets/validators.ts | 90 +++++----- .../fulfillment-sets/validators/geo-zone.ts | 36 ++++ .../src/fulfillment/mutations/service-zone.ts | 4 +- packages/types/src/fulfillment/service.ts | 33 ++-- .../http/fulfillment/admin/fulfillment-set.ts | 9 +- .../http/fulfillment/admin/service-zone.ts | 11 ++ .../types/src/workflow/fulfillment/index.ts | 2 +- ...eate-service-zones.ts => service-zones.ts} | 17 ++ 16 files changed, 576 insertions(+), 194 deletions(-) create mode 100644 packages/core-flows/src/fulfillment/steps/update-service-zones.ts create mode 100644 packages/core-flows/src/fulfillment/workflows/update-service-zones.ts create mode 100644 packages/medusa/src/api-v2/admin/fulfillment-sets/validators/geo-zone.ts rename packages/types/src/workflow/fulfillment/{create-service-zones.ts => service-zones.ts} (53%) diff --git a/integration-tests/api/__tests__/admin/fulfillment-sets.spec.ts b/integration-tests/api/__tests__/admin/fulfillment-sets.spec.ts index d24a01e67e..9499ce2ce2 100644 --- a/integration-tests/api/__tests__/admin/fulfillment-sets.spec.ts +++ b/integration-tests/api/__tests__/admin/fulfillment-sets.spec.ts @@ -26,7 +26,7 @@ medusaIntegrationTestRunner({ }) describe("POST /admin/fulfillment-sets/:id/service-zones", () => { - it("should create a service zone for a fulfillment set", async () => { + it("should create, update, and delete a service zone for a fulfillment set", async () => { const stockLocationResponse = await api.post( `/admin/stock-locations`, { @@ -120,6 +120,77 @@ medusaIntegrationTestRunner({ ]), }) ) + + const serviceZoneId = fset.service_zones[0].id + + const countryGeoZone = fset.service_zones[0].geo_zones.find( + (z) => z.type === "country" + ) + + // Updates an existing and creates a new one + const updateResponse = await api.post( + `/admin/fulfillment-sets/${fulfillmentSetId}/service-zones/${serviceZoneId}`, + { + name: "Test Zone Updated", + geo_zones: [ + { + id: countryGeoZone.id, + country_code: "us", + type: "country", + }, + { + country_code: "ca", + type: "country", + }, + ], + }, + adminHeaders + ) + + const updatedFset = updateResponse.data.fulfillment_set + + expect(updateResponse.status).toEqual(200) + expect(updatedFset).toEqual( + expect.objectContaining({ + name: "Fulfillment Set", + type: "shipping", + service_zones: expect.arrayContaining([ + expect.objectContaining({ + id: serviceZoneId, + name: "Test Zone Updated", + fulfillment_set_id: updatedFset.id, + geo_zones: expect.arrayContaining([ + expect.objectContaining({ + id: countryGeoZone.id, + country_code: "us", + type: "country", + }), + expect.objectContaining({ + country_code: "ca", + type: "country", + }), + ]), + }), + ]), + }) + ) + + const deleteResponse = await api.delete( + `/admin/fulfillment-sets/${fulfillmentSetId}/service-zones/${serviceZoneId}`, + adminHeaders + ) + + expect(deleteResponse.status).toEqual(200) + expect(deleteResponse.data).toEqual( + expect.objectContaining({ + id: serviceZoneId, + object: "service-zone", + deleted: true, + parent: expect.objectContaining({ + id: fulfillmentSetId, + }), + }) + ) }) it("should throw if invalid type is passed", async () => { @@ -251,60 +322,6 @@ medusaIntegrationTestRunner({ }) describe("POST /admin/fulfillment-sets/:id/service-zones", () => { - it("should delete a service zone for a fulfillment set", async () => { - const stockLocationResponse = await api.post( - `/admin/stock-locations`, - { - name: "test location", - }, - adminHeaders - ) - - const stockLocationId = stockLocationResponse.data.stock_location.id - - const locationWithFSetResponse = await api.post( - `/admin/stock-locations/${stockLocationId}/fulfillment-sets?fields=id,*fulfillment_sets`, - { - name: "Fulfillment Set", - type: "shipping", - }, - adminHeaders - ) - - const fulfillmentSetId = - locationWithFSetResponse.data.stock_location.fulfillment_sets[0].id - - const response = await api.post( - `/admin/fulfillment-sets/${fulfillmentSetId}/service-zones`, - { - name: "Test Zone", - geo_zones: [], - }, - adminHeaders - ) - - const fset = response.data.fulfillment_set - - const serviceZoneId = fset.service_zones[0].id - - const deleteResponse = await api.delete( - `/admin/fulfillment-sets/${fulfillmentSetId}/service-zones/${serviceZoneId}`, - adminHeaders - ) - - expect(deleteResponse.status).toEqual(200) - expect(deleteResponse.data).toEqual( - expect.objectContaining({ - id: serviceZoneId, - object: "service-zone", - deleted: true, - parent: expect.objectContaining({ - id: fulfillmentSetId, - }), - }) - ) - }) - it("should throw when fulfillment set doesn't exist", async () => { const deleteResponse = await api .delete( @@ -314,7 +331,45 @@ medusaIntegrationTestRunner({ .catch((e) => e.response) expect(deleteResponse.status).toEqual(404) - expect(deleteResponse.data.message).toEqual("FulfillmentSet with id: foo was not found") + expect(deleteResponse.data.message).toEqual( + "FulfillmentSet with id: foo was not found" + ) + }) + + it("should throw when fulfillment set doesn't have service zone", async () => { + const stockLocationResponse = await api.post( + `/admin/stock-locations`, + { + name: "test location", + }, + adminHeaders + ) + + const stockLocationId = stockLocationResponse.data.stock_location.id + + const locationWithFSetResponse = await api.post( + `/admin/stock-locations/${stockLocationId}/fulfillment-sets?fields=id,*fulfillment_sets`, + { + name: "Fulfillment Set", + type: "shipping", + }, + adminHeaders + ) + + const fulfillmentSetId = + locationWithFSetResponse.data.stock_location.fulfillment_sets[0].id + + const deleteResponse = await api + .delete( + `/admin/fulfillment-sets/${fulfillmentSetId}/service-zones/foo`, + adminHeaders + ) + .catch((e) => e.response) + + expect(deleteResponse.status).toEqual(404) + expect(deleteResponse.data.message).toEqual( + "Service zone with id: foo not found on fulfillment set" + ) }) }) }) diff --git a/packages/core-flows/src/fulfillment/steps/update-service-zones.ts b/packages/core-flows/src/fulfillment/steps/update-service-zones.ts new file mode 100644 index 0000000000..204ac20310 --- /dev/null +++ b/packages/core-flows/src/fulfillment/steps/update-service-zones.ts @@ -0,0 +1,43 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { FulfillmentWorkflow, IFulfillmentModuleService } from "@medusajs/types" +import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type StepInput = FulfillmentWorkflow.UpdateServiceZonesWorkflowInput + +export const updateServiceZonesStepId = "update-service-zones" +export const updateServiceZonesStep = createStep( + updateServiceZonesStepId, + async (input: StepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.FULFILLMENT + ) + + const { selects, relations } = getSelectsAndRelationsFromObjectArray([ + input.update, + ]) + + const prevData = await service.listServiceZones(input.selector, { + select: selects, + relations, + }) + + const updatedServiceZones = await service.updateServiceZones( + input.selector, + input.update + ) + + return new StepResponse(updatedServiceZones, prevData) + }, + async (prevData, { container }) => { + if (!prevData?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.FULFILLMENT + ) + + await service.upsertServiceZones(prevData) + } +) diff --git a/packages/core-flows/src/fulfillment/workflows/index.ts b/packages/core-flows/src/fulfillment/workflows/index.ts index 5d6c33855e..ebee8bb6b9 100644 --- a/packages/core-flows/src/fulfillment/workflows/index.ts +++ b/packages/core-flows/src/fulfillment/workflows/index.ts @@ -1,5 +1,6 @@ export * from "./add-rules-to-fulfillment-shipping-option" export * from "./create-service-zones" -export * from "./delete-service-zones" export * from "./create-shipping-options" +export * from "./delete-service-zones" export * from "./remove-rules-from-fulfillment-shipping-option" +export * from "./update-service-zones" diff --git a/packages/core-flows/src/fulfillment/workflows/update-service-zones.ts b/packages/core-flows/src/fulfillment/workflows/update-service-zones.ts new file mode 100644 index 0000000000..9255d037f4 --- /dev/null +++ b/packages/core-flows/src/fulfillment/workflows/update-service-zones.ts @@ -0,0 +1,15 @@ +import { FulfillmentWorkflow } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { updateServiceZonesStep } from "../steps/update-service-zones" + +export const updateServiceZonesWorkflowId = "update-service-zones-workflow" +export const updateServiceZonesWorkflow = createWorkflow( + updateServiceZonesWorkflowId, + ( + input: WorkflowData + ): WorkflowData => { + const serviceZones = updateServiceZonesStep(input) + + return serviceZones + } +) diff --git a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/service-zone.spec.ts b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/service-zone.spec.ts index ae7c680c22..fd7b1eb3f0 100644 --- a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/service-zone.spec.ts +++ b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/service-zone.spec.ts @@ -284,7 +284,6 @@ moduleIntegrationTestRunner({ ) const updateData = { - id: createdServiceZone.id, name: "updated-service-zone-test", geo_zones: [ { @@ -296,12 +295,13 @@ moduleIntegrationTestRunner({ } const updatedServiceZone = await service.updateServiceZones( + createdServiceZone.id, updateData ) expect(updatedServiceZone).toEqual( expect.objectContaining({ - id: updateData.id, + id: createdServiceZone.id, name: updateData.name, geo_zones: expect.arrayContaining([ expect.objectContaining({ @@ -314,7 +314,60 @@ moduleIntegrationTestRunner({ ) }) - it("should update a collection of service zones", async function () { + it("should fail on duplicated service zone name", async function () { + const fulfillmentSet = await service.create({ + name: "test", + type: "test-type", + }) + + const createData: CreateServiceZoneDTO[] = [ + { + name: "service-zone-test", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "fr", + }, + ], + }, + { + name: "service-zone-test2", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "us", + }, + ], + }, + ] + + const createdServiceZones = await service.createServiceZones( + createData + ) + + const updateData: UpdateServiceZoneDTO = { + name: "service-zone-test", + geo_zones: [ + { + type: GeoZoneType.COUNTRY, + country_code: "us", + }, + ], + } + + const err = await service + .updateServiceZones(createdServiceZones[1].id, updateData) + .catch((e) => e) + + expect(err).toBeDefined() + expect(err.message).toContain("exists") + }) + }) + + describe("on upsert", () => { + it("should upsert a collection of service zones", async function () { const fulfillmentSet = await service.create({ name: "test", type: "test-type", @@ -360,7 +413,7 @@ moduleIntegrationTestRunner({ }) ) - const updatedServiceZones = await service.updateServiceZones( + const updatedServiceZones = await service.upsertServiceZones( updateData ) @@ -385,58 +438,6 @@ moduleIntegrationTestRunner({ ) } }) - - it("should fail on duplicated service zone name", async function () { - const fulfillmentSet = await service.create({ - name: "test", - type: "test-type", - }) - - const createData: CreateServiceZoneDTO[] = [ - { - name: "service-zone-test", - fulfillment_set_id: fulfillmentSet.id, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "fr", - }, - ], - }, - { - name: "service-zone-test2", - fulfillment_set_id: fulfillmentSet.id, - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "us", - }, - ], - }, - ] - - const createdServiceZones = await service.createServiceZones( - createData - ) - - const updateData: UpdateServiceZoneDTO = { - id: createdServiceZones[1].id, - name: "service-zone-test", - geo_zones: [ - { - type: GeoZoneType.COUNTRY, - country_code: "us", - }, - ], - } - - const err = await service - .updateServiceZones(updateData) - .catch((e) => e) - - expect(err).toBeDefined() - expect(err.message).toContain("exists") - }) }) }) }) diff --git a/packages/fulfillment/src/services/fulfillment-module-service.ts b/packages/fulfillment/src/services/fulfillment-module-service.ts index 44a5f5d4aa..f1e65574ac 100644 --- a/packages/fulfillment/src/services/fulfillment-module-service.ts +++ b/packages/fulfillment/src/services/fulfillment-module-service.ts @@ -1,3 +1,4 @@ +import { Modules } from "@medusajs/modules-sdk" import { Context, DAL, @@ -11,20 +12,21 @@ import { ModulesSdkTypes, ShippingOptionDTO, UpdateFulfillmentSetDTO, + UpdateServiceZoneDTO, } from "@medusajs/types" import { - arrayDifference, EmitEvents, FulfillmentUtils, - getSetDifference, InjectManager, InjectTransactionManager, MedusaContext, MedusaError, ModulesSdkUtils, + arrayDifference, + getSetDifference, + isString, promiseAll, } from "@medusajs/utils" - import { Fulfillment, FulfillmentSet, @@ -38,7 +40,6 @@ import { import { isContextValid, validateRules } from "@utils" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import FulfillmentProviderService from "./fulfillment-provider" -import { Modules } from "@medusajs/modules-sdk" const generateMethodForModels = [ ServiceZone, @@ -791,31 +792,56 @@ export default class FulfillmentModuleService< } updateServiceZones( - data: FulfillmentTypes.UpdateServiceZoneDTO[], - sharedContext?: Context - ): Promise - updateServiceZones( + id: string, data: FulfillmentTypes.UpdateServiceZoneDTO, sharedContext?: Context ): Promise + updateServiceZones( + selector: FulfillmentTypes.FilterableServiceZoneProps, + data: FulfillmentTypes.UpdateServiceZoneDTO, + sharedContext?: Context + ): Promise @InjectManager("baseRepository_") async updateServiceZones( - data: - | FulfillmentTypes.UpdateServiceZoneDTO[] - | FulfillmentTypes.UpdateServiceZoneDTO, + idOrSelector: string | FulfillmentTypes.FilterableServiceZoneProps, + data: FulfillmentTypes.UpdateServiceZoneDTO, @MedusaContext() sharedContext: Context = {} ): Promise< FulfillmentTypes.ServiceZoneDTO[] | FulfillmentTypes.ServiceZoneDTO > { + const normalizedInput: UpdateServiceZoneDTO[] = [] + + if (isString(idOrSelector)) { + normalizedInput.push({ id: idOrSelector, ...data }) + } else { + const serviceZones = await this.serviceZoneService_.list( + { ...idOrSelector }, + {}, + sharedContext + ) + + if (!serviceZones.length) { + return [] + } + + for (const serviceZone of serviceZones) { + normalizedInput.push({ id: serviceZone.id, ...data }) + } + } + const updatedServiceZones = await this.updateServiceZones_( - data, + normalizedInput, sharedContext ) + const toReturn = isString(idOrSelector) + ? updatedServiceZones[0] + : updatedServiceZones + return await this.baseRepository_.serialize< FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[] - >(updatedServiceZones, { + >(toReturn, { populate: true, }) } @@ -873,7 +899,7 @@ export default class FulfillmentModuleService< data_.forEach((serviceZone) => { if (serviceZone.geo_zones) { - const existingServiceZone = serviceZoneMap.get(serviceZone.id)! + const existingServiceZone = serviceZoneMap.get(serviceZone.id!)! const existingGeoZones = existingServiceZone.geo_zones const updatedGeoZones = serviceZone.geo_zones const toDeleteGeoZoneIds = getSetDifference( @@ -920,7 +946,9 @@ export default class FulfillmentModuleService< FulfillmentModuleService.validateGeoZones([geoZone]) return geoZone } - return geoZonesMap.get(geoZone.id)! + const existing = geoZonesMap.get(geoZone.id)! + + return { ...existing, ...geoZone } }) } }) @@ -942,6 +970,81 @@ export default class FulfillmentModuleService< return Array.isArray(data) ? updatedServiceZones : updatedServiceZones[0] } + upsertServiceZones( + data: FulfillmentTypes.UpsertServiceZoneDTO, + sharedContext?: Context + ): Promise + upsertServiceZones( + data: FulfillmentTypes.UpsertServiceZoneDTO[], + sharedContext?: Context + ): Promise + + @InjectManager("baseRepository_") + async upsertServiceZones( + data: + | FulfillmentTypes.UpsertServiceZoneDTO + | FulfillmentTypes.UpsertServiceZoneDTO[], + sharedContext?: Context + ): Promise< + FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[] + > { + const upsertServiceZones = await this.upsertServiceZones_( + data, + sharedContext + ) + + const allServiceZones = await this.baseRepository_.serialize< + FulfillmentTypes.ServiceZoneDTO[] | FulfillmentTypes.ServiceZoneDTO + >(upsertServiceZones) + + return Array.isArray(data) ? allServiceZones : allServiceZones[0] + } + + @InjectTransactionManager("baseRepository_") + async upsertServiceZones_( + data: + | FulfillmentTypes.UpsertServiceZoneDTO[] + | FulfillmentTypes.UpsertServiceZoneDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const input = Array.isArray(data) ? data : [data] + const forUpdate = input.filter( + (serviceZone): serviceZone is FulfillmentTypes.UpdateServiceZoneDTO => + !!serviceZone.id + ) + const forCreate = input.filter( + (serviceZone): serviceZone is FulfillmentTypes.CreateServiceZoneDTO => + !serviceZone.id + ) + + const created: TServiceZoneEntity[] = [] + const updated: TServiceZoneEntity[] = [] + + if (forCreate.length) { + const createdServiceZones = await this.createServiceZones_( + forCreate, + sharedContext + ) + const toPush = Array.isArray(createdServiceZones) + ? createdServiceZones + : [createdServiceZones] + created.push(...toPush) + } + + if (forUpdate.length) { + const updatedServiceZones = await this.updateServiceZones_( + forUpdate, + sharedContext + ) + const toPush = Array.isArray(updatedServiceZones) + ? updatedServiceZones + : [updatedServiceZones] + updated.push(...toPush) + } + + return [...created, ...updated] + } + updateShippingOptions( data: FulfillmentTypes.UpdateShippingOptionDTO[], sharedContext?: Context diff --git a/packages/medusa/src/api-v2/admin/fulfillment-sets/[id]/service-zones/[zone_id]/route.ts b/packages/medusa/src/api-v2/admin/fulfillment-sets/[id]/service-zones/[zone_id]/route.ts index 90a2d7c279..7875761c4a 100644 --- a/packages/medusa/src/api-v2/admin/fulfillment-sets/[id]/service-zones/[zone_id]/route.ts +++ b/packages/medusa/src/api-v2/admin/fulfillment-sets/[id]/service-zones/[zone_id]/route.ts @@ -1,14 +1,78 @@ +import { + deleteServiceZonesWorkflow, + updateServiceZonesWorkflow, +} from "@medusajs/core-flows" import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IFulfillmentModuleService } from "@medusajs/types" -import { deleteServiceZonesWorkflow } from "../../../../../../../../core-flows/dist" +import { + AdminFulfillmentSetResponse, + AdminServiceZoneDeleteResponse, + IFulfillmentModuleService, +} from "@medusajs/types" +import { + ContainerRegistrationKeys, + MedusaError, + remoteQueryObjectFromString, +} from "@medusajs/utils" import { AuthenticatedMedusaRequest, + MedusaRequest, MedusaResponse, } from "../../../../../../types/routing" +import { AdminUpdateFulfillmentSetServiceZonesType } from "../../../validators" + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const fulfillmentModuleService = req.scope.resolve( + ModuleRegistrationName.FULFILLMENT + ) + + // ensure fulfillment set exists and that the service zone is part of it + const fulfillmentSet = await fulfillmentModuleService.retrieve( + req.params.id, + { relations: ["service_zones"] } + ) + + if (!fulfillmentSet.service_zones.find((s) => s.id === req.params.zone_id)) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Service zone with id: ${req.params.zone_id} not found on fulfillment set` + ) + } + + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const workflowInput = { + selector: { id: req.params.zone_id }, + update: req.validatedBody, + } + + const { errors } = await updateServiceZonesWorkflow(req.scope).run({ + input: workflowInput, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const [fulfillment_set] = await remoteQuery( + remoteQueryObjectFromString({ + entryPoint: "fulfillment_sets", + variables: { + id: req.params.id, + }, + fields: req.remoteQueryConfig.fields, + }) + ) + + res.status(200).json({ fulfillment_set }) +} export const DELETE = async ( req: AuthenticatedMedusaRequest, - res: MedusaResponse + res: MedusaResponse ) => { const { id, zone_id } = req.params @@ -16,7 +80,17 @@ export const DELETE = async ( ModuleRegistrationName.FULFILLMENT ) - const fulfillmentSet = await fulfillmentModuleService.retrieve(id) + // ensure fulfillment set exists and that the service zone is part of it + const fulfillmentSet = await fulfillmentModuleService.retrieve(id, { + relations: ["service_zones"], + }) + + if (!fulfillmentSet.service_zones.find((s) => s.id === zone_id)) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Service zone with id: ${zone_id} not found on fulfillment set` + ) + } const { errors } = await deleteServiceZonesWorkflow(req.scope).run({ input: { ids: [zone_id] }, diff --git a/packages/medusa/src/api-v2/admin/fulfillment-sets/middlewares.ts b/packages/medusa/src/api-v2/admin/fulfillment-sets/middlewares.ts index 683d63af69..cac37205cc 100644 --- a/packages/medusa/src/api-v2/admin/fulfillment-sets/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/fulfillment-sets/middlewares.ts @@ -6,6 +6,7 @@ import * as QueryConfig from "./query-config" import { AdminCreateFulfillmentSetServiceZonesSchema, AdminFulfillmentSetParams, + AdminUpdateFulfillmentSetServiceZonesSchema, } from "./validators" export const adminFulfillmentSetsRoutesMiddlewares: MiddlewareRoute[] = [ @@ -30,4 +31,15 @@ export const adminFulfillmentSetsRoutesMiddlewares: MiddlewareRoute[] = [ matcher: "/admin/fulfillment-sets/:id/service-zones/:zone_id", middlewares: [], }, + { + method: ["POST"], + matcher: "/admin/fulfillment-sets/:id/service-zones/:zone_id", + middlewares: [ + validateAndTransformBody(AdminUpdateFulfillmentSetServiceZonesSchema), + validateAndTransformQuery( + AdminFulfillmentSetParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, ] diff --git a/packages/medusa/src/api-v2/admin/fulfillment-sets/validators.ts b/packages/medusa/src/api-v2/admin/fulfillment-sets/validators.ts index f142519a4b..39195d216e 100644 --- a/packages/medusa/src/api-v2/admin/fulfillment-sets/validators.ts +++ b/packages/medusa/src/api-v2/admin/fulfillment-sets/validators.ts @@ -1,62 +1,44 @@ import { z } from "zod" import { createFindParams, createOperatorMap } from "../../utils/validators" - -const geoZoneBaseSchema = z.object({ - country_code: z.string(), - metadata: z.record(z.unknown()).optional(), -}) - -const geoZoneCountrySchema = geoZoneBaseSchema.merge( - z.object({ - type: z.literal("country"), - }) -) - -const geoZoneProvinceSchema = geoZoneBaseSchema.merge( - z.object({ - type: z.literal("province"), - province_code: z.string(), - }) -) - -const geoZoneCitySchema = geoZoneBaseSchema.merge( - z.object({ - type: z.literal("city"), - province_code: z.string(), - city: z.string(), - }) -) - -const geoZoneZipSchema = geoZoneBaseSchema.merge( - z.object({ - type: z.literal("zip"), - province_code: z.string(), - city: z.string(), - postal_expression: z.record(z.unknown()), - }) -) +import { + geoZoneCitySchema, + geoZoneCountrySchema, + geoZoneProvinceSchema, + geoZoneZipSchema, +} from "./validators/geo-zone" export const AdminCreateFulfillmentSetServiceZonesSchema = z .object({ name: z.string(), - geo_zones: z.array( - z.union([ - geoZoneCountrySchema, - geoZoneProvinceSchema, - geoZoneCitySchema, - geoZoneZipSchema, - ]) - ), + geo_zones: z + .array( + z.union([ + geoZoneCountrySchema, + geoZoneProvinceSchema, + geoZoneCitySchema, + geoZoneZipSchema, + ]) + ) + .optional(), }) .strict() -export type AdminCreateFulfillmentSetServiceZonesType = z.infer< - typeof AdminCreateFulfillmentSetServiceZonesSchema -> +export const AdminUpdateFulfillmentSetServiceZonesSchema = z + .object({ + name: z.string().optional(), + geo_zones: z + .array( + z.union([ + geoZoneCountrySchema.merge(z.object({ id: z.string().optional() })), + geoZoneProvinceSchema.merge(z.object({ id: z.string().optional() })), + geoZoneCitySchema.merge(z.object({ id: z.string().optional() })), + geoZoneZipSchema.merge(z.object({ id: z.string().optional() })), + ]) + ) + .optional(), + }) + .strict() -export type AdminFulfillmentSetParamsType = z.infer< - typeof AdminFulfillmentSetParams -> export const AdminFulfillmentSetParams = createFindParams({ limit: 20, offset: 0, @@ -71,3 +53,13 @@ export const AdminFulfillmentSetParams = createFindParams({ deleted_at: createOperatorMap().optional(), }) ) + +export type AdminCreateFulfillmentSetServiceZonesType = z.infer< + typeof AdminCreateFulfillmentSetServiceZonesSchema +> +export type AdminUpdateFulfillmentSetServiceZonesType = z.infer< + typeof AdminUpdateFulfillmentSetServiceZonesSchema +> +export type AdminFulfillmentSetParamsType = z.infer< + typeof AdminFulfillmentSetParams +> diff --git a/packages/medusa/src/api-v2/admin/fulfillment-sets/validators/geo-zone.ts b/packages/medusa/src/api-v2/admin/fulfillment-sets/validators/geo-zone.ts new file mode 100644 index 0000000000..0e42d031da --- /dev/null +++ b/packages/medusa/src/api-v2/admin/fulfillment-sets/validators/geo-zone.ts @@ -0,0 +1,36 @@ +import { z } from "zod" + +const geoZoneBaseSchema = z.object({ + country_code: z.string(), + metadata: z.record(z.unknown()).optional(), +}) + +export const geoZoneCountrySchema = geoZoneBaseSchema.merge( + z.object({ + type: z.literal("country"), + }) +) + +export const geoZoneProvinceSchema = geoZoneBaseSchema.merge( + z.object({ + type: z.literal("province"), + province_code: z.string(), + }) +) + +export const geoZoneCitySchema = geoZoneBaseSchema.merge( + z.object({ + type: z.literal("city"), + province_code: z.string(), + city: z.string(), + }) +) + +export const geoZoneZipSchema = geoZoneBaseSchema.merge( + z.object({ + type: z.literal("zip"), + province_code: z.string(), + city: z.string(), + postal_expression: z.record(z.unknown()), + }) +) diff --git a/packages/types/src/fulfillment/mutations/service-zone.ts b/packages/types/src/fulfillment/mutations/service-zone.ts index 6c37b3264a..72f23f0d84 100644 --- a/packages/types/src/fulfillment/mutations/service-zone.ts +++ b/packages/types/src/fulfillment/mutations/service-zone.ts @@ -17,7 +17,7 @@ export interface CreateServiceZoneDTO { } export interface UpdateServiceZoneDTO { - id: string + id?: string name?: string geo_zones?: ( | Omit @@ -27,3 +27,5 @@ export interface UpdateServiceZoneDTO { | { id: string } )[] } + +export interface UpsertServiceZoneDTO extends UpdateServiceZoneDTO {} diff --git a/packages/types/src/fulfillment/service.ts b/packages/types/src/fulfillment/service.ts index 4e0110f62e..57852f5d1e 100644 --- a/packages/types/src/fulfillment/service.ts +++ b/packages/types/src/fulfillment/service.ts @@ -1,4 +1,7 @@ +import { FindConfig } from "../common" +import { RestoreReturn, SoftDeleteReturn } from "../dal" import { IModuleService } from "../modules-sdk" +import { Context } from "../shared-context" import { FilterableFulfillmentProps, FilterableFulfillmentSetProps, @@ -18,9 +21,6 @@ import { ShippingOptionTypeDTO, ShippingProfileDTO, } from "./common" -import { FindConfig } from "../common" -import { Context } from "../shared-context" -import { RestoreReturn, SoftDeleteReturn } from "../dal" import { CreateFulfillmentSetDTO, CreateGeoZoneDTO, @@ -34,9 +34,10 @@ import { UpdateShippingOptionDTO, UpdateShippingOptionRuleDTO, UpdateShippingProfileDTO, + UpsertServiceZoneDTO, } from "./mutations" -import { CreateShippingProfileDTO } from "./mutations/shipping-profile" import { CreateFulfillmentDTO } from "./mutations/fulfillment" +import { CreateShippingProfileDTO } from "./mutations/shipping-profile" export interface IFulfillmentModuleService extends IModuleService { /** @@ -176,17 +177,29 @@ export interface IFulfillmentModuleService extends IModuleService { ): Promise /** * Update a service zone - * @param data - * @param sharedContext */ updateServiceZones( - data: UpdateServiceZoneDTO[], - sharedContext?: Context - ): Promise - updateServiceZones( + id: string, data: UpdateServiceZoneDTO, sharedContext?: Context ): Promise + updateServiceZones( + selector: FilterableServiceZoneProps, + data: UpdateServiceZoneDTO, + sharedContext?: Context + ): Promise + + /** + * Upsert a service zone + */ + upsertServiceZones( + data: UpsertServiceZoneDTO, + sharedContext?: Context + ): Promise + upsertServiceZones( + data: UpsertServiceZoneDTO[], + sharedContext?: Context + ): Promise /** * Delete a service zone * @param ids diff --git a/packages/types/src/http/fulfillment/admin/fulfillment-set.ts b/packages/types/src/http/fulfillment/admin/fulfillment-set.ts index 060ebe0e89..04d6669bc2 100644 --- a/packages/types/src/http/fulfillment/admin/fulfillment-set.ts +++ b/packages/types/src/http/fulfillment/admin/fulfillment-set.ts @@ -3,7 +3,7 @@ import { AdminServiceZoneResponse } from "./service-zone" /** * @experimental */ -export interface AdminFulfillmentSetResponse { +export interface FulfillmentSetResponse { id: string name: string type: string @@ -13,3 +13,10 @@ export interface AdminFulfillmentSetResponse { updated_at: Date deleted_at: Date | null } + +/** + * @experimental + */ +export interface AdminFulfillmentSetResponse { + fulfillment_set: FulfillmentSetResponse +} diff --git a/packages/types/src/http/fulfillment/admin/service-zone.ts b/packages/types/src/http/fulfillment/admin/service-zone.ts index 1e88e90466..d76334e259 100644 --- a/packages/types/src/http/fulfillment/admin/service-zone.ts +++ b/packages/types/src/http/fulfillment/admin/service-zone.ts @@ -1,3 +1,4 @@ +import { FulfillmentSetResponse } from "./fulfillment-set" import { AdminGeoZoneResponse } from "./geo-zone" /** @@ -12,3 +13,13 @@ export interface AdminServiceZoneResponse { updated_at: Date deleted_at: Date | null } + +/** + * @experimental + */ +export interface AdminServiceZoneDeleteResponse { + id: string + object: "service-zone" + deleted: boolean + parent: FulfillmentSetResponse +} diff --git a/packages/types/src/workflow/fulfillment/index.ts b/packages/types/src/workflow/fulfillment/index.ts index 681e6f4ef0..d21643ded9 100644 --- a/packages/types/src/workflow/fulfillment/index.ts +++ b/packages/types/src/workflow/fulfillment/index.ts @@ -1,2 +1,2 @@ -export * from "./create-service-zones" export * from "./create-shipping-options" +export * from "./service-zones" diff --git a/packages/types/src/workflow/fulfillment/create-service-zones.ts b/packages/types/src/workflow/fulfillment/service-zones.ts similarity index 53% rename from packages/types/src/workflow/fulfillment/create-service-zones.ts rename to packages/types/src/workflow/fulfillment/service-zones.ts index 100e815a82..d2c21f89d5 100644 --- a/packages/types/src/workflow/fulfillment/create-service-zones.ts +++ b/packages/types/src/workflow/fulfillment/service-zones.ts @@ -3,6 +3,7 @@ import { CreateCountryGeoZoneDTO, CreateProvinceGeoZoneDTO, CreateZipGeoZoneDTO, + FilterableServiceZoneProps, } from "../../fulfillment" interface CreateServiceZone { @@ -19,3 +20,19 @@ interface CreateServiceZone { export interface CreateServiceZonesWorkflowInput { data: CreateServiceZone[] } + +interface UpdateServiceZone { + name?: string + geo_zones?: ( + | Omit + | Omit + | Omit + | Omit + | { id: string } + )[] +} + +export interface UpdateServiceZonesWorkflowInput { + selector: FilterableServiceZoneProps + update: UpdateServiceZone +}