diff --git a/.changeset/breezy-readers-shout.md b/.changeset/breezy-readers-shout.md new file mode 100644 index 0000000000..d22d66c39c --- /dev/null +++ b/.changeset/breezy-readers-shout.md @@ -0,0 +1,7 @@ +--- +"@medusajs/workflows": patch +"@medusajs/product": patch +"@medusajs/types": patch +--- + +fix(workflows, product, types): Fix issues relating to update-variant workflow and options diff --git a/.changeset/young-items-drop.md b/.changeset/young-items-drop.md index 2e489cfb49..9e522b10a0 100644 --- a/.changeset/young-items-drop.md +++ b/.changeset/young-items-drop.md @@ -1,9 +1,10 @@ --- "@medusajs/workflows": patch "@medusajs/product": patch +"@medusajs/pricing": patch "@medusajs/medusa": patch "@medusajs/types": patch "@medusajs/utils": patch --- -feat(medusa,types,workflows,utils,product): PricingModule Integration of PriceLists into Core +feat(medusa,types,workflows,utils,product,pricing): PricingModule Integration of PriceLists into Core diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 5abe7f362b..ba8afeb437 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -125,7 +125,6 @@ jobs: env: DB_PASSWORD: postgres DB_USERNAME: postgres - SPLIT: ${{ steps['split-tests'].outputs['split'] }} integration-tests-api: needs: setup @@ -186,7 +185,8 @@ jobs: run: yarn test:integration:api env: DB_PASSWORD: postgres - SPLIT: ${{ steps['split-tests'].outputs['split'] }} + DB_USERNAME: postgres + integration-tests-plugins: needs: setup @@ -237,6 +237,7 @@ jobs: - name: Run plugin integration tests run: yarn test:integration:plugins env: + DB_USERNAME: postgres DB_PASSWORD: postgres NODE_OPTIONS: "--max_old_space_size=4096" @@ -286,4 +287,5 @@ jobs: - name: Run repository integration tests run: yarn test:integration:repositories env: + DB_USERNAME: postgres DB_PASSWORD: postgres diff --git a/integration-tests/plugins/__tests__/product/admin/update-product-variant.spec.ts b/integration-tests/plugins/__tests__/product/admin/update-product-variant.spec.ts index a7cb0ebaac..2f76bdd06c 100644 --- a/integration-tests/plugins/__tests__/product/admin/update-product-variant.spec.ts +++ b/integration-tests/plugins/__tests__/product/admin/update-product-variant.spec.ts @@ -1,5 +1,3 @@ -import { useApi } from "../../../../environment-helpers/use-api" -import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import { simpleProductFactory, @@ -7,11 +5,13 @@ import { } from "../../../../factories" import { AxiosInstance } from "axios" -import path from "path" -import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" import adminSeeder from "../../../../helpers/admin-seeder" import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types" import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" +import { getContainer } from "../../../../environment-helpers/use-container" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" jest.setTimeout(50000) @@ -62,6 +62,9 @@ describe("POST /admin/products/:id/variants/:id", () => { { options: [{ option_id: "test-product-option-1", value: "test" }], }, + { + options: [{ option_id: "test-product-option-1", value: "test 2" }], + }, ], options: [ { @@ -250,4 +253,231 @@ describe("POST /admin/products/:id/variants/:id", () => { }) ) }) + + it("should update variant option value", async () => { + const api = useApi()! as AxiosInstance + + const data = { + options: [ + { + option_id: "test-product-option-1", + value: "updated", + }, + ], + } + + await api.post( + `/admin/products/${product.id}/variants/${variant.id}`, + data, + adminHeaders + ) + + const response = await api.get( + `/admin/products/${product.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: expect.arrayContaining([ + expect.objectContaining({ + id: variant.id, + options: [ + expect.objectContaining({ + option_id: "test-product-option-1", + value: "updated", + }), + ], + }), + expect.objectContaining({ + id: product.variants[1].id, + options: [ + expect.objectContaining({ + option_id: "test-product-option-1", + value: "test 2", + }), + ], + }), + ]), + }) + ) + }) + + it("should update variant metadata", async () => { + const api = useApi()! as AxiosInstance + + const data = { + metadata: { + test: "string", + }, + } + + await api.post( + `/admin/products/${product.id}/variants/${variant.id}`, + data, + adminHeaders + ) + + const response = await api.get( + `/admin/products/${product.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: expect.arrayContaining([ + expect.objectContaining({ + id: variant.id, + metadata: { + test: "string", + }, + }), + ]), + }) + ) + }) + + it("should remove options not present in update", async () => { + const api = useApi()! as AxiosInstance + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-multiple-options", + variants: [ + { + options: [ + { option_id: "test-product-multi-option-1", value: "test" }, + { option_id: "test-product-multi-option-2", value: "test value" }, + ], + }, + ], + options: [ + { + id: "test-product-multi-option-1", + title: "Test option 1", + }, + { + id: "test-product-multi-option-2", + title: "Test option 2", + }, + ], + }) + + variant = product.variants[0] + + const data = { + options: [ + { + option_id: "test-product-multi-option-1", + value: "updated", + }, + ], + } + + await api.post( + `/admin/products/${product.id}/variants/${variant.id}`, + data, + adminHeaders + ) + + const response = await api.get( + `/admin/products/${product.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: [ + expect.objectContaining({ + id: variant.id, + options: [ + expect.objectContaining({ + option_id: "test-product-multi-option-1", + value: "updated", + }), + ], + }), + ], + }) + ) + }) + + it("should update several options in the same api call", async () => { + const api = useApi()! as AxiosInstance + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-multiple-options", + variants: [ + { + options: [ + { option_id: "test-product-multi-option-1", value: "test" }, + { option_id: "test-product-multi-option-2", value: "test value" }, + ], + }, + ], + options: [ + { + id: "test-product-multi-option-1", + title: "Test option 1", + }, + { + id: "test-product-multi-option-2", + title: "Test option 2", + }, + ], + }) + + variant = product.variants[0] + + const data = { + options: [ + { + option_id: "test-product-multi-option-1", + value: "updated", + }, + { + option_id: "test-product-multi-option-2", + value: "updated 2", + }, + ], + } + + await api.post( + `/admin/products/${product.id}/variants/${variant.id}`, + data, + adminHeaders + ) + + const response = await api.get( + `/admin/products/${product.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: [ + expect.objectContaining({ + id: variant.id, + options: [ + expect.objectContaining({ + option_id: "test-product-multi-option-1", + value: "updated", + }), + expect.objectContaining({ + option_id: "test-product-multi-option-2", + value: "updated 2", + }), + ], + }), + ], + }) + ) + }) }) diff --git a/packages/product/src/loaders/container.ts b/packages/product/src/loaders/container.ts index 085f8e21fa..7375d3a9ef 100644 --- a/packages/product/src/loaders/container.ts +++ b/packages/product/src/loaders/container.ts @@ -5,6 +5,7 @@ import { ProductCollectionRepository, ProductImageRepository, ProductOptionRepository, + ProductOptionValueRepository, ProductRepository, ProductTagRepository, ProductTypeRepository, @@ -17,6 +18,7 @@ import { ProductImageService, ProductModuleService, ProductOptionService, + ProductOptionValueService, ProductService, ProductTagService, ProductTypeService, @@ -48,6 +50,7 @@ export default async ({ productImageService: asClass(ProductImageService).singleton(), productTypeService: asClass(ProductTypeService).singleton(), productOptionService: asClass(ProductOptionService).singleton(), + productOptionValueService: asClass(ProductOptionValueService).singleton(), }) if (customRepositories) { @@ -69,6 +72,9 @@ function loadDefaultRepositories({ container }) { productTagRepository: asClass(ProductTagRepository).singleton(), productTypeRepository: asClass(ProductTypeRepository).singleton(), productOptionRepository: asClass(ProductOptionRepository).singleton(), + productOptionValueRepository: asClass( + ProductOptionValueRepository + ).singleton(), productVariantRepository: asClass(ProductVariantRepository).singleton(), }) } diff --git a/packages/product/src/models/index.ts b/packages/product/src/models/index.ts index 6017c491d7..f3f6a304fa 100644 --- a/packages/product/src/models/index.ts +++ b/packages/product/src/models/index.ts @@ -5,4 +5,5 @@ export { default as ProductTag } from "./product-tag" export { default as ProductType } from "./product-type" export { default as ProductVariant } from "./product-variant" export { default as ProductOption } from "./product-option" +export { default as ProductOptionValue } from "./product-option-value" export { default as Image } from "./product-image" diff --git a/packages/product/src/repositories/index.ts b/packages/product/src/repositories/index.ts index 77e7f8a7a1..b1f0ab1e37 100644 --- a/packages/product/src/repositories/index.ts +++ b/packages/product/src/repositories/index.ts @@ -7,3 +7,4 @@ export { ProductCategoryRepository } from "./product-category" export { ProductImageRepository } from "./product-image" export { ProductTypeRepository } from "./product-type" export { ProductOptionRepository } from "./product-option" +export { ProductOptionValueRepository } from "./product-option-value" diff --git a/packages/product/src/repositories/product-option-value.ts b/packages/product/src/repositories/product-option-value.ts new file mode 100644 index 0000000000..f2f3e10bd8 --- /dev/null +++ b/packages/product/src/repositories/product-option-value.ts @@ -0,0 +1,114 @@ +import { Context, DAL } from "@medusajs/types" +import { + CreateProductOptionValueDTO, + UpdateProductOptionValueDTO, +} from "../types/services/product-option-value" + +import { DALUtils } from "@medusajs/utils" +import { FilterQuery as MikroFilterQuery } from "@mikro-orm/core/typings" +import { FindOptions as MikroOptions } from "@mikro-orm/core/drivers/IDatabaseDriver" +import { ProductOptionValue } from "@models" +import { SqlEntityManager } from "@mikro-orm/postgresql" + +export class ProductOptionValueRepository extends DALUtils.MikroOrmBaseRepository { + protected readonly manager_: SqlEntityManager + + constructor({ manager }: { manager: SqlEntityManager }) { + // @ts-ignore + // eslint-disable-next-line prefer-rest-params + super(...arguments) + this.manager_ = manager + } + + async find( + findOptions: DAL.FindOptions = { where: {} }, + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + const findOptions_ = { ...findOptions } + + findOptions_.options ??= {} + + return await manager.find( + ProductOptionValue, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + async upsert( + optionValues: (UpdateProductOptionValueDTO | CreateProductOptionValueDTO)[], + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + + const optionValueIds: string[] = [] + + for (const optionValue of optionValues) { + if (optionValue.id) { + optionValueIds.push(optionValue.id) + } + } + + const existingOptionValues = await this.find( + { + where: { + id: { + $in: optionValueIds, + }, + }, + }, + context + ) + + const existingOptionValuesMap = new Map( + existingOptionValues.map<[string, ProductOptionValue]>((optionValue) => [ + optionValue.id, + optionValue, + ]) + ) + + const upsertedOptionValues: ProductOptionValue[] = [] + const optionValuesToCreate: ProductOptionValue[] = [] + const optionValuesToUpdate: ProductOptionValue[] = [] + + optionValues.forEach(({ option_id, ...optionValue }) => { + const existingOptionValue = optionValue.id + ? existingOptionValuesMap.get(optionValue.id) + : undefined + + if (optionValue.id && existingOptionValue) { + const updatedOptionValue = manager.assign(existingOptionValue, { + option: option_id, + ...optionValue, + }) + optionValuesToUpdate.push(updatedOptionValue) + return + } + + const newOptionValue = manager.create(ProductOptionValue, { + option: option_id, + variant: (optionValue as CreateProductOptionValueDTO).variant_id, + ...optionValue, + }) + optionValuesToCreate.push(newOptionValue) + }) + + if (optionValuesToCreate.length) { + manager.persist(optionValuesToCreate) + upsertedOptionValues.push(...optionValuesToCreate) + } + + if (optionValuesToUpdate.length) { + manager.persist(optionValuesToUpdate) + upsertedOptionValues.push(...optionValuesToUpdate) + } + + return upsertedOptionValues + } + + async delete(ids: string[], context: Context = {}): Promise { + const manager = this.getActiveManager(context) + await manager.nativeDelete(ProductOptionValue, { id: { $in: ids } }, {}) + } +} diff --git a/packages/product/src/services/index.ts b/packages/product/src/services/index.ts index 0d7ba770b1..dc485567aa 100644 --- a/packages/product/src/services/index.ts +++ b/packages/product/src/services/index.ts @@ -7,3 +7,4 @@ export { default as ProductVariantService } from "./product-variant" export { default as ProductTypeService } from "./product-type" export { default as ProductOptionService } from "./product-option" export { default as ProductImageService } from "./product-image" +export { default as ProductOptionValueService } from "./product-option-value" diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts index dbf5f0da9d..a74109e1ed 100644 --- a/packages/product/src/services/product-module-service.ts +++ b/packages/product/src/services/product-module-service.ts @@ -16,6 +16,7 @@ import { ProductCategory, ProductCollection, ProductOption, + ProductOptionValue, ProductTag, ProductType, ProductVariant, @@ -24,6 +25,7 @@ import { ProductCategoryService, ProductCollectionService, ProductOptionService, + ProductOptionValueService, ProductService, ProductTagService, ProductTypeService, @@ -53,6 +55,8 @@ import { } from "../types/services/product" import { + arrayDifference, + groupBy, InjectManager, InjectTransactionManager, isDefined, @@ -68,6 +72,10 @@ import { joinerConfig, LinkableKeys, } from "./../joiner-config" +import { + CreateProductOptionValueDTO, + UpdateProductOptionValueDTO, +} from "../types/services/product-option-value" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -79,6 +87,7 @@ type InjectedDependencies = { productImageService: ProductImageService productTypeService: ProductTypeService productOptionService: ProductOptionService + productOptionValueService: ProductOptionValueService eventBusModuleService?: IEventBusModuleService } @@ -90,7 +99,8 @@ export default class ProductModuleService< TProductCategory extends ProductCategory = ProductCategory, TProductImage extends Image = Image, TProductType extends ProductType = ProductType, - TProductOption extends ProductOption = ProductOption + TProductOption extends ProductOption = ProductOption, + TProductOptionValue extends ProductOptionValue = ProductOptionValue > implements ProductTypes.IProductModuleService { protected baseRepository_: DAL.RepositoryService @@ -108,6 +118,8 @@ export default class ProductModuleService< protected readonly productImageService_: ProductImageService protected readonly productTypeService_: ProductTypeService protected readonly productOptionService_: ProductOptionService + // eslint-disable-next-line max-len + protected readonly productOptionValueService_: ProductOptionValueService protected readonly eventBusModuleService_?: IEventBusModuleService constructor( @@ -121,6 +133,7 @@ export default class ProductModuleService< productImageService, productTypeService, productOptionService, + productOptionValueService, eventBusModuleService, }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration @@ -134,6 +147,7 @@ export default class ProductModuleService< this.productImageService_ = productImageService this.productTypeService_ = productTypeService this.productOptionService_ = productOptionService + this.productOptionValueService_ = productOptionValueService this.eventBusModuleService_ = eventBusModuleService } @@ -307,6 +321,131 @@ export default class ProductModuleService< ) } + @InjectManager("baseRepository_") + async updateVariants( + data: ProductTypes.UpdateProductVariantOnlyDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const productVariants = await this.updateVariants_(data, sharedContext) + + const updatedVariants = await this.baseRepository_.serialize< + ProductTypes.ProductVariantDTO[] + >(productVariants, { + populate: true, + }) + + return updatedVariants + } + + @InjectTransactionManager("baseRepository_") + protected async updateVariants_( + data: ProductTypes.UpdateProductVariantOnlyDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const variantIdsToUpdate = data.map(({ id }) => id) + const variants = await this.listVariants( + { id: variantIdsToUpdate }, + { relations: ["options", "options.option"] }, + sharedContext + ) + const variantsMap = new Map( + variants.map((variant) => [variant.id, variant]) + ) + + if (variants.length !== data.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Cannot update non-existing variants with ids: ${arrayDifference( + variantIdsToUpdate, + [...variantsMap.keys()] + ).join(", ")}` + ) + } + + const optionValuesToUpsert: ( + | CreateProductOptionValueDTO + | UpdateProductOptionValueDTO + )[] = [] + const optionsValuesToDelete: string[] = [] + + const toUpdate = data.map(({ id, options, ...rest }) => { + const variant = variantsMap.get(id)! + + const toUpdate: UpdateProductVariantDTO = { + id, + product_id: variant.product_id, + } + + if (options?.length) { + const optionIdToUpdateValueMap = new Map( + options.map(({ option, option_id, value }) => { + const computedOptionId = option_id ?? option.id ?? option + return [computedOptionId, value] + }) + ) + + for (const existingOptionValue of variant.options) { + if (!optionIdToUpdateValueMap.has(existingOptionValue.option.id)) { + optionsValuesToDelete.push(existingOptionValue.id) + + continue + } + + optionValuesToUpsert.push({ + id: existingOptionValue.id, + option_id: existingOptionValue.option.id, + value: optionIdToUpdateValueMap.get(existingOptionValue.option.id)!, + }) + optionIdToUpdateValueMap.delete(existingOptionValue.option.id) + } + + for (const [option_id, value] of optionIdToUpdateValueMap.entries()) { + optionValuesToUpsert.push({ + option_id, + value, + variant_id: id, + }) + } + } + + for (const [key, value] of Object.entries(rest)) { + if (variant[key] !== value) { + toUpdate[key] = value + } + } + + return toUpdate + }) + + const groups = groupBy(toUpdate, "product_id") + + const [, , productVariants]: [ + void, + TProductOptionValue[], + TProductVariant[][] + ] = await promiseAll([ + await this.productOptionValueService_.delete( + optionsValuesToDelete, + sharedContext + ), + await this.productOptionValueService_.upsert( + optionValuesToUpsert, + sharedContext + ), + await promiseAll( + [...groups.entries()].map(async ([product_id, update]) => { + return await this.productVariantService_.update( + product_id, + update.map(({ product_id, ...update }) => update), + sharedContext + ) + }) + ), + ]) + + return productVariants.flat() + } + @InjectManager("baseRepository_") async retrieveTag( tagId: string, @@ -1098,7 +1237,11 @@ export default class ProductModuleService< if (!productData.thumbnail && productData.images?.length) { productData.thumbnail = isString(productData.images[0]) ? (productData.images[0] as string) - : (productData.images[0] as { url: string }).url + : ( + productData.images[0] as { + url: string + } + ).url } if (productData.images?.length) { diff --git a/packages/product/src/services/product-option-value.ts b/packages/product/src/services/product-option-value.ts new file mode 100644 index 0000000000..f72a9fb533 --- /dev/null +++ b/packages/product/src/services/product-option-value.ts @@ -0,0 +1,44 @@ +import { ProductOptionValue } from "@models" +import { Context, DAL } from "@medusajs/types" +import { + ProductOptionRepository, + ProductOptionValueRepository, +} from "@repositories" +import { InjectTransactionManager, MedusaContext } from "@medusajs/utils" +import { + CreateProductOptionValueDTO, + UpdateProductOptionValueDTO, +} from "../types/services/product-option-value" + +type InjectedDependencies = { + productOptionValueRepository: DAL.RepositoryService +} + +export default class ProductOptionValueService< + TEntity extends ProductOptionValue = ProductOptionValue +> { + protected readonly productOptionValueRepository_: DAL.RepositoryService + + constructor({ productOptionValueRepository }: InjectedDependencies) { + this.productOptionValueRepository_ = + productOptionValueRepository as ProductOptionRepository + } + + @InjectTransactionManager("productOptionValueRepository_") + async delete( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return await this.productOptionValueRepository_.delete(ids, sharedContext) + } + + @InjectTransactionManager("productOptionValueRepository_") + async upsert( + data: (UpdateProductOptionValueDTO | CreateProductOptionValueDTO)[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await ( + this.productOptionValueRepository_ as ProductOptionValueRepository + ).upsert!(data, sharedContext)) as TEntity[] + } +} diff --git a/packages/product/src/types/services/product-option-value.ts b/packages/product/src/types/services/product-option-value.ts new file mode 100644 index 0000000000..5d375dabc4 --- /dev/null +++ b/packages/product/src/types/services/product-option-value.ts @@ -0,0 +1,14 @@ +export interface UpdateProductOptionValueDTO { + id: string + value: string + option_id: string + metadata?: Record | null +} + +export interface CreateProductOptionValueDTO { + id?: string + value: string + option_id: string + variant_id: string + metadata?: Record | null +} diff --git a/packages/product/src/types/services/product-variant.ts b/packages/product/src/types/services/product-variant.ts index 1c7a90f95f..0f4dedd361 100644 --- a/packages/product/src/types/services/product-variant.ts +++ b/packages/product/src/types/services/product-variant.ts @@ -2,6 +2,7 @@ import { CreateProductVariantOptionDTO } from "@medusajs/types" export interface UpdateProductVariantDTO { id: string + product_id: string title?: string sku?: string barcode?: string @@ -18,6 +19,6 @@ export interface UpdateProductVariantDTO { length?: number height?: number width?: number - options?: CreateProductVariantOptionDTO[] + options?: (CreateProductVariantOptionDTO & { id?: string })[] metadata?: Record } diff --git a/packages/types/src/product/common.ts b/packages/types/src/product/common.ts index 85d100ff9c..7f44de05f9 100644 --- a/packages/types/src/product/common.ts +++ b/packages/types/src/product/common.ts @@ -83,43 +83,43 @@ export interface ProductDTO { material?: string | null /** * The associated product collection. - * + * * @expandable */ collection: ProductCollectionDTO /** * The associated product categories. - * + * * @expandable */ categories?: ProductCategoryDTO[] | null /** * The associated product type. - * + * * @expandable */ type: ProductTypeDTO[] /** * The associated product tags. - * + * * @expandable */ tags: ProductTagDTO[] /** * The associated product variants. - * + * * @expandable */ variants: ProductVariantDTO[] /** * The associated product options. - * + * * @expandable */ options: ProductOptionDTO[] /** * The associated product images. - * + * * @expandable */ images: ProductImageDTO[] @@ -226,17 +226,17 @@ export interface ProductVariantDTO { width?: number | null /** * The associated product options. - * + * * @expandable */ - options: ProductOptionValueDTO + options: ProductOptionValueDTO[] /** * Holds custom data in key-value pairs. */ metadata?: Record | null /** * The associated product. - * + * * @expandable */ product: ProductDTO @@ -298,13 +298,13 @@ export interface ProductCategoryDTO { rank?: number /** * The associated parent category. - * + * * @expandable */ parent_category?: ProductCategoryDTO /** * The associated child categories. - * + * * @expandable */ category_children: ProductCategoryDTO[] @@ -410,7 +410,7 @@ export interface ProductTagDTO { metadata?: Record | null /** * The associated products. - * + * * @expandable */ products?: ProductDTO[] @@ -444,7 +444,7 @@ export interface ProductCollectionDTO { deleted_at?: string | Date /** * The associated products. - * + * * @expandable */ products?: ProductDTO[] @@ -478,7 +478,7 @@ export interface ProductTypeDTO { * @interface * * A product option's data. - * + * */ export interface ProductOptionDTO { /** @@ -491,13 +491,13 @@ export interface ProductOptionDTO { title: string /** * The associated product. - * + * * @expandable */ product: ProductDTO /** * The associated product option values. - * + * * @expandable */ values: ProductOptionValueDTO[] @@ -563,13 +563,13 @@ export interface ProductOptionValueDTO { value: string /** * The associated product option. - * + * * @expandable */ option: ProductOptionDTO /** * The associated product variant. - * + * * @expandable */ variant: ProductVariantDTO @@ -612,7 +612,7 @@ export interface FilterableProductProps /** * Filters on a product's tags. */ - tags?: { + tags?: { /** * Values to filter product tags by. */ @@ -761,7 +761,7 @@ export interface FilterableProductVariantProps /** * Filter product variants by their associated options. */ - options?: { + options?: { /** * IDs to filter options by. */ @@ -986,6 +986,7 @@ export interface CreateProductVariantOptionDTO { * The value of a product variant option. */ value: string + option_id?: string } diff --git a/packages/types/src/product/service.ts b/packages/types/src/product/service.ts index 138b33e6b0..00b3320fe1 100644 --- a/packages/types/src/product/service.ts +++ b/packages/types/src/product/service.ts @@ -26,12 +26,13 @@ import { UpdateProductOptionDTO, UpdateProductTagDTO, UpdateProductTypeDTO, + UpdateProductVariantDTO, } from "./common" - -import { FindConfig } from "../common" import { RestoreReturn, SoftDeleteReturn } from "../dal" -import { ModuleJoinerConfig } from "../modules-sdk" + import { Context } from "../shared-context" +import { FindConfig } from "../common" +import { ModuleJoinerConfig } from "../modules-sdk" export interface IProductModuleService { /** @@ -1490,6 +1491,11 @@ export interface IProductModuleService { sharedContext?: Context ): Promise + updateVariants( + data: UpdateProductVariantDTO[], + sharedContext?: Context + ): Promise + createVariants( data: CreateProductVariantDTO[], sharedContext?: Context diff --git a/packages/workflows/src/handlers/product/update-product-variants.ts b/packages/workflows/src/handlers/product/update-product-variants.ts index 3f057c91f1..e91c6775a4 100644 --- a/packages/workflows/src/handlers/product/update-product-variants.ts +++ b/packages/workflows/src/handlers/product/update-product-variants.ts @@ -1,5 +1,5 @@ import { Modules, ModulesDefinition } from "@medusajs/modules-sdk" -import { ProductTypes } from "@medusajs/types" +import { ProductTypes, UpdateProductVariantOnlyDTO } from "@medusajs/types" import { WorkflowArguments } from "../../helper" type HandlerInput = { @@ -14,21 +14,22 @@ export async function updateProductVariants({ > { const { productVariantsMap } = data const productsVariants: ProductTypes.UpdateProductVariantDTO[] = [] - const updateProductsData: ProductTypes.UpdateProductDTO[] = [] + const updateVariantsData: ProductTypes.UpdateProductVariantOnlyDTO[] = [] const productModuleService: ProductTypes.IProductModuleService = container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName) - for (const [productId, variantsData = []] of productVariantsMap) { - updateProductsData.push({ - id: productId, - variants: variantsData, - }) + for (const [product_id, variantsUpdateData = []] of productVariantsMap) { + updateVariantsData.push( + ...(variantsUpdateData as unknown as UpdateProductVariantOnlyDTO[]).map( + (update) => ({ ...update, product_id }) + ) + ) - productsVariants.push(...variantsData) + productsVariants.push(...variantsUpdateData) } - if (updateProductsData.length) { - await productModuleService.update(updateProductsData) + if (updateVariantsData.length) { + await productModuleService.updateVariants(updateVariantsData) } return productsVariants