Adds LineItemService (#11)
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
import { IdMap } from "medusa-test-utils"
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
|
||||||
export const LineItemServiceMock = {
|
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) => {
|
generate: jest.fn().mockImplementation((variantId, quantity, regionId) => {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
content: {
|
content: {
|
||||||
@@ -16,15 +22,6 @@ export const LineItemServiceMock = {
|
|||||||
quantity,
|
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(() => {
|
const mock = jest.fn().mockImplementation(() => {
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ const emptyVariant = {
|
|||||||
options: [],
|
options: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const eur10us12 = {
|
||||||
|
_id: IdMap.getId("eur-10-us-12"),
|
||||||
|
}
|
||||||
|
|
||||||
export const variants = {
|
export const variants = {
|
||||||
one: variant1,
|
one: variant1,
|
||||||
two: variant2,
|
two: variant2,
|
||||||
@@ -103,6 +107,7 @@ export const variants = {
|
|||||||
four: variant4,
|
four: variant4,
|
||||||
invalid_variant: invalidVariant,
|
invalid_variant: invalidVariant,
|
||||||
empty_variant: emptyVariant,
|
empty_variant: emptyVariant,
|
||||||
|
eur10us12: eur10us12,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProductVariantServiceMock = {
|
export const ProductVariantServiceMock = {
|
||||||
@@ -128,7 +133,8 @@ export const ProductVariantServiceMock = {
|
|||||||
if (variantId === "empty_option") {
|
if (variantId === "empty_option") {
|
||||||
return Promise.resolve(emptyVariant)
|
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) => {
|
canCoverQuantity: jest.fn().mockImplementation((variantId, quantity) => {
|
||||||
if (variantId === IdMap.getId("can-cover")) {
|
if (variantId === IdMap.getId("can-cover")) {
|
||||||
|
|||||||
@@ -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")) {
|
if (data.variants === IdMap.getId("failId")) {
|
||||||
return Promise.resolve([])
|
return Promise.resolve([])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
|
||||||
import { RegionServiceMock } from "../__mocks__/region"
|
import { RegionServiceMock } from "../__mocks__/region"
|
||||||
import { CartModelMock, carts } from "../../models/__mocks__/cart"
|
import { CartModelMock, carts } from "../../models/__mocks__/cart"
|
||||||
|
import { LineItemServiceMock } from "../__mocks__/line-item"
|
||||||
|
|
||||||
describe("CartService", () => {
|
describe("CartService", () => {
|
||||||
describe("retrieve", () => {
|
describe("retrieve", () => {
|
||||||
@@ -91,6 +92,7 @@ describe("CartService", () => {
|
|||||||
const cartService = new CartService({
|
const cartService = new CartService({
|
||||||
cartModel: CartModelMock,
|
cartModel: CartModelMock,
|
||||||
productVariantService: ProductVariantServiceMock,
|
productVariantService: ProductVariantServiceMock,
|
||||||
|
lineItemService: LineItemServiceMock,
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
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 () => {
|
it("successfully merges existing line item", async () => {
|
||||||
const lineItem = {
|
const lineItem = {
|
||||||
title: "merge line",
|
title: "merge line",
|
||||||
@@ -251,7 +206,7 @@ describe("CartService", () => {
|
|||||||
|
|
||||||
it("throws if line item not validated", async () => {
|
it("throws if line item not validated", async () => {
|
||||||
const lineItem = {
|
const lineItem = {
|
||||||
title: "merge line",
|
title: "invalid lineitem",
|
||||||
description: "This is a new line",
|
description: "This is a new line",
|
||||||
thumbnail: "test-img-yeah.com/thumb",
|
thumbnail: "test-img-yeah.com/thumb",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -14,6 +14,7 @@ class CartService extends BaseService {
|
|||||||
productService,
|
productService,
|
||||||
productVariantService,
|
productVariantService,
|
||||||
regionService,
|
regionService,
|
||||||
|
lineItemService,
|
||||||
}) {
|
}) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
@@ -32,6 +33,9 @@ class CartService extends BaseService {
|
|||||||
/** @private @const {RegionService} */
|
/** @private @const {RegionService} */
|
||||||
this.regionService_ = regionService
|
this.regionService_ = regionService
|
||||||
|
|
||||||
|
/** @private @const {LineItemService} */
|
||||||
|
this.lineItemService_ = lineItemService
|
||||||
|
|
||||||
/** @private @const {PaymentProviderService} */
|
/** @private @const {PaymentProviderService} */
|
||||||
this.paymentProviderService_ = paymentProviderService
|
this.paymentProviderService_ = paymentProviderService
|
||||||
}
|
}
|
||||||
@@ -54,47 +58,6 @@ class CartService extends BaseService {
|
|||||||
return value
|
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
|
* Contents of a line item
|
||||||
* @typedef {(object | array)} LineItemContent
|
* @typedef {(object | array)} LineItemContent
|
||||||
@@ -245,7 +208,7 @@ class CartService extends BaseService {
|
|||||||
* @retur {Promise} the result of the update operation
|
* @retur {Promise} the result of the update operation
|
||||||
*/
|
*/
|
||||||
async addLineItem(cartId, lineItem) {
|
async addLineItem(cartId, lineItem) {
|
||||||
const validatedLineItem = this.validateLineItem_(lineItem)
|
const validatedLineItem = this.lineItemService_.validate(lineItem)
|
||||||
|
|
||||||
const cart = await this.retrieve(cartId)
|
const cart = await this.retrieve(cartId)
|
||||||
if (!cart) {
|
if (!cart) {
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user