diff --git a/.changeset/small-eggs-search.md b/.changeset/small-eggs-search.md new file mode 100644 index 0000000000000..629183957f13a --- /dev/null +++ b/.changeset/small-eggs-search.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): variant creation with prices in productservice.create diff --git a/integration-tests/plugins/__tests__/services/product.ts b/integration-tests/plugins/__tests__/services/product.ts new file mode 100644 index 0000000000000..ad3b8eeedf4a4 --- /dev/null +++ b/integration-tests/plugins/__tests__/services/product.ts @@ -0,0 +1,147 @@ +import path from "path" +import { initDb, useDb } from "../../../environment-helpers/use-db" +import { bootstrapApp } from "../../../environment-helpers/bootstrap-app" +import { setPort } from "../../../environment-helpers/use-api" + +jest.setTimeout(30000) + +describe("product", () => { + let dbConnection + let medusaContainer + let productService + + let express + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")) + dbConnection = await initDb({ cwd } as any) + const { container, port, app } = await bootstrapApp({ cwd }) + + setPort(port) + express = app.listen(port, () => { + process.send!(port) + }) + + medusaContainer = container + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + + express.close() + }) + + afterEach(async () => { + jest.clearAllMocks() + const db = useDb() + await db.teardown() + }) + + describe("product service", () => { + it("should create variant prices correctly in service creation", async () => { + productService = medusaContainer.resolve("productService") + + const payload = { + title: "test-product", + handle: "test-product", + options: [{ title: "test-option" }], + variants: [ + { + title: "test-variant", + inventory_quantity: 10, + sku: "test", + options: [{ value: "large", title: "test-option" }], + prices: [{ amount: "100", currency_code: "usd" }], + }, + ], + } + + const { id } = await productService.create(payload) + + const result = await productService.retrieve(id, { + relations: ["variants", "variants.prices", "variants.options"], + }) + + expect(result).toEqual( + expect.objectContaining({ + variants: [ + expect.objectContaining({ + options: [expect.objectContaining({ value: "large" })], + prices: [ + expect.objectContaining({ amount: 100, currency_code: "usd" }), + ], + }), + ], + }) + ) + }) + + it("should fail to create a variant without options on for a product with options", async () => { + const payload = { + title: "test-product", + handle: "test-product", + options: [{ title: "test-option" }], + variants: [ + { + title: "test-variant", + inventory_quantity: 10, + sku: "test", + prices: [{ amount: "100", currency_code: "usd" }], + }, + ], + } + + let error + + try { + await productService.create(payload) + } catch (err) { + error = err + } + + expect(error.message).toEqual( + "Product options length does not match variant options length. Product has 1 and variant has 0." + ) + }) + + it("should create a product and variant without options", async () => { + const payload = { + title: "test-product", + handle: "test-product", + variants: [ + { + title: "test-variant", + inventory_quantity: 10, + sku: "test", + prices: [{ amount: "100", currency_code: "usd" }], + }, + ], + } + + const { id } = await productService.create(payload) + + const result = await productService.retrieve(id, { + relations: [ + "options", + "variants", + "variants.prices", + "variants.options", + ], + }) + + expect(result).toEqual( + expect.objectContaining({ + options: [], + variants: [ + expect.objectContaining({ + prices: [ + expect.objectContaining({ amount: 100, currency_code: "usd" }), + ], + }), + ], + }) + ) + }) + }) +}) diff --git a/packages/medusa/src/services/__tests__/product.js b/packages/medusa/src/services/__tests__/product.js index 61cbbc0b4d4cd..89683f2757808 100644 --- a/packages/medusa/src/services/__tests__/product.js +++ b/packages/medusa/src/services/__tests__/product.js @@ -78,7 +78,7 @@ describe("ProductService", () => { title: "Suit", options: [], collection: { id: IdMap.getId("cat"), title: "Suits" }, - variants: product.variants, + variants: product.variants ?? [], }), findOneWithRelations: () => ({ id: IdMap.getId("ironman"), @@ -117,6 +117,13 @@ describe("ProductService", () => { Promise.resolve({ id: IdMap.getId("cat"), title: "Suits" }), } + const productVariantService = { + withTransaction: function () { + return this + }, + create: (id, data) => Promise.resolve(data), + } + const productService = new ProductService({ manager: MockManager, productRepository, @@ -124,6 +131,7 @@ describe("ProductService", () => { productCollectionService, productTagRepository, productTypeRepository, + productVariantService, featureFlagRouter: new FlagRouter({}), }) @@ -131,7 +139,7 @@ describe("ProductService", () => { jest.clearAllMocks() }) - it("successfully create a product", async () => { + it("should successfully create a product", async () => { await productService.create({ title: "Suit", options: [], @@ -158,16 +166,6 @@ describe("ProductService", () => { expect(productRepository.create).toHaveBeenCalledTimes(1) expect(productRepository.create).toHaveBeenCalledWith({ title: "Suit", - variants: [ - { - id: "test1", - title: "green", - }, - { - id: "test2", - title: "blue", - }, - ], }) expect(productTagRepository.upsertTags).toHaveBeenCalledTimes(1) @@ -197,10 +195,12 @@ describe("ProductService", () => { variants: [ { id: "test1", + options: [], title: "green", }, { id: "test2", + options: [], title: "blue", }, ], diff --git a/packages/medusa/src/services/product.ts b/packages/medusa/src/services/product.ts index 5c544d0a3b539..9ea7eb77dfcee 100644 --- a/packages/medusa/src/services/product.ts +++ b/packages/medusa/src/services/product.ts @@ -1,6 +1,8 @@ import { - buildRelations, - buildSelects, FlagRouter, objectToStringPath + buildRelations, + buildSelects, + FlagRouter, + objectToStringPath, } from "@medusajs/utils" import { isDefined, MedusaError } from "medusa-core-utils" import { EntityManager, In } from "typeorm" @@ -8,19 +10,19 @@ import { ProductVariantService, SearchService } from "." import { TransactionBaseService } from "../interfaces" import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" import { - Product, - ProductCategory, - ProductOption, - ProductTag, - ProductType, - ProductVariant, - SalesChannel, - ShippingProfile, + Product, + ProductCategory, + ProductOption, + ProductTag, + ProductType, + ProductVariant, + SalesChannel, + ShippingProfile, } from "../models" import { ImageRepository } from "../repositories/image" import { - FindWithoutRelationsOptions, - ProductRepository, + FindWithoutRelationsOptions, + ProductRepository, } from "../repositories/product" import { ProductCategoryRepository } from "../repositories/product-category" import { ProductOptionRepository } from "../repositories/product-option" @@ -29,15 +31,16 @@ import { ProductTypeRepository } from "../repositories/product-type" import { ProductVariantRepository } from "../repositories/product-variant" import { Selector } from "../types/common" import { - CreateProductInput, - FilterableProductProps, - FindProductConfig, - ProductOptionInput, - ProductSelector, - UpdateProductInput, + CreateProductInput, + FilterableProductProps, + FindProductConfig, + ProductOptionInput, + ProductSelector, + UpdateProductInput, } from "../types/product" import { buildQuery, isString, setMetadata } from "../utils" import EventBusService from "./event-bus" +import { CreateProductVariantInput } from "../types/product-variant" type InjectedDependencies = { manager: EntityManager @@ -429,6 +432,7 @@ class ProductService extends TransactionBaseService { tags, type, images, + variants, sales_channels: salesChannels, categories: categories, ...rest @@ -501,6 +505,27 @@ class ProductService extends TransactionBaseService { }) ) + if (variants) { + const toCreate = variants.map((variant) => { + return { + ...variant, + options: + variant.options?.map((option, index) => { + return { + option_id: product.options[index].id, + ...option, + } + }) ?? [], + } + }) + product.variants = await this.productVariantService_ + .withTransaction(manager) + .create( + product.id, + toCreate as unknown as CreateProductVariantInput[] + ) + } + const result = await this.retrieve(product.id, { relations: ["options"], })