diff --git a/.changeset/five-roses-swim.md b/.changeset/five-roses-swim.md new file mode 100644 index 0000000000000..d4b5caceac6d8 --- /dev/null +++ b/.changeset/five-roses-swim.md @@ -0,0 +1,7 @@ +--- +"@medusajs/stock-location": patch +"@medusajs/core-flows": patch +"@medusajs/types": patch +--- + +fix(stock-location,core-flows,types): update existing address when updating stock location address diff --git a/integration-tests/http/__tests__/stock-location/admin/stock-location.spec.ts b/integration-tests/http/__tests__/stock-location/admin/stock-location.spec.ts index 33ca3320456ca..c5de5666e0aa0 100644 --- a/integration-tests/http/__tests__/stock-location/admin/stock-location.spec.ts +++ b/integration-tests/http/__tests__/stock-location/admin/stock-location.spec.ts @@ -165,6 +165,58 @@ medusaIntegrationTestRunner({ expect(response.status).toEqual(200) expect(response.data.stock_location.name).toEqual("new name") }) + + it("should update stock location address without creating new addresses", async () => { + const response = await api.post( + `/admin/stock-locations/${location1.id}`, + { + name: "new name", + address: { + address_1: "test", + country_code: "dk", + }, + }, + adminHeaders + ) + + const firstAddressId = response.data.stock_location.address.id + + expect(response.status).toEqual(200) + expect(response.data.stock_location).toEqual( + expect.objectContaining({ + name: "new name", + address: expect.objectContaining({ + id: firstAddressId, + address_1: "test", + country_code: "dk", + }), + }) + ) + + const response2 = await api.post( + `/admin/stock-locations/${location1.id}`, + { + name: "new name 2", + address: { + address_1: "test 2", + country_code: "dk", + }, + }, + adminHeaders + ) + + expect(response2.status).toEqual(200) + expect(response2.data.stock_location).toEqual( + expect.objectContaining({ + name: "new name 2", + address: expect.objectContaining({ + id: firstAddressId, + address_1: "test 2", + country_code: "dk", + }), + }) + ) + }) }) describe("Get stock location", () => { diff --git a/packages/core/core-flows/src/stock-location/steps/upsert-stock-location-addresses.ts b/packages/core/core-flows/src/stock-location/steps/upsert-stock-location-addresses.ts new file mode 100644 index 0000000000000..ec2c82c38335d --- /dev/null +++ b/packages/core/core-flows/src/stock-location/steps/upsert-stock-location-addresses.ts @@ -0,0 +1,73 @@ +import { + IStockLocationService, + UpsertStockLocationAddressInput, +} from "@medusajs/framework/types" +import { + getSelectsAndRelationsFromObjectArray, + promiseAll, +} from "@medusajs/framework/utils" +import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" + +import { Modules } from "@medusajs/framework/utils" + +export const upsertStockLocationAddressesStepId = + "upsert-stock-location-addresses-step" +/** + * This step upserts stock location addresses matching the specified filters. + */ +export const upsertStockLocationAddressesStep = createStep( + upsertStockLocationAddressesStepId, + async (input: UpsertStockLocationAddressInput[], { container }) => { + const stockLocationService = container.resolve( + Modules.STOCK_LOCATION + ) + + const stockLocationAddressIds = input.map((i) => i.id!).filter(Boolean) + const { selects, relations } = getSelectsAndRelationsFromObjectArray(input) + + const dataToUpdate = await stockLocationService.listStockLocationAddresses( + { id: stockLocationAddressIds }, + { select: selects, relations } + ) + + const updateIds = dataToUpdate.map((du) => du.id) + + const updatedAddresses = + await stockLocationService.upsertStockLocationAddresses(input) + + const dataToDelete = updatedAddresses.filter( + (address) => !updateIds.includes(address.id) + ) + + return new StepResponse(updatedAddresses, { dataToUpdate, dataToDelete }) + }, + async (revertData, { container }) => { + if (!revertData) { + return + } + + const stockLocationService = container.resolve( + Modules.STOCK_LOCATION + ) + + const promises: any[] = [] + + if (revertData.dataToDelete) { + promises.push( + stockLocationService.deleteStockLocationAddresses( + revertData.dataToDelete.map((d) => d.id!) + ) + ) + } + + if (revertData.dataToUpdate) { + promises.push( + stockLocationService.upsertStockLocationAddresses( + revertData.dataToUpdate + ) + ) + } + + await promiseAll(promises) + } +) diff --git a/packages/core/core-flows/src/stock-location/workflows/update-stock-locations.ts b/packages/core/core-flows/src/stock-location/workflows/update-stock-locations.ts index 1dfe47e2b9f75..6b4d264f773f5 100644 --- a/packages/core/core-flows/src/stock-location/workflows/update-stock-locations.ts +++ b/packages/core/core-flows/src/stock-location/workflows/update-stock-locations.ts @@ -1,15 +1,19 @@ import { + FilterableStockLocationProps, StockLocationDTO, UpdateStockLocationInput, - FilterableStockLocationProps, + UpsertStockLocationAddressInput, } from "@medusajs/framework/types" import { WorkflowData, WorkflowResponse, createWorkflow, + transform, } from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "../../common" import { updateStockLocationsStep } from "../steps" +import { upsertStockLocationAddressesStep } from "../steps/upsert-stock-location-addresses" export interface UpdateStockLocationsWorkflowInput { selector: FilterableStockLocationProps @@ -24,6 +28,50 @@ export const updateStockLocationsWorkflow = createWorkflow( ( input: WorkflowData ): WorkflowResponse => { - return new WorkflowResponse(updateStockLocationsStep(input)) + const stockLocationsQuery = useQueryGraphStep({ + entity: "stock_location", + filters: input.selector, + fields: ["id", "address.id"], + }).config({ name: "get-stock-location" }) + + const stockLocations = transform( + { stockLocationsQuery }, + ({ stockLocationsQuery }) => stockLocationsQuery.data + ) + + const normalizedData = transform( + { input, stockLocations }, + ({ input, stockLocations }) => { + const { address, address_id, ...stockLocationInput } = input.update + const addressesInput: UpsertStockLocationAddressInput[] = [] + + if (address) { + for (const stockLocation of stockLocations) { + if (stockLocation.address?.id) { + addressesInput.push({ + id: stockLocation.address?.id!, + ...address, + }) + } else { + addressesInput.push(address) + } + } + } + + return { + stockLocationInput: { + selector: input.selector, + update: stockLocationInput, + }, + addressesInput, + } + } + ) + + upsertStockLocationAddressesStep(normalizedData.addressesInput) + + return new WorkflowResponse( + updateStockLocationsStep(normalizedData.stockLocationInput) + ) } ) diff --git a/packages/core/types/src/stock-location/common.ts b/packages/core/types/src/stock-location/common.ts index ef53256d2e3c4..4e3792f3243aa 100644 --- a/packages/core/types/src/stock-location/common.ts +++ b/packages/core/types/src/stock-location/common.ts @@ -452,3 +452,19 @@ export type UpsertStockLocationInput = Partial & { */ id?: string } + +export type UpdateStockLocationAddressInput = StockLocationAddressInput & { + id: string +} + +export type UpsertStockLocationAddressInput = StockLocationAddressInput & { + id?: string +} + +export interface FilterableStockLocationAddressProps + extends BaseFilterable { + /** + * The IDs to filter stock location's address by. + */ + id?: string | string[] +} diff --git a/packages/core/types/src/stock-location/service.ts b/packages/core/types/src/stock-location/service.ts index 4d58ad1c482a3..2569429dee399 100644 --- a/packages/core/types/src/stock-location/service.ts +++ b/packages/core/types/src/stock-location/service.ts @@ -1,14 +1,17 @@ +import { FindConfig } from "../common/common" +import { RestoreReturn, SoftDeleteReturn } from "../dal" +import { IModuleService } from "../modules-sdk" +import { Context } from "../shared-context" import { CreateStockLocationInput, + FilterableStockLocationAddressProps, FilterableStockLocationProps, + StockLocationAddressDTO, StockLocationDTO, UpdateStockLocationInput, + UpsertStockLocationAddressInput, UpsertStockLocationInput, } from "./common" -import { RestoreReturn, SoftDeleteReturn } from "../dal" -import { Context } from "../shared-context" -import { FindConfig } from "../common/common" -import { IModuleService } from "../modules-sdk" /** * The main service interface for the Stock Location Module. @@ -333,4 +336,50 @@ export interface IStockLocationService extends IModuleService { config?: RestoreReturn, sharedContext?: Context ): Promise | void> + + /** + * This method retrieves a paginated list of stock location addresses based on optional filters and configuration. + * + * @param {FilterableStockLocationAddressProps} selector - The filters to apply on the retrieved stock location address. + * @param {FindConfig} config - The configurations determining how the stock location address is retrieved. Its properties, such as `select` or `relations`, accept the + * attributes or relations associated with a stock location address. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The list of stock location addressess. + * + */ + listStockLocationAddresses( + selector: FilterableStockLocationAddressProps, + config?: FindConfig, + context?: Context + ): Promise + + /** + * This method updates or creates stock location addresses + * + * @param {Partial[]} data - The list of Make all properties in t optional + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The created or updated stock location address + * + * @example + * {example-code} + */ + upsertStockLocationAddresses( + data: UpsertStockLocationAddressInput[], + sharedContext?: Context + ): Promise + + /** + * This method deletes a stock location address by its ID. + * + * @param {string} id - The ID of the stock location address. + * @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the stock location address is deleted successfully. + * + * @example + * await stockLocationModuleService.deleteStockLocationAddresses("sla_123") + */ + deleteStockLocationAddresses( + id: string | string[], + context?: Context + ): Promise } diff --git a/packages/modules/stock-location/src/services/stock-location-module.ts b/packages/modules/stock-location/src/services/stock-location-module.ts index 09c9c49e9e87e..0762f5993c0e6 100644 --- a/packages/modules/stock-location/src/services/stock-location-module.ts +++ b/packages/modules/stock-location/src/services/stock-location-module.ts @@ -11,7 +11,9 @@ import { ModulesSdkTypes, StockLocationAddressInput, StockLocationTypes, + UpdateStockLocationAddressInput, UpdateStockLocationInput, + UpsertStockLocationAddressInput, UpsertStockLocationInput, } from "@medusajs/framework/types" import { @@ -258,4 +260,62 @@ export default class StockLocationModuleService ) { return await this.stockLocationAddressService_.update(input, context) } + + async upsertStockLocationAddresses( + data: UpsertStockLocationAddressInput, + context?: Context + ): Promise + async upsertStockLocationAddresses( + data: UpsertStockLocationAddressInput[], + context?: Context + ): Promise + + @InjectManager() + async upsertStockLocationAddresses( + data: UpsertStockLocationAddressInput | UpsertStockLocationAddressInput[], + @MedusaContext() context: Context = {} + ): Promise< + | StockLocationTypes.StockLocationAddressDTO + | StockLocationTypes.StockLocationAddressDTO[] + > { + const input = Array.isArray(data) ? data : [data] + + const result = await this.upsertStockLocationAddresses_(input, context) + + return await this.baseRepository_.serialize< + | StockLocationTypes.StockLocationAddressDTO[] + | StockLocationTypes.StockLocationAddressDTO + >(Array.isArray(data) ? result : result[0]) + } + + @InjectTransactionManager() + async upsertStockLocationAddresses_( + input: UpsertStockLocationAddressInput[], + @MedusaContext() context: Context = {} + ) { + const toUpdate = input.filter( + (location): location is UpdateStockLocationAddressInput => !!location.id + ) as UpdateStockLocationAddressInput[] + const toCreate = input.filter( + (location) => !location.id + ) as StockLocationAddressInput[] + + const operations: Promise< + | InferEntityType[] + | InferEntityType + >[] = [] + + if (toCreate.length) { + operations.push( + this.stockLocationAddressService_.create(toCreate, context) + ) + } + if (toUpdate.length) { + operations.push( + this.stockLocationAddressService_.update(toUpdate, context) + ) + } + + return (await promiseAll(operations)).flat() + } }