From 706ca8ac45d60f17798db6285cbea4d18240989b Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Thu, 28 May 2020 11:44:11 +0200 Subject: [PATCH] Makes ProductVariant operations go through ProductService (#68) Ensures that we can reliably manage product variants and their relationship to products --- packages/medusa-test-utils/src/id-map.js | 14 +- .../src/api/middlewares/error-handler.js | 1 + packages/medusa/src/api/routes/admin/index.js | 2 - .../__tests__/add-option-value.js | 40 --- .../__tests__/create-product-variant.js | 65 ----- .../__tests__/delete-option-value.js | 39 --- .../__tests__/delete-product-variant.js | 46 ---- .../__tests__/get-product-variant.js | 38 --- .../__tests__/list-product-variants.js | 28 -- .../__tests__/publish-product-variant.js | 38 --- .../__tests__/set-currency-price.js | 42 --- .../__tests__/set-region-price.js | 40 --- .../__tests__/update-product-variant.js | 70 ----- .../product-variants/add-option-value.js | 28 -- .../create-product-variant.js | 30 --- .../product-variants/delete-option-value.js | 26 -- .../delete-product-variant.js | 16 -- .../product-variants/get-product-variant.js | 12 - .../routes/admin/product-variants/index.js | 49 ---- .../product-variants/list-product-variants.js | 10 - .../publish-product-variant.js | 12 - .../product-variants/set-currency-price.js | 28 -- .../product-variants/set-region-price.js | 28 -- .../update-product-variant.js | 33 --- .../admin/products/__tests__/add-option.js | 8 +- .../admin/products/__tests__/add-variant.js | 42 --- .../products/__tests__/create-product.js | 4 +- .../products/__tests__/create-variant.js | 60 +++++ .../admin/products/__tests__/delete-option.js | 2 +- .../{remove-variant.js => delete-variant.js} | 13 +- .../admin/products/__tests__/get-product.js | 4 +- .../admin/products/__tests__/get-variants.js | 39 +++ .../admin/products/__tests__/list-products.js | 8 +- .../products/__tests__/publish-product.js | 2 +- .../products/__tests__/update-variant.js | 178 +++++++++++++ .../api/routes/admin/products/add-option.js | 12 +- .../api/routes/admin/products/add-variant.js | 23 -- .../routes/admin/products/create-product.js | 2 +- .../routes/admin/products/create-variant.js | 48 ++++ .../routes/admin/products/delete-option.js | 6 +- .../{remove-variant.js => delete-variant.js} | 14 +- .../api/routes/admin/products/get-product.js | 2 +- .../api/routes/admin/products/get-variants.js | 8 + .../src/api/routes/admin/products/index.js | 24 +- .../routes/admin/products/list-products.js | 2 +- .../routes/admin/products/publish-product.js | 2 +- .../routes/admin/products/update-option.js | 33 +-- .../routes/admin/products/update-product.js | 2 +- .../routes/admin/products/update-variant.js | 91 +++++++ .../admin/regions/__tests__/add-country.js | 4 +- .../__tests__/add-fulfillment-provider.js | 4 +- .../regions/__tests__/add-payment-provider.js | 4 +- .../admin/regions/__tests__/get-region.js | 4 +- .../store/carts/__tests__/create-line-item.js | 8 +- .../routes/store/carts/__tests__/get-cart.js | 2 +- .../routes/store/carts/create-line-item.js | 4 +- .../src/api/routes/store/carts/get-cart.js | 16 +- .../store/products/__tests__/get-product.js | 4 +- .../api/routes/store/products/get-product.js | 24 +- .../routes/store/products/list-products.js | 21 +- packages/medusa/src/models/__mocks__/order.js | 6 +- .../medusa/src/models/__mocks__/product.js | 7 + packages/medusa/src/models/cart.js | 4 +- packages/medusa/src/models/order.js | 2 +- .../medusa/src/models/schemas/discount.js | 14 + .../medusa/src/services/__mocks__/auth.js | 2 +- .../medusa/src/services/__mocks__/cart.js | 2 +- .../medusa/src/services/__mocks__/order.js | 4 +- .../src/services/__mocks__/product-variant.js | 21 ++ .../medusa/src/services/__mocks__/product.js | 15 +- .../medusa/src/services/__mocks__/region.js | 5 +- .../src/services/__tests__/line-item.js | 12 +- .../medusa/src/services/__tests__/product.js | 245 ++++++++++++++---- packages/medusa/src/services/cart.js | 10 +- packages/medusa/src/services/line-item.js | 5 +- .../medusa/src/services/product-variant.js | 25 +- packages/medusa/src/services/product.js | 109 ++++++-- packages/medusa/src/services/region.js | 12 +- packages/medusa/src/services/totals.js | 4 +- packages/medusa/src/services/user.js | 2 +- packages/medusa/yarn.lock | 15 -- 81 files changed, 975 insertions(+), 1005 deletions(-) delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/__tests__/add-option-value.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/__tests__/create-product-variant.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/__tests__/delete-option-value.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/__tests__/delete-product-variant.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/__tests__/get-product-variant.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/__tests__/list-product-variants.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/__tests__/publish-product-variant.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/__tests__/set-currency-price.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/__tests__/set-region-price.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/__tests__/update-product-variant.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/add-option-value.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/create-product-variant.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/delete-option-value.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/delete-product-variant.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/get-product-variant.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/index.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/list-product-variants.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/publish-product-variant.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/set-currency-price.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/set-region-price.js delete mode 100644 packages/medusa/src/api/routes/admin/product-variants/update-product-variant.js delete mode 100644 packages/medusa/src/api/routes/admin/products/__tests__/add-variant.js create mode 100644 packages/medusa/src/api/routes/admin/products/__tests__/create-variant.js rename packages/medusa/src/api/routes/admin/products/__tests__/{remove-variant.js => delete-variant.js} (74%) create mode 100644 packages/medusa/src/api/routes/admin/products/__tests__/get-variants.js create mode 100644 packages/medusa/src/api/routes/admin/products/__tests__/update-variant.js delete mode 100644 packages/medusa/src/api/routes/admin/products/add-variant.js create mode 100644 packages/medusa/src/api/routes/admin/products/create-variant.js rename packages/medusa/src/api/routes/admin/products/{remove-variant.js => delete-variant.js} (52%) create mode 100644 packages/medusa/src/api/routes/admin/products/get-variants.js create mode 100644 packages/medusa/src/api/routes/admin/products/update-variant.js create mode 100644 packages/medusa/src/models/schemas/discount.js diff --git a/packages/medusa-test-utils/src/id-map.js b/packages/medusa-test-utils/src/id-map.js index 686cfaf235..ffc435ce24 100644 --- a/packages/medusa-test-utils/src/id-map.js +++ b/packages/medusa-test-utils/src/id-map.js @@ -1,15 +1,21 @@ import mongoose from "mongoose" +String.prototype.equals = function(that) { + return this === that +} + class IdMap { ids = {} - getId(key) { + getId(key, backend=false) { if (this.ids[key]) { return this.ids[key] } - const id = `${mongoose.Types.ObjectId()}` - this.ids[key] = id - return id + + const mongooseId = `${mongoose.Types.ObjectId()}` + this.ids[key] = mongooseId + + return mongooseId } } diff --git a/packages/medusa/src/api/middlewares/error-handler.js b/packages/medusa/src/api/middlewares/error-handler.js index 661131ad87..0aafd74533 100644 --- a/packages/medusa/src/api/middlewares/error-handler.js +++ b/packages/medusa/src/api/middlewares/error-handler.js @@ -16,6 +16,7 @@ export default () => { break case MedusaError.Types.DB_ERROR: statusCode = 500 + logger.error(err) break default: break 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 b2582b1de4..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: { - optionId: IdMap.getId("testOption"), - optionValue: "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 51bfc19f40..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/__tests__/create-product-variant.js +++ /dev/null @@ -1,65 +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: [{}], - }, - 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: [{}], - }) - }) - }) - - 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 94adc1b33f..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: { - optionId: 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__/set-currency-price.js b/packages/medusa/src/api/routes/admin/product-variants/__tests__/set-currency-price.js deleted file mode 100644 index de6da320ec..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/__tests__/set-currency-price.js +++ /dev/null @@ -1,42 +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/currency-price", () => { - describe("successful sets currency price", () => { - let subject - - beforeAll(async () => { - subject = await request( - "POST", - `/admin/product-variants/${IdMap.getId("testVariant")}/currency-price`, - { - payload: { - currencyCode: "DKK", - 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"), - "DKK", - 100 - ) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/product-variants/__tests__/set-region-price.js b/packages/medusa/src/api/routes/admin/product-variants/__tests__/set-region-price.js deleted file mode 100644 index 7898b23243..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/__tests__/set-region-price.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/region-price", () => { - describe("successfully sets region price", () => { - let subject - - beforeAll(async () => { - subject = await request( - "POST", - `/admin/product-variants/${IdMap.getId("testVariant")}/region-price`, - { - payload: { - regionId: 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 - ) - }) - }) -}) 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 fd8437d4b3..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/__tests__/update-product-variant.js +++ /dev/null @@ -1,70 +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: [{}], - }, - 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: [{}], - } - ) - }) - }) - - 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 ef5bb070a6..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({ - optionId: Validator.objectId().required(), - optionValue: 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.optionId, - value.optionValue - ) - - res.status(200).json(productVariant) - } catch (err) { - throw err - } -} diff --git a/packages/medusa/src/api/routes/admin/product-variants/create-product-variant.js b/packages/medusa/src/api/routes/admin/product-variants/create-product-variant.js deleted file mode 100644 index 6a9b5d53ac..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/create-product-variant.js +++ /dev/null @@ -1,30 +0,0 @@ -import { MedusaError, Validator } from "medusa-core-utils" - -export default async (req, res) => { - const schema = Validator.object().keys({ - title: Validator.string().required(), - prices: Validator.array() - .items({}) - .required(), - options: Validator.array().items({}), - 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 productVariantService = req.scope.resolve("productVariantService") - const productVariant = await productVariantService.createDraft(value) - - 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 8db3a0e1c8..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({ - optionId: 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.optionId - ) - - 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 e6ed9f65f7..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/index.js +++ /dev/null @@ -1,49 +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/currency-price", - middlewares.wrap(require("./set-currency-price").default) - ) - - route.post( - "/:id/region-price", - middlewares.wrap(require("./set-region-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-currency-price.js b/packages/medusa/src/api/routes/admin/product-variants/set-currency-price.js deleted file mode 100644 index 948f6e1ed3..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/set-currency-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({ - currencyCode: Validator.string().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.setCurrencyPrice( - id, - value.currencyCode, - value.amount - ) - - res.status(200).json(productVariant) - } catch (err) { - throw err - } -} 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 d3564985a3..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({ - regionId: 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 6907449879..0000000000 --- a/packages/medusa/src/api/routes/admin/product-variants/update-product-variant.js +++ /dev/null @@ -1,33 +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({}) - .optional(), - options: Validator.array() - .items({}) - .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-option.js b/packages/medusa/src/api/routes/admin/products/__tests__/add-option.js index 3c25957fb7..a2d188279d 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/add-option.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/add-option.js @@ -12,7 +12,7 @@ describe("POST /admin/products/:id/options", () => { `/admin/products/${IdMap.getId("productWithOptions")}/options`, { payload: { - optionTitle: "Test option", + option_title: "Test option", }, adminSession: { jwt: { @@ -36,8 +36,10 @@ describe("POST /admin/products/:id/options", () => { }) it("returns the updated product decorated", () => { - expect(subject.body._id).toEqual(IdMap.getId("productWithOptions")) - expect(subject.body.decorated).toEqual(true) + expect(subject.body.product._id).toEqual( + IdMap.getId("productWithOptions") + ) + expect(subject.body.product.decorated).toEqual(true) }) }) }) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/add-variant.js b/packages/medusa/src/api/routes/admin/products/__tests__/add-variant.js deleted file mode 100644 index 49137a1d0a..0000000000 --- a/packages/medusa/src/api/routes/admin/products/__tests__/add-variant.js +++ /dev/null @@ -1,42 +0,0 @@ -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("successful add variant", () => { - let subject - - beforeAll(async () => { - subject = await request( - "POST", - `/admin/products/${IdMap.getId( - "productWithOptions" - )}/variants/${IdMap.getId("variant2")}`, - { - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), - }, - }, - } - ) - }) - - it("returns 200", () => { - expect(subject.status).toEqual(200) - }) - - it("calls service addVariant", () => { - expect(ProductServiceMock.addVariant).toHaveBeenCalledTimes(1) - expect(ProductServiceMock.addVariant).toHaveBeenCalledWith( - IdMap.getId("productWithOptions"), - IdMap.getId("variant2") - ) - }) - - it("returns the updated product decorated", () => { - expect(subject.body._id).toEqual(IdMap.getId("productWithOptions")) - expect(subject.body.decorated).toEqual(true) - }) - }) -}) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js index b4c4695308..eac06fe22e 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js @@ -27,8 +27,8 @@ describe("POST /admin/products", () => { }) it("returns created product draft", () => { - expect(subject.body._id).toEqual(IdMap.getId("product1")) - expect(subject.body.decorated).toEqual(true) + expect(subject.body.product._id).toEqual(IdMap.getId("product1")) + expect(subject.body.product.decorated).toEqual(true) }) it("calls service createDraft", () => { diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/create-variant.js b/packages/medusa/src/api/routes/admin/products/__tests__/create-variant.js new file mode 100644 index 0000000000..5e5494b4cc --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/__tests__/create-variant.js @@ -0,0 +1,60 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { ProductServiceMock } from "../../../../../services/__mocks__/product" + +describe("POST /admin/products/:id/variants", () => { + describe("successful add variant", () => { + let subject + + beforeAll(async () => { + subject = await request( + "POST", + `/admin/products/${IdMap.getId("productWithOptions")}/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("calls service addVariant", () => { + expect(ProductServiceMock.createVariant).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.createVariant).toHaveBeenCalledWith( + IdMap.getId("productWithOptions"), + { + title: "Test Product Variant", + options: [], + prices: [ + { + currency_code: "DKK", + amount: 1234, + }, + ], + } + ) + }) + + it("returns the updated product decorated", () => { + expect(subject.body.product._id).toEqual( + IdMap.getId("productWithOptions") + ) + expect(subject.body.product.decorated).toEqual(true) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/delete-option.js b/packages/medusa/src/api/routes/admin/products/__tests__/delete-option.js index 757eb17c1f..e77240935c 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/delete-option.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/delete-option.js @@ -25,7 +25,7 @@ describe("DELETE /admin/products/:id/options/:optionId", () => { it("returns 200 and correct delete info", () => { expect(subject.status).toEqual(200) expect(subject.body).toEqual({ - optionId: IdMap.getId("option1"), + option_id: IdMap.getId("option1"), object: "option", deleted: true, }) 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 74% 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..408b92dd08 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,16 +27,19 @@ 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") ) }) - it("returns decorated product with variant removed", () => { - expect(subject.body._id).toEqual(IdMap.getId("productWithOptions")) - expect(subject.body.decorated).toEqual(true) + it("returns delete result", () => { + expect(subject.body).toEqual({ + variant_id: IdMap.getId("variant1"), + object: "product-variant", + deleted: true, + }) }) }) }) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js index e2f6b7fab5..e6ae586169 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/get-product.js @@ -32,8 +32,8 @@ describe("GET /admin/products/:id", () => { }) it("returns product decorated", () => { - expect(subject.body._id).toEqual(IdMap.getId("product1")) - expect(subject.body.decorated).toEqual(true) + expect(subject.body.product._id).toEqual(IdMap.getId("product1")) + expect(subject.body.product.decorated).toEqual(true) }) }) }) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/get-variants.js b/packages/medusa/src/api/routes/admin/products/__tests__/get-variants.js new file mode 100644 index 0000000000..c727cf1dd8 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/__tests__/get-variants.js @@ -0,0 +1,39 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { ProductServiceMock } from "../../../../../services/__mocks__/product" + +describe("GET /admin/products/:id/variants", () => { + describe("successfully gets a product", () => { + let subject + + beforeAll(async () => { + subject = await request( + "GET", + `/admin/products/${IdMap.getId("product1")}/variants`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls get product from productSerice", () => { + expect(ProductServiceMock.retrieveVariants).toHaveBeenCalledTimes(1) + expect(ProductServiceMock.retrieveVariants).toHaveBeenCalledWith( + IdMap.getId("product1") + ) + }) + + it("returns variants", () => { + expect(subject.body.variants[0]._id).toEqual(IdMap.getId("1")) + expect(subject.body.variants[1]._id).toEqual(IdMap.getId("2")) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/list-products.js b/packages/medusa/src/api/routes/admin/products/__tests__/list-products.js index 23ffb60262..c4b8bc82b0 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/list-products.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/list-products.js @@ -21,10 +21,10 @@ describe("GET /admin/products", () => { it("returns 200 and decorated products", () => { expect(subject.status).toEqual(200) - expect(subject.body[0]._id).toEqual(products.product1._id) - expect(subject.body[0].decorated).toEqual(true) - expect(subject.body[1]._id).toEqual(products.product2._id) - expect(subject.body[1].decorated).toEqual(true) + expect(subject.body.products[0]._id).toEqual(products.product1._id) + expect(subject.body.products[0].decorated).toEqual(true) + expect(subject.body.products[1]._id).toEqual(products.product2._id) + expect(subject.body.products[1].decorated).toEqual(true) }) it("calls update", () => { diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/publish-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/publish-product.js index 6e22c7e4b3..b3e5ed31e5 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/publish-product.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/publish-product.js @@ -25,7 +25,7 @@ describe("POST /admin/products/:id/publish", () => { }) it("returns product with published flag true", () => { - expect(subject.body.published).toEqual(true) + expect(subject.body.product.published).toEqual(true) }) it("calls service publish", () => { 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..92d97b4548 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/__tests__/update-variant.js @@ -0,0 +1,178 @@ +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.product._id).toEqual( + IdMap.getId("productWithOptions") + ) + expect(subject.body.product.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.product._id).toEqual( + IdMap.getId("productWithOptions") + ) + expect(subject.body.product.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.product._id).toEqual( + IdMap.getId("productWithOptions") + ) + expect(subject.body.product.decorated).toEqual(true) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/products/add-option.js b/packages/medusa/src/api/routes/admin/products/add-option.js index 82d25098b9..550e8e6061 100644 --- a/packages/medusa/src/api/routes/admin/products/add-option.js +++ b/packages/medusa/src/api/routes/admin/products/add-option.js @@ -4,7 +4,7 @@ export default async (req, res) => { const { id } = req.params const schema = Validator.object().keys({ - optionTitle: Validator.string().required(), + option_title: Validator.string().required(), }) const { value, error } = schema.validate(req.body) if (error) { @@ -13,10 +13,9 @@ export default async (req, res) => { try { const productService = req.scope.resolve("productService") - const product = await productService.retrieve(id) - await productService.addOption(product._id, value.optionTitle) - let newProduct = await productService.retrieve(product._id) - newProduct = await productService.decorate(newProduct, [ + const newProduct = await productService.addOption(id, value.option_title) + + const data = await productService.decorate(newProduct, [ "title", "description", "tags", @@ -26,9 +25,8 @@ export default async (req, res) => { "variants", "published", ]) - res.json(newProduct) + res.json({ product: data }) } catch (err) { - console.log(err) throw err } } diff --git a/packages/medusa/src/api/routes/admin/products/add-variant.js b/packages/medusa/src/api/routes/admin/products/add-variant.js deleted file mode 100644 index c427d0fd00..0000000000 --- a/packages/medusa/src/api/routes/admin/products/add-variant.js +++ /dev/null @@ -1,23 +0,0 @@ -export default async (req, res) => { - const { id, variantId } = req.params - - try { - const productService = req.scope.resolve("productService") - const product = await productService.retrieve(id) - await productService.addVariant(product._id, variantId) - let newProduct = await productService.retrieve(product._id) - newProduct = await productService.decorate(newProduct, [ - "title", - "description", - "tags", - "handle", - "images", - "options", - "variants", - "published", - ]) - res.json(newProduct) - } catch (err) { - throw err - } -} diff --git a/packages/medusa/src/api/routes/admin/products/create-product.js b/packages/medusa/src/api/routes/admin/products/create-product.js index 84cc36a596..2afbe02077 100644 --- a/packages/medusa/src/api/routes/admin/products/create-product.js +++ b/packages/medusa/src/api/routes/admin/products/create-product.js @@ -29,7 +29,7 @@ export default async (req, res) => { "variants", "published", ]) - res.json(newProduct) + res.json({ product: newProduct }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/products/create-variant.js b/packages/medusa/src/api/routes/admin/products/create-variant.js new file mode 100644 index 0000000000..7cfed5f766 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/create-variant.js @@ -0,0 +1,48 @@ +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() + .items({ + currency_code: Validator.string().required(), + amount: Validator.number().required(), + }) + .required(), + options: Validator.array() + .items({ + option_id: Validator.objectId().required(), + value: Validator.string().required(), + }) + .default([]), + 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 product = await productService.createVariant(id, value) + const data = await productService.decorate(product, [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ]) + res.json({ product: data }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/products/delete-option.js b/packages/medusa/src/api/routes/admin/products/delete-option.js index 33911faa91..5ee09ee263 100644 --- a/packages/medusa/src/api/routes/admin/products/delete-option.js +++ b/packages/medusa/src/api/routes/admin/products/delete-option.js @@ -1,11 +1,11 @@ export default async (req, res) => { - const { id, optionId } = req.params + const { id, option_id } = req.params try { const productService = req.scope.resolve("productService") - await productService.deleteOption(id, optionId) + await productService.deleteOption(id, option_id) res.json({ - optionId, + option_id, object: "option", deleted: true, }) diff --git a/packages/medusa/src/api/routes/admin/products/remove-variant.js b/packages/medusa/src/api/routes/admin/products/delete-variant.js similarity index 52% rename from packages/medusa/src/api/routes/admin/products/remove-variant.js rename to packages/medusa/src/api/routes/admin/products/delete-variant.js index d91212de82..99cbcf2ed2 100644 --- a/packages/medusa/src/api/routes/admin/products/remove-variant.js +++ b/packages/medusa/src/api/routes/admin/products/delete-variant.js @@ -1,11 +1,10 @@ export default async (req, res) => { - const { id, variantId } = req.params + const { id, variant_id } = 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, [ + const product = await productService.deleteVariant(id, variant_id) + const data = await productService.decorate(product, [ "title", "description", "tags", @@ -15,7 +14,12 @@ export default async (req, res) => { "variants", "published", ]) - res.json(updatedProduct) + + res.json({ + variant_id, + object: "product-variant", + deleted: true, + }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/products/get-product.js b/packages/medusa/src/api/routes/admin/products/get-product.js index 2a5a7ad8db..b7e01755e2 100644 --- a/packages/medusa/src/api/routes/admin/products/get-product.js +++ b/packages/medusa/src/api/routes/admin/products/get-product.js @@ -15,5 +15,5 @@ export default async (req, res) => { "published", ]) - res.json(product) + res.json({ product }) } diff --git a/packages/medusa/src/api/routes/admin/products/get-variants.js b/packages/medusa/src/api/routes/admin/products/get-variants.js new file mode 100644 index 0000000000..f37032cba5 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/products/get-variants.js @@ -0,0 +1,8 @@ +export default async (req, res) => { + const { id } = req.params + + const productService = req.scope.resolve("productService") + const variants = await productService.retrieveVariants(id) + + res.json({ variants }) +} diff --git a/packages/medusa/src/api/routes/admin/products/index.js b/packages/medusa/src/api/routes/admin/products/index.js index 90782013bc..3004091a57 100644 --- a/packages/medusa/src/api/routes/admin/products/index.js +++ b/packages/medusa/src/api/routes/admin/products/index.js @@ -12,23 +12,35 @@ 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.get( + "/:id/variants", + middlewares.wrap(require("./get-variants").default) + ) + route.post( - "/:id/options/:optionId", + "/:id/variants/:variant_id", + middlewares.wrap(require("./update-variant").default) + ) + + route.post( + "/:id/options/:option_id", middlewares.wrap(require("./update-option").default) ) route.post("/:id/options", middlewares.wrap(require("./add-option").default)) route.delete( - "/:id/variants/:variantId", - middlewares.wrap(require("./remove-variant").default) + "/:id/variants/:variant_id", + middlewares.wrap(require("./delete-variant").default) ) route.delete("/:id", middlewares.wrap(require("./delete-product").default)) route.delete( - "/:id/options/:optionId", + "/:id/options/:option_id", middlewares.wrap(require("./delete-option").default) ) diff --git a/packages/medusa/src/api/routes/admin/products/list-products.js b/packages/medusa/src/api/routes/admin/products/list-products.js index 26d6be1d25..427591e58f 100644 --- a/packages/medusa/src/api/routes/admin/products/list-products.js +++ b/packages/medusa/src/api/routes/admin/products/list-products.js @@ -17,7 +17,7 @@ export default async (req, res) => { ]) ) ) - res.json(products) + res.json({ products }) } catch (error) { throw error } diff --git a/packages/medusa/src/api/routes/admin/products/publish-product.js b/packages/medusa/src/api/routes/admin/products/publish-product.js index 7f622e0c4c..4f7cc97dbc 100644 --- a/packages/medusa/src/api/routes/admin/products/publish-product.js +++ b/packages/medusa/src/api/routes/admin/products/publish-product.js @@ -16,7 +16,7 @@ export default async (req, res) => { "variants", "published", ]) - res.json(publishedProduct) + res.json({ product: publishedProduct }) } catch (error) { throw error } diff --git a/packages/medusa/src/api/routes/admin/products/update-option.js b/packages/medusa/src/api/routes/admin/products/update-option.js index 2d214ab811..1a07876fae 100644 --- a/packages/medusa/src/api/routes/admin/products/update-option.js +++ b/packages/medusa/src/api/routes/admin/products/update-option.js @@ -1,11 +1,10 @@ import { MedusaError, Validator } from "medusa-core-utils" export default async (req, res) => { - const { id, optionId } = req.params + const { id, option_id } = req.params const schema = Validator.object().keys({ title: Validator.string(), - values: Validator.array().items(), }) const { value, error } = schema.validate(req.body) @@ -15,23 +14,25 @@ export default async (req, res) => { try { const productService = req.scope.resolve("productService") - const product = await productService.retrieve(id) - await productService.updateOption(product._id, optionId, value) + const product = await productService.updateOption(id, option_id, value) - let newProduct = await productService.retrieve(product._id) - newProduct = await productService.decorate(newProduct, [ - "title", - "description", - "tags", - "handle", - "images", - "options", - "variants", - "published", - ]) + const data = await productService.decorate( + product, + [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ], + ["variants"] + ) - res.json(newProduct) + res.json({ product: data }) } catch (err) { throw err } diff --git a/packages/medusa/src/api/routes/admin/products/update-product.js b/packages/medusa/src/api/routes/admin/products/update-product.js index 91972b240e..5ebd93b3d2 100644 --- a/packages/medusa/src/api/routes/admin/products/update-product.js +++ b/packages/medusa/src/api/routes/admin/products/update-product.js @@ -37,7 +37,7 @@ export default async (req, res) => { "variants", "published", ]) - res.json(newProduct) + res.json({ product: newProduct }) } 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..b25f4d5db2 --- /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({ product: data }) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/admin/regions/__tests__/add-country.js b/packages/medusa/src/api/routes/admin/regions/__tests__/add-country.js index 7e9b524552..cb7bdaab39 100644 --- a/packages/medusa/src/api/routes/admin/regions/__tests__/add-country.js +++ b/packages/medusa/src/api/routes/admin/regions/__tests__/add-country.js @@ -7,7 +7,7 @@ describe("POST /admin/regions/:region_id/countries", () => { let subject beforeAll(async () => { - const id = IdMap.getId("region") + const id = IdMap.getId("testRegion") subject = await request("POST", `/admin/regions/${id}/countries`, { payload: { country_code: "se", @@ -27,7 +27,7 @@ describe("POST /admin/regions/:region_id/countries", () => { it("calls service addCountry", () => { expect(RegionServiceMock.addCountry).toHaveBeenCalledTimes(1) expect(RegionServiceMock.addCountry).toHaveBeenCalledWith( - IdMap.getId("region"), + IdMap.getId("testRegion"), "se" ) }) diff --git a/packages/medusa/src/api/routes/admin/regions/__tests__/add-fulfillment-provider.js b/packages/medusa/src/api/routes/admin/regions/__tests__/add-fulfillment-provider.js index 7670a850f6..74b9df9f9f 100644 --- a/packages/medusa/src/api/routes/admin/regions/__tests__/add-fulfillment-provider.js +++ b/packages/medusa/src/api/routes/admin/regions/__tests__/add-fulfillment-provider.js @@ -7,7 +7,7 @@ describe("POST /admin/regions/:region_id/fulfillment-providers", () => { let subject beforeAll(async () => { - const id = IdMap.getId("region") + const id = IdMap.getId("testRegion") subject = await request( "POST", `/admin/regions/${id}/fulfillment-providers`, @@ -31,7 +31,7 @@ describe("POST /admin/regions/:region_id/fulfillment-providers", () => { it("calls service addCountry", () => { expect(RegionServiceMock.addFulfillmentProvider).toHaveBeenCalledTimes(1) expect(RegionServiceMock.addFulfillmentProvider).toHaveBeenCalledWith( - IdMap.getId("region"), + IdMap.getId("testRegion"), "default_provider" ) }) diff --git a/packages/medusa/src/api/routes/admin/regions/__tests__/add-payment-provider.js b/packages/medusa/src/api/routes/admin/regions/__tests__/add-payment-provider.js index 327fbfd374..4f8417d746 100644 --- a/packages/medusa/src/api/routes/admin/regions/__tests__/add-payment-provider.js +++ b/packages/medusa/src/api/routes/admin/regions/__tests__/add-payment-provider.js @@ -7,7 +7,7 @@ describe("POST /admin/regions/:region_id/payment-providers", () => { let subject beforeAll(async () => { - const id = IdMap.getId("region") + const id = IdMap.getId("testRegion") subject = await request( "POST", `/admin/regions/${id}/payment-providers`, @@ -31,7 +31,7 @@ describe("POST /admin/regions/:region_id/payment-providers", () => { it("calls service addCountry", () => { expect(RegionServiceMock.addPaymentProvider).toHaveBeenCalledTimes(1) expect(RegionServiceMock.addPaymentProvider).toHaveBeenCalledWith( - IdMap.getId("region"), + IdMap.getId("testRegion"), "default_provider" ) }) diff --git a/packages/medusa/src/api/routes/admin/regions/__tests__/get-region.js b/packages/medusa/src/api/routes/admin/regions/__tests__/get-region.js index 7d4cb3601d..14d1ee9a43 100644 --- a/packages/medusa/src/api/routes/admin/regions/__tests__/get-region.js +++ b/packages/medusa/src/api/routes/admin/regions/__tests__/get-region.js @@ -7,7 +7,7 @@ describe("GET /admin/regions/:region_id", () => { let subject beforeAll(async () => { - const id = IdMap.getId("region") + const id = IdMap.getId("testRegion") subject = await request("GET", `/admin/regions/${id}`, { adminSession: { jwt: { @@ -24,7 +24,7 @@ describe("GET /admin/regions/:region_id", () => { it("calls service addCountry", () => { expect(RegionServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(RegionServiceMock.retrieve).toHaveBeenCalledWith( - IdMap.getId("region") + IdMap.getId("testRegion") ) }) }) diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/create-line-item.js b/packages/medusa/src/api/routes/store/carts/__tests__/create-line-item.js index 5c511807f1..dd4c8b6076 100644 --- a/packages/medusa/src/api/routes/store/carts/__tests__/create-line-item.js +++ b/packages/medusa/src/api/routes/store/carts/__tests__/create-line-item.js @@ -32,8 +32,8 @@ describe("POST /store/carts/:id", () => { expect(LineItemServiceMock.generate).toHaveBeenCalledTimes(1) expect(LineItemServiceMock.generate).toHaveBeenCalledWith( IdMap.getId("testVariant"), - 3, - IdMap.getId("testRegion") + IdMap.getId("testRegion"), + 3 ) }) @@ -71,8 +71,8 @@ describe("POST /store/carts/:id", () => { expect(LineItemServiceMock.generate).toHaveBeenCalledTimes(1) expect(LineItemServiceMock.generate).toHaveBeenCalledWith( IdMap.getId("fail"), - 3, - IdMap.getId("testRegion") + IdMap.getId("testRegion"), + 3 ) }) diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/get-cart.js b/packages/medusa/src/api/routes/store/carts/__tests__/get-cart.js index 2fa20a5afb..a3e37b7247 100644 --- a/packages/medusa/src/api/routes/store/carts/__tests__/get-cart.js +++ b/packages/medusa/src/api/routes/store/carts/__tests__/get-cart.js @@ -43,7 +43,7 @@ describe("GET /store/carts", () => { expect(CartServiceMock.retrieve).toHaveBeenCalledWith("none") }) - it("returns products", () => { + it("returns 404 status", () => { expect(subject.status).toEqual(404) }) }) diff --git a/packages/medusa/src/api/routes/store/carts/create-line-item.js b/packages/medusa/src/api/routes/store/carts/create-line-item.js index bb9a6fc69b..9446a75606 100644 --- a/packages/medusa/src/api/routes/store/carts/create-line-item.js +++ b/packages/medusa/src/api/routes/store/carts/create-line-item.js @@ -20,8 +20,8 @@ export default async (req, res) => { const lineItem = await lineItemService.generate( value.variant_id, - value.quantity, - cart.region_id + cart.region_id, + value.quantity ) await cartService.addLineItem(cart._id, lineItem) diff --git a/packages/medusa/src/api/routes/store/carts/get-cart.js b/packages/medusa/src/api/routes/store/carts/get-cart.js index 34d45f6fa5..2611ccdd1a 100644 --- a/packages/medusa/src/api/routes/store/carts/get-cart.js +++ b/packages/medusa/src/api/routes/store/carts/get-cart.js @@ -1,15 +1,13 @@ export default async (req, res) => { const { id } = req.params - const cartService = req.scope.resolve("cartService") - let cart = await cartService.retrieve(id) + try { + const cartService = req.scope.resolve("cartService") + let cart = await cartService.retrieve(id) + cart = await cartService.decorate(cart) - if (!cart) { - res.sendStatus(404) - return + res.json(cart) + } catch (err) { + throw err } - - cart = await cartService.decorate(cart) - - res.json(cart) } diff --git a/packages/medusa/src/api/routes/store/products/__tests__/get-product.js b/packages/medusa/src/api/routes/store/products/__tests__/get-product.js index 64014cf697..e45284eee5 100644 --- a/packages/medusa/src/api/routes/store/products/__tests__/get-product.js +++ b/packages/medusa/src/api/routes/store/products/__tests__/get-product.js @@ -33,8 +33,8 @@ describe("Get product by id", () => { }) it("returns product decorated", () => { - expect(subject.body._id).toEqual(IdMap.getId("product1")) - expect(subject.body.decorated).toEqual(true) + expect(subject.body.product._id).toEqual(IdMap.getId("product1")) + expect(subject.body.product.decorated).toEqual(true) }) }) }) diff --git a/packages/medusa/src/api/routes/store/products/get-product.js b/packages/medusa/src/api/routes/store/products/get-product.js index 94de2897a7..82b235010a 100644 --- a/packages/medusa/src/api/routes/store/products/get-product.js +++ b/packages/medusa/src/api/routes/store/products/get-product.js @@ -13,16 +13,20 @@ export default async (req, res) => { const productService = req.scope.resolve("productService") let product = await productService.retrieve(value) - product = await productService.decorate(product, [ - "title", - "description", - "tags", - "handle", - "images", - "options", - "variants", - "published", - ]) + product = await productService.decorate( + product, + [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ], + ["variants"] + ) res.json(product) } diff --git a/packages/medusa/src/api/routes/store/products/list-products.js b/packages/medusa/src/api/routes/store/products/list-products.js index 6881f48cd3..f44805c72b 100644 --- a/packages/medusa/src/api/routes/store/products/list-products.js +++ b/packages/medusa/src/api/routes/store/products/list-products.js @@ -6,5 +6,24 @@ export default async (req, res) => { const productService = req.scope.resolve("productService") const products = await productService.list(selector) - res.json(products) + const data = await Promise.all( + products.map(p => + productService.decorate( + p, + [ + "title", + "description", + "tags", + "handle", + "images", + "options", + "variants", + "published", + ], + ["variants"] + ) + ) + ) + + res.json(data) } diff --git a/packages/medusa/src/models/__mocks__/order.js b/packages/medusa/src/models/__mocks__/order.js index dff006bbcd..e3c198ed5f 100644 --- a/packages/medusa/src/models/__mocks__/order.js +++ b/packages/medusa/src/models/__mocks__/order.js @@ -39,7 +39,7 @@ export const orders = { quantity: 10, }, ], - region: IdMap.getId("region-france"), + region_id: IdMap.getId("region-france"), customer_id: IdMap.getId("test-customer"), payment_method: { provider_id: "default_provider", @@ -102,7 +102,7 @@ export const orders = { quantity: 10, }, ], - region: IdMap.getId("region-france"), + region_id: IdMap.getId("region-france"), customer_id: IdMap.getId("test-customer"), payment_method: { provider_id: "default_provider", @@ -182,7 +182,7 @@ export const orders = { quantity: 10, }, ], - region: IdMap.getId("region-france"), + region_id: IdMap.getId("region-france"), customer_id: IdMap.getId("test-customer"), payment_method: { provider_id: "default_provider", diff --git a/packages/medusa/src/models/__mocks__/product.js b/packages/medusa/src/models/__mocks__/product.js index 0e1f1b0550..5b55d53994 100644 --- a/packages/medusa/src/models/__mocks__/product.js +++ b/packages/medusa/src/models/__mocks__/product.js @@ -10,6 +10,13 @@ export const ProductModelMock = { }), deleteOne: jest.fn().mockReturnValue(Promise.resolve()), findOne: jest.fn().mockImplementation(query => { + if (query._id === IdMap.getId("fakeId")) { + return Promise.resolve({ + _id: IdMap.getId("fakeId"), + title: "Product With Variants", + variants: ["1", "2", "3"], + }) + } if (query._id === IdMap.getId("productWithFourVariants")) { return Promise.resolve({ _id: IdMap.getId("productWithFourVariants"), diff --git a/packages/medusa/src/models/cart.js b/packages/medusa/src/models/cart.js index d89eae225f..1393e3b088 100644 --- a/packages/medusa/src/models/cart.js +++ b/packages/medusa/src/models/cart.js @@ -8,7 +8,7 @@ import LineItemSchema from "./schemas/line-item" import PaymentMethodSchema from "./schemas/payment-method" import ShippingMethodSchema from "./schemas/shipping-method" import AddressSchema from "./schemas/address" -import DiscountModel from "./discount" +import DiscountSchema from "./schemas/discount" class CartModel extends BaseModel { static modelName = "Cart" @@ -19,7 +19,7 @@ class CartModel extends BaseModel { shipping_address: { type: AddressSchema }, items: { type: [LineItemSchema], default: [] }, region_id: { type: String, required: true }, - discounts: { type: [DiscountModel.schema], default: [] }, + discounts: { type: [DiscountSchema], default: [] }, customer_id: { type: String, default: "" }, payment_sessions: { type: [PaymentMethodSchema], default: [] }, shipping_options: { type: [ShippingMethodSchema], default: [] }, diff --git a/packages/medusa/src/models/order.js b/packages/medusa/src/models/order.js index 03cfb03371..53e4601570 100644 --- a/packages/medusa/src/models/order.js +++ b/packages/medusa/src/models/order.js @@ -21,7 +21,7 @@ class OrderModel extends BaseModel { billing_address: { type: AddressSchema, required: true }, shipping_address: { type: AddressSchema, required: true }, items: { type: [LineItemSchema], required: true }, - region: { type: String, required: true }, + region_id: { type: String, required: true }, discounts: { type: [DiscountModel.schema], default: [] }, customer_id: { type: String, required: true }, payment_method: { type: PaymentMethodSchema, required: true }, diff --git a/packages/medusa/src/models/schemas/discount.js b/packages/medusa/src/models/schemas/discount.js new file mode 100644 index 0000000000..3ccf038215 --- /dev/null +++ b/packages/medusa/src/models/schemas/discount.js @@ -0,0 +1,14 @@ +import mongoose from "mongoose" + +import DiscountRule from "./discount-rule" + +export default new mongoose.Schema({ + code: { type: String }, + discount_rule: { type: DiscountRule }, + usage_count: { type: Number }, + disabled: { type: Boolean }, + starts_at: { type: Date }, + ends_at: { type: Date }, + regions: { type: [String], default: [] }, + metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, +}) diff --git a/packages/medusa/src/services/__mocks__/auth.js b/packages/medusa/src/services/__mocks__/auth.js index fd6f3411c8..6cc537c01b 100644 --- a/packages/medusa/src/services/__mocks__/auth.js +++ b/packages/medusa/src/services/__mocks__/auth.js @@ -1,7 +1,7 @@ import { IdMap } from "medusa-test-utils" const adminUser = { - _id: IdMap.getId("admin_user"), + _id: IdMap.getId("admin_user", true), password: "1235", name: "hi", } diff --git a/packages/medusa/src/services/__mocks__/cart.js b/packages/medusa/src/services/__mocks__/cart.js index 462f2f0fda..4d8b8c61c7 100644 --- a/packages/medusa/src/services/__mocks__/cart.js +++ b/packages/medusa/src/services/__mocks__/cart.js @@ -208,7 +208,7 @@ export const CartServiceMock = { if (cartId === IdMap.getId("cartWithPaySessions")) { return Promise.resolve(carts.cartWithPaySessions) } - return Promise.resolve(undefined) + throw new MedusaError(MedusaError.Types.NOT_FOUND, "cart not found") }), addLineItem: jest.fn().mockImplementation((cartId, lineItem) => { return Promise.resolve() diff --git a/packages/medusa/src/services/__mocks__/order.js b/packages/medusa/src/services/__mocks__/order.js index 781cbbf565..0d3eee56fa 100644 --- a/packages/medusa/src/services/__mocks__/order.js +++ b/packages/medusa/src/services/__mocks__/order.js @@ -41,7 +41,7 @@ export const orders = { quantity: 10, }, ], - region: IdMap.getId("testRegion"), + region_id: IdMap.getId("testRegion"), customer_id: IdMap.getId("testCustomer"), payment_method: { provider_id: "default_provider", @@ -94,7 +94,7 @@ export const orders = { quantity: 10, }, ], - region: IdMap.getId("region-france"), + region_id: IdMap.getId("region-france"), customer_id: IdMap.getId("test-customer"), payment_method: { provider_id: "default_provider", diff --git a/packages/medusa/src/services/__mocks__/product-variant.js b/packages/medusa/src/services/__mocks__/product-variant.js index 20bc943116..3259567737 100644 --- a/packages/medusa/src/services/__mocks__/product-variant.js +++ b/packages/medusa/src/services/__mocks__/product-variant.js @@ -103,6 +103,7 @@ const emptyVariant = { const eur10us12 = { _id: IdMap.getId("eur-10-us-12"), + title: "EUR10US-12", } export const variants = { @@ -193,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 e8bdcfd3a9..29cfbc6ea2 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) => { @@ -56,8 +56,16 @@ export const ProductServiceMock = { addOption: jest.fn().mockImplementation((productId, optionTitle) => { return Promise.resolve(products.productWithOptions) }), - updateOption: jest.fn().mockReturnValue(Promise.resolve()), + updateOption: jest + .fn() + .mockReturnValue(Promise.resolve(products.productWithOptions)), + updateOptionValue: jest.fn().mockReturnValue(Promise.resolve()), deleteOption: jest.fn().mockReturnValue(Promise.resolve()), + retrieveVariants: jest + .fn() + .mockReturnValue( + Promise.resolve([{ _id: IdMap.getId("1") }, { _id: IdMap.getId("2") }]) + ), retrieve: jest.fn().mockImplementation(productId => { if (productId === IdMap.getId("product1")) { return Promise.resolve(products.product1) @@ -101,6 +109,7 @@ export const ProductServiceMock = { { _id: "1234", title: "test", + thumbnail: "test.1234", }, ]) } diff --git a/packages/medusa/src/services/__mocks__/region.js b/packages/medusa/src/services/__mocks__/region.js index 7685b15711..3df3e94cb9 100644 --- a/packages/medusa/src/services/__mocks__/region.js +++ b/packages/medusa/src/services/__mocks__/region.js @@ -21,18 +21,21 @@ export const regions = { }, regionUs: { _id: IdMap.getId("region-us"), + tax_rate: 0.25, name: "USA", countries: ["US"], currency_code: "usd", }, regionGermany: { _id: IdMap.getId("region-de"), + tax_rate: 0.25, name: "Germany", countries: ["DE"], currency_code: "eur", }, regionSweden: { _id: IdMap.getId("region-se"), + tax_rate: 0.25, name: "Sweden", countries: ["SE"], currency_code: "sek", @@ -56,7 +59,7 @@ export const RegionServiceMock = { if (regionId === IdMap.getId("region-se")) { return Promise.resolve(regions.regionSweden) } - return Promise.resolve(undefined) + throw Error(regionId + "not found") }), delete: jest.fn().mockImplementation(data => Promise.resolve()), create: jest.fn().mockImplementation(data => Promise.resolve()), diff --git a/packages/medusa/src/services/__tests__/line-item.js b/packages/medusa/src/services/__tests__/line-item.js index d496eaaea2..ac8e2e29bf 100644 --- a/packages/medusa/src/services/__tests__/line-item.js +++ b/packages/medusa/src/services/__tests__/line-item.js @@ -24,24 +24,22 @@ describe("LineItemService", () => { it("generates line item and successfully defaults quantity of content to 1", () => { expect(result).toEqual({ + title: "test", + description: "EUR10US-12", + thumbnail: "test.1234", content: { unit_price: 10, variant: { _id: IdMap.getId("eur-10-us-12"), + title: "EUR10US-12", }, product: { _id: "1234", title: "test", + thumbnail: "test.1234", }, quantity: 1, }, - product: { - _id: "1234", - title: "test", - }, - variant: { - _id: IdMap.getId("eur-10-us-12"), - }, quantity: 2, }) }) diff --git a/packages/medusa/src/services/__tests__/product.js b/packages/medusa/src/services/__tests__/product.js index 07fb66ba76..01455b166f 100644 --- a/packages/medusa/src/services/__tests__/product.js +++ b/packages/medusa/src/services/__tests__/product.js @@ -130,7 +130,7 @@ describe("ProductService", () => { }) const fakeProduct = { - _id: "1234", + _id: IdMap.getId("fakeId"), variants: ["1", "2", "3"], tags: "testtag1, testtag2", handle: "test-product", @@ -148,7 +148,7 @@ describe("ProductService", () => { ["variants"] ) expect(decorated).toEqual({ - _id: "1234", + _id: IdMap.getId("fakeId"), metadata: { testKey: "testValue" }, variants: [variants.one, variants.two, variants.three], }) @@ -161,7 +161,7 @@ describe("ProductService", () => { ["variants"] ) expect(decorated).toEqual({ - _id: "1234", + _id: IdMap.getId("fakeId"), metadata: { testKey: "testValue" }, handle: "test-product", variants: [variants.one, variants.two, variants.three], @@ -174,7 +174,7 @@ describe("ProductService", () => { "tags", ]) expect(decorated).toEqual({ - _id: "1234", + _id: IdMap.getId("fakeId"), metadata: { testKey: "testValue" }, tags: "testtag1, testtag2", handle: "test-product", @@ -184,7 +184,7 @@ describe("ProductService", () => { it("returns decorated product with metadata", async () => { const decorated = await productService.decorate(fakeProduct, []) expect(decorated).toEqual({ - _id: "1234", + _id: IdMap.getId("fakeId"), metadata: { testKey: "testValue" }, }) }) @@ -310,79 +310,168 @@ describe("ProductService", () => { }) }) - describe("addVariant", () => { + describe("createVariant", () => { const productService = new ProductService({ productModel: ProductModelMock, productVariantService: ProductVariantServiceMock, }) - beforeEach(() => { + afterEach(() => { jest.clearAllMocks() }) 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.retrieve).toBeCalledTimes(1) - expect(ProductVariantServiceMock.retrieve).toBeCalledWith("1") - expect(ProductModelMock.findOne).toBeCalledTimes(1) + 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(ProductModelMock.findOne).toBeCalledTimes(2) 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("add variant to product successfully", async () => { + await productService.createVariant( + IdMap.getId("productWithFourVariants"), + { + title: "variant1", + options: [ + { + option_id: IdMap.getId("color_id"), + value: "blue", + }, + { + option_id: IdMap.getId("size_id"), + value: "1600", + }, + ], + } + ) + + 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: "1600", + }, + ], + }) + + expect(ProductModelMock.findOne).toBeCalledTimes(2) + expect(ProductModelMock.findOne).toBeCalledWith({ + _id: IdMap.getId("productWithFourVariants"), + }) + + expect(ProductModelMock.updateOne).toBeCalledTimes(1) + expect(ProductModelMock.updateOne).toBeCalledWith( + { _id: IdMap.getId("productWithFourVariants") }, + { $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 +628,7 @@ describe("ProductService", () => { }) }) - describe("removeVariant", () => { + describe("deleteVariant", () => { const productService = new ProductService({ productModel: ProductModelMock, productVariantService: ProductVariantServiceMock, @@ -550,11 +639,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 +838,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/cart.js b/packages/medusa/src/services/cart.js index b6767d8c6a..ec39c9710d 100644 --- a/packages/medusa/src/services/cart.js +++ b/packages/medusa/src/services/cart.js @@ -23,6 +23,7 @@ class CartService extends BaseService { shippingOptionService, shippingProfileService, discountService, + totalsService, }) { super() @@ -55,6 +56,9 @@ class CartService extends BaseService { /** @private @const {DiscountService} */ this.discountService_ = discountService + + /** @private @const {DiscountService} */ + this.totalsService_ = totalsService } /** @@ -64,7 +68,7 @@ class CartService extends BaseService { */ validateId_(rawId) { const schema = Validator.objectId() - const { value, error } = schema.validate(rawId) + const { value, error } = schema.validate(rawId.toString()) if (error) { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, @@ -221,7 +225,9 @@ class CartService extends BaseService { * @return {Cart} return the decorated cart. */ async decorate(cart, fields, expandFields = []) { - return cart + const c = cart.toObject() + c.total = await this.totalsService_.getTotal(cart) + return c } /** diff --git a/packages/medusa/src/services/line-item.js b/packages/medusa/src/services/line-item.js index f8f60c6b92..92c5e5ec0e 100644 --- a/packages/medusa/src/services/line-item.js +++ b/packages/medusa/src/services/line-item.js @@ -101,9 +101,10 @@ class LineItemService extends BaseService { ) return { - variant, - product, + title: product.title, + description: variant.title, quantity, + thumbnail: product.thumbnail, content: { unit_price, variant, diff --git a/packages/medusa/src/services/product-variant.js b/packages/medusa/src/services/product-variant.js index 83c0a05ac5..a4315a1945 100644 --- a/packages/medusa/src/services/product-variant.js +++ b/packages/medusa/src/services/product-variant.js @@ -28,7 +28,7 @@ class ProductVariantService extends BaseService { */ validateId_(rawId) { const schema = Validator.objectId() - const { value, error } = schema.validate(rawId) + const { value, error } = schema.validate(rawId.toString()) if (error) { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, @@ -61,6 +61,7 @@ class ProductVariantService extends BaseService { return variant } + // TODO: Validate productVariant /** * Creates an unpublished product variant. * @param {object} variant - the variant to create @@ -296,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: variantId, "options.option_id": optionId }, + { $set: { "options.$.value": `${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 b5e2e5a255..bf999adbcc 100644 --- a/packages/medusa/src/services/product.js +++ b/packages/medusa/src/services/product.js @@ -29,7 +29,7 @@ class ProductService extends BaseService { */ validateId_(rawId) { const schema = Validator.objectId() - const { value, error } = schema.validate(rawId) + const { value, error } = schema.validate(rawId.toString()) if (error) { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, @@ -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, @@ -184,7 +192,7 @@ class ProductService extends BaseService { } product.options.forEach(option => { - if (!variant.options.find(vo => vo.option_id === option._id)) { + if (!variant.options.find(vo => option._id.equals(vo.option_id))) { throw new MedusaError( MedusaError.Types.INVALID_DATA, `Variant options do not contain value for ${option.title}` @@ -193,15 +201,18 @@ class ProductService extends BaseService { }) let combinationExists = false - if (product.variants) { + if (product.variants && product.variants.length) { + const variants = await this.retrieveVariants(productId) // Check if option value of the variant to add already exists. Go through // each existing variant. Check if this variants option values are // identical to the option values of the variant being added. - combinationExists = product.variants.some(async vId => { - const v = await this.productVariantService_.retrieve(vId) - return v.options.reduce((acc, option, index) => { - return acc && option.value === variant.options[index].value - }, true) + combinationExists = variants.some(v => { + return v.options.every(option => { + const variantOption = variant.options.find(o => + option.option_id.equals(o.option_id) + ) + return option.value === variantOption.value + }) }) } @@ -212,9 +223,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 } } ) } @@ -414,11 +427,11 @@ class ProductService extends BaseService { async deleteOption(productId, optionId) { const product = await this.retrieve(productId) - if (!product.options.find(o => o._id === optionId)) { + if (!product.options.find(o => o._id.equals(optionId))) { return Promise.resolve() } - if (product.variants) { + if (product.variants.length) { // For the option we want to delete, make sure that all variants have the // same option values. The reason for doing is, that we want to avoid // duplicate variants. For example, if we have a product with size and @@ -430,14 +443,14 @@ class ProductService extends BaseService { const firstVariant = await this.productVariantService_.retrieve( product.variants[0] ) - const valueToMatch = firstVariant.options.find( - o => o.option_id === optionId + const valueToMatch = firstVariant.options.find(o => + o.option_id.equals(optionId) ).value const equalsFirst = await Promise.all( product.variants.map(async vId => { const v = await this.productVariantService_.retrieve(vId) - const option = v.options.find(o => o.option_id === optionId) + const option = v.options.find(o => o.option_id.equals(optionId)) return option.value === valueToMatch }) ) @@ -479,9 +492,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 +507,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.equals(optionId)) { + return option.value === value + } + + const toUpdateOption = toUpdate.options.find(o => + option.option_id.equals(o.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. @@ -503,11 +570,7 @@ class ProductService extends BaseService { const requiredFields = ["_id", "metadata"] const decorated = _.pick(product, fields.concat(requiredFields)) if (expandFields.includes("variants")) { - decorated.variants = await Promise.all( - product.variants.map(variantId => - this.productVariantService_.retrieve(variantId) - ) - ) + decorated.variants = await this.retrieveVariants(product._id) } return decorated } diff --git a/packages/medusa/src/services/region.js b/packages/medusa/src/services/region.js index 48a35f60e7..ba9a5661e4 100644 --- a/packages/medusa/src/services/region.js +++ b/packages/medusa/src/services/region.js @@ -166,7 +166,7 @@ class RegionService extends BaseService { */ validateId_(rawId) { const schema = Validator.objectId() - const { value, error } = schema.validate(rawId) + const { value, error } = schema.validate(rawId.toString()) if (error) { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, @@ -195,6 +195,16 @@ class RegionService extends BaseService { return region } + /** + * Lists all regions based on a query + * @param {string} regionId - the id of the region to retrieve + * @return {Region} the region + */ + async list(query) { + const regions = await this.regionModel_.find(query) + return regions + } + /** * Deletes a region. * @param {string} regionId - the region to delete diff --git a/packages/medusa/src/services/totals.js b/packages/medusa/src/services/totals.js index 1e039f4405..0d13ba2d88 100644 --- a/packages/medusa/src/services/totals.js +++ b/packages/medusa/src/services/totals.js @@ -74,7 +74,7 @@ class TotalsService extends BaseService { async getTaxTotal(object) { const subtotal = this.getSubtotal(object) const shippingTotal = this.getShippingTotal(object) - const region = await this.regionService_.retrieve(object.region) + const region = await this.regionService_.retrieve(object.region_id) const { tax_rate } = region return (subtotal + shippingTotal) * tax_rate } @@ -101,7 +101,7 @@ class TotalsService extends BaseService { const subtotal = this.getSubtotal({ items: lineItems }) - const region = await this.regionService_.retrieve(order.region) + const region = await this.regionService_.retrieve(order.region_id) // if nothing is discounted, return the subtotal of line items if (!discount) { diff --git a/packages/medusa/src/services/user.js b/packages/medusa/src/services/user.js index c0ccff3a08..7c7a6207a6 100644 --- a/packages/medusa/src/services/user.js +++ b/packages/medusa/src/services/user.js @@ -26,7 +26,7 @@ class UserService extends BaseService { */ validateId_(rawId) { const schema = Validator.objectId() - const { value, error } = schema.validate(rawId) + const { value, error } = schema.validate(rawId.toString()) if (error) { throw new MedusaError( MedusaError.Types.INVALID_ARGUMENT, diff --git a/packages/medusa/yarn.lock b/packages/medusa/yarn.lock index db34658fff..a9d4ee9261 100644 --- a/packages/medusa/yarn.lock +++ b/packages/medusa/yarn.lock @@ -4522,14 +4522,6 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -medusa-core-utils@^0.3.0: - version "0.1.39" - resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-0.1.39.tgz#d57816c9bd43f9a92883650c1e66add1665291df" - integrity sha512-R8+U1ile7if+nR6Cjh5exunx0ETV0OfkWUUBUpz1KmHSDv0V0CcvQqU9lcZesPFDEbu3Y2iEjsCqidVA4nG2nQ== - dependencies: - "@hapi/joi" "^16.1.8" - joi-objectid "^3.0.1" - medusa-interfaces@^0.1.27: version "0.1.27" resolved "https://registry.yarnpkg.com/medusa-interfaces/-/medusa-interfaces-0.1.27.tgz#e77f9a9f82a7118eac8b35c1498ef8a5cec78898" @@ -4537,13 +4529,6 @@ medusa-interfaces@^0.1.27: dependencies: mongoose "^5.8.0" -medusa-test-utils@^0.3.0: - version "0.1.39" - resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-0.1.39.tgz#b7c166006a2fa4f02e52ab3bfafc19a3ae787f3e" - integrity sha512-M/Br8/HYvl7x2oLnme4NxdQwoyV0XUyOWiCyvPp7q1HUTB684lhJf1MikZVrcSjsh2L1rpyi3GRbKdf4cpJWvw== - dependencies: - mongoose "^5.8.0" - memory-pager@^1.0.2: version "1.5.0" resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5"