Adds ProductVariantService
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import IdMap from "../../helpers/id-map"
|
||||
|
||||
export const ProductVariantModelMock = {
|
||||
create: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
updateOne: jest.fn().mockImplementation((query, update) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
findOne: jest.fn().mockImplementation(query => {
|
||||
if (query._id === IdMap.getId("validId")) {
|
||||
return Promise.resolve({
|
||||
_id: IdMap.getId("validId"),
|
||||
title: "test",
|
||||
})
|
||||
}
|
||||
if (query._id === IdMap.getId("testVariant")) {
|
||||
return Promise.resolve({
|
||||
_id: IdMap.getId("testVariant"),
|
||||
title: "test",
|
||||
})
|
||||
}
|
||||
if (query._id === IdMap.getId("deleteId")) {
|
||||
return Promise.resolve({
|
||||
_id: IdMap.getId("deleteId"),
|
||||
title: "test",
|
||||
})
|
||||
}
|
||||
if (query._id === IdMap.getId("failId")) {
|
||||
return Promise.reject(new Error("test error"))
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import IdMap from "../../../helpers/id-map"
|
||||
import IdMap from "../../helpers/id-map"
|
||||
|
||||
export const ProductModelMock = {
|
||||
create: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
@@ -15,6 +15,8 @@ class ProductVariantModel extends BaseModel {
|
||||
prices: { type: [MoneyAmountSchema], default: [], required: true },
|
||||
options: { type: [OptionValueSchema], default: [] },
|
||||
image: { type: String, default: "" },
|
||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||
published: { type: Boolean, default: false },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import mongoose from "mongoose"
|
||||
|
||||
export default new mongoose.Schema({
|
||||
product_id: { type: mongoose.Types.ObjectId, required: true },
|
||||
title: { type: String, required: true },
|
||||
values: { type: [String], default: [] },
|
||||
})
|
||||
|
||||
@@ -60,6 +60,21 @@ const variant4 = {
|
||||
],
|
||||
}
|
||||
|
||||
const variant5 = {
|
||||
_id: "5",
|
||||
title: "Variant with valid id",
|
||||
options: [
|
||||
{
|
||||
option_id: IdMap.getId("color_id"),
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
option_id: IdMap.getId("size_id"),
|
||||
value: "50",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const invalidVariant = {
|
||||
_id: "invalid_option",
|
||||
title: "variant3",
|
||||
@@ -104,6 +119,9 @@ export const ProductVariantServiceMock = {
|
||||
if (variantId === "4") {
|
||||
return Promise.resolve(variant4)
|
||||
}
|
||||
if (variantId === IdMap.getId("validId")) {
|
||||
return Promise.resolve(variant5)
|
||||
}
|
||||
if (variantId === "invalid_option") {
|
||||
return Promise.resolve(invalidVariant)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
import IdMap from "../../helpers/id-map"
|
||||
|
||||
export const ProductServiceMock = {
|
||||
createDraft: jest.fn().mockImplementation(data => {
|
||||
return Promise.resolve(data)
|
||||
}),
|
||||
list: jest.fn().mockImplementation(data => {
|
||||
if (data.variants === IdMap.getId("testVariant")) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
_id: "1234",
|
||||
title: "test",
|
||||
options: [
|
||||
{
|
||||
_id: IdMap.getId("testOptionId"),
|
||||
title: "testOption",
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
}
|
||||
return Promise.resolve([])
|
||||
}),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import IdMap from "../../../helpers/id-map"
|
||||
|
||||
const variant1 = {
|
||||
_id: "1",
|
||||
title: "variant1",
|
||||
options: [
|
||||
{
|
||||
option_id: IdMap.getId("color_id"),
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
option_id: IdMap.getId("size_id"),
|
||||
value: "160",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const variant2 = {
|
||||
_id: "2",
|
||||
title: "variant2",
|
||||
options: [
|
||||
{
|
||||
option_id: IdMap.getId("color_id"),
|
||||
value: "black",
|
||||
},
|
||||
{
|
||||
option_id: IdMap.getId("size_id"),
|
||||
value: "160",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const variant3 = {
|
||||
_id: "3",
|
||||
title: "variant3",
|
||||
options: [
|
||||
{
|
||||
option_id: IdMap.getId("color_id"),
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
option_id: IdMap.getId("size_id"),
|
||||
value: "150",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const variant4 = {
|
||||
_id: "4",
|
||||
title: "variant4",
|
||||
options: [
|
||||
{
|
||||
option_id: IdMap.getId("color_id"),
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
option_id: IdMap.getId("size_id"),
|
||||
value: "50",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const invalidVariant = {
|
||||
_id: "invalid_option",
|
||||
title: "variant3",
|
||||
options: [
|
||||
{
|
||||
option_id: "invalid_id",
|
||||
value: "blue",
|
||||
},
|
||||
{
|
||||
option_id: IdMap.getId("size_id"),
|
||||
value: "150",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const emptyVariant = {
|
||||
_id: "empty_option",
|
||||
title: "variant3",
|
||||
options: [],
|
||||
}
|
||||
|
||||
export const variants = {
|
||||
one: variant1,
|
||||
two: variant2,
|
||||
three: variant3,
|
||||
four: variant4,
|
||||
invalid_variant: invalidVariant,
|
||||
empty_variant: emptyVariant,
|
||||
}
|
||||
|
||||
export const ProductVariantServiceMock = {
|
||||
retrieve: jest.fn().mockImplementation(variantId => {
|
||||
if (variantId === "1") {
|
||||
return Promise.resolve(variant1)
|
||||
}
|
||||
if (variantId === "2") {
|
||||
return Promise.resolve(variant2)
|
||||
}
|
||||
if (variantId === "3") {
|
||||
return Promise.resolve(variant3)
|
||||
}
|
||||
if (variantId === "4") {
|
||||
return Promise.resolve(variant4)
|
||||
}
|
||||
if (variantId === "invalid_option") {
|
||||
return Promise.resolve(invalidVariant)
|
||||
}
|
||||
if (variantId === "empty_option") {
|
||||
return Promise.resolve(emptyVariant)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
delete: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
addOptionValue: jest.fn().mockImplementation((variantId, optionId, value) => {
|
||||
return Promise.resolve({})
|
||||
}),
|
||||
deleteOptionValue: jest.fn().mockImplementation((variantId, optionId) => {
|
||||
return Promise.resolve({})
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
import mongoose from "mongoose"
|
||||
import ProductVariantService from "../product-variant"
|
||||
import { ProductVariantModelMock } from "../../models/__mocks__/product-variant"
|
||||
import IdMap from "../../helpers/id-map"
|
||||
import { ProductServiceMock } from "../__mocks__/product"
|
||||
|
||||
describe("ProductVariantService", () => {
|
||||
describe("retrieve", () => {
|
||||
describe("successfully get product variant", () => {
|
||||
let res
|
||||
beforeAll(async () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
res = await productVariantService.retrieve(IdMap.getId("validId"))
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls model layer findOne", () => {
|
||||
expect(ProductVariantModelMock.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantModelMock.findOne).toHaveBeenCalledWith({
|
||||
_id: IdMap.getId("validId"),
|
||||
})
|
||||
})
|
||||
|
||||
it("returns correct variant", () => {
|
||||
expect(res.title).toEqual("test")
|
||||
})
|
||||
})
|
||||
|
||||
describe("query fail", () => {
|
||||
let res
|
||||
beforeAll(async () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
await productVariantService
|
||||
.retrieve(IdMap.getId("failId"))
|
||||
.catch(err => {
|
||||
res = err
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls model layer findOne", () => {
|
||||
expect(ProductVariantModelMock.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantModelMock.findOne).toHaveBeenCalledWith({
|
||||
_id: IdMap.getId("failId"),
|
||||
})
|
||||
})
|
||||
|
||||
it("model query throws error", () => {
|
||||
expect(res.name).toEqual("database_error")
|
||||
expect(res.message).toEqual("test error")
|
||||
})
|
||||
})
|
||||
})
|
||||
describe("createDraft", () => {
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks()
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
productVariantService.createDraft({
|
||||
title: "Test Prod",
|
||||
image: "test-image",
|
||||
options: [],
|
||||
prices: [
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 100,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("calls model layer create", () => {
|
||||
expect(ProductVariantModelMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantModelMock.create).toHaveBeenCalledWith({
|
||||
title: "Test Prod",
|
||||
image: "test-image",
|
||||
options: [],
|
||||
prices: [
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 100,
|
||||
},
|
||||
],
|
||||
published: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("publishVariant", () => {
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks()
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
productVariantService.publish(IdMap.getId("variantId"))
|
||||
})
|
||||
|
||||
it("calls model layer create", () => {
|
||||
expect(ProductVariantModelMock.create).toHaveBeenCalledTimes(0)
|
||||
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantModelMock.updateOne).toHaveBeenCalledWith(
|
||||
{ _id: IdMap.getId("variantId") },
|
||||
{ $set: { published: true } }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls updateOne with correct params", async () => {
|
||||
const id = mongoose.Types.ObjectId()
|
||||
|
||||
await productVariantService.update(`${id}`, { title: "new title" })
|
||||
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledTimes(1)
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledWith(
|
||||
{ _id: `${id}` },
|
||||
{ $set: { title: "new title" } },
|
||||
{ runValidators: true }
|
||||
)
|
||||
})
|
||||
|
||||
it("throw error on invalid variant id type", async () => {
|
||||
try {
|
||||
await productVariantService.update(19314235, { title: "new title" })
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"The variantId could not be casted to an ObjectId"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("throws error when trying to update metadata", async () => {
|
||||
const id = mongoose.Types.ObjectId()
|
||||
try {
|
||||
await productVariantService.update(`${id}`, {
|
||||
metadata: { key: "value" },
|
||||
})
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual("Use setMetadata to update metadata fields")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("decorate", () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
const fakeVariant = {
|
||||
_id: "1234",
|
||||
title: "test",
|
||||
image: "test-image",
|
||||
prices: [
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 100,
|
||||
},
|
||||
],
|
||||
metadata: { testKey: "testValue" },
|
||||
published: true,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns decorated product", async () => {
|
||||
const decorated = await productVariantService.decorate(fakeVariant, [])
|
||||
expect(decorated).toEqual({
|
||||
_id: "1234",
|
||||
metadata: { testKey: "testValue" },
|
||||
})
|
||||
})
|
||||
|
||||
it("returns decorated product with handle", async () => {
|
||||
const decorated = await productVariantService.decorate(fakeVariant, [
|
||||
"prices",
|
||||
])
|
||||
expect(decorated).toEqual({
|
||||
_id: "1234",
|
||||
metadata: { testKey: "testValue" },
|
||||
prices: [
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 100,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("setMetadata", () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls updateOne with correct params", async () => {
|
||||
const id = mongoose.Types.ObjectId()
|
||||
await productVariantService.setMetadata(
|
||||
`${id}`,
|
||||
"metadata",
|
||||
"testMetadata"
|
||||
)
|
||||
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledTimes(1)
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledWith(
|
||||
{ _id: `${id}` },
|
||||
{ $set: { "metadata.metadata": "testMetadata" } }
|
||||
)
|
||||
})
|
||||
|
||||
it("throw error on invalid key type", async () => {
|
||||
const id = mongoose.Types.ObjectId()
|
||||
|
||||
try {
|
||||
await productVariantService.setMetadata(`${id}`, 1234, "nono")
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"Key type is invalid. Metadata keys must be strings"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("throws error on invalid variantId type", async () => {
|
||||
try {
|
||||
await productVariantService.setMetadata("fakeVariantId", 1234, "nono")
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"The variantId could not be casted to an ObjectId"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("addOptionValue", () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
productService: ProductServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("it successfully adds option value", async () => {
|
||||
await productVariantService.addOptionValue(
|
||||
IdMap.getId("testVariant"),
|
||||
IdMap.getId("testOptionId"),
|
||||
"testValue"
|
||||
)
|
||||
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledTimes(1)
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledWith(
|
||||
{ _id: IdMap.getId("testVariant") },
|
||||
{
|
||||
$push: {
|
||||
options: {
|
||||
option_id: IdMap.getId("testOptionId"),
|
||||
value: "testValue",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("it successfully casts numeric option value to string", async () => {
|
||||
await productVariantService.addOptionValue(
|
||||
IdMap.getId("testVariant"),
|
||||
IdMap.getId("testOptionId"),
|
||||
1234
|
||||
)
|
||||
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledTimes(1)
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledWith(
|
||||
{ _id: IdMap.getId("testVariant") },
|
||||
{
|
||||
$push: {
|
||||
options: {
|
||||
option_id: IdMap.getId("testOptionId"),
|
||||
value: "1234",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("throw error if product with variant does not exist", async () => {
|
||||
try {
|
||||
await productVariantService.addOptionValue(
|
||||
IdMap.getId("failId"),
|
||||
IdMap.getId("testOptionId"),
|
||||
"testValue"
|
||||
)
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`Products with variant: ${IdMap.getId("failId")} was not found`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("throw error if product does not have option id", async () => {
|
||||
try {
|
||||
await productVariantService.addOptionValue(
|
||||
IdMap.getId("testVariant"),
|
||||
IdMap.getId("failOptionId"),
|
||||
"testValue"
|
||||
)
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`Associated product does not have option: ${IdMap.getId(
|
||||
"failOptionId"
|
||||
)}`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("throw error if option value is not string", async () => {
|
||||
try {
|
||||
await productVariantService.addOptionValue(
|
||||
IdMap.getId("testVariant"),
|
||||
IdMap.getId("testOptionId"),
|
||||
{}
|
||||
)
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`Option value is not of type string or number`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("deleteOptionValue", () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
productService: ProductServiceMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully deletes option value from variant", async () => {
|
||||
await productVariantService.deleteOptionValue(
|
||||
IdMap.getId("testVariant"),
|
||||
IdMap.getId("testing")
|
||||
)
|
||||
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledTimes(1)
|
||||
expect(ProductVariantModelMock.updateOne).toBeCalledWith(
|
||||
{ _id: IdMap.getId("testVariant") },
|
||||
{ $pull: { options: { option_id: IdMap.getId("testing") } } }
|
||||
)
|
||||
})
|
||||
|
||||
it("throw error if product still has the option id of the option value we are trying to delete", async () => {
|
||||
try {
|
||||
await productVariantService.deleteOptionValue(
|
||||
IdMap.getId("testVariant"),
|
||||
IdMap.getId("testOptionId")
|
||||
)
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
`Associated product has option with id: ${IdMap.getId(
|
||||
"testOptionId"
|
||||
)}`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete", () => {
|
||||
const productVariantService = new ProductVariantService({
|
||||
productVariantModel: ProductVariantModelMock,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("deletes all variants and product successfully", async () => {
|
||||
await productVariantService.delete(IdMap.getId("deleteId"))
|
||||
|
||||
expect(ProductVariantModelMock.deleteOne).toBeCalledTimes(1)
|
||||
expect(ProductVariantModelMock.deleteOne).toBeCalledWith({
|
||||
_id: IdMap.getId("deleteId"),
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,10 @@
|
||||
import mongoose from "mongoose"
|
||||
import ProductService from "../product"
|
||||
import { ProductModelMock } from "./mocks/product-model"
|
||||
import { ProductModelMock } from "../../models/__mocks__/product"
|
||||
import {
|
||||
ProductVariantServiceMock,
|
||||
variants,
|
||||
} from "./mocks/product-variant-service"
|
||||
} from "../__mocks__/product-variant"
|
||||
import IdMap from "../../helpers/id-map"
|
||||
|
||||
describe("ProductService", () => {
|
||||
@@ -107,6 +107,7 @@ describe("ProductService", () => {
|
||||
jest.clearAllMocks()
|
||||
const productService = new ProductService({
|
||||
productModel: ProductModelMock,
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
})
|
||||
|
||||
productService.publish(IdMap.getId("productId"))
|
||||
@@ -189,7 +190,7 @@ describe("ProductService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("add metadata to product model", () => {
|
||||
describe("setMetadata", () => {
|
||||
const productService = new ProductService({
|
||||
productModel: ProductModelMock,
|
||||
})
|
||||
@@ -232,7 +233,7 @@ describe("ProductService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("update product", () => {
|
||||
describe("update", () => {
|
||||
const productService = new ProductService({
|
||||
productModel: ProductModelMock,
|
||||
})
|
||||
@@ -285,7 +286,7 @@ describe("ProductService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete product", () => {
|
||||
describe("delete", () => {
|
||||
const productService = new ProductService({
|
||||
productModel: ProductModelMock,
|
||||
productVariantService: ProductVariantServiceMock,
|
||||
@@ -307,16 +308,6 @@ describe("ProductService", () => {
|
||||
_id: IdMap.getId("deleteId"),
|
||||
})
|
||||
})
|
||||
|
||||
it("throw error on invalid product id type", async () => {
|
||||
try {
|
||||
await productService.update(19314235, { title: "new title" })
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"The productId could not be casted to an ObjectId"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("addVariant", () => {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import _ from "lodash"
|
||||
import { BaseService } from "../interfaces"
|
||||
import { Validator, MedusaError } from "medusa-core-utils"
|
||||
|
||||
/**
|
||||
* Provides layer to manipulate products.
|
||||
* Provides layer to manipulate product variants.
|
||||
* @implements BaseService
|
||||
*/
|
||||
class ProductVariantService extends BaseService {
|
||||
/** @param { productModel: (ProductModel) } */
|
||||
constructor({ productVariantModel, eventBusService }) {
|
||||
/** @param { productVariantModel: (ProductVariantModel) } */
|
||||
constructor({ productVariantModel, eventBusService, productService }) {
|
||||
super()
|
||||
|
||||
/** @private @const {ProductVariantModel} */
|
||||
@@ -14,36 +16,250 @@ class ProductVariantService extends BaseService {
|
||||
|
||||
/** @private @const {EventBus} */
|
||||
this.eventBus_ = eventBusService
|
||||
|
||||
/** @private @const {ProductService} */
|
||||
this.productService_ = productService
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unpublished product.
|
||||
* @param {object} product - the product to create
|
||||
* Used to validate product ids. Throws an error if the cast fails
|
||||
* @param {string} rawId - the raw product id to validate.
|
||||
* @return {string} the validated id
|
||||
*/
|
||||
validateId_(rawId) {
|
||||
const schema = Validator.objectId()
|
||||
const { value, error } = schema.validate(rawId)
|
||||
if (error) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"The variantId could not be casted to an ObjectId"
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a product variant by id.
|
||||
* @param {string} variantId - the id of the product to get.
|
||||
* @return {Promise<Product>} the product document.
|
||||
*/
|
||||
retrieve(variantId) {
|
||||
const validatedId = this.validateId_(variantId)
|
||||
return this.productVariantModel_
|
||||
.findOne({ _id: validatedId })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unpublished product variant.
|
||||
* @param {object} variant - the variant to create
|
||||
* @return {Promise} resolves to the creation result.
|
||||
*/
|
||||
createDraft(productVariant) {
|
||||
return this.productVariantModel_.create({
|
||||
...productVariant,
|
||||
published: false,
|
||||
})
|
||||
return this.productVariantModel_
|
||||
.create({
|
||||
...productVariant,
|
||||
published: false,
|
||||
})
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an publishes product.
|
||||
* @param {string} productId - ID of the product to publish.
|
||||
* Creates an publishes variant.
|
||||
* @param {string} variantId - ID of the variant to publish.
|
||||
* @return {Promise} resolves to the creation result.
|
||||
*/
|
||||
publish(variantId) {
|
||||
return this.productVariantModel_
|
||||
.updateOne({ _id: variantId }, { $set: { published: true } })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a variant. Metadata updates and price updates should
|
||||
* use dedicated methods, e.g. `setMetadata`, etc. The function
|
||||
* will throw errors if metadata updates and price updates are attempted.
|
||||
* @param {string} variantId - the id of the variant. Must be a string that
|
||||
* can be casted to an ObjectId
|
||||
* @param {object} update - an object with the update values.
|
||||
* @return {Promise} resolves to the update result.
|
||||
*/
|
||||
update(variantId, update) {
|
||||
const validatedId = this.validateId_(variantId)
|
||||
|
||||
if (update.metadata) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Use setMetadata to update metadata fields"
|
||||
)
|
||||
}
|
||||
|
||||
return this.productVariantModel_
|
||||
.updateOne(
|
||||
{ _id: validatedId },
|
||||
{ $set: update },
|
||||
{ runValidators: true }
|
||||
)
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds option value to a varaint.
|
||||
* Fails when product with variant does not exists or
|
||||
* if that product does not have an option with the given
|
||||
* option id. Fails if given variant is not found.
|
||||
* 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 addOptionValue(variantId, optionId, optionValue) {
|
||||
const products = await this.productService_.list({ variants: variantId })
|
||||
if (!products.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Products with variant: ${variantId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
const product = products[0]
|
||||
if (!product.options.find(o => o._id === optionId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Associated product does not have option: ${optionId}`
|
||||
)
|
||||
}
|
||||
|
||||
const variant = await this.retrieve(variantId)
|
||||
if (!variant) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Variant with ${variantId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
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 },
|
||||
{ $set: { published: true } }
|
||||
{ $push: { options: { option_id: optionId, value: `${optionValue}` } } }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Deletes option value from given variant.
|
||||
* Fails when product with variant does not exists or
|
||||
* if that product has an option with the given
|
||||
* option id.
|
||||
* This method should only be used from the product service.
|
||||
* @param {string} variantId - the variant to decorate.
|
||||
* @param {string} optionId - the option from product.
|
||||
* @return {Promise} the result of the update operation.
|
||||
*/
|
||||
addOptionValue(variantId, optionId, optionValue) {
|
||||
async deleteOptionValue(variantId, optionId) {
|
||||
const products = await this.productService_.list({ variants: variantId })
|
||||
if (!products.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Products with variant: ${variantId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
const product = products[0]
|
||||
if (product.options.find(o => o._id === optionId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Associated product has option with id: ${optionId}`
|
||||
)
|
||||
}
|
||||
|
||||
return this.productVariantModel_.updateOne(
|
||||
{ _id: variantId },
|
||||
{ $pull: { options: { option_id: optionId } } }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} selector - the query object for find
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
list(selector) {
|
||||
return this.productVariantModel_.find(selector)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a variant from given variant id.
|
||||
* @param {string} variantId - the id of the variant to delete. Must be
|
||||
* castable as an ObjectId
|
||||
* @return {Promise} the result of the delete operation.
|
||||
*/
|
||||
async delete(variantId) {
|
||||
const variant = await this.retrieve(variantId)
|
||||
// Delete is idempotent, but we return a promise to allow then-chaining
|
||||
if (!variant) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return this.productVariantModel_
|
||||
.deleteOne({ _id: variantId })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorates a variant with variant variants.
|
||||
* @param {ProductVariant} variant - the variant to decorate.
|
||||
* @param {string[]} fields - the fields to include.
|
||||
* @param {string[]} expandFields - fields to expand.
|
||||
* @return {ProductVariant} return the decorated variant.
|
||||
*/
|
||||
async decorate(variant, fields, expandFields = []) {
|
||||
const requiredFields = ["_id", "metadata"]
|
||||
const decorated = _.pick(variant, fields.concat(requiredFields))
|
||||
return decorated
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated method to set metadata for a variant.
|
||||
* To ensure that plugins does not overwrite each
|
||||
* others metadata fields, setMetadata is provided.
|
||||
* @param {string} variantId - the variant to decorate.
|
||||
* @param {string} key - key for metadata field
|
||||
* @param {string} value - value for metadata field.
|
||||
* @return {Promise} resolves to the updated result.
|
||||
*/
|
||||
setMetadata(variantId, key, value) {
|
||||
const validatedId = this.validateId_(variantId)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Key type is invalid. Metadata keys must be strings"
|
||||
)
|
||||
}
|
||||
|
||||
const keyPath = `metadata.${key}`
|
||||
return this.productVariantModel_
|
||||
.updateOne({ _id: validatedId }, { $set: { [keyPath]: value } })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,10 +41,10 @@ class ProductService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} selector - the query object for find
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
list(query) {
|
||||
const selector = {}
|
||||
list(selector) {
|
||||
return this.productModel_.find(selector)
|
||||
}
|
||||
|
||||
@@ -477,15 +477,9 @@ class ProductService extends BaseService {
|
||||
`To delete an option, first delete all variants, such that when option is deleted, no duplicate variants will exist. For more info check MEDUSA.com`
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
product.variants.map(async variantId =>
|
||||
this.productVariantService_.deleteOptionValue(variantId, optionId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return this.productModel_.updateOne(
|
||||
const result = await this.productModel_.updateOne(
|
||||
{ _id: productId },
|
||||
{
|
||||
$pull: {
|
||||
@@ -495,6 +489,17 @@ class ProductService extends BaseService {
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// If we reached this point, we can delete option value from variants
|
||||
if (product.variants) {
|
||||
await Promise.all(
|
||||
product.variants.map(async variantId =>
|
||||
this.productVariantService_.deleteOptionValue(variantId, optionId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -543,7 +548,9 @@ class ProductService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets metadata for a product
|
||||
* Dedicated method to set metadata for a product.
|
||||
* To ensure that plugins does not overwrite each
|
||||
* others metadata fields, setMetadata is provided.
|
||||
* @param {string} productId - the product to decorate.
|
||||
* @param {string} key - key for metadata field
|
||||
* @param {string} value - value for metadata field.
|
||||
|
||||
Reference in New Issue
Block a user