diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/create-cart.js b/packages/medusa/src/api/routes/store/carts/__tests__/create-cart.js new file mode 100644 index 0000000000..70bcdff74f --- /dev/null +++ b/packages/medusa/src/api/routes/store/carts/__tests__/create-cart.js @@ -0,0 +1,151 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { CartServiceMock } from "../../../../../services/__mocks__/cart" +import { LineItemServiceMock } from "../../../../../services/__mocks__/line-item" + +describe("POST /store/carts", () => { + describe("successfully creates a cart", () => { + let subject + + beforeAll(async () => { + subject = await request("POST", `/store/carts`, { + payload: { + region_id: IdMap.getId("testRegion"), + }, + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls CartService create", () => { + expect(CartServiceMock.create).toHaveBeenCalledTimes(1) + expect(CartServiceMock.create).toHaveBeenCalledWith({ + region_id: IdMap.getId("testRegion"), + }) + }) + + it("returns 201", () => { + expect(subject.status).toEqual(201) + }) + + it("returns the cart", () => { + expect(subject.body._id).toEqual(IdMap.getId("regionCart")) + }) + }) + + describe("handles failed create operation", () => { + let subject + + beforeAll(async () => { + subject = await request("POST", `/store/carts`, { + payload: { + region_id: IdMap.getId("fail"), + }, + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("returns error", () => { + expect(subject.status).toEqual(400) + expect(subject.body.message).toEqual("Region not found") + }) + }) + + describe("returns invalid data if region_id is not set", () => { + let subject + + beforeAll(async () => { + subject = await request("POST", `/store/carts`) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("returns error", () => { + expect(subject.status).toEqual(400) + }) + }) + + describe("creates cart with line items", () => { + let subject + + beforeAll(async () => { + subject = await request("POST", `/store/carts`, { + payload: { + region_id: IdMap.getId("testRegion"), + items: [ + { + variant_id: IdMap.getId("testVariant"), + quantity: 3, + }, + { + variant_id: IdMap.getId("testVariant1"), + quantity: 1, + }, + ], + }, + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("returns 201", () => { + expect(subject.status).toEqual(201) + }) + + it("calls line item generate", () => { + expect(LineItemServiceMock.generate).toHaveBeenCalledTimes(2) + expect(LineItemServiceMock.generate).toHaveBeenCalledWith( + IdMap.getId("testVariant"), + 3, + IdMap.getId("testRegion") + ) + expect(LineItemServiceMock.generate).toHaveBeenCalledWith( + IdMap.getId("testVariant1"), + 1, + IdMap.getId("testRegion") + ) + }) + + it("returns cart", () => { + expect(subject.body._id).toEqual(IdMap.getId("regionCart")) + }) + }) + + describe("fails if line items are not formatted correctly", () => { + let subject + + beforeAll(async () => { + subject = await request("POST", `/store/carts`, { + payload: { + region_id: IdMap.getId("testRegion"), + items: [ + { + quantity: 3, + }, + { + variant_id: IdMap.getId("testVariant1"), + quantity: 1, + }, + ], + }, + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("returns 400", () => { + expect(subject.status).toEqual(400) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/store/carts/__tests__/get-cart.js b/packages/medusa/src/api/routes/store/carts/__tests__/get-cart.js new file mode 100644 index 0000000000..78826d19e8 --- /dev/null +++ b/packages/medusa/src/api/routes/store/carts/__tests__/get-cart.js @@ -0,0 +1,49 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { CartServiceMock } from "../../../../../services/__mocks__/cart" + +describe("GET /store/carts", () => { + describe("successfully gets a cart", () => { + let subject + + beforeAll(async () => { + subject = await request("GET", `/store/carts/${IdMap.getId("emptyCart")}`) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls get product from productSerice", () => { + expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(1) + expect(CartServiceMock.retrieve).toHaveBeenCalledWith( + IdMap.getId("emptyCart") + ) + }) + + it("returns products", () => { + expect(subject.body._id).toEqual(IdMap.getId("emptyCart")) + }) + }) + + describe("returns 404 on undefined cart", () => { + let subject + + beforeAll(async () => { + subject = await request("GET", `/store/carts/none`) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls get product from productSerice", () => { + expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(1) + expect(CartServiceMock.retrieve).toHaveBeenCalledWith("none") + }) + + it("returns products", () => { + expect(subject.status).toEqual(404) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/store/carts/create-cart.js b/packages/medusa/src/api/routes/store/carts/create-cart.js new file mode 100644 index 0000000000..166aa31643 --- /dev/null +++ b/packages/medusa/src/api/routes/store/carts/create-cart.js @@ -0,0 +1,42 @@ +import { Validator, MedusaError } from "medusa-core-utils" + +export default async (req, res) => { + const schema = Validator.object().keys({ + region_id: Validator.string().required(), + items: Validator.array() + .items({ + variant_id: Validator.string().required(), + quantity: Validator.number().required(), + }) + .optional(), + }) + + const { value, error } = schema.validate(req.body) + if (error) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details) + } + + try { + const lineItemService = req.scope.resolve("lineItemService") + const cartService = req.scope.resolve("cartService") + let cart = await cartService.create({ region_id: value.region_id }) + + if (value.items) { + await Promise.all( + value.items.map(async i => { + const lineItem = await lineItemService.generate( + i.variant_id, + i.quantity, + value.region_id + ) + await cartService.addLineItem(cart._id, lineItem) + }) + ) + } + + cart = await cartService.retrieve(cart._id) + res.status(201).json(cart) + } catch (err) { + throw err + } +} diff --git a/packages/medusa/src/api/routes/store/carts/get-cart.js b/packages/medusa/src/api/routes/store/carts/get-cart.js new file mode 100644 index 0000000000..41771ed682 --- /dev/null +++ b/packages/medusa/src/api/routes/store/carts/get-cart.js @@ -0,0 +1,13 @@ +export default async (req, res) => { + const { cartId } = req.params + + const cartService = req.scope.resolve("cartService") + const cart = await cartService.retrieve(cartId) + + if (!cart) { + res.sendStatus(404) + return + } + + res.json(cart) +} diff --git a/packages/medusa/src/api/routes/store/carts/index.js b/packages/medusa/src/api/routes/store/carts/index.js new file mode 100644 index 0000000000..92f1822682 --- /dev/null +++ b/packages/medusa/src/api/routes/store/carts/index.js @@ -0,0 +1,13 @@ +import { Router } from "express" +import middlewares from "../../../middlewares" + +const route = Router() + +export default app => { + app.use("/carts", route) + + route.get("/:cartId", middlewares.wrap(require("./get-cart").default)) + route.post("/", middlewares.wrap(require("./create-cart").default)) + + return app +} diff --git a/packages/medusa/src/api/routes/store/index.js b/packages/medusa/src/api/routes/store/index.js index b5b5b7e2fc..9c5974338e 100644 --- a/packages/medusa/src/api/routes/store/index.js +++ b/packages/medusa/src/api/routes/store/index.js @@ -1,5 +1,6 @@ import { Router } from "express" -import productRoutes from './products' +import productRoutes from "./products" +import cartRoutes from "./carts" import middlewares from "../../middlewares" const route = Router() @@ -8,6 +9,7 @@ export default app => { app.use("/store", route) productRoutes(route) + cartRoutes(route) return app } diff --git a/packages/medusa/src/models/cart.js b/packages/medusa/src/models/cart.js index 4deb50913c..515f5bd6ee 100644 --- a/packages/medusa/src/models/cart.js +++ b/packages/medusa/src/models/cart.js @@ -14,14 +14,14 @@ class CartModel extends BaseModel { static schema = { email: { type: String }, - billing_address: { type: AddressSchema }, - shipping_address: { type: AddressSchema }, + billing_address: { type: AddressSchema, default: {} }, + shipping_address: { type: AddressSchema, default: {} }, items: { type: [LineItemSchema], default: [] }, - region_id: { type: String }, + region_id: { type: String, required: true }, discounts: { type: [String], default: [] }, - customer_id: { type: String }, - payment_method: { type: PaymentMethodSchema }, - shipping_methods: { type: [ShippingMethodSchema] }, + customer_id: { type: String, default: "" }, + payment_method: { type: PaymentMethodSchema, default: {} }, + shipping_methods: { type: [ShippingMethodSchema], default: [] }, metadata: { type: mongoose.Schema.Types.Mixed, default: {} }, } } diff --git a/packages/medusa/src/services/__mocks__/cart.js b/packages/medusa/src/services/__mocks__/cart.js new file mode 100644 index 0000000000..c972ec4ab7 --- /dev/null +++ b/packages/medusa/src/services/__mocks__/cart.js @@ -0,0 +1,43 @@ +import { MedusaError } from "medusa-core-utils" +import { IdMap } from "medusa-test-utils" + +export const carts = { + emptyCart: { + _id: IdMap.getId("emptyCart"), + items: [], + }, + regionCart: { + _id: IdMap.getId("regionCart"), + name: "Product 1", + region_id: IdMap.getId("testRegion"), + }, +} + +export const CartServiceMock = { + create: jest.fn().mockImplementation(data => { + if (data.region_id === IdMap.getId("testRegion")) { + return Promise.resolve(carts.regionCart) + } + if (data.region_id === IdMap.getId("fail")) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, "Region not found") + } + }), + retrieve: 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) + }), + addLineItem: jest.fn().mockImplementation((cartId, lineItem) => { + return Promise.resolve() + }), +} + +const mock = jest.fn().mockImplementation(() => { + return CartServiceMock +}) + +export default mock diff --git a/packages/medusa/src/services/__mocks__/line-item.js b/packages/medusa/src/services/__mocks__/line-item.js new file mode 100644 index 0000000000..eb2db5c170 --- /dev/null +++ b/packages/medusa/src/services/__mocks__/line-item.js @@ -0,0 +1,34 @@ +import { IdMap } from "medusa-test-utils" + +export const LineItemServiceMock = { + generate: jest.fn().mockImplementation((variantId, quantity, regionId) => { + return Promise.resolve({ + content: { + variant: { + _id: variantId, + }, + product: { + _id: `p_${variantId}`, + }, + quantity: 1, + unit_price: 100, + }, + 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(() => { + return LineItemServiceMock +}) + +export default mock diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index 496cd81cc8..039b16448c 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -65,6 +65,28 @@ describe("CartService", () => { }) }) + describe("create", () => { + const cartService = new CartService({ + cartModel: CartModelMock, + regionService: RegionServiceMock, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("successfully creates a cart", async () => { + await cartService.create({ + region_id: IdMap.getId("testRegion"), + }) + + expect(CartModelMock.create).toHaveBeenCalledTimes(1) + expect(CartModelMock.create).toHaveBeenCalledWith({ + region_id: IdMap.getId("testRegion"), + }) + }) + }) + describe("addLineItem", () => { const cartService = new CartService({ cartModel: CartModelMock, diff --git a/packages/medusa/src/services/cart.js b/packages/medusa/src/services/cart.js index c78f0387b4..0c7932133c 100644 --- a/packages/medusa/src/services/cart.js +++ b/packages/medusa/src/services/cart.js @@ -194,6 +194,37 @@ class CartService extends BaseService { }) } + /** + * Creates a cart. + * @param {Object} data - the data to create the cart with + * @return {Promise} the result of the create operation + */ + async create(data) { + const { region_id } = data + if (!region_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `A region_id must be provided when creating a cart` + ) + } + + const region = await this.regionService_.retrieve(region_id) + if (!region) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `A region with id: ${region_id} does not exist` + ) + } + + return this.cartModel_ + .create({ + region_id, + }) + .catch(err => { + throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) + }) + } + /** * Decorates a cart. * @param {Cart} cart - the cart to decorate.