diff --git a/integration-tests/modules/__tests__/fulfillment/admin/shipping-option-rules.spec.ts b/integration-tests/modules/__tests__/shipping-options/admin/shipping-option-rules.spec.ts similarity index 100% rename from integration-tests/modules/__tests__/fulfillment/admin/shipping-option-rules.spec.ts rename to integration-tests/modules/__tests__/shipping-options/admin/shipping-option-rules.spec.ts diff --git a/integration-tests/modules/__tests__/fulfillment/admin/shipping-options.spec.ts b/integration-tests/modules/__tests__/shipping-options/admin/shipping-options.spec.ts similarity index 59% rename from integration-tests/modules/__tests__/fulfillment/admin/shipping-options.spec.ts rename to integration-tests/modules/__tests__/shipping-options/admin/shipping-options.spec.ts index 57196ab4d3..7a647d78d5 100644 --- a/integration-tests/modules/__tests__/fulfillment/admin/shipping-options.spec.ts +++ b/integration-tests/modules/__tests__/shipping-options/admin/shipping-options.spec.ts @@ -202,6 +202,141 @@ medusaIntegrationTestRunner({ ) }) }) + + describe("POST /admin/shipping-options/:id", () => { + it("should throw error when required params are missing", 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 + ) + + const shippingOptionId = response.data.shipping_option.id + + const updateShippingOptionPayload = { + } + + let err = await api + .post( + `/admin/shipping-options/${shippingOptionId}`, + updateShippingOptionPayload, + adminHeaders + ) + .catch((e) => e.response) + + const errorsFields = [ + { + code: "invalid_type", + expected: "string", + received: "undefined", + path: ["id"], + 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/integration-tests/modules/__tests__/fulfillment/workflows/create-shipping-options.ts b/integration-tests/modules/__tests__/shipping-options/workflows/create-shipping-options.ts similarity index 100% rename from integration-tests/modules/__tests__/fulfillment/workflows/create-shipping-options.ts rename to integration-tests/modules/__tests__/shipping-options/workflows/create-shipping-options.ts diff --git a/integration-tests/modules/__tests__/shipping-options/workflows/update-shipping-options.ts b/integration-tests/modules/__tests__/shipping-options/workflows/update-shipping-options.ts new file mode 100644 index 0000000000..a786771465 --- /dev/null +++ b/integration-tests/modules/__tests__/shipping-options/workflows/update-shipping-options.ts @@ -0,0 +1,326 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + FulfillmentSetDTO, + FulfillmentWorkflow, + IFulfillmentModuleService, + IRegionModuleService, + ServiceZoneDTO, + ShippingProfileDTO, + UpdateShippingOptionsWorkflowInput, +} from "@medusajs/types" +import { medusaIntegrationTestRunner } from "medusa-test-utils/dist" +import { + createShippingOptionsWorkflow, + updateShippingOptionsWorkflow, +} from "@medusajs/core-flows" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, + RuleOperator, +} from "@medusajs/utils" + +jest.setTimeout(100000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const provider_id = "manual_test-provider" + +medusaIntegrationTestRunner({ + env, + testSuite: ({ getContainer }) => { + let service: IFulfillmentModuleService + let container + + beforeAll(() => { + container = getContainer() + service = container.resolve(ModuleRegistrationName.FULFILLMENT) + }) + + describe("Fulfillment workflows", () => { + let fulfillmentSet: FulfillmentSetDTO + let serviceZone: ServiceZoneDTO + let shippingProfile: ShippingProfileDTO + + beforeEach(async () => { + shippingProfile = await service.createShippingProfiles({ + name: "test", + type: "default", + }) + + fulfillmentSet = await service.create({ + name: "Test fulfillment set", + type: "manual_test", + }) + + serviceZone = await service.createServiceZones({ + name: "Test service zone", + fulfillment_set_id: fulfillmentSet.id, + geo_zones: [ + { + type: "country", + country_code: "US", + }, + ], + }) + }) + + it("should update shipping options", async () => { + const regionService = container.resolve( + ModuleRegistrationName.REGION + ) as IRegionModuleService + + const [region] = await regionService.create([ + { + name: "Test region", + currency_code: "eur", + countries: ["fr"], + }, + ]) + + const shippingOptionData: FulfillmentWorkflow.CreateShippingOptionsWorkflowInput = + { + name: "Test shipping option", + price_type: "flat", + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + provider_id, + type: { + code: "manual-type", + label: "Manual Type", + description: "Manual Type Description", + }, + prices: [ + { + currency_code: "usd", + amount: 10, + }, + { + region_id: region.id, + amount: 100, + }, + ], + rules: [ + { + attribute: "total", + operator: RuleOperator.EQ, + value: "100", + }, + ], + } + + const { result } = await createShippingOptionsWorkflow(container).run({ + input: [shippingOptionData], + }) + + const updateData: UpdateShippingOptionsWorkflowInput = { + id: result[0].id, + name: "Test shipping option", + price_type: "flat", + type: { + code: "manual-type", + label: "Manual Type", + description: "Manual Type Description", + }, + } + + await updateShippingOptionsWorkflow(container).run({ + input: [updateData], + }) + + const remoteQuery = container.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) + + const remoteQueryObject = remoteQueryObjectFromString({ + entryPoint: "shipping_option", + variables: { + id: result[0].id, + }, + fields: [ + "id", + "name", + "price_type", + "service_zone_id", + "shipping_profile_id", + "provider_id", + "data", + "metadata", + "type.*", + "created_at", + "updated_at", + "deleted_at", + "shipping_option_type_id", + "prices.*", + ], + }) + + const [createdShippingOption] = await remoteQuery(remoteQueryObject) + + const prices = createdShippingOption.prices + delete createdShippingOption.prices + + expect(createdShippingOption).toEqual( + expect.objectContaining({ + id: result[0].id, + name: updateData.name, + price_type: updateData.price_type, + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + provider_id: provider_id, + data: null, + metadata: null, + type: expect.objectContaining({ + id: expect.any(String), + code: updateData.type.code, + label: updateData.type.label, + description: updateData.type.description, + }), + shipping_option_type_id: expect.any(String), + }) + ) + + expect(prices).toHaveLength(2) + expect(prices).toContainEqual( + expect.objectContaining({ + currency_code: "usd", + amount: 10, + }) + ) + expect(prices).toContainEqual( + expect.objectContaining({ + currency_code: "eur", + amount: 100, + rules_count: 1, + }) + ) + }) + + it("should revert the shipping options", async () => { + const regionService = container.resolve( + ModuleRegistrationName.REGION + ) as IRegionModuleService + + const [region] = await regionService.create([ + { + name: "Test region", + currency_code: "eur", + countries: ["fr"], + }, + ]) + + const shippingOptionData: FulfillmentWorkflow.CreateShippingOptionsWorkflowInput = + { + name: "Test shipping option", + price_type: "flat", + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + provider_id, + type: { + code: "manual-type", + label: "Manual Type", + description: "Manual Type Description", + }, + prices: [ + { + currency_code: "usd", + amount: 10, + }, + { + region_id: region.id, + amount: 100, + }, + ], + rules: [ + { + attribute: "total", + operator: RuleOperator.EQ, + value: "100", + }, + ], + } + + const updateWorkflow = await updateShippingOptionsWorkflow(container) + + updateWorkflow.addAction( + "throw", + { + invoke: async function failStep() { + throw new Error(`Failed to update shipping options`) + }, + }, + { + noCompensation: true, + } + ) + + const { result } = await createShippingOptionsWorkflow(container).run({ + input: [shippingOptionData], + }) + + const updateData: UpdateShippingOptionsWorkflowInput = { + id: result[0].id, + name: "Test shipping option", + price_type: "flat", + type: { + code: "manual-type", + label: "Manual Type", + description: "Manual Type Description", + }, + } + + const { errors } = await updateWorkflow.run({ + input: [updateData], + throwOnError: false, + }) + + expect(errors).toHaveLength(1) + expect(errors[0].error.message).toEqual( + `Failed to update shipping options` + ) + + const remoteQuery = container.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) + + const remoteQueryObject = remoteQueryObjectFromString({ + entryPoint: "shipping_option", + fields: [ + "id", + "name", + "price_type", + "service_zone_id", + "shipping_profile_id", + "provider_id", + "data", + "metadata", + "type.*", + "created_at", + "updated_at", + "deleted_at", + "shipping_option_type_id", + ], + }) + + const createdShippingOptions = await remoteQuery(remoteQueryObject) + + expect(createdShippingOptions).toHaveLength(1) + expect(createdShippingOptions[0]).toEqual( + expect.objectContaining({ + name: shippingOptionData.name, + price_type: shippingOptionData.price_type, + service_zone_id: serviceZone.id, + shipping_profile_id: shippingProfile.id, + provider_id: provider_id, + data: null, + metadata: null, + type: expect.objectContaining({ + id: expect.any(String), + code: shippingOptionData.type.code, + label: shippingOptionData.type.label, + description: shippingOptionData.type.description, + }), + }) + ) + }) + }) + }, +}) diff --git a/packages/core-flows/src/fulfillment/steps/create-shipping-options.ts b/packages/core-flows/src/fulfillment/steps/create-shipping-options.ts deleted file mode 100644 index c2a05f4b49..0000000000 --- a/packages/core-flows/src/fulfillment/steps/create-shipping-options.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { createStep, StepResponse } from "@medusajs/workflows-sdk" -import { - FulfillmentWorkflow, - IFulfillmentModuleService, - ShippingOptionDTO, -} from "@medusajs/types" -import { ModuleRegistrationName } from "@medusajs/modules-sdk" - -type StepInput = Omit< - FulfillmentWorkflow.CreateShippingOptionsWorkflowInput, - "prices" ->[] - -export const createShippingOptionsStepId = "create-shipping-options-step" -export const createShippingOptionsStep = createStep( - createShippingOptionsStepId, - async (input: StepInput, { container }) => { - if (!input?.length) { - return new StepResponse([], []) - } - - const fulfillmentService = container.resolve( - ModuleRegistrationName.FULFILLMENT - ) - const createdShippingOptions: ShippingOptionDTO[] = - await fulfillmentService.createShippingOptions(input) - - const shippingOptionIds = createdShippingOptions.map((s) => s.id) - - return new StepResponse(createdShippingOptions, shippingOptionIds) - }, - async (shippingOptionIds, { container }) => { - if (!shippingOptionIds?.length) { - return - } - - const fulfillmentService = container.resolve( - ModuleRegistrationName.FULFILLMENT - ) - - await fulfillmentService.deleteShippingOptions(shippingOptionIds) - } -) diff --git a/packages/core-flows/src/fulfillment/steps/index.ts b/packages/core-flows/src/fulfillment/steps/index.ts index 7c4893c164..7405526dfa 100644 --- a/packages/core-flows/src/fulfillment/steps/index.ts +++ b/packages/core-flows/src/fulfillment/steps/index.ts @@ -2,6 +2,6 @@ export * from "./add-rules-to-fulfillment-shipping-option" export * from "./add-shipping-options-prices" export * from "./create-fulfillment-set" export * from "./create-service-zones" -export * from "./create-shipping-options" +export * from "./upsert-shipping-options" export * from "./delete-service-zones" export * from "./remove-rules-from-fulfillment-shipping-option" diff --git a/packages/core-flows/src/fulfillment/steps/upsert-shipping-options.ts b/packages/core-flows/src/fulfillment/steps/upsert-shipping-options.ts new file mode 100644 index 0000000000..c0daedd9be --- /dev/null +++ b/packages/core-flows/src/fulfillment/steps/upsert-shipping-options.ts @@ -0,0 +1,100 @@ +import { createStep, StepResponse } from "@medusajs/workflows-sdk" +import { + FulfillmentWorkflow, + IFulfillmentModuleService, + ShippingOptionDTO, +} from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + arrayDifference, + getSelectsAndRelationsFromObjectArray, +} from "@medusajs/utils" +import { UpsertShippingOptionDTO } from "@medusajs/types/src" + +type StepInput = Omit< + | FulfillmentWorkflow.CreateShippingOptionsWorkflowInput + | FulfillmentWorkflow.UpdateShippingOptionsWorkflowInput, + "prices" +>[] + +export const upsertShippingOptionsStepId = "create-shipping-options-step" +export const upsertShippingOptionsStep = createStep( + upsertShippingOptionsStepId, + async (input: StepInput, { container }) => { + if (!input?.length) { + return new StepResponse([], {}) + } + + const fulfillmentService = container.resolve( + ModuleRegistrationName.FULFILLMENT + ) + + const toUpdate: FulfillmentWorkflow.UpdateShippingOptionsWorkflowInput[] = + [] + + ;( + input as FulfillmentWorkflow.UpdateShippingOptionsWorkflowInput[] + ).forEach((inputItem) => { + if (!!inputItem.id) { + return toUpdate.push(inputItem) + } + return + }) + + let toUpdatePreviousData: ShippingOptionDTO[] = [] + + if (toUpdate.length) { + const { selects, relations } = + getSelectsAndRelationsFromObjectArray(toUpdate) + toUpdatePreviousData = await fulfillmentService.listShippingOptions( + { + id: toUpdate.map((s) => s.id), + }, + { + select: selects, + relations, + } + ) + } + + const upsertedShippingOptions: ShippingOptionDTO[] = + await fulfillmentService.upsertShippingOptions( + input as UpsertShippingOptionDTO[] + ) + + const upsertedShippingOptionIds = upsertedShippingOptions.map((s) => s.id) + + const updatedIds = toUpdate.map((s) => s.id) + return new StepResponse(upsertedShippingOptions, { + updatedPreviousData: toUpdatePreviousData, + createdIds: arrayDifference( + upsertedShippingOptionIds, + updatedIds + ) as string[], + }) + }, + async (shippingOptionIds: any, { container }) => { + if ( + !shippingOptionIds?.updatedPreviousData?.length && + !shippingOptionIds?.createdIds?.length + ) { + return + } + + const fulfillmentService = container.resolve( + ModuleRegistrationName.FULFILLMENT + ) + + if (shippingOptionIds.updatedPreviousData.length) { + await fulfillmentService.upsertShippingOptions( + shippingOptionIds.updatedPreviousData + ) + } + + if (shippingOptionIds.createdIds.length) { + await fulfillmentService.deleteShippingOptions( + shippingOptionIds.createdIds + ) + } + } +) diff --git a/packages/core-flows/src/fulfillment/workflows/create-shipping-options.ts b/packages/core-flows/src/fulfillment/workflows/create-shipping-options.ts index ffa143f104..ff5b3eacfa 100644 --- a/packages/core-flows/src/fulfillment/workflows/create-shipping-options.ts +++ b/packages/core-flows/src/fulfillment/workflows/create-shipping-options.ts @@ -6,7 +6,7 @@ import { } from "@medusajs/workflows-sdk" import { createShippingOptionsPriceSetsStep, - createShippingOptionsStep, + upsertShippingOptionsStep, } from "../steps" import { setShippingOptionsPriceSetsStep } from "../steps/set-shipping-options-price-sets" import { createPricingRuleTypesStep } from "../../pricing" @@ -34,7 +34,7 @@ export const createShippingOptionsWorkflow = createWorkflow( } }) - const createdShippingOptions = createShippingOptionsStep( + const createdShippingOptions = upsertShippingOptionsStep( data.shippingOptions ) diff --git a/packages/core-flows/src/fulfillment/workflows/index.ts b/packages/core-flows/src/fulfillment/workflows/index.ts index ebee8bb6b9..4a0b55ed35 100644 --- a/packages/core-flows/src/fulfillment/workflows/index.ts +++ b/packages/core-flows/src/fulfillment/workflows/index.ts @@ -1,6 +1,7 @@ export * from "./add-rules-to-fulfillment-shipping-option" export * from "./create-service-zones" export * from "./create-shipping-options" +export * from "./update-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-shipping-options.ts b/packages/core-flows/src/fulfillment/workflows/update-shipping-options.ts new file mode 100644 index 0000000000..32bd321cc6 --- /dev/null +++ b/packages/core-flows/src/fulfillment/workflows/update-shipping-options.ts @@ -0,0 +1,92 @@ +import { FulfillmentWorkflow } from "@medusajs/types" +import { + createWorkflow, + transform, + WorkflowData, +} from "@medusajs/workflows-sdk" +import { upsertShippingOptionsStep } from "../steps" + +export const updateShippingOptionsWorkflowId = + "update-shipping-options-workflow" +export const updateShippingOptionsWorkflow = createWorkflow( + updateShippingOptionsWorkflowId, + ( + input: WorkflowData< + FulfillmentWorkflow.UpdateShippingOptionsWorkflowInput[] + > + ): WorkflowData => { + const data = transform(input, (data) => { + const shippingOptionsIndexToPrices = data.map((option, index) => { + return { + shipping_option_index: index, + prices: option.prices, + } + }) + + return { + shippingOptions: data, + shippingOptionsIndexToPrices, + } + }) + + const updatedShippingOptions = upsertShippingOptionsStep( + data.shippingOptions + ) + + /*const normalizedShippingOptionsPrices = transform( + { + shippingOptions: updatedShippingOptions, + shippingOptionsIndexToPrices: data.shippingOptionsIndexToPrices, + }, + (data) => { + const ruleTypes = new Set>() + const shippingOptionsPrices = data.shippingOptionsIndexToPrices.map( + ({ shipping_option_index, prices }) => { + prices?.forEach((price) => { + if ("region_id" in price) { + ruleTypes.add({ + name: "region_id", + rule_attribute: "region_id", + }) + } + }) + + return { + id: data.shippingOptions[shipping_option_index].id, + prices, + } + } + ) + + return { + shippingOptionsPrices, + ruleTypes: Array.from(ruleTypes) as UpdateRuleTypeDTO[], + } + } + )*/ + + /*updatePricingRuleTypesStep(normalizedShippingOptionsPrices.ruleTypes)*/ + + /*const shippingOptionsPriceSetsLinkData = updateShippingOptionsPriceSetsStep( + normalizedShippingOptionsPrices.shippingOptionsPrices + ) + + const normalizedLinkData = transform( + { + shippingOptionsPriceSetsLinkData, + }, + (data) => { + return data.shippingOptionsPriceSetsLinkData.map((item) => { + return { + id: item.id, + price_sets: [item.priceSetId], + } + }) + } + )*/ + + /*setShippingOptionsPriceSetsStep(normalizedLinkData)*/ + + return updatedShippingOptions + } +) diff --git a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts index d13144022b..723ce665c5 100644 --- a/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts +++ b/packages/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts @@ -9,6 +9,7 @@ import { resolve } from "path" import { FulfillmentProviderService } from "@services" import { FulfillmentProviderServiceFixtures } from "../../__fixtures__/providers" import { GeoZoneType } from "@medusajs/utils" +import { UpdateShippingOptionDTO } from "@medusajs/types/src" jest.setTimeout(100000) @@ -592,7 +593,7 @@ moduleIntegrationTestRunner({ shippingOptionData ) - const updateData = { + const updateData: UpdateShippingOptionDTO = { id: shippingOption.id, name: "updated-test", price_type: "calculated", @@ -617,6 +618,7 @@ moduleIntegrationTestRunner({ } const updatedShippingOption = await service.updateShippingOptions( + updateData.id!, updateData ) @@ -690,7 +692,7 @@ moduleIntegrationTestRunner({ shippingOptionData ) - const updateData = { + const updateData: Partial = { id: shippingOption.id, name: "updated-test", price_type: "calculated", @@ -702,7 +704,7 @@ moduleIntegrationTestRunner({ }, } - await service.updateShippingOptions(updateData) + await service.updateShippingOptions(updateData.id!, updateData) const updatedShippingOption = await service.retrieveShippingOption( shippingOption.id, @@ -837,7 +839,7 @@ moduleIntegrationTestRunner({ }, ] - const updatedShippingOption = await service.updateShippingOptions( + const updatedShippingOption = await service.upsertShippingOptions( updateData ) @@ -943,7 +945,7 @@ moduleIntegrationTestRunner({ } const err = await service - .updateShippingOptions(shippingOptionData) + .updateShippingOptions(shippingOptionData.id!, shippingOptionData) .catch((e) => e) expect(err).toBeDefined() @@ -988,7 +990,7 @@ moduleIntegrationTestRunner({ ] const err = await service - .updateShippingOptions(updateData) + .updateShippingOptions(updateData[0].id!, updateData[0]) .catch((e) => e) expect(err).toBeDefined() @@ -1035,7 +1037,7 @@ moduleIntegrationTestRunner({ ] const err = await service - .updateShippingOptions(updateData) + .updateShippingOptions(updateData[0].id!, updateData[0]) .catch((e) => e) expect(err).toBeDefined() diff --git a/packages/fulfillment/src/services/fulfillment-module-service.ts b/packages/fulfillment/src/services/fulfillment-module-service.ts index f1e65574ac..c8861ff0e5 100644 --- a/packages/fulfillment/src/services/fulfillment-module-service.ts +++ b/packages/fulfillment/src/services/fulfillment-module-service.ts @@ -1,4 +1,3 @@ -import { Modules } from "@medusajs/modules-sdk" import { Context, DAL, @@ -26,6 +25,7 @@ import { getSetDifference, isString, promiseAll, + Modules } from "@medusajs/utils" import { Fulfillment, @@ -40,6 +40,7 @@ import { import { isContextValid, validateRules } from "@utils" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import FulfillmentProviderService from "./fulfillment-provider" +import { UpdateShippingOptionsInput } from "../types/service" const generateMethodForModels = [ ServiceZone, @@ -1046,40 +1047,56 @@ export default class FulfillmentModuleService< } updateShippingOptions( - data: FulfillmentTypes.UpdateShippingOptionDTO[], - sharedContext?: Context - ): Promise - updateShippingOptions( + id: string, data: FulfillmentTypes.UpdateShippingOptionDTO, sharedContext?: Context ): Promise + updateShippingOptions( + selector: FulfillmentTypes.FilterableShippingOptionProps, + data: FulfillmentTypes.UpdateShippingOptionDTO, + sharedContext?: Context + ): Promise @InjectManager("baseRepository_") async updateShippingOptions( - data: - | FulfillmentTypes.UpdateShippingOptionDTO[] - | FulfillmentTypes.UpdateShippingOptionDTO, + idOrSelector: string | FulfillmentTypes.FilterableShippingOptionProps, + data: FulfillmentTypes.UpdateShippingOptionDTO, @MedusaContext() sharedContext: Context = {} ): Promise< FulfillmentTypes.ShippingOptionDTO[] | FulfillmentTypes.ShippingOptionDTO > { + const normalizedInput: UpdateShippingOptionsInput[] = [] + + if (isString(idOrSelector)) { + normalizedInput.push({ id: idOrSelector, ...data }) + } else { + const shippingOptions = await this.shippingOptionService_.list( + idOrSelector, + {}, + sharedContext + ) + shippingOptions.forEach((shippingOption) => { + normalizedInput.push({ id: shippingOption.id, ...data }) + }) + } + const updatedShippingOptions = await this.updateShippingOptions_( - data, + normalizedInput, sharedContext ) - return await this.baseRepository_.serialize< + const serialized = await this.baseRepository_.serialize< FulfillmentTypes.ShippingOptionDTO | FulfillmentTypes.ShippingOptionDTO[] >(updatedShippingOptions, { populate: true, }) + + return isString(idOrSelector) ? serialized[0] : serialized } @InjectTransactionManager("baseRepository_") async updateShippingOptions_( - data: - | FulfillmentTypes.UpdateShippingOptionDTO[] - | FulfillmentTypes.UpdateShippingOptionDTO, + data: UpdateShippingOptionsInput[] | UpdateShippingOptionsInput, @MedusaContext() sharedContext: Context = {} ): Promise { const dataArray = Array.isArray(data) ? data : [data] @@ -1182,6 +1199,82 @@ export default class FulfillmentModuleService< : updatedShippingOptions[0] } + async upsertShippingOptions( + data: FulfillmentTypes.UpsertShippingOptionDTO[], + sharedContext?: Context + ): Promise + async upsertShippingOptions( + data: FulfillmentTypes.UpsertShippingOptionDTO, + sharedContext?: Context + ): Promise + + @InjectManager("baseRepository_") + async upsertShippingOptions( + data: + | FulfillmentTypes.UpsertShippingOptionDTO[] + | FulfillmentTypes.UpsertShippingOptionDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise< + FulfillmentTypes.ShippingOptionDTO[] | FulfillmentTypes.ShippingOptionDTO + > { + const upsertedShippingOptions = await this.upsertShippingOptions_( + data, + sharedContext + ) + + const allShippingOptions = await this.baseRepository_.serialize< + FulfillmentTypes.ShippingOptionDTO[] | FulfillmentTypes.ShippingOptionDTO + >(upsertedShippingOptions) + + return Array.isArray(data) ? allShippingOptions : allShippingOptions[0] + } + + @InjectTransactionManager("baseRepository_") + async upsertShippingOptions_( + data: + | FulfillmentTypes.UpsertShippingOptionDTO[] + | FulfillmentTypes.UpsertShippingOptionDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const input = Array.isArray(data) ? data : [data] + const forUpdate = input.filter( + (shippingOption): shippingOption is UpdateShippingOptionsInput => + !!shippingOption.id + ) + const forCreate = input.filter( + ( + shippingOption + ): shippingOption is FulfillmentTypes.CreateShippingOptionDTO => + !shippingOption.id + ) + + let created: TShippingOptionEntity[] = [] + let updated: TShippingOptionEntity[] = [] + + if (forCreate.length) { + const createdShippingOptions = await this.createShippingOptions_( + forCreate, + sharedContext + ) + const toPush = Array.isArray(createdShippingOptions) + ? createdShippingOptions + : [createdShippingOptions] + created.push(...toPush) + } + if (forUpdate.length) { + const updatedShippingOptions = await this.updateShippingOptions_( + forUpdate, + sharedContext + ) + const toPush = Array.isArray(updatedShippingOptions) + ? updatedShippingOptions + : [updatedShippingOptions] + updated.push(...toPush) + } + + return [...created, ...updated] + } + updateShippingProfiles( data: FulfillmentTypes.UpdateShippingProfileDTO[], sharedContext?: Context @@ -1415,7 +1508,7 @@ export default class FulfillmentModuleService< protected static validateMissingShippingOptions_( shippingOptions: ShippingOption[], - shippingOptionsData: FulfillmentTypes.UpdateShippingOptionDTO[] + shippingOptionsData: UpdateShippingOptionsInput[] ) { const missingShippingOptionIds = arrayDifference( shippingOptionsData.map((s) => s.id), diff --git a/packages/fulfillment/src/types/service.ts b/packages/fulfillment/src/types/service.ts new file mode 100644 index 0000000000..8ea3b52219 --- /dev/null +++ b/packages/fulfillment/src/types/service.ts @@ -0,0 +1,6 @@ +import { FulfillmentTypes } from "@medusajs/types" + +export type UpdateShippingOptionsInput = Required< + Pick +> & + FulfillmentTypes.UpdateShippingOptionDTO diff --git a/packages/medusa/src/api-v2/admin/shipping-options/[id]/route.ts b/packages/medusa/src/api-v2/admin/shipping-options/[id]/route.ts new file mode 100644 index 0000000000..3e4e993035 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/shipping-options/[id]/route.ts @@ -0,0 +1,44 @@ +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { AdminShippingOptionRetrieveResponse } from "@medusajs/types" +import { AdminUpdateShippingOptionType } from "../validators" +import { updateShippingOptionsWorkflow } from "@medusajs/core-flows" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../types/routing" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const shippingOptionPayload = req.validatedBody + + const workflow = updateShippingOptionsWorkflow(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/middlewares.ts b/packages/medusa/src/api-v2/admin/shipping-options/middlewares.ts index c6198b6f19..ece6240c46 100644 --- a/packages/medusa/src/api-v2/admin/shipping-options/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/shipping-options/middlewares.ts @@ -5,6 +5,7 @@ import { AdminGetShippingOptionParams, AdminShippingOptionRulesBatchAdd, AdminShippingOptionRulesBatchRemove, + AdminUpdateShippingOption, } from "./validators" import { retrieveTransformQueryConfig } from "./query-config" import { validateAndTransformBody } from "../../utils/validate-body" @@ -28,6 +29,18 @@ export const adminShippingOptionRoutesMiddlewares: MiddlewareRoute[] = [ ], }, + { + method: ["POST"], + matcher: "/admin/shipping-options/:id", + middlewares: [ + validateAndTransformQuery( + AdminGetShippingOptionParams, + retrieveTransformQueryConfig + ), + validateAndTransformBody(AdminUpdateShippingOption), + ], + }, + { method: ["POST"], matcher: "/admin/shipping-options/:id/rules/batch/add", diff --git a/packages/medusa/src/api-v2/admin/shipping-options/validators.ts b/packages/medusa/src/api-v2/admin/shipping-options/validators.ts index 19e2626eb2..813100f8b5 100644 --- a/packages/medusa/src/api-v2/admin/shipping-options/validators.ts +++ b/packages/medusa/src/api-v2/admin/shipping-options/validators.ts @@ -43,7 +43,7 @@ export type AdminShippingOptionRulesBatchRemoveType = z.infer< * SHIPPING OPTIONS */ -export const AdminCreateShippingOptionType = z +export const AdminCreateShippingOptionTypeObject = z .object({ label: z.string(), description: z.string(), @@ -74,7 +74,7 @@ export const AdminCreateShippingOption = z data: z.record(z.unknown()).optional(), price_type: z.nativeEnum(ShippingOptionPriceTypeEnum), provider_id: z.string(), - type: AdminCreateShippingOptionType, + type: AdminCreateShippingOptionTypeObject, prices: AdminCreateShippingOptionPriceWithCurrency.or( AdminCreateShippingOptionPriceWithRegion ).array(), @@ -85,3 +85,18 @@ export const AdminCreateShippingOption = z export type AdminCreateShippingOptionType = z.infer< typeof AdminCreateShippingOption > + +export const AdminUpdateShippingOption = z + .object({ + id: z.string(), + name: z.string().optional(), + data: z.record(z.unknown()).optional(), + price_type: z.nativeEnum(ShippingOptionPriceTypeEnum).optional(), + provider_id: z.string().optional(), + type: AdminCreateShippingOptionTypeObject.optional(), + }) + .strict() + +export type AdminUpdateShippingOptionType = z.infer< + typeof AdminUpdateShippingOption +> \ No newline at end of file diff --git a/packages/types/src/fulfillment/mutations/shipping-option.ts b/packages/types/src/fulfillment/mutations/shipping-option.ts index 8a2d01849f..8bc12c5a87 100644 --- a/packages/types/src/fulfillment/mutations/shipping-option.ts +++ b/packages/types/src/fulfillment/mutations/shipping-option.ts @@ -14,7 +14,7 @@ export interface CreateShippingOptionDTO { } export interface UpdateShippingOptionDTO { - id: string + id?: string name?: string price_type?: ShippingOptionPriceType service_zone_id?: string @@ -27,3 +27,7 @@ export interface UpdateShippingOptionDTO { | { id: string } )[] } + +export interface UpsertShippingOptionDTO extends UpdateShippingOptionDTO { + +} diff --git a/packages/types/src/fulfillment/service.ts b/packages/types/src/fulfillment/service.ts index 57852f5d1e..13fe28bf14 100644 --- a/packages/types/src/fulfillment/service.ts +++ b/packages/types/src/fulfillment/service.ts @@ -35,6 +35,7 @@ import { UpdateShippingOptionRuleDTO, UpdateShippingProfileDTO, UpsertServiceZoneDTO, + UpsertShippingOptionDTO, } from "./mutations" import { CreateFulfillmentDTO } from "./mutations/fulfillment" import { CreateShippingProfileDTO } from "./mutations/shipping-profile" @@ -376,19 +377,35 @@ export interface IFulfillmentModuleService extends IModuleService { data: CreateShippingOptionDTO, sharedContext?: Context ): Promise + /** * Update a shipping option + * @param id * @param data * @param sharedContext */ updateShippingOptions( - data: UpdateShippingOptionDTO[], - sharedContext?: Context - ): Promise - updateShippingOptions( + id: string, data: UpdateShippingOptionDTO, sharedContext?: Context ): Promise + updateShippingOptions( + selector: FilterableShippingOptionProps, + data: UpdateShippingOptionDTO, + sharedContext?: Context + ): Promise + + /** + * Upsert a shipping option + */ + upsertShippingOptions( + data: UpsertShippingOptionDTO, + sharedContext?: Context + ): Promise + upsertShippingOptions( + data: UpsertShippingOptionDTO[], + sharedContext?: Context + ): Promise /** * Delete a shippingOption * @param ids diff --git a/packages/types/src/workflow/fulfillment/index.ts b/packages/types/src/workflow/fulfillment/index.ts index d21643ded9..b510e2b481 100644 --- a/packages/types/src/workflow/fulfillment/index.ts +++ b/packages/types/src/workflow/fulfillment/index.ts @@ -1,2 +1,3 @@ export * from "./create-shipping-options" +export * from "./update-shipping-options" export * from "./service-zones" diff --git a/packages/types/src/workflow/fulfillment/update-shipping-options.ts b/packages/types/src/workflow/fulfillment/update-shipping-options.ts new file mode 100644 index 0000000000..184fc43cf9 --- /dev/null +++ b/packages/types/src/workflow/fulfillment/update-shipping-options.ts @@ -0,0 +1,36 @@ +import { ShippingOptionPriceType } from "../../fulfillment" +import { RuleOperatorType } from "../../common" + +export interface UpdateShippingOptionsWorkflowInput { + id: string + name?: string + service_zone_id?: string + shipping_profile_id?: string + data?: Record + price_type?: ShippingOptionPriceType + provider_id?: string + type?: { + label: string + description: string + code: string + } + prices?: ( + | { + currency_code: string + amount: number + } + | { + region_id: string + amount: number + } + )[] + rules?: { + attribute: string + operator: RuleOperatorType + value: string | string[] + }[] +} + +export type UpdateShippingOptionsWorkflowOutput = { + id: string +}[]