From 751adec7f7c053621d13822f16065d798b7fc039 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Tue, 28 Jan 2020 16:25:28 +0100 Subject: [PATCH] Adds cart service skeleton --- packages/medusa/src/models/__mocks__/cart.js | 28 +++++ packages/medusa/src/models/cart.js | 25 ++++ packages/medusa/src/models/schemas/address.js | 16 +++ .../medusa/src/models/schemas/line-item.js | 29 +++++ .../medusa/src/services/__mocks__/region.js | 31 +++++ .../medusa/src/services/__tests__/cart.js | 62 ++++++++++ packages/medusa/src/services/cart.js | 115 ++++++++++++++++++ 7 files changed, 306 insertions(+) create mode 100644 packages/medusa/src/models/__mocks__/cart.js create mode 100644 packages/medusa/src/models/cart.js create mode 100644 packages/medusa/src/models/schemas/address.js create mode 100644 packages/medusa/src/models/schemas/line-item.js create mode 100644 packages/medusa/src/services/__mocks__/region.js create mode 100644 packages/medusa/src/services/__tests__/cart.js create mode 100644 packages/medusa/src/services/cart.js diff --git a/packages/medusa/src/models/__mocks__/cart.js b/packages/medusa/src/models/__mocks__/cart.js new file mode 100644 index 0000000000..7e54b9016e --- /dev/null +++ b/packages/medusa/src/models/__mocks__/cart.js @@ -0,0 +1,28 @@ +import IdMap from "../../helpers/id-map" + +export const carts = { + emptyCart: { + _id: IdMap.getId("emptyCart"), + title: "test", + region: IdMap.getId("testRegion"), + items: [], + shippingAddress: {}, + billingAddress: {}, + discounts: [], + customer_id: "", + }, +} + +export const CartModelMock = { + 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("emptyCart")) { + return Promise.resolve(carts.emptyCart) + } + return Promise.resolve(undefined) + }), +} diff --git a/packages/medusa/src/models/cart.js b/packages/medusa/src/models/cart.js new file mode 100644 index 0000000000..6cda42bd2d --- /dev/null +++ b/packages/medusa/src/models/cart.js @@ -0,0 +1,25 @@ +/******************************************************************************* + * models/product-variant.js + * + ******************************************************************************/ +import mongoose from "mongoose" +import { BaseModel } from "../interfaces" + +import LineItemSchema from "./schemas/line-item" +import AddressSchema from "./schemas/address" + +class CartModel extends BaseModel { + static modelName = "Cart" + + static schema = { + email: { type: String }, + billingAddress: { type: AddressSchema }, + shippingAddress: { type: AddressSchema }, + items: { type: [LinetItemSchema], default: [] }, + region: { type: String, required: true }, + discounts: { type: [String], default: true }, + customer_id: { type: String }, + } +} + +export default CartModel diff --git a/packages/medusa/src/models/schemas/address.js b/packages/medusa/src/models/schemas/address.js new file mode 100644 index 0000000000..8fff08669d --- /dev/null +++ b/packages/medusa/src/models/schemas/address.js @@ -0,0 +1,16 @@ +/******************************************************************************* + * + ******************************************************************************/ +import mongoose from "mongoose" + +export default new mongoose.Schema({ + first_name: { type: String, required: true }, + last_name: { type: String, required: true }, + address1: { type: String, required: true }, + address2: { type: String }, + city: { type: String, required: true }, + country_code: { type: String, required: true }, + province: { type: String }, + postal_code: { type: String, required: true }, + metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, +}) diff --git a/packages/medusa/src/models/schemas/line-item.js b/packages/medusa/src/models/schemas/line-item.js new file mode 100644 index 0000000000..d9ca9be0eb --- /dev/null +++ b/packages/medusa/src/models/schemas/line-item.js @@ -0,0 +1,29 @@ +/******************************************************************************* + * + ******************************************************************************/ +import mongoose from "mongoose" + +export default new mongoose.Schema({ + title: { type: String, required: true }, + description: { type: String }, + thumbnail: { type: String }, + + // mongoose doesn't allow multi-type validation but this field allows both + // an object containing: + // { + // variant: (ProductVariantSchema), + // product: (ProductSchema) + // } + // + // and and array containing: + // [ + // { + // variant: (ProductVariantSchema), + // product: (ProductSchema) + // } + // ] + // validation is done in the cart service. + content: { type: mongoose.Schema.Types.Mixed, required: true }, + quantity: { type: Number, required: true }, + metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, +}) diff --git a/packages/medusa/src/services/__mocks__/region.js b/packages/medusa/src/services/__mocks__/region.js new file mode 100644 index 0000000000..1aa96b9feb --- /dev/null +++ b/packages/medusa/src/services/__mocks__/region.js @@ -0,0 +1,31 @@ +import IdMap from "../../helpers/id-map" + +export const regions = { + testRegion: { + _id: IdMap.getId("testRegion"), + name: "Test Region", + countries: ["DK", "US", "DE"], + tax_rate: 0.25, + payment_providers: ["default_provider"], + shipping_providers: ["test_shipper"], + currency_code: "usd", + }, +} + +export const RegionServiceMock = { + retrieve: jest.fn().mockImplementation(regionId => { + if (regionId === IdMap.getId("testRegion")) { + return Promise.resolve(regions.testRegion) + } + return Promise.resolve(undefined) + }), + list: jest.fn().mockImplementation(data => { + return Promise.resolve([regions.testRegion]) + }), +} + +const mock = jest.fn().mockImplementation(() => { + return RegionServiceMock +}) + +export default mock diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js new file mode 100644 index 0000000000..b3f86cf55d --- /dev/null +++ b/packages/medusa/src/services/__tests__/cart.js @@ -0,0 +1,62 @@ +import mongoose from "mongoose" +import CartService from "../cart" +import { RegionServiceMock } from "../__mocks__/region" +import { CartModelMock, carts } from "../../models/__mocks__/cart" +import IdMap from "../../helpers/id-map" + +describe("CartService", () => { + describe("retrieve", () => { + let result + beforeAll(async () => { + jest.clearAllMocks() + const cartService = new CartService({ + cartModel: CartModelMock, + }) + result = await cartService.retrieve(IdMap.getId("emptyCart")) + }) + + it("calls cart model functions", () => { + expect(CartModelMock.findOne).toHaveBeenCalledTimes(1) + expect(CartModelMock.findOne).toHaveBeenCalledWith({ + _id: IdMap.getId("emptyCart"), + }) + }) + + it("returns the cart", () => { + expect(result).toEqual(carts.emptyCart) + }) + }) + + describe("setMetadata", () => { + const cartService = new CartService({ + cartModel: CartModelMock, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("calls updateOne with correct params", async () => { + const id = mongoose.Types.ObjectId() + await cartService.setMetadata(`${id}`, "metadata", "testMetadata") + + expect(CartModelMock.updateOne).toBeCalledTimes(1) + expect(CartModelMock.updateOne).toBeCalledWith( + { _id: `${id}` }, + { $set: { "metadata.metadata": "testMetadata" } } + ) + }) + + it("throw error on invalid key type", async () => { + const id = mongoose.Types.ObjectId() + + try { + await cartService.setMetadata(`${id}`, 1234, "nono") + } catch (err) { + expect(err.message).toEqual( + "Key type is invalid. Metadata keys must be strings" + ) + } + }) + }) +}) diff --git a/packages/medusa/src/services/cart.js b/packages/medusa/src/services/cart.js new file mode 100644 index 0000000000..8163527d05 --- /dev/null +++ b/packages/medusa/src/services/cart.js @@ -0,0 +1,115 @@ +import mongoose from "mongoose" +import _ from "lodash" +import { Validator, MedusaError } from "medusa-core-utils" +import { BaseService } from "../interfaces" + +/** + * Provides layer to manipulate carts. + * @implements BaseService + */ +class CartService extends BaseService { + constructor({ + cartModel, + regionService, + productService, + productVariantService, + eventBusService, + }) { + super() + + /** @private @const {CartModel} */ + this.cartModel_ = cartModel + + /** @private @const {EventBus} */ + this.eventBus_ = eventBusService + + /** @private @const {ProductVariantService} */ + this.productVariantService_ = productVariantService + + /** @private @const {ProductService} */ + this.productService_ = productService + + /** @private @const {RegionService} */ + this.regionService_ = regionService + } + + /** + * Used to validate cart ids. Throws an error if the cast fails + * @param {string} rawId - the raw cart 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 cartId could not be casted to an ObjectId" + ) + } + + return value + } + + /** + * @param {Object} selector - the query object for find + * @return {Promise} the result of the find operation + */ + list(selector) { + return this.cartModel_.find(selector) + } + + /** + * Gets a cart by id. + * @param {string} cartId - the id of the cart to get. + * @return {Promise} the cart document. + */ + retrieve(cartId) { + const validatedId = this.validateId_(cartId) + return this.cartModel_.findOne({ _id: validatedId }).catch(err => { + throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + }) + } + + /** + * Decorates a cart. + * @param {Cart} cart - the cart to decorate. + * @param {string[]} fields - the fields to include. + * @param {string[]} expandFields - fields to expand. + * @return {Cart} return the decorated cart. + */ + async decorate(cart, fields, expandFields = []) { + const requiredFields = ["_id", "metadata"] + const decorated = _.pick(product, fields.concat(requiredFields)) + return decorated + } + + /** + * Dedicated method to set metadata for a cart. + * To ensure that plugins does not overwrite each + * others metadata fields, setMetadata is provided. + * @param {string} cartId - the cart to apply metadata to. + * @param {string} key - key for metadata field + * @param {string} value - value for metadata field. + * @return {Promise} resolves to the updated result. + */ + setMetadata(cartId, key, value) { + const validatedId = this.validateId_(cartId) + + 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.cartModel_ + .updateOne({ _id: validatedId }, { $set: { [keyPath]: value } }) + .catch(err => { + throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + }) + } +} + +export default CartService