Adds cart service skeleton
This commit is contained in:
1 parent
b16f32f63f
commit
751adec7f7
7 files changed
+306
No files matched your search
@@ -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)
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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: {} },
|
||||||
|
})
|
||||||
@@ -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: {} },
|
||||||
|
})
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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<Cart>} 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
|
||||||
Reference in new issue
Block a user