Adds LineItemService (#11)

This commit is contained in:
Oliver Windall Juhl
2020-03-10 10:46:01 +01:00
committed by GitHub
parent 245557ac2e
commit 8d51a3f716
7 changed files with 208 additions and 101 deletions

View File

@@ -1,6 +1,12 @@
import { IdMap } from "medusa-test-utils"
export const LineItemServiceMock = {
validate: jest.fn().mockImplementation(data => {
if (data.title === "invalid lineitem") {
throw new Error(`"content" is required`)
}
return data
}),
generate: jest.fn().mockImplementation((variantId, quantity, regionId) => {
return Promise.resolve({
content: {
@@ -16,15 +22,6 @@ export const LineItemServiceMock = {
quantity,
})
}),
validate: jest.fn().mockImplementation(cartId => {
if (cartId === IdMap.getId("regionCart")) {
return Promise.resolve(carts.regionCart)
}
if (cartId === IdMap.getId("emptyCart")) {
return Promise.resolve(carts.emptyCart)
}
return Promise.resolve(undefined)
}),
}
const mock = jest.fn().mockImplementation(() => {

View File

@@ -96,6 +96,10 @@ const emptyVariant = {
options: [],
}
const eur10us12 = {
_id: IdMap.getId("eur-10-us-12"),
}
export const variants = {
one: variant1,
two: variant2,
@@ -103,6 +107,7 @@ export const variants = {
four: variant4,
invalid_variant: invalidVariant,
empty_variant: emptyVariant,
eur10us12: eur10us12,
}
export const ProductVariantServiceMock = {
@@ -128,7 +133,8 @@ export const ProductVariantServiceMock = {
if (variantId === "empty_option") {
return Promise.resolve(emptyVariant)
}
return Promise.resolve(undefined)
if (variantId === IdMap.getId("eur-10-us-12"))
return Promise.resolve(eur10us12)
}),
canCoverQuantity: jest.fn().mockImplementation((variantId, quantity) => {
if (variantId === IdMap.getId("can-cover")) {

View File

@@ -32,7 +32,14 @@ export const ProductServiceMock = {
},
])
}
if (data.variants === IdMap.getId("eur-10-us-12")) {
return Promise.resolve([
{
_id: "1234",
title: "test",
},
])
}
if (data.variants === IdMap.getId("failId")) {
return Promise.resolve([])
}

View File

@@ -8,6 +8,7 @@ import {
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
import { RegionServiceMock } from "../__mocks__/region"
import { CartModelMock, carts } from "../../models/__mocks__/cart"
import { LineItemServiceMock } from "../__mocks__/line-item"
describe("CartService", () => {
describe("retrieve", () => {
@@ -91,6 +92,7 @@ describe("CartService", () => {
const cartService = new CartService({
cartModel: CartModelMock,
productVariantService: ProductVariantServiceMock,
lineItemService: LineItemServiceMock,
})
beforeEach(() => {
@@ -128,53 +130,6 @@ describe("CartService", () => {
)
})
it("successfully defaults quantity of content to 1", async () => {
const lineItem = {
title: "New Line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
content: {
unit_price: 123,
variant: {
_id: IdMap.getId("can-cover"),
},
product: {
_id: IdMap.getId("product"),
},
},
quantity: 10,
}
await cartService.addLineItem(IdMap.getId("emptyCart"), lineItem)
expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(CartModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("emptyCart"),
},
{
$push: {
items: {
title: "New Line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
content: {
unit_price: 123,
variant: {
_id: IdMap.getId("can-cover"),
},
product: {
_id: IdMap.getId("product"),
},
quantity: 1,
},
quantity: 10,
},
},
}
)
})
it("successfully merges existing line item", async () => {
const lineItem = {
title: "merge line",
@@ -251,7 +206,7 @@ describe("CartService", () => {
it("throws if line item not validated", async () => {
const lineItem = {
title: "merge line",
title: "invalid lineitem",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
}

View File

@@ -0,0 +1,49 @@
import mongoose from "mongoose"
import { IdMap } from "medusa-test-utils"
import LineItemService from "../line-item"
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
import { ProductServiceMock } from "../__mocks__/product"
import { RegionServiceMock } from "../__mocks__/region"
describe("LineItemService", () => {
describe("generate", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const lineItemService = new LineItemService({
productVariantService: ProductVariantServiceMock,
productService: ProductServiceMock,
regionService: RegionServiceMock,
})
result = await lineItemService.generate(
IdMap.getId("eur-10-us-12"),
IdMap.getId("region-france"),
2
)
})
it("generates line item and successfully defaults quantity of content to 1", () => {
expect(result).toEqual({
content: {
unit_price: 10,
variant: {
_id: IdMap.getId("eur-10-us-12"),
},
product: {
_id: "1234",
title: "test",
},
quantity: 1,
},
product: {
_id: "1234",
title: "test",
},
variant: {
_id: IdMap.getId("eur-10-us-12"),
},
quantity: 2,
})
})
})
})

View File

@@ -14,6 +14,7 @@ class CartService extends BaseService {
productService,
productVariantService,
regionService,
lineItemService,
}) {
super()
@@ -32,6 +33,9 @@ class CartService extends BaseService {
/** @private @const {RegionService} */
this.regionService_ = regionService
/** @private @const {LineItemService} */
this.lineItemService_ = lineItemService
/** @private @const {PaymentProviderService} */
this.paymentProviderService_ = paymentProviderService
}
@@ -54,47 +58,6 @@ class CartService extends BaseService {
return value
}
/**
* Used to validate line items.
* @param {object} rawLineItem - the raw cart id to validate.
* @return {object} the validated id
*/
validateLineItem_(rawLineItem) {
const content = Validator.object({
unit_price: Validator.number().required(),
variant: Validator.object().required(),
product: Validator.object().required(),
quantity: Validator.number()
.integer()
.min(1)
.default(1),
})
const lineItemSchema = Validator.object({
title: Validator.string().required(),
description: Validator.string(),
thumbnail: Validator.string(),
content: Validator.alternatives()
.try(content, Validator.array().items(content))
.required(),
quantity: Validator.number()
.integer()
.min(1)
.required(),
metadata: Validator.object(),
})
const { value, error } = lineItemSchema.validate(rawLineItem)
if (error) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
error.details[0].message
)
}
return value
}
/**
* Contents of a line item
* @typedef {(object | array)} LineItemContent
@@ -245,7 +208,7 @@ class CartService extends BaseService {
* @retur {Promise} the result of the update operation
*/
async addLineItem(cartId, lineItem) {
const validatedLineItem = this.validateLineItem_(lineItem)
const validatedLineItem = this.lineItemService_.validate(lineItem)
const cart = await this.retrieve(cartId)
if (!cart) {

View File

@@ -0,0 +1,130 @@
import { Validator, MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
/**
* Provides layer to manipulate line items.
* @implements BaseService
*/
class LineItemService extends BaseService {
constructor({ productVariantService, productService, regionService }) {
super()
/** @private @const {ProductVariantService} */
this.productVariantService_ = productVariantService
/** @private @const {ProductService} */
this.productService_ = productService
/** @private @const {RegionService} */
this.regionService_ = regionService
}
/**
* Used to validate line items.
* @param {object} rawLineItem - the raw line item to validate.
* @return {object} the validated id
*/
validate(rawLineItem) {
const content = Validator.object({
unit_price: Validator.number().required(),
variant: Validator.object().required(),
product: Validator.object().required(),
quantity: Validator.number()
.integer()
.min(1)
.default(1),
})
const lineItemSchema = Validator.object({
title: Validator.string().required(),
description: Validator.string(),
thumbnail: Validator.string(),
content: Validator.alternatives()
.try(content, Validator.array().items(content))
.required(),
quantity: Validator.number()
.integer()
.min(1)
.required(),
metadata: Validator.object(),
})
const { value, error } = lineItemSchema.validate(rawLineItem)
if (error) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
error.details[0].message
)
}
return value
}
/**
* Contents of a line item
* @typedef {(object | array)} LineItemContent
* @property {number} unit_price - the price of the content
* @property {object} variant - the product variant of the content
* @property {object} product - the product of the content
* @property {number} quantity - the quantity of the content
*/
/**
* A collection of contents grouped in the same line item
* @typedef {LineItemContent[]} LineItemContentArray
*/
/**
* Generates a line item.
* @param {string} variantId - id of the line item variant
* @param {*} regionId - id of the cart region
* @param {*} quantity - number of items
*/
async generate(variantId, regionId, quantity) {
const variant = await this.productVariantService_.retrieve(variantId)
if (!variant) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Variant: ${variantId} was not found`
)
}
const region = await await this.regionService_.retrieve(regionId)
if (!region) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Region: ${regionId} was not found`
)
}
const products = await this.productService_.list({ variants: variantId })
// this should never fail, since a variant must have a product associated
// with it to exists, but better safe than sorry
if (!products.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Could not find product for variant with id: ${variantId}`
)
}
const product = products[0]
const unit_price = await this.productVariantService_.getRegionPrice(
variantId,
regionId
)
return {
variant,
product,
quantity,
content: {
unit_price,
variant,
product,
quantity: 1,
},
}
}
}
export default LineItemService