diff --git a/.changeset/twelve-bears-try.md b/.changeset/twelve-bears-try.md new file mode 100644 index 0000000000..b75c71fabc --- /dev/null +++ b/.changeset/twelve-bears-try.md @@ -0,0 +1,7 @@ +--- +"@medusajs/medusa": patch +"@medusajs/medusa-js": patch +"@medusajs/stock-location": major +--- + +Stock locations module added diff --git a/packages/medusa-js/src/resources/admin/index.ts b/packages/medusa-js/src/resources/admin/index.ts index 1506cd1b2e..9d853b0567 100644 --- a/packages/medusa-js/src/resources/admin/index.ts +++ b/packages/medusa-js/src/resources/admin/index.ts @@ -24,6 +24,7 @@ import AdminReturnsResource from "./returns" import AdminSalesChannelsResource from "./sales-channels" import AdminShippingOptionsResource from "./shipping-options" import AdminShippingProfilesResource from "./shipping-profiles" +import AdminStockLocationsResource from "./stock-locations" import AdminStoresResource from "./store" import AdminSwapsResource from "./swaps" import AdminTaxRatesResource from "./tax-rates" @@ -59,6 +60,7 @@ class Admin extends BaseResource { public salesChannels = new AdminSalesChannelsResource(this.client) public swaps = new AdminSwapsResource(this.client) public shippingProfiles = new AdminShippingProfilesResource(this.client) + public stockLocations = new AdminStockLocationsResource(this.client) public store = new AdminStoresResource(this.client) public shippingOptions = new AdminShippingOptionsResource(this.client) public regions = new AdminRegionsResource(this.client) diff --git a/packages/medusa-js/src/resources/admin/sales-channels.ts b/packages/medusa-js/src/resources/admin/sales-channels.ts index ad04c8261e..f01deea02d 100644 --- a/packages/medusa-js/src/resources/admin/sales-channels.ts +++ b/packages/medusa-js/src/resources/admin/sales-channels.ts @@ -7,6 +7,8 @@ import { AdminSalesChannelsListRes, AdminDeleteSalesChannelsChannelProductsBatchReq, AdminPostSalesChannelsChannelProductsBatchReq, + AdminPostSalesChannelsChannelStockLocationsReq, + AdminDeleteSalesChannelsChannelStockLocationsReq, } from "@medusajs/medusa" import { ResponsePromise } from "../../typings" import BaseResource from "../base" @@ -121,6 +123,38 @@ class AdminSalesChannelsResource extends BaseResource { const path = `/admin/sales-channels/${salesChannelId}/products/batch` return this.client.request("POST", path, payload, {}, customHeaders) } + + /** + * Add a location to a sales channel + * @experimental This feature is under development and may change in the future. + * To use this feature please enable featureflag `sales_channels` in your medusa backend project. + * @description Add a stock location to a SalesChannel + * @returns the Medusa SalesChannel + */ + addLocation( + salesChannelId: string, + payload: AdminPostSalesChannelsChannelStockLocationsReq, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/sales-channels/${salesChannelId}/stock-locations` + return this.client.request("POST", path, payload, {}, customHeaders) + } + + /** + * remove a location from a sales channel + * @experimental This feature is under development and may change in the future. + * To use this feature please enable featureflag `sales_channels` in your medusa backend project. + * @description Remove a stock location from a SalesChannel + * @returns an deletion result + */ + removeLocation( + salesChannelId: string, + payload: AdminDeleteSalesChannelsChannelStockLocationsReq, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/sales-channels/${salesChannelId}/stock-locations` + return this.client.request("DELETE", path, payload, {}, customHeaders) + } } export default AdminSalesChannelsResource diff --git a/packages/medusa-js/src/resources/admin/stock-locations.ts b/packages/medusa-js/src/resources/admin/stock-locations.ts new file mode 100644 index 0000000000..148b945b4f --- /dev/null +++ b/packages/medusa-js/src/resources/admin/stock-locations.ts @@ -0,0 +1,78 @@ +import { + AdminGetStockLocationsParams, + AdminStockLocationsRes, + AdminPostStockLocationsLocationReq, + AdminPostStockLocationsReq, + AdminStockLocationsListRes, +} from "@medusajs/medusa" +import { ResponsePromise } from "../../typings" +import BaseResource from "../base" +import qs from "qs" + +class AdminStockLocationsResource extends BaseResource { + /** retrieve an stock location + * @experimental This feature is under development and may change in the future. + * To use this feature please install @medusajs/stock-location + * @description gets a medusa stock location + * @returns a medusa stock location + */ + create( + payload: AdminPostStockLocationsReq, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/stock-locations` + return this.client.request("POST", path, payload, {}, customHeaders) + } + + /** retrieve an stock location + * @experimental This feature is under development and may change in the future. + * To use this feature please install @medusajs/stock-location + * @description gets a medusa stock location + * @returns a medusa stock location + */ + retrieve( + itemId: string, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/stock-locations/${itemId}` + return this.client.request("GET", path, undefined, {}, customHeaders) + } + + /** update an stock location + * @experimental This feature is under development and may change in the future. + * To use this feature please install @medusajs/stock-location + * @description updates an stock location + * @returns the updated medusa stock location + */ + update( + stockLocationId: string, + payload: AdminPostStockLocationsLocationReq, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/stock-locations/${stockLocationId}` + return this.client.request("POST", path, payload, {}, customHeaders) + } + + /** + * Retrieve a list of stock locations + * @experimental This feature is under development and may change in the future. + * To use this feature please install @medusajs/stock-location + * @description Retrieve a list of stock locations + * @returns the list of stock locations as well as the pagination properties + */ + list( + query?: AdminGetStockLocationsParams, + customHeaders: Record = {} + ): ResponsePromise { + let path = `/admin/stock-locations` + + if (query) { + const queryString = qs.stringify(query) + path += `?${queryString}` + } + + return this.client.request("GET", path, undefined, {}, customHeaders) + } +} + +export default AdminStockLocationsResource diff --git a/packages/medusa/src/api/index.js b/packages/medusa/src/api/index.js index d08d2a8124..cbb680926f 100644 --- a/packages/medusa/src/api/index.js +++ b/packages/medusa/src/api/index.js @@ -44,6 +44,7 @@ export * from "./routes/admin/returns" export * from "./routes/admin/sales-channels" export * from "./routes/admin/shipping-options" export * from "./routes/admin/shipping-profiles" +export * from "./routes/admin/stock-locations" export * from "./routes/admin/store" export * from "./routes/admin/swaps" export * from "./routes/admin/tax-rates" diff --git a/packages/medusa/src/api/middlewares/check-registered-modules.ts b/packages/medusa/src/api/middlewares/check-registered-modules.ts new file mode 100644 index 0000000000..18347d7110 --- /dev/null +++ b/packages/medusa/src/api/middlewares/check-registered-modules.ts @@ -0,0 +1,15 @@ +import { NextFunction, Request, Response } from "express" + +export function checkRegisteredModules(services: { + [serviceName: string]: string +}): (req: Request, res: Response, next: NextFunction) => Promise { + return async (req: Request, res: Response, next: NextFunction) => { + for (const service of Object.keys(services)) { + if (!req.scope.resolve(service, { allowUnregistered: true })) { + return next(new Error(services[service])) + } + } + + next() + } +} diff --git a/packages/medusa/src/api/routes/admin/index.js b/packages/medusa/src/api/routes/admin/index.js index d1b455f5bf..2f75a00778 100644 --- a/packages/medusa/src/api/routes/admin/index.js +++ b/packages/medusa/src/api/routes/admin/index.js @@ -28,6 +28,7 @@ import returnRoutes from "./returns" import salesChannelRoutes from "./sales-channels" import shippingOptionRoutes from "./shipping-options" import shippingProfileRoutes from "./shipping-profiles" +import stockLocationRoutes from "./stock-locations" import storeRoutes from "./store" import swapRoutes from "./swaps" import taxRateRoutes from "./tax-rates" @@ -98,6 +99,7 @@ export default (app, container, config) => { salesChannelRoutes(route) shippingOptionRoutes(route, featureFlagRouter) shippingProfileRoutes(route) + stockLocationRoutes(route) storeRoutes(route) swapRoutes(route) taxRateRoutes(route) diff --git a/packages/medusa/src/api/routes/admin/returns/cancel-return.ts b/packages/medusa/src/api/routes/admin/returns/cancel-return.ts index c443ad014a..124a9d0631 100644 --- a/packages/medusa/src/api/routes/admin/returns/cancel-return.ts +++ b/packages/medusa/src/api/routes/admin/returns/cancel-return.ts @@ -75,7 +75,7 @@ export default async (req, res) => { result = await claimService.retrieve(result.claim_order_id) } - const order = await orderService.retrieve(result.order_id, { + const order = await orderService.retrieve(result.order_id!, { select: defaultAdminOrdersFields, relations: defaultAdminOrdersRelations, }) diff --git a/packages/medusa/src/api/routes/admin/sales-channels/associate-stock-location.ts b/packages/medusa/src/api/routes/admin/sales-channels/associate-stock-location.ts new file mode 100644 index 0000000000..11fa9c1f7e --- /dev/null +++ b/packages/medusa/src/api/routes/admin/sales-channels/associate-stock-location.ts @@ -0,0 +1,112 @@ +import { IsString } from "class-validator" +import { Request, Response } from "express" +import { EntityManager } from "typeorm" + +import { + SalesChannelService, + SalesChannelLocationService, +} from "../../../../services" + +/** + * @oas [post] /sales-channels/{id}/stock-locations + * operationId: "PostSalesChannelsSalesChannelStockLocation" + * summary: "Associate a stock location to a Sales Channel" + * description: "Associates a stock location to a Sales Channel." + * x-authenticated: true + * parameters: + * - (path) id=* {string} The ID of the Sales Channel. + * requestBody: + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/AdminPostSalesChannelsChannelStockLocationsReq" + * x-codeSamples: + * - lang: JavaScript + * label: JS Client + * source: | + * import Medusa from "@medusajs/medusa-js" + * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) + * // must be previously logged in or use api token + * medusa.admin.salesChannels.addLocation(sales_channel_id, { + * location_id: 'App' + * }) + * .then(({ sales_channel }) => { + * console.log(sales_channel.id); + * }); + * - lang: Shell + * label: cURL + * source: | + * curl --location --request POST 'https://medusa-url.com/admin/sales-channels/{id}/stock-locations' \ + * --header 'Authorization: Bearer {api_token}' \ + * --header 'Content-Type: application/json' \ + * --data-raw '{ + * "locaton_id": "stock_location_id" + * }' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - Sales Channel + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * type: object + * properties: + * sales_channel: + * $ref: "#/components/schemas/SalesChannel" + * "400": + * $ref: "#/components/responses/400_error" + * "401": + * $ref: "#/components/responses/unauthorized" + * "404": + * $ref: "#/components/responses/not_found_error" + * "409": + * $ref: "#/components/responses/invalid_state_error" + * "422": + * $ref: "#/components/responses/invalid_request_error" + * "500": + * $ref: "#/components/responses/500_error" + */ +export default async (req: Request, res: Response) => { + const { id } = req.params + const { validatedBody } = req as { + validatedBody: AdminPostSalesChannelsChannelStockLocationsReq + } + + const salesChannelService: SalesChannelService = req.scope.resolve( + "salesChannelService" + ) + const channelLocationService: SalesChannelLocationService = req.scope.resolve( + "salesChannelLocationService" + ) + + const manager: EntityManager = req.scope.resolve("manager") + await manager.transaction(async (transactionManager) => { + return await channelLocationService + .withTransaction(transactionManager) + .associateLocation(id, validatedBody.location_id) + }) + + const channel = await salesChannelService.retrieve(id) + + res.status(200).json({ sales_channel: channel }) +} + +/** + * @schema AdminPostSalesChannelsChannelStockLocationsReq + * type: object + * required: + * - location_id + * properties: + * location_id: + * description: The ID of the stock location + * type: string + */ + +export class AdminPostSalesChannelsChannelStockLocationsReq { + @IsString() + location_id: string +} diff --git a/packages/medusa/src/api/routes/admin/sales-channels/index.ts b/packages/medusa/src/api/routes/admin/sales-channels/index.ts index d203233abe..7ed63440bf 100644 --- a/packages/medusa/src/api/routes/admin/sales-channels/index.ts +++ b/packages/medusa/src/api/routes/admin/sales-channels/index.ts @@ -13,6 +13,8 @@ import { AdminPostSalesChannelsReq } from "./create-sales-channel" import { AdminDeleteSalesChannelsChannelProductsBatchReq } from "./delete-products-batch" import { AdminGetSalesChannelsParams } from "./list-sales-channels" import { AdminPostSalesChannelsSalesChannelReq } from "./update-sales-channel" +import { AdminPostSalesChannelsChannelStockLocationsReq } from "./associate-stock-location" +import { AdminDeleteSalesChannelsChannelStockLocationsReq } from "./remove-stock-location" const route = Router() @@ -43,6 +45,16 @@ export default (app) => { transformBody(AdminPostSalesChannelsSalesChannelReq), middlewares.wrap(require("./update-sales-channel").default) ) + salesChannelRouter.post( + "/stock-locations", + transformBody(AdminPostSalesChannelsChannelStockLocationsReq), + middlewares.wrap(require("./associate-stock-location").default) + ) + salesChannelRouter.delete( + "/stock-locations", + transformBody(AdminDeleteSalesChannelsChannelStockLocationsReq), + middlewares.wrap(require("./remove-stock-location").default) + ) salesChannelRouter.delete( "/products/batch", transformBody(AdminDeleteSalesChannelsChannelProductsBatchReq), @@ -81,3 +93,5 @@ export * from "./delete-sales-channel" export * from "./get-sales-channel" export * from "./list-sales-channels" export * from "./update-sales-channel" +export * from "./associate-stock-location" +export * from "./remove-stock-location" diff --git a/packages/medusa/src/api/routes/admin/sales-channels/remove-stock-location.ts b/packages/medusa/src/api/routes/admin/sales-channels/remove-stock-location.ts new file mode 100644 index 0000000000..423cdd71df --- /dev/null +++ b/packages/medusa/src/api/routes/admin/sales-channels/remove-stock-location.ts @@ -0,0 +1,116 @@ +import { IsString } from "class-validator" +import { Request, Response } from "express" +import { EntityManager } from "typeorm" + +import { SalesChannelLocationService } from "../../../../services" + +/** + * @oas [delete] /sales-channels/{id}/stock-locations + * operationId: "DeleteSalesChannelsSalesChannelStockLocation" + * summary: "Remove a stock location from a Sales Channel" + * description: "Removes a stock location from a Sales Channel." + * x-authenticated: true + * parameters: + * - (path) id=* {string} The ID of the Sales Channel. + * requestBody: + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/AdminDeleteSalesChannelsChannelStockLocationsReq" + * x-codeSamples: + * - lang: JavaScript + * label: JS Client + * source: | + * import Medusa from "@medusajs/medusa-js" + * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) + * // must be previously logged in or use api token + * medusa.admin.salesChannels.removeLocation(sales_channel_id, { + * location_id: 'App' + * }) + * .then(({ sales_channel }) => { + * console.log(sales_channel.id); + * }); + * - lang: Shell + * label: cURL + * source: | + * curl --location --request DELETE 'https://medusa-url.com/admin/sales-channels/{id}/stock-locations' \ + * --header 'Authorization: Bearer {api_token}' \ + * --header 'Content-Type: application/json' \ + * --data-raw '{ + * "locaton_id": "stock_location_id" + * }' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - Sales Channel + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * type: object + * properties: + * id: + * type: string + * description: The ID of the removed stock location from a sales channel + * object: + * type: string + * description: The type of the object that was removed. + * default: stock-location + * deleted: + * type: boolean + * description: Whether or not the items were deleted. + * default: true + * "400": + * $ref: "#/components/responses/400_error" + * "401": + * $ref: "#/components/responses/unauthorized" + * "404": + * $ref: "#/components/responses/not_found_error" + * "409": + * $ref: "#/components/responses/invalid_state_error" + * "422": + * $ref: "#/components/responses/invalid_request_error" + * "500": + * $ref: "#/components/responses/500_error" + */ +export default async (req: Request, res: Response) => { + const { id } = req.params + const { validatedBody } = req as { + validatedBody: AdminDeleteSalesChannelsChannelStockLocationsReq + } + + const channelLocationService: SalesChannelLocationService = req.scope.resolve( + "salesChannelLocationService" + ) + + const manager: EntityManager = req.scope.resolve("manager") + await manager.transaction(async (transactionManager) => { + await channelLocationService + .withTransaction(transactionManager) + .removeLocation(id, validatedBody.location_id) + }) + + res.json({ + id, + object: "stock-location", + deleted: true, + }) +} + +/** + * @schema AdminDeleteSalesChannelsChannelStockLocationsReq + * type: object + * required: + * - location_id + * properties: + * location_id: + * description: The ID of the stock location + * type: string + */ +export class AdminDeleteSalesChannelsChannelStockLocationsReq { + @IsString() + location_id: string +} diff --git a/packages/medusa/src/api/routes/admin/stock-locations/create-stock-location.ts b/packages/medusa/src/api/routes/admin/stock-locations/create-stock-location.ts new file mode 100644 index 0000000000..e56a4c0d6a --- /dev/null +++ b/packages/medusa/src/api/routes/admin/stock-locations/create-stock-location.ts @@ -0,0 +1,155 @@ +import { Request, Response } from "express" +import { Type } from "class-transformer" +import { ValidateNested, IsOptional, IsString, IsObject } from "class-validator" + +import { IStockLocationService } from "../../../../interfaces" +import { FindParams } from "../../../../types/common" + +/** + * @oas [post] /stock-locations + * operationId: "PostStockLocations" + * summary: "Create a Stock Location" + * description: "Creates a Stock Location." + * x-authenticated: true + * parameters: + * - (query) expand {string} Comma separated list of relations to include in the results. + * - (query) fields {string} Comma separated list of fields to include in the results. + * requestBody: + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/AdminPostStockLocationsReq" + * x-codeSamples: + * - lang: JavaScript + * label: JS Client + * source: | + * import Medusa from "@medusajs/medusa-js" + * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) + * // must be previously logged in or use api token + * medusa.admin.stockLocations.create({ + * name: 'Main Warehouse', + * location_id: 'sloc' + * }) + * .then(({ stock_location }) => { + * console.log(stock_location.id); + * }); + * - lang: Shell + * label: cURL + * source: | + * curl --location --request POST 'https://medusa-url.com/admin/stock-locations' \ + * --header 'Authorization: Bearer {api_token}' \ + * --header 'Content-Type: application/json' \ + * --data-raw '{ + * "name": "App" + * }' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - Stock Location + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * type: object + * properties: + * stock_location: + * $ref: "#/components/schemas/StockLocationDTO" + * "400": + * $ref: "#/components/responses/400_error" + * "401": + * $ref: "#/components/responses/unauthorized" + * "404": + * $ref: "#/components/responses/not_found_error" + * "409": + * $ref: "#/components/responses/invalid_state_error" + * "422": + * $ref: "#/components/responses/invalid_request_error" + * "500": + * $ref: "#/components/responses/500_error" + */ +export default async (req: Request, res: Response) => { + const locationService: IStockLocationService = req.scope.resolve( + "stockLocationService" + ) + + const createdStockLocation = await locationService.create( + req.validatedBody as AdminPostStockLocationsReq + ) + + const stockLocation = await locationService.retrieve( + createdStockLocation.id, + req.retrieveConfig + ) + + res.status(200).json({ stock_location: stockLocation }) +} + +class StockLocationAddress { + @IsString() + address_1: string + + @IsOptional() + @IsString() + address_2?: string + + @IsOptional() + @IsString() + city?: string + + @IsString() + country_code: string + + @IsOptional() + @IsString() + phone?: string + + @IsOptional() + @IsString() + postal_code?: string + + @IsOptional() + @IsString() + province?: string +} + +/** + * @schema AdminPostStockLocationsReq + * type: object + * required: + * - name + * properties: + * name: + * description: the name of the stock location + * type: string + * address_id: + * description: the stock location address ID + * type: string + * metadata: + * type: object + * description: An optional key-value map with additional details + * example: {car: "white"} + * address: + * $ref: "#/components/schemas/StockLocationAddressInput" + */ +export class AdminPostStockLocationsReq { + @IsString() + name: string + + @IsOptional() + @ValidateNested() + @Type(() => StockLocationAddress) + address?: StockLocationAddress + + @IsOptional() + @IsString() + address_id?: string + + @IsObject() + @IsOptional() + metadata?: Record +} + +export class AdminPostStockLocationsParams extends FindParams {} diff --git a/packages/medusa/src/api/routes/admin/stock-locations/get-stock-location.ts b/packages/medusa/src/api/routes/admin/stock-locations/get-stock-location.ts new file mode 100644 index 0000000000..b3fba0da2a --- /dev/null +++ b/packages/medusa/src/api/routes/admin/stock-locations/get-stock-location.ts @@ -0,0 +1,58 @@ +import { IStockLocationService } from "../../../../interfaces" +import { Request, Response } from "express" +import { FindParams } from "../../../../types/common" + +/** + * @oas [get] /stock-locations/{id} + * operationId: "GetStockLocationsStockLocation" + * summary: "Get a Stock Location" + * description: "Retrieves the Stock Location." + * x-authenticated: true + * parameters: + * - (path) id=* {string} The ID of the Stock Location. + * - (query) expand {string} Comma separated list of relations to include in the results. + * - (query) fields {string} Comma separated list of fields to include in the results. + * x-codeSamples: + * - lang: JavaScript + * label: JS Client + * source: | + * import Medusa from "@medusajs/medusa-js" + * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) + * // must be previously logged in or use api token + * medusa.admin.stockLocations.retrieve(stock_location_id) + * .then(({ stock_location }) => { + * console.log(stock_location.id); + * }); + * - lang: Shell + * label: cURL + * source: | + * curl --location --request GET 'https://medusa-url.com/admin/stock-locations/{id}' \ + * --header 'Authorization: Bearer {api_token}' \ + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - Stock Location + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * type: object + * properties: + * stock_location: + * $ref: "#/components/schemas/StockLocationDTO" + */ +export default async (req: Request, res: Response) => { + const { id } = req.params + + const locationService: IStockLocationService = req.scope.resolve( + "stockLocationService" + ) + const stockLocation = await locationService.retrieve(id, req.retrieveConfig) + + res.status(200).json({ stock_location: stockLocation }) +} + +export class AdminGetStockLocationsLocationParams extends FindParams {} diff --git a/packages/medusa/src/api/routes/admin/stock-locations/index.ts b/packages/medusa/src/api/routes/admin/stock-locations/index.ts new file mode 100644 index 0000000000..59e1f7328f --- /dev/null +++ b/packages/medusa/src/api/routes/admin/stock-locations/index.ts @@ -0,0 +1,101 @@ +import { Router } from "express" +import "reflect-metadata" +import { DeleteResponse, PaginatedResponse } from "../../../../types/common" +import { StockLocationDTO } from "../../../../types/stock-location" +import middlewares, { + transformBody, + transformQuery, +} from "../../../middlewares" +import { AdminGetStockLocationsParams } from "./list-stock-locations" +import { AdminGetStockLocationsLocationParams } from "./get-stock-location" +import { + AdminPostStockLocationsLocationParams, + AdminPostStockLocationsLocationReq, +} from "./update-stock-location" +import { + AdminPostStockLocationsParams, + AdminPostStockLocationsReq, +} from "./create-stock-location" +import { checkRegisteredModules } from "../../../middlewares/check-registered-modules" + +const route = Router() + +export default (app) => { + app.use( + "/stock-locations", + checkRegisteredModules({ + stockLocationService: + "Stock Locations are not enabled. Please add a Stock Location module to enable this functionality.", + }), + route + ) + + route.get( + "/", + transformQuery(AdminGetStockLocationsParams, { + defaultFields: defaultAdminStockLocationFields, + defaultRelations: defaultAdminStockLocationRelations, + isList: true, + }), + middlewares.wrap(require("./list-stock-locations").default) + ) + route.post( + "/", + transformQuery(AdminPostStockLocationsParams, { + defaultFields: defaultAdminStockLocationFields, + defaultRelations: defaultAdminStockLocationRelations, + isList: false, + }), + transformBody(AdminPostStockLocationsReq), + middlewares.wrap(require("./create-stock-location").default) + ) + + route.get( + "/:id", + transformQuery(AdminGetStockLocationsLocationParams, { + defaultFields: defaultAdminStockLocationFields, + defaultRelations: defaultAdminStockLocationRelations, + isList: false, + }), + middlewares.wrap(require("./get-stock-location").default) + ) + + route.post( + "/:id", + transformQuery(AdminPostStockLocationsLocationParams, { + defaultFields: defaultAdminStockLocationFields, + defaultRelations: defaultAdminStockLocationRelations, + isList: false, + }), + transformBody(AdminPostStockLocationsLocationReq), + middlewares.wrap(require("./update-stock-location").default) + ) + + return app +} + +export const defaultAdminStockLocationFields: (keyof StockLocationDTO)[] = [ + "id", + "name", + "address_id", + "metadata", + "created_at", + "updated_at", +] + +export const defaultAdminStockLocationRelations = [] + +export type AdminStockLocationsRes = { + stock_location: StockLocationDTO +} + +export type AdminStockLocationsDeleteRes = DeleteResponse + +export type AdminStockLocationsListRes = PaginatedResponse & { + stock_locations: StockLocationDTO[] +} + +export * from "./list-stock-locations" +export * from "./get-stock-location" +export * from "./create-stock-location" +export * from "./update-stock-location" diff --git a/packages/medusa/src/api/routes/admin/stock-locations/list-stock-locations.ts b/packages/medusa/src/api/routes/admin/stock-locations/list-stock-locations.ts new file mode 100644 index 0000000000..84065dad08 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/stock-locations/list-stock-locations.ts @@ -0,0 +1,179 @@ +import { IsOptional } from "class-validator" +import { IsType } from "../../../../utils/validators/is-type" + +import { IStockLocationService } from "../../../../interfaces" +import { extendedFindParamsMixin } from "../../../../types/common" +import { Request, Response } from "express" + +/** + * @oas [get] /stock-locations + * operationId: "GetStockLocations" + * summary: "List Stock Locations" + * description: "Retrieves a list of stock locations" + * x-authenticated: true + * parameters: + * - (query) id {string} ID of the stock location + * - (query) name {string} Name of the stock location + * - (query) order {string} The field to order the results by. + * - in: query + * name: created_at + * description: Date comparison for when resulting collections were created. + * schema: + * type: object + * properties: + * lt: + * type: string + * description: filter by dates less than this date + * format: date + * gt: + * type: string + * description: filter by dates greater than this date + * format: date + * lte: + * type: string + * description: filter by dates less than or equal to this date + * format: date + * gte: + * type: string + * description: filter by dates greater than or equal to this date + * format: date + * - in: query + * name: updated_at + * description: Date comparison for when resulting collections were updated. + * schema: + * type: object + * properties: + * lt: + * type: string + * description: filter by dates less than this date + * format: date + * gt: + * type: string + * description: filter by dates greater than this date + * format: date + * lte: + * type: string + * description: filter by dates less than or equal to this date + * format: date + * gte: + * type: string + * description: filter by dates greater than or equal to this date + * format: date + * - in: query + * name: deleted_at + * description: Date comparison for when resulting collections were deleted. + * schema: + * type: object + * properties: + * lt: + * type: string + * description: filter by dates less than this date + * format: date + * gt: + * type: string + * description: filter by dates greater than this date + * format: date + * lte: + * type: string + * description: filter by dates less than or equal to this date + * format: date + * gte: + * type: string + * description: filter by dates greater than or equal to this date + * format: date + * - (query) offset=0 {integer} How many stock locations to skip in the result. + * - (query) limit=20 {integer} Limit the number of stock locations returned. + * - (query) expand {string} (Comma separated) Which fields should be expanded in each stock location of the result. + * - (query) fields {string} (Comma separated) Which fields should be included in each stock location of the result. + * x-codeSamples: + * - lang: JavaScript + * label: JS Client + * source: | + * import Medusa from "@medusajs/medusa-js" + * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) + * // must be previously logged in or use api token + * medusa.admin.stockLocations.list() + * .then(({ stock_locations, limit, offset, count }) => { + * console.log(stock_locations.length); + * }); + * - lang: Shell + * label: cURL + * source: | + * curl --location --request GET 'https://medusa-url.com/admin/stock-locations' \ + * --header 'Authorization: Bearer {api_token}' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - Sales Channel + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * type: object + * properties: + * stock_locations: + * type: array + * items: + * $ref: "#/components/schemas/StockLocationDTO" + * count: + * type: integer + * description: The total number of items available + * offset: + * type: integer + * description: The number of items skipped before these items + * limit: + * type: integer + * description: The number of items per page + * "400": + * $ref: "#/components/responses/400_error" + * "401": + * $ref: "#/components/responses/unauthorized" + * "404": + * $ref: "#/components/responses/not_found_error" + * "409": + * $ref: "#/components/responses/invalid_state_error" + * "422": + * $ref: "#/components/responses/invalid_request_error" + * "500": + * $ref: "#/components/responses/500_error" + */ +export default async (req: Request, res: Response) => { + const stockLocationService: IStockLocationService = req.scope.resolve( + "stockLocationService" + ) + + const { filterableFields, listConfig } = req + const { skip, take } = listConfig + + const [locations, count] = await stockLocationService.listAndCount( + filterableFields, + listConfig + ) + + res.status(200).json({ + stock_locations: locations, + count, + offset: skip, + limit: take, + }) +} + +export class AdminGetStockLocationsParams extends extendedFindParamsMixin({ + limit: 20, + offset: 0, +}) { + @IsOptional() + @IsType([String, [String]]) + id?: string | string[] + + @IsOptional() + @IsType([String, [String]]) + name?: string | string[] + + @IsOptional() + @IsType([String, [String]]) + address_id?: string | string[] +} diff --git a/packages/medusa/src/api/routes/admin/stock-locations/update-stock-location.ts b/packages/medusa/src/api/routes/admin/stock-locations/update-stock-location.ts new file mode 100644 index 0000000000..bf0b81ce43 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/stock-locations/update-stock-location.ts @@ -0,0 +1,154 @@ +import { Request, Response } from "express" +import { Type } from "class-transformer" +import { ValidateNested, IsOptional, IsString, IsObject } from "class-validator" + +import { IStockLocationService } from "../../../../interfaces" +import { FindParams } from "../../../../types/common" + +/** + * @oas [post] /stock-locations/{id} + * operationId: "PostStockLocationsStockLocation" + * summary: "Update a Stock Location" + * description: "Updates a Stock Location." + * x-authenticated: true + * parameters: + * - (path) id=* {string} The ID of the Stock Location. + * - (query) expand {string} Comma separated list of relations to include in the results. + * - (query) fields {string} Comma separated list of fields to include in the results. + * requestBody: + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/AdminPostStockLocationsLocationReq" + * x-codeSamples: + * - lang: JavaScript + * label: JS Client + * source: | + * import Medusa from "@medusajs/medusa-js" + * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) + * // must be previously logged in or use api token + * medusa.admin.stockLocations.update(stock_location_id, { + * name: 'App' + * }) + * .then(({ stock_location }) => { + * console.log(stock_location.id); + * }); + * - lang: Shell + * label: cURL + * source: | + * curl --location --request POST 'https://medusa-url.com/admin/stock-locations/{id}' \ + * --header 'Authorization: Bearer {api_token}' \ + * --header 'Content-Type: application/json' \ + * --data-raw '{ + * "name": "App" + * }' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - Stock Location + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * type: object + * properties: + * stock_location: + * $ref: "#/components/schemas/StockLocationDTO" + * "400": + * $ref: "#/components/responses/400_error" + * "401": + * $ref: "#/components/responses/unauthorized" + * "404": + * $ref: "#/components/responses/not_found_error" + * "409": + * $ref: "#/components/responses/invalid_state_error" + * "422": + * $ref: "#/components/responses/invalid_request_error" + * "500": + * $ref: "#/components/responses/500_error" + */ +export default async (req: Request, res: Response) => { + const { id } = req.params + + const locationService: IStockLocationService = req.scope.resolve( + "stockLocationService" + ) + + await locationService.update( + id, + req.validatedBody as AdminPostStockLocationsLocationReq + ) + + const stockLocation = await locationService.retrieve(id, req.retrieveConfig) + + res.status(200).json({ stock_location: stockLocation }) +} + +class StockLocationAddress { + @IsString() + address_1: string + + @IsOptional() + @IsString() + address_2?: string + + @IsOptional() + @IsString() + city?: string + + @IsString() + country_code: string + + @IsOptional() + @IsString() + phone?: string + + @IsOptional() + @IsString() + postal_code?: string + + @IsOptional() + @IsString() + province?: string +} + +/** + * @schema AdminPostStockLocationsLocationReq + * type: object + * properties: + * name: + * description: the name of the stock location + * type: string + * address_id: + * description: the stock location address ID + * type: string + * metadata: + * type: object + * description: An optional key-value map with additional details + * example: {car: "white"} + * address: + * $ref: "#/components/schemas/StockLocationAddressInput" + */ +export class AdminPostStockLocationsLocationReq { + @IsOptional() + @IsString() + name?: string + + @IsOptional() + @ValidateNested() + @Type(() => StockLocationAddress) + address?: StockLocationAddress + + @IsOptional() + @IsString() + address_id?: string + + @IsObject() + @IsOptional() + metadata?: Record +} + +export class AdminPostStockLocationsLocationParams extends FindParams {} diff --git a/packages/medusa/src/index.js b/packages/medusa/src/index.js index e92a3faca5..1848d319ee 100644 --- a/packages/medusa/src/index.js +++ b/packages/medusa/src/index.js @@ -9,3 +9,6 @@ export * from "./types/global" export * from "./models" export * from "./services" export * from "./utils" +export * from "./types/global" +export * from "./types/stock-location" +export * from "./types/common" diff --git a/packages/medusa/src/interfaces/services/event-bus.ts b/packages/medusa/src/interfaces/services/event-bus.ts new file mode 100644 index 0000000000..1eac78d5e1 --- /dev/null +++ b/packages/medusa/src/interfaces/services/event-bus.ts @@ -0,0 +1,3 @@ +export interface IEventBusService { + emit(event: string, data: any): Promise +} diff --git a/packages/medusa/src/interfaces/services/index.ts b/packages/medusa/src/interfaces/services/index.ts index 33f7e70a3d..facfa6d934 100644 --- a/packages/medusa/src/interfaces/services/index.ts +++ b/packages/medusa/src/interfaces/services/index.ts @@ -1,3 +1,4 @@ export * from "./cache" +export * from "./event-bus" export * from "./stock-location" export * from "./inventory" diff --git a/packages/medusa/src/interfaces/services/stock-location.ts b/packages/medusa/src/interfaces/services/stock-location.ts index 995dd531ef..e5e8ef29f0 100644 --- a/packages/medusa/src/interfaces/services/stock-location.ts +++ b/packages/medusa/src/interfaces/services/stock-location.ts @@ -18,7 +18,10 @@ export interface IStockLocationService { config?: FindConfig ): Promise<[StockLocationDTO[], number]> - retrieve(id: string): Promise + retrieve( + id: string, + config?: FindConfig + ): Promise create(input: CreateStockLocationInput): Promise diff --git a/packages/medusa/src/models/index.ts b/packages/medusa/src/models/index.ts index 454fe565d1..8e213c83da 100644 --- a/packages/medusa/src/models/index.ts +++ b/packages/medusa/src/models/index.ts @@ -60,6 +60,7 @@ export * from "./region" export * from "./return" export * from "./return-item" export * from "./return-reason" +export * from "./sales-channel-location" export * from "./sales-channel" export * from "./sales-channel-location" export * from "./shipping-method" diff --git a/packages/medusa/src/models/line-item.ts b/packages/medusa/src/models/line-item.ts index 1632a347a9..58f03375b1 100644 --- a/packages/medusa/src/models/line-item.ts +++ b/packages/medusa/src/models/line-item.ts @@ -108,8 +108,8 @@ export class LineItem extends BaseEntity { @Column() title: string - @Column({ nullable: true }) - description: string + @Column({ nullable: true, type: "text" }) + description: string | null @Column({ type: "text", nullable: true }) thumbnail: string | null @@ -126,14 +126,14 @@ export class LineItem extends BaseEntity { @Column({ default: true }) allow_discounts: boolean - @Column({ nullable: true }) - has_shipping: boolean + @Column({ nullable: true, type: "boolean" }) + has_shipping: boolean | null @Column({ type: "int" }) unit_price: number @Index() - @Column({ nullable: true }) + @Column({ nullable: true, type: "text" }) variant_id: string | null @ManyToOne(() => ProductVariant, { eager: true }) @@ -144,13 +144,13 @@ export class LineItem extends BaseEntity { quantity: number @Column({ nullable: true, type: "int" }) - fulfilled_quantity: number + fulfilled_quantity: number | null @Column({ nullable: true, type: "int" }) - returned_quantity: number + returned_quantity: number | null @Column({ nullable: true, type: "int" }) - shipped_quantity: number + shipped_quantity: number | null @DbAwareColumn({ type: "jsonb", nullable: true }) metadata: Record diff --git a/packages/medusa/src/models/return.ts b/packages/medusa/src/models/return.ts index e985c85070..b2a9c53544 100644 --- a/packages/medusa/src/models/return.ts +++ b/packages/medusa/src/models/return.ts @@ -41,24 +41,24 @@ export class Return extends BaseEntity { items: ReturnItem[] @Index() - @Column({ nullable: true }) - swap_id: string + @Column({ nullable: true, type: "text" }) + swap_id: string | null @OneToOne(() => Swap, (swap) => swap.return_order) @JoinColumn({ name: "swap_id" }) swap: Swap @Index() - @Column({ nullable: true }) - claim_order_id: string + @Column({ nullable: true, type: "text" }) + claim_order_id: string | null @OneToOne(() => ClaimOrder, (co) => co.return_order) @JoinColumn({ name: "claim_order_id" }) claim_order: ClaimOrder @Index() - @Column({ nullable: true }) - order_id: string + @Column({ nullable: true, type: "text" }) + order_id: string | null @ManyToOne(() => Order, (o) => o.returns) @JoinColumn({ name: "order_id" }) @@ -69,13 +69,13 @@ export class Return extends BaseEntity { }) shipping_method: ShippingMethod + @Index() + @Column({ nullable: true, type: "text" }) + location_id: string | null + @DbAwareColumn({ type: "jsonb", nullable: true }) shipping_data: Record - @Index() - @Column({ nullable: true }) - location_id: string - @Column({ type: "int" }) refund_amount: number @@ -83,13 +83,13 @@ export class Return extends BaseEntity { received_at: Date @Column({ type: "boolean", nullable: true }) - no_notification: boolean + no_notification: boolean | null @DbAwareColumn({ type: "jsonb", nullable: true }) - metadata: Record + metadata: Record | null - @Column({ nullable: true }) - idempotency_key: string + @Column({ nullable: true, type: "text" }) + idempotency_key: string | null @BeforeInsert() private beforeInsert(): void { diff --git a/packages/medusa/src/models/sales-channel-location.ts b/packages/medusa/src/models/sales-channel-location.ts index fc42526bc3..7c127e4165 100644 --- a/packages/medusa/src/models/sales-channel-location.ts +++ b/packages/medusa/src/models/sales-channel-location.ts @@ -4,6 +4,7 @@ import { FeatureFlagEntity } from "../utils/feature-flag-decorators" import { BaseEntity } from "../interfaces" import { generateEntityId } from "../utils" +@FeatureFlagEntity("sales_channels") export class SalesChannelLocation extends BaseEntity { @Index() @Column({ type: "text" }) diff --git a/packages/medusa/src/models/store.ts b/packages/medusa/src/models/store.ts index 228ad7e042..e8b046bff6 100644 --- a/packages/medusa/src/models/store.ts +++ b/packages/medusa/src/models/store.ts @@ -45,23 +45,23 @@ export class Store extends BaseEntity { }) currencies: Currency[] - @Column({ nullable: true }) - swap_link_template: string + @Column({ nullable: true, type: "text" }) + swap_link_template: string | null - @Column({ nullable: true }) - payment_link_template: string + @Column({ nullable: true, type: "text" }) + payment_link_template: string | null - @Column({ nullable: true }) - invite_link_template: string + @Column({ nullable: true, type: "text" }) + invite_link_template: string | null @Column({ nullable: true }) default_location_id: string @DbAwareColumn({ type: "jsonb", nullable: true }) - metadata: Record + metadata: Record | null - @FeatureFlagColumn("sales_channels", { nullable: true }) - default_sales_channel_id: string + @FeatureFlagColumn("sales_channels", { nullable: true, type: "text" }) + default_sales_channel_id: string | null @FeatureFlagDecorators("sales_channels", [ OneToOne(() => SalesChannel), diff --git a/packages/medusa/src/repositories/product-variant.ts b/packages/medusa/src/repositories/product-variant.ts index fd452c283d..41814b7041 100644 --- a/packages/medusa/src/repositories/product-variant.ts +++ b/packages/medusa/src/repositories/product-variant.ts @@ -2,7 +2,6 @@ import { EntityRepository, FindConditions, FindManyOptions, - FindOperator, OrderByCondition, Repository, } from "typeorm" diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index 2b227888e8..ba6f9157cd 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -37,27 +37,27 @@ import { import { buildQuery, setMetadata } from "../utils" import { FlagRouter } from "../utils/flag-router" import { validateEmail } from "../utils/is-email" -import CustomShippingOptionService from "./custom-shipping-option" -import CustomerService from "./customer" -import DiscountService from "./discount" -import EventBusService from "./event-bus" -import GiftCardService from "./gift-card" -import { - NewTotalsService, - ProductVariantInventoryService, - SalesChannelService, -} from "./index" -import LineItemService from "./line-item" -import LineItemAdjustmentService from "./line-item-adjustment" -import PaymentProviderService from "./payment-provider" -import ProductService from "./product" -import ProductVariantService from "./product-variant" -import RegionService from "./region" -import ShippingOptionService from "./shipping-option" -import StoreService from "./store" -import TaxProviderService from "./tax-provider" -import TotalsService from "./totals" import { PaymentSessionInput } from "../types/payment" +import { + CustomShippingOptionService, + CustomerService, + DiscountService, + EventBusService, + GiftCardService, + LineItemService, + LineItemAdjustmentService, + NewTotalsService, + PaymentProviderService, + ProductService, + ProductVariantService, + ProductVariantInventoryService, + RegionService, + ShippingOptionService, + StoreService, + TaxProviderService, + TotalsService, + SalesChannelService, +} from "." type InjectedDependencies = { manager: EntityManager @@ -584,11 +584,7 @@ class CartService extends TransactionBaseService { { sales_channel_id }: { sales_channel_id: string | null }, lineItem: LineItemValidateData ): Promise { - if (!sales_channel_id) { - return true - } - - if (!lineItem.variant_id) { + if (!sales_channel_id || !lineItem.variant_id) { return true } diff --git a/packages/medusa/src/services/claim-item.ts b/packages/medusa/src/services/claim-item.ts index d7b6a49bd0..0b02ac5ed1 100644 --- a/packages/medusa/src/services/claim-item.ts +++ b/packages/medusa/src/services/claim-item.ts @@ -77,7 +77,7 @@ class ClaimItemService extends TransactionBaseService { ) } - if (lineItem.fulfilled_quantity < quantity) { + if (lineItem.fulfilled_quantity! < quantity) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "Cannot claim more of an item than has been fulfilled." diff --git a/packages/medusa/src/services/claim.ts b/packages/medusa/src/services/claim.ts index cf82aef951..824a25d597 100644 --- a/packages/medusa/src/services/claim.ts +++ b/packages/medusa/src/services/claim.ts @@ -332,8 +332,8 @@ export default class ClaimService extends TransactionBaseService { lines as LineItem[] ) } - let newItems: LineItem[] = [] + if (isDefined(additional_items)) { newItems = await Promise.all( additional_items.map(async (i) => diff --git a/packages/medusa/src/services/fulfillment.ts b/packages/medusa/src/services/fulfillment.ts index 7fab2dd642..a5659216c3 100644 --- a/packages/medusa/src/services/fulfillment.ts +++ b/packages/medusa/src/services/fulfillment.ts @@ -145,7 +145,7 @@ class FulfillmentService extends TransactionBaseService { return null } - if (quantity > item.quantity - item.fulfilled_quantity) { + if (quantity > item.quantity - item.fulfilled_quantity!) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "Cannot fulfill more items than have been purchased" @@ -285,7 +285,7 @@ class FulfillmentService extends TransactionBaseService { for (const fItem of fulfillment.items) { const item = await lineItemServiceTx.retrieve(fItem.item_id) - const fulfilledQuantity = item.fulfilled_quantity - fItem.quantity + const fulfilledQuantity = item.fulfilled_quantity! - fItem.quantity await lineItemServiceTx.update(item.id, { fulfilled_quantity: fulfilledQuantity, }) diff --git a/packages/medusa/src/services/order.ts b/packages/medusa/src/services/order.ts index 297d96551a..02b0a70026 100644 --- a/packages/medusa/src/services/order.ts +++ b/packages/medusa/src/services/order.ts @@ -30,22 +30,26 @@ import { UpdateOrderInput } from "../types/orders" import { CreateShippingMethodDto } from "../types/shipping-options" import { buildQuery, isString, setMetadata } from "../utils" import { FlagRouter } from "../utils/flag-router" -import CartService from "./cart" -import CustomerService from "./customer" -import DiscountService from "./discount" -import DraftOrderService from "./draft-order" -import EventBusService from "./event-bus" -import FulfillmentService from "./fulfillment" -import FulfillmentProviderService from "./fulfillment-provider" -import GiftCardService from "./gift-card" -import LineItemService from "./line-item" -import PaymentProviderService from "./payment-provider" -import RegionService from "./region" -import ShippingOptionService from "./shipping-option" -import ShippingProfileService from "./shipping-profile" -import TotalsService from "./totals" -import ProductVariantInventoryService from "./product-variant-inventory" -import { NewTotalsService, TaxProviderService } from "./index" + +import { + CartService, + CustomerService, + DiscountService, + DraftOrderService, + EventBusService, + FulfillmentService, + FulfillmentProviderService, + GiftCardService, + ProductVariantInventoryService, + LineItemService, + PaymentProviderService, + RegionService, + ShippingOptionService, + ShippingProfileService, + TotalsService, + NewTotalsService, + TaxProviderService, +} from "." export const ORDER_CART_ALREADY_EXISTS_ERROR = "Order from cart already exists" @@ -1264,7 +1268,7 @@ class OrderService extends TransactionBaseService { return null } - if (quantity > item.quantity - item.fulfilled_quantity) { + if (quantity > item.quantity - item.fulfilled_quantity!) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "Cannot fulfill more items than have been purchased" diff --git a/packages/medusa/src/services/return.ts b/packages/medusa/src/services/return.ts index 1920be5815..cc660bc4f1 100644 --- a/packages/medusa/src/services/return.ts +++ b/packages/medusa/src/services/return.ts @@ -1,6 +1,5 @@ import { isDefined, MedusaError } from "medusa-core-utils" import { DeepPartial, EntityManager } from "typeorm" -import { ProductVariantInventoryService } from "." import { TransactionBaseService } from "../interfaces" import { FulfillmentStatus, @@ -17,13 +16,17 @@ import { FindConfig, Selector } from "../types/common" import { OrdersReturnItem } from "../types/orders" import { CreateReturnInput, UpdateReturnInput } from "../types/return" import { buildQuery, setMetadata } from "../utils" -import FulfillmentProviderService from "./fulfillment-provider" -import LineItemService from "./line-item" -import OrderService from "./order" -import ReturnReasonService from "./return-reason" -import ShippingOptionService from "./shipping-option" -import TaxProviderService from "./tax-provider" -import TotalsService from "./totals" + +import { + FulfillmentProviderService, + ProductVariantInventoryService, + LineItemService, + OrderService, + ReturnReasonService, + ShippingOptionService, + TaxProviderService, + TotalsService, +} from "." type InjectedDependencies = { manager: EntityManager @@ -226,7 +229,7 @@ class ReturnService extends TransactionBaseService { ) } - const returnable = item.quantity - item.returned_quantity + const returnable = item.quantity - item.returned_quantity! if (quantity > returnable) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, @@ -590,7 +593,7 @@ class ReturnService extends TransactionBaseService { const order = await this.orderService_ .withTransaction(manager) - .retrieve(orderId, { + .retrieve(orderId!, { relations: [ "items", "returns", @@ -671,14 +674,15 @@ class ReturnService extends TransactionBaseService { }) } - const inventoryServiceTx = + const productVarInventoryTx = this.productVariantInventoryService_.withTransaction(manager) + for (const line of newLines) { const orderItem = order.items.find((i) => i.id === line.item_id) - if (orderItem && orderItem?.variant_id) { - await inventoryServiceTx.adjustInventory( + if (orderItem && orderItem.variant_id) { + await productVarInventoryTx.adjustInventory( orderItem.variant_id, - returnObj.location_id, + returnObj.location_id!, line.received_quantity ) } diff --git a/packages/medusa/src/services/sales-channel-location.ts b/packages/medusa/src/services/sales-channel-location.ts index 5e7940d3a7..a53322f283 100644 --- a/packages/medusa/src/services/sales-channel-location.ts +++ b/packages/medusa/src/services/sales-channel-location.ts @@ -11,13 +11,17 @@ type InjectedDependencies = { manager: EntityManager } +/** + * Service for managing the stock locations of sales channels + */ + class SalesChannelLocationService extends TransactionBaseService { protected manager_: EntityManager protected transactionManager_: EntityManager | undefined protected readonly salesChannelService_: SalesChannelService - protected readonly eventBusService_: EventBusService - protected readonly stockLocationService_: IStockLocationService + protected readonly eventBusService: EventBusService + protected readonly stockLocationService: IStockLocationService constructor({ salesChannelService, @@ -30,14 +34,15 @@ class SalesChannelLocationService extends TransactionBaseService { this.manager_ = manager this.salesChannelService_ = salesChannelService - this.eventBusService_ = eventBusService - this.stockLocationService_ = stockLocationService + this.eventBusService = eventBusService + this.stockLocationService = stockLocationService } /** - * Removes location from sales channel - * @param salesChannelId sales channel id - * @param locationId location id + * Removes an association between a sales channel and a stock location. + * @param {string} salesChannelId - The ID of the sales channel. + * @param {string} locationId - The ID of the stock location. + * @returns {Promise} A promise that resolves when the association has been removed. */ async removeLocation( salesChannelId: string, @@ -51,9 +56,10 @@ class SalesChannelLocationService extends TransactionBaseService { } /** - * Links location to sales channel - * @param salesChannelId sales channel id - * @param locationId location id + * Associates a sales channel with a stock location. + * @param {string} salesChannelId - The ID of the sales channel. + * @param {string} locationId - The ID of the stock location. + * @returns {Promise} A promise that resolves when the association has been created. */ async associateLocation( salesChannelId: string, @@ -63,7 +69,7 @@ class SalesChannelLocationService extends TransactionBaseService { const salesChannel = await this.salesChannelService_ .withTransaction(manager) .retrieve(salesChannelId) - const stockLocation = await this.stockLocationService_.retrieve(locationId) + const stockLocation = await this.stockLocationService.retrieve(locationId) const salesChannelLocation = manager.create(SalesChannelLocation, { sales_channel_id: salesChannel.id, @@ -74,9 +80,9 @@ class SalesChannelLocationService extends TransactionBaseService { } /** - * Lists all locations associated with sales channel by id - * @param salesChannelId sales channel id - * @returns list of location ids associated with sales channel + * Lists the stock locations associated with a sales channel. + * @param {string} salesChannelId - The ID of the sales channel. + * @returns {Promise} A promise that resolves with an array of location IDs. */ async listLocations(salesChannelId: string): Promise { const manager = this.transactionManager_ || this.manager_ diff --git a/packages/medusa/src/types/stock-location.ts b/packages/medusa/src/types/stock-location.ts index 4f7662b28e..e5f401ac00 100644 --- a/packages/medusa/src/types/stock-location.ts +++ b/packages/medusa/src/types/stock-location.ts @@ -1,21 +1,132 @@ import { StringComparisonOperator } from "./common" +/** + * @schema StockLocationAddressDTO + * title: "Stock Location Address" + * description: "Represents a Stock Location Address" + * type: object + * required: + * - address_1 + * - country_code + * - created_at + * - updated_at + * properties: + * id: + * type: string + * description: The stock location address' ID + * example: laddr_51G4ZW853Y6TFXWPG5ENJ81X42 + * address_1: + * type: string + * description: Stock location address + * example: 35, Jhon Doe Ave + * address_2: + * type: string + * description: Stock location address' complement + * example: apartment 4432 + * city: + * type: string + * description: Stock location address' city + * example: Mexico city + * country_code: + * type: string + * description: Stock location address' country + * example: MX + * phone: + * type: string + * description: Stock location address' phone number + * example: +1 555 61646 + * postal_code: + * type: string + * description: Stock location address' postal code + * example: HD3-1G8 + * province: + * type: string + * description: Stock location address' province + * example: Sinaloa + * created_at: + * type: string + * description: "The date with timezone at which the resource was created." + * format: date-time + * updated_at: + * type: string + * description: "The date with timezone at which the resource was updated." + * format: date-time + * deleted_at: + * type: string + * description: "The date with timezone at which the resource was deleted." + * format: date-time + * metadata: + * type: object + * description: An optional key-value map with additional details + * example: {car: "white"} + */ export type StockLocationAddressDTO = { id?: string address_1: string - address_2?: string - city?: string - country_code?: string - phone?: string - postal_code?: string - province?: string + address_2?: string | null + country_code: string + city?: string | null + phone?: string | null + postal_code?: string | null + province?: string | null + metadata?: Record | null + created_at: string | Date + updated_at: string | Date + deleted_at: string | Date | null } +/** + * @schema StockLocationDTO + * title: "Stock Location" + * description: "Represents a Stock Location" + * type: object + * required: + * - id + * - name + * - address_id + * - created_at + * - updated_at + * properties: + * id: + * type: string + * description: The stock location's ID + * example: sloc_51G4ZW853Y6TFXWPG5ENJ81X42 + * address_id: + * type: string + * description: Stock location address' ID + * example: laddr_05B2ZE853Y6FTXWPW85NJ81A44 + * name: + * type: string + * description: The name of the stock location + * example: Main Warehouse + * address: + * description: "The Address of the Stock Location" + * allOf: + * - $ref: "#/components/schemas/StockLocationAddressDTO" + * - type: object + * metadata: + * type: object + * description: An optional key-value map with additional details + * example: {car: "white"} + * created_at: + * type: string + * description: "The date with timezone at which the resource was created." + * format: date-time + * updated_at: + * type: string + * description: "The date with timezone at which the resource was updated." + * format: date-time + * deleted_at: + * type: string + * description: "The date with timezone at which the resource was deleted." + * format: date-time + */ export type StockLocationDTO = { id: string name: string metadata: Record | null address_id: string + address?: StockLocationAddressDTO created_at: string | Date updated_at: string | Date deleted_at: string | Date | null @@ -26,23 +137,115 @@ export type FilterableStockLocationProps = { name?: string | string[] | StringComparisonOperator } +/** + * @schema StockLocationAddressInput + * title: "Stock Location Address Input" + * description: "Represents a Stock Location Address Input" + * type: object + * required: + * - address_1 + * - country_code + * properties: + * address_1: + * type: string + * description: Stock location address + * example: 35, Jhon Doe Ave + * address_2: + * type: string + * description: Stock location address' complement + * example: apartment 4432 + * city: + * type: string + * description: Stock location address' city + * example: Mexico city + * country_code: + * type: string + * description: Stock location address' country + * example: MX + * phone: + * type: string + * description: Stock location address' phone number + * example: +1 555 61646 + * postal_code: + * type: string + * description: Stock location address' postal code + * example: HD3-1G8 + * province: + * type: string + * description: Stock location address' province + * example: Sinaloa + * metadata: + * type: object + * description: An optional key-value map with additional details + * example: {car: "white"} + */ export type StockLocationAddressInput = { address_1: string address_2?: string + country_code: string city?: string - country_code?: string phone?: string province?: string postal_code?: string + metadata?: Record } +/** + * @schema CreateStockLocationInput + * title: "Create Stock Location Input" + * description: "Represents the Input to create a Stock Location" + * type: object + * required: + * - name + * properties: + * name: + * type: string + * description: The stock location name + * address_id: + * type: string + * description: The Stock location address ID + * address: + * description: Stock location address object + * allOf: + * - $ref: "#/components/schemas/StockLocationAddressInput" + * - type: object + * metadata: + * type: object + * description: An optional key-value map with additional details + * example: {car: "white"} + */ export type CreateStockLocationInput = { name: string + address_id?: string address?: string | StockLocationAddressInput + metadata?: Record } +/** + * @schema UpdateStockLocationInput + * title: "Update Stock Location Input" + * description: "Represents the Input to update a Stock Location" + * type: object + * properties: + * name: + * type: string + * description: The stock location name + * address_id: + * type: string + * description: The Stock location address ID + * address: + * description: Stock location address object + * allOf: + * - $ref: "#/components/schemas/StockLocationAddressInput" + * - type: object + * metadata: + * type: object + * description: An optional key-value map with additional details + * example: {car: "white"} + */ export type UpdateStockLocationInput = { name?: string address_id?: string address?: StockLocationAddressInput + metadata?: Record } diff --git a/packages/stock-location/.gitignore b/packages/stock-location/.gitignore new file mode 100644 index 0000000000..874c6c69d3 --- /dev/null +++ b/packages/stock-location/.gitignore @@ -0,0 +1,6 @@ +/dist +node_modules +.DS_store +.env* +.env +*.sql diff --git a/packages/stock-location/.npmignore b/packages/stock-location/.npmignore new file mode 100644 index 0000000000..4a2bc7f77c --- /dev/null +++ b/packages/stock-location/.npmignore @@ -0,0 +1,10 @@ +src +.turbo +.prettierrc +.env +.babelrc.js +.eslintrc +.gitignore +ormconfig.json +tsconfig.json +jest.config.md diff --git a/packages/stock-location/jest.config.js b/packages/stock-location/jest.config.js new file mode 100644 index 0000000000..7de5bf104a --- /dev/null +++ b/packages/stock-location/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + globals: { + "ts-jest": { + tsConfig: "tsconfig.json", + isolatedModules: false, + }, + }, + transform: { + "^.+\\.[jt]s?$": "ts-jest", + }, + testEnvironment: `node`, + moduleFileExtensions: [`js`, `ts`], +} diff --git a/packages/stock-location/package.json b/packages/stock-location/package.json new file mode 100644 index 0000000000..82c87511fc --- /dev/null +++ b/packages/stock-location/package.json @@ -0,0 +1,35 @@ +{ + "name": "@medusajs/stock-location", + "version": "1.0.0", + "description": "Stock Location Module for Medusa", + "main": "dist/index.js", + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/stock-location" + }, + "publishConfig": { + "access": "public" + }, + "author": "Medusa", + "license": "MIT", + "devDependencies": { + "@medusajs/medusa": "^1.7.1", + "cross-env": "^5.2.1", + "jest": "^25.5.2", + "ts-jest": "^25.5.1", + "typescript": "^4.4.4" + }, + "scripts": { + "watch": "tsc --build --watch", + "prepare": "cross-env NODE_ENV=production yarn run build", + "build": "tsc --build", + "test": "jest --passWithNoTests", + "test:unit": "jest --passWithNoTests" + }, + "peerDependencies": { + "@medusajs/medusa": "^1.7.1", + "medusa-interfaces": "1.3.3", + "typeorm": "^0.2.31" + } +} diff --git a/packages/stock-location/src/config.ts b/packages/stock-location/src/config.ts new file mode 100644 index 0000000000..a404b0fff6 --- /dev/null +++ b/packages/stock-location/src/config.ts @@ -0,0 +1 @@ +export const CONNECTION_NAME = "stock_location_connection" diff --git a/packages/stock-location/src/index.js b/packages/stock-location/src/index.js new file mode 100644 index 0000000000..072ac428df --- /dev/null +++ b/packages/stock-location/src/index.js @@ -0,0 +1,7 @@ +import ConnectionLoader from "./loaders/connection" +import StockLocationService from "./services/stock-location" +import * as SchemaMigration from "./migrations/schema-migrations/1665749860179-setup" + +export const service = StockLocationService +export const migrations = [SchemaMigration] +export const loaders = [ConnectionLoader] diff --git a/packages/stock-location/src/loaders/connection.ts b/packages/stock-location/src/loaders/connection.ts new file mode 100644 index 0000000000..5cc2d64e64 --- /dev/null +++ b/packages/stock-location/src/loaders/connection.ts @@ -0,0 +1,22 @@ +import { ConfigModule } from "@medusajs/medusa" +import { ConnectionOptions, createConnection } from "typeorm" +import { CONNECTION_NAME } from "../config" + +import { StockLocation, StockLocationAddress } from "../models" + +export default async ({ + configModule, +}: { + configModule: ConfigModule +}): Promise => { + await createConnection({ + name: CONNECTION_NAME, + type: configModule.projectConfig.database_type, + url: configModule.projectConfig.database_url, + database: configModule.projectConfig.database_database, + schema: configModule.projectConfig.database_schema, + extra: configModule.projectConfig.database_extra || {}, + entities: [StockLocation, StockLocationAddress], + logging: configModule.projectConfig.database_logging || false, + } as ConnectionOptions) +} diff --git a/packages/stock-location/src/migrations/schema-migrations/1665749860179-setup.ts b/packages/stock-location/src/migrations/schema-migrations/1665749860179-setup.ts new file mode 100644 index 0000000000..3a941c5227 --- /dev/null +++ b/packages/stock-location/src/migrations/schema-migrations/1665749860179-setup.ts @@ -0,0 +1,94 @@ +import { ConfigModule } from "@medusajs/medusa" +import { + createConnection, + ConnectionOptions, + MigrationInterface, + QueryRunner, +} from "typeorm" + +import { CONNECTION_NAME } from "../../config" + +export const up = async ({ configModule }: { configModule: ConfigModule }) => { + const connection = await createConnection({ + name: CONNECTION_NAME, + type: configModule.projectConfig.database_type, + url: configModule.projectConfig.database_url, + database: configModule.projectConfig.database_database, + schema: configModule.projectConfig.database_schema, + extra: configModule.projectConfig.database_extra || {}, + migrations: [setup1665749860179], + logging: true, + } as ConnectionOptions) + + await connection.runMigrations() +} + +export const down = async ({ + configModule, +}: { + configModule: ConfigModule +}) => { + const connection = await createConnection({ + name: CONNECTION_NAME, + type: configModule.projectConfig.database_type, + url: configModule.projectConfig.database_url, + database: configModule.projectConfig.database_database, + schema: configModule.projectConfig.database_schema, + extra: configModule.projectConfig.database_extra || {}, + migrations: [setup1665749860179], + logging: true, + } as ConnectionOptions) + + await connection.undoLastMigration({ transaction: "all" }) +} + +export class setup1665749860179 implements MigrationInterface { + name = "setup1665749860179" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "stock_location_address" + ( + "id" CHARACTER VARYING NOT NULL, + "created_at" TIMESTAMP WITH TIME zone NOT NULL DEFAULT Now(), + "updated_at" TIMESTAMP WITH TIME zone NOT NULL DEFAULT Now(), + "deleted_at" TIMESTAMP WITH TIME zone, + "address_1" TEXT NOT NULL, + "address_2" TEXT, + "city" TEXT, + "country_code" TEXT NOT NULL, + "phone" TEXT, + "province" TEXT, + "postal_code" TEXT, + "metadata" JSONB, + CONSTRAINT "PK_b79bc27285bede680501b7b81a5" PRIMARY KEY ("id") + ); + + CREATE INDEX "IDX_stock_location_address_country_code" ON "stock_location_address" ("country_code"); + + CREATE TABLE "stock_location" + ( + "id" CHARACTER VARYING NOT NULL, + "created_at" TIMESTAMP WITH time zone NOT NULL DEFAULT Now(), + "updated_at" TIMESTAMP WITH time zone NOT NULL DEFAULT Now(), + "deleted_at" TIMESTAMP WITH time zone, + "name" TEXT NOT NULL, + "address_id" TEXT NOT NULL, + "metadata" JSONB, + CONSTRAINT "PK_adf770067d0df1421f525fa25cc" PRIMARY KEY ("id") + ); + + CREATE INDEX "IDX_stock_location_address_id" ON "stock_location" ("address_id"); + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP INDEX "IDX_stock_location_address_id"; + DROP TABLE "stock_location"; + + DROP INDEX "IDX_stock_location_address_country_code"; + DROP TABLE "stock_location_address"; + `) + } +} diff --git a/packages/stock-location/src/models/index.ts b/packages/stock-location/src/models/index.ts new file mode 100644 index 0000000000..732b15fc23 --- /dev/null +++ b/packages/stock-location/src/models/index.ts @@ -0,0 +1,2 @@ +export * from "./stock-location" +export * from "./stock-location-address" diff --git a/packages/stock-location/src/models/stock-location-address.ts b/packages/stock-location/src/models/stock-location-address.ts new file mode 100644 index 0000000000..1db150c2ad --- /dev/null +++ b/packages/stock-location/src/models/stock-location-address.ts @@ -0,0 +1,35 @@ +import { BeforeInsert, Column, Entity, Index } from "typeorm" +import { SoftDeletableEntity, generateEntityId } from "@medusajs/medusa" + +@Entity() +export class StockLocationAddress extends SoftDeletableEntity { + @Column({ type: "text" }) + address_1: string + + @Column({ type: "text", nullable: true }) + address_2: string | null + + @Column({ type: "text", nullable: true }) + city: string | null + + @Index() + @Column({ type: "text" }) + country_code: string + + @Column({ type: "text", nullable: true }) + phone: string | null + + @Column({ type: "text", nullable: true }) + province: string | null + + @Column({ type: "text", nullable: true }) + postal_code: string | null + + @Column({ type: "jsonb", nullable: true }) + metadata: Record | null + + @BeforeInsert() + private beforeInsert(): void { + this.id = generateEntityId(this.id, "laddr") + } +} diff --git a/packages/stock-location/src/models/stock-location.ts b/packages/stock-location/src/models/stock-location.ts new file mode 100644 index 0000000000..204e1ff302 --- /dev/null +++ b/packages/stock-location/src/models/stock-location.ts @@ -0,0 +1,33 @@ +import { + BeforeInsert, + Column, + Entity, + Index, + JoinColumn, + ManyToOne, +} from "typeorm" +import { SoftDeletableEntity, generateEntityId } from "@medusajs/medusa" + +import { StockLocationAddress } from "." + +@Entity() +export class StockLocation extends SoftDeletableEntity { + @Column({ type: "text" }) + name: string + + @Index() + @Column({ type: "text" }) + address_id: string + + @ManyToOne(() => StockLocationAddress) + @JoinColumn({ name: "address_id" }) + address: StockLocationAddress | null + + @Column({ type: "jsonb", nullable: true }) + metadata: Record | null + + @BeforeInsert() + private beforeInsert(): void { + this.id = generateEntityId(this.id, "sloc") + } +} diff --git a/packages/stock-location/src/services/index.ts b/packages/stock-location/src/services/index.ts new file mode 100644 index 0000000000..65d66a6208 --- /dev/null +++ b/packages/stock-location/src/services/index.ts @@ -0,0 +1 @@ +export { default as StockLocationService } from "./stock-location" diff --git a/packages/stock-location/src/services/stock-location.ts b/packages/stock-location/src/services/stock-location.ts new file mode 100644 index 0000000000..d53468c553 --- /dev/null +++ b/packages/stock-location/src/services/stock-location.ts @@ -0,0 +1,253 @@ +import { getConnection, EntityManager } from "typeorm" +import { isDefined, MedusaError } from "medusa-core-utils" +import { + FindConfig, + buildQuery, + FilterableStockLocationProps, + CreateStockLocationInput, + UpdateStockLocationInput, + StockLocationAddressInput, + IEventBusService, + setMetadata, +} from "@medusajs/medusa" + +import { StockLocation, StockLocationAddress } from "../models" +import { CONNECTION_NAME } from "../config" + +type InjectedDependencies = { + eventBusService: IEventBusService +} + +/** + * Service for managing stock locations. + */ + +export default class StockLocationService { + static Events = { + CREATED: "stock-location.created", + UPDATED: "stock-location.updated", + DELETED: "stock-location.deleted", + } + + protected readonly manager_: EntityManager + + protected readonly eventBusService_: IEventBusService + + constructor({ eventBusService }: InjectedDependencies) { + this.eventBusService_ = eventBusService + } + + private getManager(): EntityManager { + const connection = getConnection(CONNECTION_NAME) + return connection.manager + } + + /** + * Lists all stock locations that match the given selector. + * @param {FilterableStockLocationProps} [selector={}] - Properties to filter by. + * @param {FindConfig} [config={ relations: [], skip: 0, take: 10 }] - Additional configuration for the query. + * @return {Promise} A list of stock locations. + */ + async list( + selector: FilterableStockLocationProps = {}, + config: FindConfig = { relations: [], skip: 0, take: 10 } + ): Promise { + const manager = this.getManager() + const locationRepo = manager.getRepository(StockLocation) + + const query = buildQuery(selector, config) + return await locationRepo.find(query) + } + + /** + * Lists all stock locations that match the given selector and returns the count of matching stock locations. + * @param {FilterableStockLocationProps} [selector={}] - Properties to filter by. + * @param {FindConfig} [config={ relations: [], skip: 0, take: 10 }] - Additional configuration for the query. + * @return {Promise<[StockLocation[], number]>} A list of stock locations and the count of matching stock locations. + */ + async listAndCount( + selector: FilterableStockLocationProps = {}, + config: FindConfig = { relations: [], skip: 0, take: 10 } + ): Promise<[StockLocation[], number]> { + const manager = this.getManager() + const locationRepo = manager.getRepository(StockLocation) + + const query = buildQuery(selector, config) + return await locationRepo.findAndCount(query) + } + + /** + * Retrieves a stock location by its ID. + * @param {string} stockLocationId - The ID of the stock location. + * @param {FindConfig} [config={}] - Additional configuration for the query. + * @return {Promise} The stock location. + * @throws {MedusaError} If the stock location ID is not defined. + * @throws {MedusaError} If the stock location with the given ID was not found. + */ + async retrieve( + stockLocationId: string, + config: FindConfig = {} + ): Promise { + if (!isDefined(stockLocationId)) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `"stockLocationId" must be defined` + ) + } + + const manager = this.getManager() + const locationRepo = manager.getRepository(StockLocation) + + const query = buildQuery({ id: stockLocationId }, config) + const loc = await locationRepo.findOne(query) + + if (!loc) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `StockLocation with id ${stockLocationId} was not found` + ) + } + + return loc + } + + /** + * Creates a new stock location. + * @param {CreateStockLocationInput} data - The input data for creating a stock location. + * @returns {Promise} - The created stock location. + */ + async create(data: CreateStockLocationInput): Promise { + const defaultManager = this.getManager() + return await defaultManager.transaction(async (manager) => { + const locationRepo = manager.getRepository(StockLocation) + + const loc = locationRepo.create({ + name: data.name, + }) + + if (isDefined(data.address) || isDefined(data.address_id)) { + if (typeof data.address === "string" || data.address_id) { + const addrId = (data.address ?? data.address_id) as string + const address = await this.retrieve(addrId, { + select: ["id"], + }) + loc.address_id = address.id + } else { + const locAddressRepo = manager.getRepository(StockLocationAddress) + const locAddress = locAddressRepo.create(data.address!) + const addressResult = await locAddressRepo.save(locAddress) + loc.address_id = addressResult.id + } + } + + const { metadata } = data + if (metadata) { + loc.metadata = setMetadata(loc, metadata) + } + + const result = await locationRepo.save(loc) + + await this.eventBusService_.emit(StockLocationService.Events.CREATED, { + id: result.id, + }) + + return result + }) + } + + /** + * Updates an existing stock location. + * @param {string} stockLocationId - The ID of the stock location to update. + * @param {UpdateStockLocationInput} updateData - The update data for the stock location. + * @returns {Promise} - The updated stock location. + */ + + async update( + stockLocationId: string, + updateData: UpdateStockLocationInput + ): Promise { + const defaultManager = this.getManager() + return await defaultManager.transaction(async (manager) => { + const locationRepo = manager.getRepository(StockLocation) + + const item = await this.retrieve(stockLocationId) + + const { address, metadata, ...data } = updateData + + if (address) { + if (item.address_id) { + await this.updateAddress(item.address_id, address, { manager }) + } else { + const locAddressRepo = manager.getRepository(StockLocationAddress) + const locAddress = locAddressRepo.create(address) + const addressResult = await locAddressRepo.save(locAddress) + data.address_id = addressResult.id + } + } + + if (metadata) { + item.metadata = setMetadata(item, metadata) + } + + const toSave = locationRepo.merge(item, data) + await locationRepo.save(toSave) + + await this.eventBusService_.emit(StockLocationService.Events.UPDATED, { + id: stockLocationId, + }) + + return item + }) + } + + /** + * Updates an address for a stock location. + * @param {string} addressId - The ID of the address to update. + * @param {StockLocationAddressInput} address - The update data for the address. + * @param {Object} context - Context for the update. + * @param {EntityManager} context.manager - The entity manager to use for the update. + * @returns {Promise} - The updated stock location address. + */ + + protected async updateAddress( + addressId: string, + address: StockLocationAddressInput, + context: { manager?: EntityManager } = {} + ): Promise { + const manager = context.manager || this.getManager() + const locationAddressRepo = manager.getRepository(StockLocationAddress) + + const existingAddress = await locationAddressRepo.findOne(addressId) + if (!existingAddress) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `StockLocation address with id ${addressId} was not found` + ) + } + + const toSave = locationAddressRepo.merge(existingAddress, address) + + const { metadata } = address + if (metadata) { + toSave.metadata = setMetadata(toSave, metadata) + } + + return await locationAddressRepo.save(toSave) + } + + /** + * Deletes a stock location. + * @param {string} id - The ID of the stock location to delete. + * @returns {Promise} - An empty promise. + */ + async delete(id: string): Promise { + const manager = this.getManager() + const locationRepo = manager.getRepository(StockLocation) + + await locationRepo.softRemove({ id }) + + await this.eventBusService_.emit(StockLocationService.Events.DELETED, { + id, + }) + } +} diff --git a/packages/stock-location/tsconfig.json b/packages/stock-location/tsconfig.json new file mode 100644 index 0000000000..0fc6130e78 --- /dev/null +++ b/packages/stock-location/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "lib": [ + "es5", + "es6", + "es2019" + ], + "target": "es5", + "outDir": "./dist", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true // to use ES5 specific tooling + }, + "include": ["./src/**/*", "index.d.ts"], + "exclude": [ + "./dist/**/*", + "./src/**/__tests__", + "./src/**/__mocks__", + "node_modules" + ] +} \ No newline at end of file diff --git a/scripts/build-openapi.js b/scripts/build-openapi.js index c6bcff1d99..1ac078629a 100755 --- a/scripts/build-openapi.js +++ b/scripts/build-openapi.js @@ -3,111 +3,141 @@ const fs = require("fs") const OAS = require("oas-normalize") const swaggerInline = require("swagger-inline") -const { exec } = require("child_process"); +const { exec } = require("child_process") -const isDryRun = process.argv.indexOf('--dry-run') !== -1; +const isDryRun = process.argv.indexOf("--dry-run") !== -1 // Storefront API swaggerInline( - ["./packages/medusa/src/models", "./packages/medusa/src/api/middlewares" , "./packages/medusa/src/api/routes/store"], + [ + "./packages/medusa/src/models", + "./packages/medusa/src/types", + "./packages/medusa/src/api/middlewares", + "./packages/medusa/src/api/routes/store", + ], { base: "./docs/api/store-spec3-base.yaml", } -).then((gen) => { - const oas = new OAS(gen) - oas - .validate(true) - .then(() => { - if (!isDryRun) { - fs.writeFileSync("./docs/api/store-spec3.json", gen) - } - }) - .catch((err) => { - console.log("Error in store") - console.error(err) - process.exit(1) - }) -}) -.catch((err) => { - console.log("Error in store") - console.error(err) - process.exit(1) -}); +) + .then((gen) => { + const oas = new OAS(gen) + oas + .validate(true) + .then(() => { + if (!isDryRun) { + fs.writeFileSync("./docs/api/store-spec3.json", gen) + } + }) + .catch((err) => { + console.log("Error in store") + console.error(err) + process.exit(1) + }) + }) + .catch((err) => { + console.log("Error in store") + console.error(err) + process.exit(1) + }) swaggerInline( - ["./packages/medusa/src/models", "./packages/medusa/src/api/middlewares" , "./packages/medusa/src/api/routes/store"], + [ + "./packages/medusa/src/models", + "./packages/medusa/src/types", + "./packages/medusa/src/api/middlewares", + "./packages/medusa/src/api/routes/store", + ], { base: "./docs/api/store-spec3-base.yaml", format: "yaml", } -).then((gen) => { - if (!isDryRun) { - fs.writeFileSync("./docs/api/store-spec3.yaml", gen) - exec("rm -rf docs/api/store/ && yarn run -- redocly split docs/api/store-spec3.yaml --outDir=docs/api/store/", (error, stdout, stderr) => { - if (error) { - throw new Error(`error: ${error.message}`) - } - console.log(`${stderr || stdout}`); - }); - } else { - console.log('No errors occurred while generating Store API Reference'); - } -}) -.catch((err) => { - console.log("Error in store") - console.error(err) - process.exit(1) -}) +) + .then((gen) => { + if (!isDryRun) { + fs.writeFileSync("./docs/api/store-spec3.yaml", gen) + exec( + "rm -rf docs/api/store/ && yarn run -- redocly split docs/api/store-spec3.yaml --outDir=docs/api/store/", + (error, stdout, stderr) => { + if (error) { + throw new Error(`error: ${error.message}`) + } + console.log(`${stderr || stdout}`) + } + ) + } else { + console.log("No errors occurred while generating Store API Reference") + } + }) + .catch((err) => { + console.log("Error in store") + console.error(err) + process.exit(1) + }) // Admin API swaggerInline( - ["./packages/medusa/src/models", "./packages/medusa/src/api/middlewares" , "./packages/medusa/src/api/routes/admin"], + [ + "./packages/medusa/src/models", + "./packages/medusa/src/types", + "./packages/medusa/src/api/middlewares", + "./packages/medusa/src/api/routes/admin", + ], { base: "./docs/api/admin-spec3-base.yaml", } -).then((gen) => { - const oas = new OAS(gen) - oas - .validate(true) - .then(() => { - if (!isDryRun) { - fs.writeFileSync("./docs/api/admin-spec3.json", gen) - } - }) - .catch((err) => { - console.log("Error in admin") - console.error(err) - process.exit(1) - }) -}) -.catch((err) => { - console.log("Error in admin") - console.error(err) - process.exit(1) -}) +) + .then((gen) => { + const oas = new OAS(gen) + oas + .validate(true) + .then(() => { + if (!isDryRun) { + fs.writeFileSync("./docs/api/admin-spec3.json", gen) + } + }) + .catch((err) => { + console.log("Error in admin") + console.error(err) + process.exit(1) + }) + }) + .catch((err) => { + console.log("Error in admin") + console.error(err) + process.exit(1) + }) swaggerInline( - ["./packages/medusa/src/models", "./packages/medusa/src/api/middlewares" , "./packages/medusa/src/api/routes/admin"], + [ + "./packages/medusa/src/models", + "./packages/medusa/src/types", + "./packages/medusa/src/api/middlewares", + "./packages/medusa/src/api/routes/admin", + ], { base: "./docs/api/admin-spec3-base.yaml", format: "yaml", } -).then((gen) => { - if (!isDryRun) { - fs.writeFileSync("./docs/api/admin-spec3.yaml", gen) - exec("rm -rf docs/api/admin/ && yarn run -- redocly split docs/api/admin-spec3.yaml --outDir=docs/api/admin/", (error, stdout, stderr) => { - if (error) { - throw new Error(`error: ${error.message}`) - } - console.log(`${stderr || stdout}`); - return; - }); - } else { - console.log('No errors occurred while generating Admin API Reference'); - } -}) -.catch((err) => { - console.log("Error in admin") - console.error(err) - process.exit(1) -}) \ No newline at end of file +) + .then((gen) => { + if (!isDryRun) { + fs.writeFileSync("./docs/api/admin-spec3.yaml", gen) + exec( + "rm -rf docs/api/admin/ && yarn run -- redocly split docs/api/admin-spec3.yaml --outDir=docs/api/admin/", + (error, stdout, stderr) => { + if (error) { + throw new Error(`error: ${error.message}`) + } + console.log(`${stderr || stdout}`) + return + } + ) + } else { + console.log("No errors occurred while generating Admin API Reference") + } + }) + .catch((err) => { + console.log("Error in admin") + console.error(err) + process.exit(1) + }) diff --git a/yarn.lock b/yarn.lock index e7c74d45f0..1107514d3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4466,6 +4466,22 @@ __metadata: languageName: unknown linkType: soft +"@medusajs/stock-location@workspace:packages/stock-location": + version: 0.0.0-use.local + resolution: "@medusajs/stock-location@workspace:packages/stock-location" + dependencies: + "@medusajs/medusa": ^1.7.1 + cross-env: ^5.2.1 + jest: ^25.5.2 + ts-jest: ^25.5.1 + typescript: ^4.4.4 + peerDependencies: + "@medusajs/medusa": ^1.7.1 + medusa-interfaces: 1.3.3 + typeorm: ^0.2.31 + languageName: unknown + linkType: soft + "@microsoft/fetch-event-source@npm:2.0.1": version: 2.0.1 resolution: "@microsoft/fetch-event-source@npm:2.0.1"