diff --git a/packages/medusa/src/services/__mocks__/line-item.js b/packages/medusa/src/services/__mocks__/line-item.js index eb2db5c170..34b23c5562 100644 --- a/packages/medusa/src/services/__mocks__/line-item.js +++ b/packages/medusa/src/services/__mocks__/line-item.js @@ -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(() => { diff --git a/packages/medusa/src/services/__mocks__/product-variant.js b/packages/medusa/src/services/__mocks__/product-variant.js index 182dc61503..7b777d83d7 100644 --- a/packages/medusa/src/services/__mocks__/product-variant.js +++ b/packages/medusa/src/services/__mocks__/product-variant.js @@ -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")) { diff --git a/packages/medusa/src/services/__mocks__/product.js b/packages/medusa/src/services/__mocks__/product.js index c20a403da0..e29ad2d50c 100644 --- a/packages/medusa/src/services/__mocks__/product.js +++ b/packages/medusa/src/services/__mocks__/product.js @@ -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([]) } diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index 039b16448c..2c2a08776b 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -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", } diff --git a/packages/medusa/src/services/__tests__/line-item.js b/packages/medusa/src/services/__tests__/line-item.js new file mode 100644 index 0000000000..d496eaaea2 --- /dev/null +++ b/packages/medusa/src/services/__tests__/line-item.js @@ -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, + }) + }) + }) +}) diff --git a/packages/medusa/src/services/cart.js b/packages/medusa/src/services/cart.js index 0c7932133c..39702fb495 100644 --- a/packages/medusa/src/services/cart.js +++ b/packages/medusa/src/services/cart.js @@ -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) { diff --git a/packages/medusa/src/services/line-item.js b/packages/medusa/src/services/line-item.js new file mode 100644 index 0000000000..5195009710 --- /dev/null +++ b/packages/medusa/src/services/line-item.js @@ -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