diff --git a/packages/medusa/src/api/routes/admin/index.js b/packages/medusa/src/api/routes/admin/index.js index b5cc164c42..814421aa8a 100644 --- a/packages/medusa/src/api/routes/admin/index.js +++ b/packages/medusa/src/api/routes/admin/index.js @@ -3,7 +3,6 @@ import middlewares from "../../middlewares" import authRoutes from "./auth" import productRoutes from "./products" import userRoutes from "./users" -import productVariantRoutes from "./product-variants" import regionRoutes from "./regions" import shippingOptionRoutes from "./shipping-options" import shippingProfileRoutes from "./shipping-profiles" @@ -36,7 +35,6 @@ export default (app, container) => { shippingProfileRoutes(route) discountRoutes(route) orderRoutes(route) - productVariantRoutes(route) return app } diff --git a/packages/medusa/src/api/routes/admin/product-variants/__tests__/add-option-value.js b/packages/medusa/src/api/routes/admin/product-variants/__tests__/add-option-value.js deleted file mode 100644 index 24039c598e..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/__tests__/add-option-value.js +++ /dev/null @@ -1,40 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" - -describe("POST /admin/product-variants/:id/options", () => { - describe("successful add option value", () => { - let subject - - beforeAll(async () => { - subject = await request( - "POST", - `/admin/product-variants/${IdMap.getId("testVariant")}/options`, - { - payload: { - option_id: IdMap.getId("testOption"), - value: "test", - }, - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("calls service addOption", () => { - expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledTimes(1) - expect(ProductVariantServiceMock.addOptionValue).toHaveBeenCalledWith( - IdMap.getId("testVariant"), - IdMap.getId("testOption"), - "test" - ) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/product-variants/__tests__/create-product-variant.js b/packages/medusa/src/api/routes/admin/product-variants/__tests__/create-product-variant.js deleted file mode 100644 index 0c685508cd..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/__tests__/create-product-variant.js +++ /dev/null @@ -1,75 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" - -describe("POST /admin/product-variants", () => { - describe("successful creation", () => { - let subject - - beforeAll(async () => { - subject = await request("POST", "/admin/product-variants", { - payload: { - title: "Test Product Variant", - prices: [ - { - currency_code: "DKK", - amount: 1234, - }, - ], - }, - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - }) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("returns created product draft", () => { - expect(subject.body._id).toEqual(IdMap.getId("testVariant")) - }) - - it("calls service createDraft", () => { - expect(ProductVariantServiceMock.createDraft).toHaveBeenCalledTimes(1) - expect(ProductVariantServiceMock.createDraft).toHaveBeenCalledWith({ - title: "Test Product Variant", - prices: [ - { - currency_code: "DKK", - amount: 1234, - }, - ], - }) - }) - }) - - describe("invalid data returns error details", () => { - let subject - - beforeAll(async () => { - subject = await request("POST", "/admin/products", { - payload: { - image: "image", - }, - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - }) - }) - - it("returns 400", () => { - expect(subject.status).toEqual(400) - }) - - it("returns error details", () => { - expect(subject.body.name).toEqual("invalid_data") - expect(subject.body.message[0].message).toEqual(`"title" is required`) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/product-variants/__tests__/delete-option-value.js b/packages/medusa/src/api/routes/admin/product-variants/__tests__/delete-option-value.js deleted file mode 100644 index 1720b1fde3..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/__tests__/delete-option-value.js +++ /dev/null @@ -1,39 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" - -describe("DELETE /admin/product-variants/:id/options", () => { - describe("successfully deletes option value", () => { - let subject - - beforeAll(async () => { - subject = await request( - "DELETE", - `/admin/product-variants/${IdMap.getId("testVariant")}/options`, - { - payload: { - option_id: IdMap.getId("testOption"), - }, - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - it("calls service deleteOptionValue", () => { - expect(ProductVariantServiceMock.deleteOptionValue).toHaveBeenCalledTimes( - 1 - ) - expect(ProductVariantServiceMock.deleteOptionValue).toHaveBeenCalledWith( - IdMap.getId("testVariant"), - IdMap.getId("testOption") - ) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/product-variants/__tests__/delete-product-variant.js b/packages/medusa/src/api/routes/admin/product-variants/__tests__/delete-product-variant.js deleted file mode 100644 index 83203d10a4..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/__tests__/delete-product-variant.js +++ /dev/null @@ -1,46 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" - -describe("DELETE /admin/product-variants/:id", () => { - describe("successfully deletes a product variant", () => { - let subject - - beforeAll(async () => { - subject = await request( - "DELETE", - `/admin/product-variants/${IdMap.getId("testVariant")}`, - { - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it("calls ProductVariantService delete", () => { - expect(ProductVariantServiceMock.delete).toHaveBeenCalledTimes(1) - expect(ProductVariantServiceMock.delete).toHaveBeenCalledWith( - IdMap.getId("testVariant") - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("returns correct delete data", () => { - expect(subject.body).toEqual({ - id: IdMap.getId("testVariant"), - object: "productVariant", - deleted: true, - }) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/product-variants/__tests__/get-product-variant.js b/packages/medusa/src/api/routes/admin/product-variants/__tests__/get-product-variant.js deleted file mode 100644 index eeea2f1935..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/__tests__/get-product-variant.js +++ /dev/null @@ -1,38 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" - -describe("GET /admin/product-variants/:id", () => { - describe("successfully gets a product variant", () => { - let subject - - beforeAll(async () => { - subject = await request( - "GET", - `/admin/product-variants/${IdMap.getId("testVariant")}`, - { - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - afterAll(() => { - jest.clearAllMocks() - }) - - it("calls productVariantService retrieve", () => { - expect(ProductVariantServiceMock.retrieve).toHaveBeenCalledTimes(1) - expect(ProductVariantServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("testVariant") - ) - }) - - it("returns product", () => { - expect(subject.body._id).toEqual(IdMap.getId("testVariant")) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/product-variants/__tests__/list-product-variants.js b/packages/medusa/src/api/routes/admin/product-variants/__tests__/list-product-variants.js deleted file mode 100644 index fb3ba5eb3c..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/__tests__/list-product-variants.js +++ /dev/null @@ -1,28 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" - -describe("GET /admin/product-variants", () => { - describe("successfully lists product variants", () => { - let subject - - beforeAll(async () => { - subject = await request("GET", `/admin/product-variants`, { - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - }) - }) - - it("calls ProductVariantService list", () => { - expect(ProductVariantServiceMock.list).toHaveBeenCalledTimes(1) - }) - - it("returns 200 and product variants", () => { - expect(subject.status).toEqual(200) - expect(subject.body[0]._id).toEqual(IdMap.getId("testVariant")) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/product-variants/__tests__/publish-product-variant.js b/packages/medusa/src/api/routes/admin/product-variants/__tests__/publish-product-variant.js deleted file mode 100644 index dc6bedf158..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/__tests__/publish-product-variant.js +++ /dev/null @@ -1,38 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" - -describe("POST /admin/product-variants/:id/publish", () => { - describe("successful publish", () => { - let subject - - beforeAll(async () => { - subject = await request( - "POST", - `/admin/product-variants/${IdMap.getId("publish")}/publish`, - { - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("returns product with published flag true", () => { - expect(subject.body.published).toEqual(true) - }) - - it("calls service publish", () => { - expect(ProductVariantServiceMock.publish).toHaveBeenCalledTimes(1) - expect(ProductVariantServiceMock.publish).toHaveBeenCalledWith( - IdMap.getId("publish") - ) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/product-variants/__tests__/update-prices.js b/packages/medusa/src/api/routes/admin/product-variants/__tests__/update-prices.js deleted file mode 100644 index 09e193b6c2..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/__tests__/update-prices.js +++ /dev/null @@ -1,77 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" - -describe("POST /admin/product-variants/:id/prices", () => { - describe("successfully sets region price", () => { - let subject - - beforeAll(async () => { - subject = await request( - "POST", - `/admin/product-variants/${IdMap.getId("testVariant")}/prices`, - { - payload: { - region_id: IdMap.getId("region-fr"), - amount: 100, - }, - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("calls service setCurrencyPrice", () => { - expect(ProductVariantServiceMock.setRegionPrice).toHaveBeenCalledTimes(1) - expect(ProductVariantServiceMock.setRegionPrice).toHaveBeenCalledWith( - IdMap.getId("testVariant"), - IdMap.getId("region-fr"), - 100 - ) - }) - }) - - describe("successfully sets currency price", () => { - let subject - - beforeAll(async () => { - subject = await request( - "POST", - `/admin/product-variants/${IdMap.getId("testVariant")}/prices`, - { - payload: { - currency_code: "EUR", - amount: 100, - }, - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("calls service setCurrencyPrice", () => { - expect(ProductVariantServiceMock.setCurrencyPrice).toHaveBeenCalledTimes( - 1 - ) - expect(ProductVariantServiceMock.setCurrencyPrice).toHaveBeenCalledWith( - IdMap.getId("testVariant"), - "EUR", - 100 - ) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/product-variants/__tests__/update-product-variant.js b/packages/medusa/src/api/routes/admin/product-variants/__tests__/update-product-variant.js deleted file mode 100644 index 4463ad0969..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/__tests__/update-product-variant.js +++ /dev/null @@ -1,80 +0,0 @@ -import { IdMap } from "medusa-test-utils" -import { request } from "../../../../../helpers/test-request" -import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" - -describe("POST /admin/product-variants/:id", () => { - describe("successful update", () => { - let subject - - beforeAll(async () => { - subject = await request( - "POST", - `/admin/product-variants/${IdMap.getId("testVariant")}`, - { - payload: { - title: "Test Product Variant Updated", - prices: [ - { - currency_code: "DKK", - amount: 1234, - }, - ], - }, - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("calls service update", () => { - expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(1) - expect(ProductVariantServiceMock.update).toHaveBeenCalledWith( - IdMap.getId("testVariant"), - { - title: "Test Product Variant Updated", - prices: [ - { - currency_code: "DKK", - amount: 1234, - }, - ], - } - ) - }) - }) - - describe("handles failed update operation", () => { - it("throws if metadata is to be updated", async () => { - try { - await request( - "POST", - `/admin/product-variants/${IdMap.getId("testVariant")}`, - { - payload: { - _id: IdMap.getId("testVariant"), - title: "Product 1", - metadata: "Test Description", - }, - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - } catch (error) { - expect(error.status).toEqual(400) - expect(error.message).toEqual( - "Use setMetadata to update metadata fields" - ) - } - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/product-variants/add-option-value.js b/packages/medusa/src/api/routes/admin/product-variants/add-option-value.js deleted file mode 100644 index a27174d6b3..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/add-option-value.js +++ /dev/null @@ -1,28 +0,0 @@ -import { MedusaError, Validator } from "medusa-core-utils" - -export default async (req, res) => { - const { id } = req.params - - const schema = Validator.object().keys({ - option_id: Validator.objectId().required(), - value: Validator.string().required(), - }) - - const { value, error } = schema.validate(req.body) - if (error) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) - } - - try { - const productVariantService = req.scope.resolve("productVariantService") - const productVariant = await productVariantService.addOptionValue( - id, - value.option_id, - value.value - ) - - res.status(200).json(productVariant) - } catch (err) { - throw err - } -} diff --git a/packages/medusa/src/api/routes/admin/product-variants/add-price.js b/packages/medusa/src/api/routes/admin/product-variants/add-price.js deleted file mode 100644 index f93c227f97..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/add-price.js +++ /dev/null @@ -1,42 +0,0 @@ -import { MedusaError, Validator } from "medusa-core-utils" - -export default async (req, res) => { - const { id } = req.params - - const schema = Validator.object() - .keys({ - region_id: Validator.string(), - currency_code: Validator.string(), - amount: Validator.number().required(), - }) - .xor("region_id", "currency_code") - - const { value, error } = schema.validate(req.body) - if (error) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) - } - - try { - const productVariantService = req.scope.resolve("productVariantService") - - if (value.region_id) { - const productVariant = await productVariantService.setRegionPrice( - id, - value.region_id, - value.amount - ) - - res.status(200).json(productVariant) - } else { - const productVariant = await productVariantService.setCurrencyPrice( - id, - value.currency_code, - value.amount - ) - - res.status(200).json(productVariant) - } - } catch (err) { - throw err - } -} diff --git a/packages/medusa/src/api/routes/admin/product-variants/delete-option-value.js b/packages/medusa/src/api/routes/admin/product-variants/delete-option-value.js deleted file mode 100644 index 16d0f28b2b..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/delete-option-value.js +++ /dev/null @@ -1,26 +0,0 @@ -import { MedusaError, Validator } from "medusa-core-utils" - -export default async (req, res) => { - const { id } = req.params - - const schema = Validator.object().keys({ - option_id: Validator.objectId().required(), - }) - - const { value, error } = schema.validate(req.body) - if (error) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) - } - - try { - const productVariantService = req.scope.resolve("productVariantService") - const productVariant = await productVariantService.deleteOptionValue( - id, - value.option_id - ) - - res.status(200).json(productVariant) - } catch (err) { - throw err - } -} diff --git a/packages/medusa/src/api/routes/admin/product-variants/delete-product-variant.js b/packages/medusa/src/api/routes/admin/product-variants/delete-product-variant.js deleted file mode 100644 index 7912044260..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/delete-product-variant.js +++ /dev/null @@ -1,16 +0,0 @@ -export default async (req, res) => { - const { id } = req.params - - try { - const productVariantService = req.scope.resolve("productVariantService") - await productVariantService.delete(id) - - res.json({ - id: id, - object: "productVariant", - deleted: true, - }) - } catch (err) { - throw err - } -} diff --git a/packages/medusa/src/api/routes/admin/product-variants/get-product-variant.js b/packages/medusa/src/api/routes/admin/product-variants/get-product-variant.js deleted file mode 100644 index a2a0beecae..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/get-product-variant.js +++ /dev/null @@ -1,12 +0,0 @@ -export default async (req, res) => { - const { id } = req.params - - try { - const productVariantService = req.scope.resolve("productVariantService") - const productVariant = await productVariantService.retrieve(id) - - res.json(productVariant) - } catch (error) { - throw error - } -} diff --git a/packages/medusa/src/api/routes/admin/product-variants/index.js b/packages/medusa/src/api/routes/admin/product-variants/index.js deleted file mode 100644 index 768d1d7ecc..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/index.js +++ /dev/null @@ -1,41 +0,0 @@ -import { Router } from "express" -import middlewares from "../../../middlewares" - -const route = Router() - -export default app => { - app.use("/product-variants", route) - - route.post("/", middlewares.wrap(require("./create-product-variant").default)) - route.post( - "/:id", - middlewares.wrap(require("./update-product-variant").default) - ) - - route.post( - "/:id/publish", - middlewares.wrap(require("./publish-product-variant").default) - ) - - route.post("/:id/prices", middlewares.wrap(require("./add-price").default)) - - route.post( - "/:id/options", - middlewares.wrap(require("./add-option-value").default) - ) - - route.delete( - "/:id/options", - middlewares.wrap(require("./delete-option-value").default) - ) - - route.delete( - "/:id", - middlewares.wrap(require("./delete-product-variant").default) - ) - - route.get("/:id", middlewares.wrap(require("./get-product-variant").default)) - route.get("/", middlewares.wrap(require("./list-product-variants").default)) - - return app -} diff --git a/packages/medusa/src/api/routes/admin/product-variants/list-product-variants.js b/packages/medusa/src/api/routes/admin/product-variants/list-product-variants.js deleted file mode 100644 index 11b0262b50..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/list-product-variants.js +++ /dev/null @@ -1,10 +0,0 @@ -export default async (req, res) => { - try { - const productVariantService = req.scope.resolve("productVariantService") - const productVariants = await productVariantService.list({}) - - res.json(productVariants) - } catch (error) { - throw error - } -} diff --git a/packages/medusa/src/api/routes/admin/product-variants/publish-product-variant.js b/packages/medusa/src/api/routes/admin/product-variants/publish-product-variant.js deleted file mode 100644 index 4cb2c18d17..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/publish-product-variant.js +++ /dev/null @@ -1,12 +0,0 @@ -export default async (req, res) => { - const { id } = req.params - - try { - const productVariantService = req.scope.resolve("productVariantService") - const productVariant = await productVariantService.publish(id) - - res.json(productVariant) - } catch (error) { - throw error - } -} diff --git a/packages/medusa/src/api/routes/admin/product-variants/set-region-price.js b/packages/medusa/src/api/routes/admin/product-variants/set-region-price.js deleted file mode 100644 index b27f6712cc..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/set-region-price.js +++ /dev/null @@ -1,28 +0,0 @@ -import { MedusaError, Validator } from "medusa-core-utils" - -export default async (req, res) => { - const { id } = req.params - - const schema = Validator.object().keys({ - region_id: Validator.objectId().required(), - amount: Validator.number().required(), - }) - - const { value, error } = schema.validate(req.body) - if (error) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) - } - - try { - const productVariantService = req.scope.resolve("productVariantService") - const productVariant = await productVariantService.setRegionPrice( - id, - value.regionId, - value.amount - ) - - res.status(200).json(productVariant) - } catch (err) { - throw err - } -} diff --git a/packages/medusa/src/api/routes/admin/product-variants/update-product-variant.js b/packages/medusa/src/api/routes/admin/product-variants/update-product-variant.js deleted file mode 100644 index a687666b65..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/update-product-variant.js +++ /dev/null @@ -1,39 +0,0 @@ -import { MedusaError, Validator } from "medusa-core-utils" - -export default async (req, res) => { - const { id } = req.params - - const schema = Validator.object().keys({ - title: Validator.string().optional(), - prices: Validator.array() - .items({ - currency_code: Validator.string().required(), - amount: Validator.number().required(), - }) - .optional(), - options: Validator.array() - .items({ - option_id: Validator.objectId().required(), - value: Validator.string().required(), - }) - .optional(), - image: Validator.string().optional(), - inventory_quantity: Validator.number().optional(), - allow_backorder: Validator.boolean().optional(), - manage_inventory: Validator.boolean().optional(), - }) - - const { value, error } = schema.validate(req.body) - if (error) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) - } - - try { - const productVariantService = req.scope.resolve("productVariantService") - const productVariant = await productVariantService.update(id, value) - - res.status(200).json(productVariant) - } catch (err) { - throw err - } -} diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/add-variant.js b/packages/medusa/src/api/routes/admin/products/__tests__/create-variant.js similarity index 56% rename from packages/medusa/src/api/routes/admin/products/__tests__/add-variant.js rename to packages/medusa/src/api/routes/admin/products/__tests__/create-variant.js index 49137a1d0a..3b2aa306f1 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/add-variant.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/create-variant.js @@ -2,17 +2,24 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" import { ProductServiceMock } from "../../../../../services/__mocks__/product" -describe("POST /admin/products/:id/variants/:variantId", () => { +describe("POST /admin/products/:id/variants", () => { describe("successful add variant", () => { let subject beforeAll(async () => { subject = await request( "POST", - `/admin/products/${IdMap.getId( - "productWithOptions" - )}/variants/${IdMap.getId("variant2")}`, + `/admin/products/${IdMap.getId("productWithOptions")}/variants`, { + payload: { + title: "Test Product Variant", + prices: [ + { + currency_code: "DKK", + amount: 1234, + }, + ], + }, adminSession: { jwt: { userId: IdMap.getId("admin_user"), @@ -27,10 +34,18 @@ describe("POST /admin/products/:id/variants/:variantId", () => { }) it("calls service addVariant", () => { - expect(ProductServiceMock.addVariant).toHaveBeenCalledTimes(1) - expect(ProductServiceMock.addVariant).toHaveBeenCalledWith( + expect(ProductServiceMock.createVariant).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.createVariant).toHaveBeenCalledWith( IdMap.getId("productWithOptions"), - IdMap.getId("variant2") + { + title: "Test Product Variant", + prices: [ + { + currency_code: "DKK", + amount: 1234, + }, + ], + } ) }) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/remove-variant.js b/packages/medusa/src/api/routes/admin/products/__tests__/delete-variant.js similarity index 89% rename from packages/medusa/src/api/routes/admin/products/__tests__/remove-variant.js rename to packages/medusa/src/api/routes/admin/products/__tests__/delete-variant.js index 4147b59d0d..813f13f118 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/remove-variant.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/delete-variant.js @@ -27,8 +27,8 @@ describe("POST /admin/products/:id/variants/:variantId", () => { }) it("calls service removeVariant", () => { - expect(ProductServiceMock.removeVariant).toHaveBeenCalledTimes(1) - expect(ProductServiceMock.removeVariant).toHaveBeenCalledWith( + expect(ProductServiceMock.deleteVariant).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.deleteVariant).toHaveBeenCalledWith( IdMap.getId("productWithOptions"), IdMap.getId("variant1") ) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/update-variant.js b/packages/medusa/src/api/routes/admin/products/__tests__/update-variant.js new file mode 100644 index 0000000000..918808e637 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/__tests__/update-variant.js @@ -0,0 +1,172 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { ProductServiceMock } from "../../../../../services/__mocks__/product" +import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" + +describe("POST /admin/products/:id/variants/:variantId", () => { + describe("successful updates variant prices", () => { + let subject + + beforeAll(async () => { + jest.clearAllMocks() + subject = await request( + "POST", + `/admin/products/${IdMap.getId( + "productWithOptions" + )}/variants/${IdMap.getId("variant1")}`, + { + payload: { + title: "hi", + prices: [ + { + region_id: IdMap.getId("region-fr"), + amount: 100, + }, + { + currency_code: "DKK", + amount: 100, + }, + ], + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls service removeVariant", () => { + expect(ProductVariantServiceMock.setCurrencyPrice).toHaveBeenCalledTimes( + 1 + ) + expect(ProductVariantServiceMock.setCurrencyPrice).toHaveBeenCalledWith( + IdMap.getId("variant1"), + "DKK", + 100 + ) + + expect(ProductVariantServiceMock.setRegionPrice).toHaveBeenCalledTimes(1) + expect(ProductVariantServiceMock.setRegionPrice).toHaveBeenCalledWith( + IdMap.getId("variant1"), + IdMap.getId("region-fr"), + 100 + ) + }) + + it("filters prices", () => { + expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(1) + expect(ProductVariantServiceMock.update).toHaveBeenCalledWith( + IdMap.getId("variant1"), + { + title: "hi", + } + ) + }) + + it("returns decorated product with variant removed", () => { + expect(subject.body._id).toEqual(IdMap.getId("productWithOptions")) + expect(subject.body.decorated).toEqual(true) + }) + }) + + describe("successful updates options", () => { + let subject + + beforeAll(async () => { + jest.clearAllMocks() + subject = await request( + "POST", + `/admin/products/${IdMap.getId( + "productWithOptions" + )}/variants/${IdMap.getId("variant1")}`, + { + payload: { + options: [ + { + option_id: IdMap.getId("option_id"), + value: 100, + }, + ], + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls service removeVariant", () => { + expect(ProductServiceMock.updateOptionValue).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.updateOptionValue).toHaveBeenCalledWith( + IdMap.getId("productWithOptions"), + IdMap.getId("variant1"), + IdMap.getId("option_id"), + 100 + ) + }) + + it("returns decorated product with variant removed", () => { + expect(subject.body._id).toEqual(IdMap.getId("productWithOptions")) + expect(subject.body.decorated).toEqual(true) + }) + }) + + describe("successful updates variant", () => { + let subject + + beforeAll(async () => { + jest.clearAllMocks() + subject = await request( + "POST", + `/admin/products/${IdMap.getId( + "productWithOptions" + )}/variants/${IdMap.getId("variant1")}`, + { + payload: { + title: "hi", + inventory_quantity: 123, + allow_backorder: true, + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("calls variant update", () => { + expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(1) + expect(ProductVariantServiceMock.update).toHaveBeenCalledWith( + IdMap.getId("variant1"), + { + title: "hi", + inventory_quantity: 123, + allow_backorder: true, + } + ) + }) + + it("returns decorated product with variant removed", () => { + expect(subject.body._id).toEqual(IdMap.getId("productWithOptions")) + expect(subject.body.decorated).toEqual(true) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/product-variants/create-product-variant.js b/packages/medusa/src/api/routes/admin/products/create-variant.js similarity index 71% rename from packages/medusa/src/api/routes/admin/product-variants/create-product-variant.js rename to packages/medusa/src/api/routes/admin/products/create-variant.js index f4029ddf14..03da3d7734 100644 --- a/packages/medusa/src/api/routes/admin/product-variants/create-product-variant.js +++ b/packages/medusa/src/api/routes/admin/products/create-variant.js @@ -1,6 +1,7 @@ import { MedusaError, Validator } from "medusa-core-utils" export default async (req, res) => { + const { id } = req.params const schema = Validator.object().keys({ title: Validator.string().required(), prices: Validator.array() @@ -26,10 +27,19 @@ export default async (req, res) => { } try { - const productVariantService = req.scope.resolve("productVariantService") - const productVariant = await productVariantService.createDraft(value) - - res.status(200).json(productVariant) + const productService = req.scope.resolve("productService") + const product = await productService.createVariant(id, value) + const data = await productService.decorate(product, [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ]) + res.json(data) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/products/add-variant.js b/packages/medusa/src/api/routes/admin/products/delete-variant.js similarity index 80% rename from packages/medusa/src/api/routes/admin/products/add-variant.js rename to packages/medusa/src/api/routes/admin/products/delete-variant.js index 719a06a17d..1de4f4ee57 100644 --- a/packages/medusa/src/api/routes/admin/products/add-variant.js +++ b/packages/medusa/src/api/routes/admin/products/delete-variant.js @@ -3,7 +3,7 @@ export default async (req, res) => { try { const productService = req.scope.resolve("productService") - const product = await productService.addVariant(id, variantId) + const product = await productService.deleteVariant(id, variantId) const data = await productService.decorate(product, [ "title", "description", @@ -14,7 +14,7 @@ export default async (req, res) => { "variants", "published", ]) - res.json(data) + res.json(product) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/products/index.js b/packages/medusa/src/api/routes/admin/products/index.js index 90782013bc..3ffcdf6a9c 100644 --- a/packages/medusa/src/api/routes/admin/products/index.js +++ b/packages/medusa/src/api/routes/admin/products/index.js @@ -12,10 +12,17 @@ export default app => { "/:id/publish", middlewares.wrap(require("./publish-product").default) ) + route.post( - "/:id/variants/:variantId", - middlewares.wrap(require("./add-variant").default) + "/:id/variants", + middlewares.wrap(require("./create-variant").default) ) + + route.post( + "/:id/variants/:variant_id", + middlewares.wrap(require("./update-variant").default) + ) + route.post( "/:id/options/:optionId", middlewares.wrap(require("./update-option").default) @@ -24,7 +31,7 @@ export default app => { route.delete( "/:id/variants/:variantId", - middlewares.wrap(require("./remove-variant").default) + middlewares.wrap(require("./delete-variant").default) ) route.delete("/:id", middlewares.wrap(require("./delete-product").default)) route.delete( diff --git a/packages/medusa/src/api/routes/admin/products/remove-variant.js b/packages/medusa/src/api/routes/admin/products/remove-variant.js deleted file mode 100644 index d91212de82..0000000000 --- a/packages/medusa/src/api/routes/admin/products/remove-variant.js +++ /dev/null @@ -1,22 +0,0 @@ -export default async (req, res) => { - const { id, variantId } = req.params - - try { - const productService = req.scope.resolve("productService") - await productService.removeVariant(id, variantId) - let updatedProduct = await productService.retrieve(id) - updatedProduct = await productService.decorate(updatedProduct, [ - "title", - "description", - "tags", - "handle", - "images", - "options", - "variants", - "published", - ]) - res.json(updatedProduct) - } catch (err) { - throw err - } -} diff --git a/packages/medusa/src/api/routes/admin/products/update-variant.js b/packages/medusa/src/api/routes/admin/products/update-variant.js new file mode 100644 index 0000000000..0d1e8d009b --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/update-variant.js @@ -0,0 +1,91 @@ +import _ from "lodash" +import { MedusaError, Validator } from "medusa-core-utils" + +export default async (req, res) => { + const { id, variant_id } = req.params + const schema = Validator.object().keys({ + title: Validator.string().optional(), + prices: Validator.array().items( + Validator.object() + .keys({ + region_id: Validator.string(), + currency_code: Validator.string(), + amount: Validator.number().required(), + }) + .xor("region_id", "currency_code") + ), + options: Validator.array().items({ + option_id: Validator.objectId().required(), + value: Validator.alternatives( + Validator.string(), + Validator.number() + ).required(), + }), + image: Validator.string().optional(), + inventory_quantity: Validator.number().optional(), + allow_backorder: Validator.boolean().optional(), + manage_inventory: Validator.boolean().optional(), + metadata: Validator.object().optional(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const productService = req.scope.resolve("productService") + const productVariantService = req.scope.resolve("productVariantService") + + if (value.prices && value.prices.length) { + for (const price of value.prices) { + if (price.region_id) { + await productVariantService.setRegionPrice( + variant_id, + price.region_id, + price.amount + ) + } else { + await productVariantService.setCurrencyPrice( + variant_id, + price.currency_code, + price.amount + ) + } + } + } + + if (value.options && value.options.length) { + for (const option of value.options) { + await productService.updateOptionValue( + id, + variant_id, + option.option_id, + option.value + ) + } + } + + delete value.prices + delete value.options + + if (!_.isEmpty(value)) { + await productVariantService.update(variant_id, value) + } + + const product = await productService.retrieve(id) + const data = await productService.decorate(product, [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ]) + res.json(data) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/services/__mocks__/product-variant.js b/packages/medusa/src/services/__mocks__/product-variant.js index 2498abec2c..3259567737 100644 --- a/packages/medusa/src/services/__mocks__/product-variant.js +++ b/packages/medusa/src/services/__mocks__/product-variant.js @@ -194,10 +194,30 @@ export const ProductVariantServiceMock = { update: jest.fn().mockReturnValue(Promise.resolve()), setCurrencyPrice: jest.fn().mockReturnValue(Promise.resolve()), setRegionPrice: jest.fn().mockReturnValue(Promise.resolve()), + updateOptionValue: jest.fn().mockReturnValue(Promise.resolve()), addOptionValue: jest.fn().mockImplementation((variantId, optionId, value) => { return Promise.resolve({}) }), list: jest.fn().mockImplementation(data => { + if (data._id && data._id.$in) { + return Promise.resolve( + data._id.$in.map(id => { + if (id === "1") { + return variant1 + } + if (id === "2") { + return variant2 + } + if (id === "3") { + return variant3 + } + if (id === "4") { + return variant4 + } + }) + ) + } + return Promise.resolve([testVariant]) }), deleteOptionValue: jest.fn().mockImplementation((variantId, optionId) => { diff --git a/packages/medusa/src/services/__mocks__/product.js b/packages/medusa/src/services/__mocks__/product.js index 470351c8ca..884c3d436d 100644 --- a/packages/medusa/src/services/__mocks__/product.js +++ b/packages/medusa/src/services/__mocks__/product.js @@ -43,10 +43,10 @@ export const ProductServiceMock = { delete: jest.fn().mockImplementation(_ => { return Promise.resolve() }), - addVariant: jest.fn().mockImplementation((productId, variantId) => { + createVariant: jest.fn().mockImplementation((productId, value) => { return Promise.resolve(products.productWithOptions) }), - removeVariant: jest.fn().mockImplementation((productId, variantId) => { + deleteVariant: jest.fn().mockImplementation((productId, variantId) => { return Promise.resolve(products.productWithOptions) }), decorate: jest.fn().mockImplementation((product, fields) => { @@ -57,6 +57,7 @@ export const ProductServiceMock = { return Promise.resolve(products.productWithOptions) }), updateOption: jest.fn().mockReturnValue(Promise.resolve()), + updateOptionValue: jest.fn().mockReturnValue(Promise.resolve()), deleteOption: jest.fn().mockReturnValue(Promise.resolve()), retrieve: jest.fn().mockImplementation(productId => { if (productId === IdMap.getId("product1")) { diff --git a/packages/medusa/src/services/__tests__/product.js b/packages/medusa/src/services/__tests__/product.js index 07fb66ba76..e06a69e625 100644 --- a/packages/medusa/src/services/__tests__/product.js +++ b/packages/medusa/src/services/__tests__/product.js @@ -310,7 +310,7 @@ describe("ProductService", () => { }) }) - describe("addVariant", () => { + describe("createVariant", () => { const productService = new ProductService({ productModel: ProductModelMock, productVariantService: ProductVariantServiceMock, @@ -321,68 +321,112 @@ describe("ProductService", () => { }) it("add variant to product successfilly", async () => { - await productService.addVariant(IdMap.getId("variantProductId"), "1") + await productService.createVariant(IdMap.getId("variantProductId"), { + title: "variant1", + options: [ + { + option_id: IdMap.getId("color_id"), + value: "blue", + }, + { + option_id: IdMap.getId("size_id"), + value: "160", + }, + ], + }) + + expect(ProductVariantServiceMock.createDraft).toBeCalledTimes(1) + expect(ProductVariantServiceMock.createDraft).toBeCalledWith({ + title: "variant1", + options: [ + { + option_id: IdMap.getId("color_id"), + value: "blue", + }, + { + option_id: IdMap.getId("size_id"), + value: "160", + }, + ], + }) - expect(ProductVariantServiceMock.retrieve).toBeCalledTimes(1) - expect(ProductVariantServiceMock.retrieve).toBeCalledWith("1") expect(ProductModelMock.findOne).toBeCalledTimes(1) expect(ProductModelMock.findOne).toBeCalledWith({ _id: IdMap.getId("variantProductId"), }) + expect(ProductModelMock.updateOne).toBeCalledTimes(1) expect(ProductModelMock.updateOne).toBeCalledWith( { _id: IdMap.getId("variantProductId") }, - { $push: { variants: "1" } } + { $push: { variants: expect.stringMatching(/.*/) } } ) }) it("throws error if option id is not present in product", async () => { - try { - await productService.addVariant( - IdMap.getId("variantProductId"), - "invalid_option" - ) - } catch (err) { - expect(err.message).toEqual( - "Variant options do not contain value for Color" - ) - } + await expect( + productService.createVariant(IdMap.getId("variantProductId"), { + title: "variant3", + options: [ + { + option_id: "invalid_id", + value: "blue", + }, + { + option_id: IdMap.getId("size_id"), + value: "150", + }, + ], + }) + ).rejects.toThrow("Variant options do not contain value for Color") }) it("throws error if product variant options is empty", async () => { - try { - await productService.addVariant( - IdMap.getId("variantProductId"), - "empty_option" - ) - } catch (err) { - expect(err.message).toEqual( - "Product options length does not match variant options length. Product has 2 and variant has 0." - ) - } + await expect( + productService.createVariant(IdMap.getId("variantProductId"), { + title: "variant3", + options: [], + }) + ).rejects.toThrow( + "Product options length does not match variant options length. Product has 2 and variant has 0." + ) }) it("throws error if product options is empty and product variant contains options", async () => { - try { - await productService.addVariant( - IdMap.getId("emptyVariantProductId"), - "1" - ) - } catch (err) { - expect(err.message).toEqual( - "Product options length does not match variant options length. Product has 0 and variant has 2." - ) - } + await expect( + productService.createVariant(IdMap.getId("emptyVariantProductId"), { + title: "variant1", + options: [ + { + option_id: IdMap.getId("color_id"), + value: "blue", + }, + { + option_id: IdMap.getId("size_id"), + value: "160", + }, + ], + }) + ).rejects.toThrow( + "Product options length does not match variant options length. Product has 0 and variant has 2." + ) }) it("throws error if option values of added variant already exists", async () => { - try { - await productService.addVariant(IdMap.getId("productWithVariants"), "3") - } catch (err) { - expect(err.message).toEqual( - "Variant with provided options already exists" - ) - } + await expect( + productService.createVariant(IdMap.getId("productWithVariants"), { + title: "variant3", + options: [ + { + option_id: IdMap.getId("color_id"), + value: "blue", + }, + { + option_id: IdMap.getId("size_id"), + value: "150", + }, + ], + }) + ).rejects.toThrow("Variant with provided options already exists") }) }) @@ -539,7 +583,7 @@ describe("ProductService", () => { }) }) - describe("removeVariant", () => { + describe("deleteVariant", () => { const productService = new ProductService({ productModel: ProductModelMock, productVariantService: ProductVariantServiceMock, @@ -550,11 +594,14 @@ describe("ProductService", () => { }) it("removes variant from product", async () => { - await productService.removeVariant( + await productService.deleteVariant( IdMap.getId("productWithVariants"), "1" ) + expect(ProductVariantServiceMock.delete).toBeCalledTimes(1) + expect(ProductVariantServiceMock.delete).toBeCalledWith("1") + expect(ProductModelMock.updateOne).toBeCalledTimes(1) expect(ProductModelMock.updateOne).toBeCalledWith( { _id: IdMap.getId("productWithVariants") }, @@ -746,4 +793,55 @@ describe("ProductService", () => { } }) }) + + describe("updateOptionValue", () => { + const productService = new ProductService({ + productModel: ProductModelMock, + productVariantService: ProductVariantServiceMock, + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it("successfully updates an option value", async () => { + await productService.updateOptionValue( + IdMap.getId("productWithVariants"), + "1", + IdMap.getId("color_id"), + "Blue" + ) + + expect(ProductVariantServiceMock.updateOptionValue).toBeCalledTimes(1) + expect(ProductVariantServiceMock.updateOptionValue).toBeCalledWith( + "1", + IdMap.getId("color_id"), + "Blue" + ) + }) + + it("throws product-variant relationship isn't valid", async () => { + await expect( + productService.updateOptionValue( + IdMap.getId("productWithFourVariants"), + "invalid_variant", + IdMap.getId("color_id"), + "Blue" + ) + ).rejects.toThrow("The variant could not be found in the product") + }) + + it("throws if combination exists", async () => { + await expect( + productService.updateOptionValue( + IdMap.getId("productWithFourVariants"), + "1", + IdMap.getId("color_id"), + "black" + ) + ).rejects.toThrow( + "A variant with the given option value combination already exist" + ) + }) + }) }) diff --git a/packages/medusa/src/services/product-variant.js b/packages/medusa/src/services/product-variant.js index 3de62e2174..d359e715ae 100644 --- a/packages/medusa/src/services/product-variant.js +++ b/packages/medusa/src/services/product-variant.js @@ -297,6 +297,28 @@ class ProductVariantService extends BaseService { ) } + /** + * Updates variant's option value. + * Option value must be of type string or number. + * @param {string} variantId - the variant to decorate. + * @param {string} optionId - the option from product. + * @param {string | number} optionValue - option value to add. + * @return {Promise} the result of the update operation. + */ + async updateOptionValue(variantId, optionId, optionValue) { + if (typeof optionValue !== "string" && typeof optionValue !== "number") { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Option value is not of type string or number` + ) + } + + return this.productVariantModel_.updateOne( + { _id: variant._id, "options.option_id": optionId }, + { $set: { "options.$.option_id": `${optionValue}` } } + ) + } + /** * Adds option value to a varaint. * Fails when product with variant does not exists or diff --git a/packages/medusa/src/services/product.js b/packages/medusa/src/services/product.js index af2cea350b..9557a0ea53 100644 --- a/packages/medusa/src/services/product.js +++ b/packages/medusa/src/services/product.js @@ -71,6 +71,16 @@ class ProductService extends BaseService { return product } + /** + * Gets all variants belonging to a product. + * @param {string} productId - the id of the product to get variants from. + * @return {Promise} an array of variants + */ + async retrieveVariants(productId) { + const product = await this.retrieve(productId) + return this.productVariantService_.list({ _id: { $in: product.variants } }) + } + /** * Creates an unpublished product. * @param {object} product - the product to create @@ -171,11 +181,9 @@ class ProductService extends BaseService { * @param {string} variantId - the variant to add to the product * @return {Promise} the result of update */ - async addVariant(productId, variantId) { + async createVariant(productId, variant) { const product = await this.retrieve(productId) - const variant = await this.productVariantService_.retrieve(variantId) - if (product.options.length !== variant.options.length) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -212,9 +220,11 @@ class ProductService extends BaseService { ) } + const newVariant = await this.productVariantService_.createDraft(variant) + return this.productModel_.updateOne( { _id: product._id }, - { $push: { variants: variantId } } + { $push: { variants: newVariant._id } } ) } @@ -479,9 +489,11 @@ class ProductService extends BaseService { * @param {string} variantId - the variant to remove from product * @return {Promise} the result of update */ - async removeVariant(productId, variantId) { + async deleteVariant(productId, variantId) { const product = await this.retrieve(productId) + await this.productVariantService_.delete(variantId) + return this.productModel_.updateOne( { _id: product._id }, { @@ -492,6 +504,58 @@ class ProductService extends BaseService { ) } + async updateOptionValue(productId, variantId, optionId, value) { + const product = await this.retrieve(productId) + + // Check if the product-to-variant relationship holds + if (!product.variants.includes(variantId)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "The variant could not be found in the product" + ) + } + + // Retrieve all variants + const variants = await this.retrieveVariants(productId) + const toUpdate = variants.find(v => v._id.equals(variantId)) + + // Check if an update would create duplicate variants + const canUpdate = variants.every(v => { + // The variant we update is irrelevant + if (v._id.equals(variantId)) { + return true + } + + // Check if the variant's options are identical to the variant we + // are updating + const hasMatchingOptions = v.options.every(option => { + if (option.option_id === optionId) { + return option.value === value + } + + const toUpdateOption = toUpdate.options.find( + o => o.option_id === option.option_id + ) + return toUpdateOption.value === option.value + }) + + return !hasMatchingOptions + }) + + if (!canUpdate) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "A variant with the given option value combination already exist" + ) + } + + return this.productVariantService_.updateOptionValue( + variantId, + optionId, + value + ) + } + /** * Decorates a product with product variants. * @param {Product} product - the product to decorate.