Makes ProductVariant operations go through ProductService (#68)

Ensures that we can reliably manage product variants and their relationship to products
This commit is contained in:
Sebastian Rindom
2020-05-28 11:44:11 +02:00
committed by GitHub
parent ad4f9479ea
commit 706ca8ac45
81 changed files with 975 additions and 1005 deletions
@@ -16,6 +16,7 @@ export default () => {
break
case MedusaError.Types.DB_ERROR:
statusCode = 500
logger.error(err)
break
default:
break
@@ -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
}
@@ -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"
)
})
})
})
@@ -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`)
})
})
})
@@ -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")
)
})
})
})
@@ -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,
})
})
})
})
@@ -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"))
})
})
})
@@ -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"))
})
})
})
@@ -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")
)
})
})
})
@@ -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
)
})
})
})
@@ -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
)
})
})
})
@@ -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"
)
}
})
})
})
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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)
})
})
})
@@ -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)
})
})
})
@@ -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", () => {
@@ -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)
})
})
})
@@ -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,
})
@@ -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,
})
})
})
})
@@ -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)
})
})
})
@@ -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"))
})
})
})
@@ -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", () => {
@@ -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", () => {
@@ -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)
})
})
})
@@ -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
}
}
@@ -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
}
}
@@ -29,7 +29,7 @@ export default async (req, res) => {
"variants",
"published",
])
res.json(newProduct)
res.json({ product: newProduct })
} catch (err) {
throw err
}
@@ -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
}
}
@@ -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,
})
@@ -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
}
@@ -15,5 +15,5 @@ export default async (req, res) => {
"published",
])
res.json(product)
res.json({ product })
}
@@ -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 })
}
@@ -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)
)
@@ -17,7 +17,7 @@ export default async (req, res) => {
])
)
)
res.json(products)
res.json({ products })
} catch (error) {
throw error
}
@@ -16,7 +16,7 @@ export default async (req, res) => {
"variants",
"published",
])
res.json(publishedProduct)
res.json({ product: publishedProduct })
} catch (error) {
throw error
}
@@ -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
}
@@ -37,7 +37,7 @@ export default async (req, res) => {
"variants",
"published",
])
res.json(newProduct)
res.json({ product: newProduct })
} catch (err) {
throw err
}
@@ -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
}
}
@@ -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"
)
})
@@ -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"
)
})
@@ -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"
)
})
@@ -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")
)
})
})
@@ -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
)
})
@@ -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)
})
})
@@ -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)
@@ -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)
}
@@ -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)
})
})
})
@@ -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)
}
@@ -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)
}