Completes Cart Service and store/carts endpoints (#18)

Completes Cart Service to allow shopping and checkout flows.
This commit is contained in:
Sebastian Rindom
2020-03-16 09:48:52 +01:00
committed by GitHub
parent 8e7b66a205
commit 51aaf5105c
31 changed files with 2091 additions and 45 deletions
@@ -7,6 +7,7 @@ export default () => {
let statusCode = 500
switch (err.name) {
case MedusaError.Types.NOT_ALLOWED:
case MedusaError.Types.INVALID_DATA:
statusCode = 400
break
@@ -0,0 +1,166 @@
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/:id/shipping-methods", () => {
describe("successfully adds a shipping method", () => {
let subject
beforeAll(async () => {
const cartId = IdMap.getId("fr-cart")
subject = await request(
"POST",
`/store/carts/${cartId}/shipping-methods`,
{
payload: {
option_id: IdMap.getId("freeShipping"),
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls CartService retrieveShippingOption", () => {
expect(CartServiceMock.retrieveShippingOption).toHaveBeenCalledTimes(1)
expect(CartServiceMock.retrieveShippingOption).toHaveBeenCalledWith(
IdMap.getId("fr-cart"),
IdMap.getId("freeShipping")
)
})
it("calls CartService addShipping", () => {
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledTimes(1)
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledWith(
IdMap.getId("fr-cart"),
{
_id: IdMap.getId("freeShipping"),
profile_id: "default_profile",
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns the cart", () => {
expect(subject.body._id).toEqual(IdMap.getId("fr-cart"))
expect(subject.body.decorated).toEqual(true)
})
})
describe("successfully adds a shipping method with additional data", () => {
let subject
beforeAll(async () => {
const cartId = IdMap.getId("fr-cart")
subject = await request(
"POST",
`/store/carts/${cartId}/shipping-methods`,
{
payload: {
option_id: IdMap.getId("freeShipping"),
data: {
extra_id: "id",
},
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls CartService retrieveShippingOption", () => {
expect(CartServiceMock.retrieveShippingOption).toHaveBeenCalledTimes(1)
expect(CartServiceMock.retrieveShippingOption).toHaveBeenCalledWith(
IdMap.getId("fr-cart"),
IdMap.getId("freeShipping")
)
})
it("calls CartService addShipping", () => {
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledTimes(1)
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledWith(
IdMap.getId("fr-cart"),
{
_id: IdMap.getId("freeShipping"),
profile_id: "default_profile",
data: {
extra_id: "id",
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns the cart", () => {
expect(subject.body._id).toEqual(IdMap.getId("fr-cart"))
expect(subject.body.decorated).toEqual(true)
})
})
describe("additional data without overwriting", () => {
let subject
beforeAll(async () => {
const cartId = IdMap.getId("emptyCart")
subject = await request(
"POST",
`/store/carts/${cartId}/shipping-methods`,
{
payload: {
option_id: IdMap.getId("withData"),
data: {
extra_id: "id",
},
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls CartService retrieveShippingOption", () => {
expect(CartServiceMock.retrieveShippingOption).toHaveBeenCalledTimes(1)
expect(CartServiceMock.retrieveShippingOption).toHaveBeenCalledWith(
IdMap.getId("emptyCart"),
IdMap.getId("withData")
)
})
it("calls CartService addShipping", () => {
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledTimes(1)
expect(CartServiceMock.addShippingMethod).toHaveBeenCalledWith(
IdMap.getId("emptyCart"),
{
_id: IdMap.getId("withData"),
profile_id: "default_profile",
data: {
extra_id: "id",
some_data: "yes",
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns the cart", () => {
expect(subject.body._id).toEqual(IdMap.getId("emptyCart"))
expect(subject.body.decorated).toEqual(true)
})
})
})
@@ -26,12 +26,13 @@ describe("POST /store/carts", () => {
})
})
it("returns 201", () => {
expect(subject.status).toEqual(201)
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns the cart", () => {
expect(subject.body._id).toEqual(IdMap.getId("regionCart"))
expect(subject.body.decorated).toEqual(true)
})
})
@@ -97,8 +98,8 @@ describe("POST /store/carts", () => {
jest.clearAllMocks()
})
it("returns 201", () => {
expect(subject.status).toEqual(201)
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls line item generate", () => {
@@ -117,6 +118,7 @@ describe("POST /store/carts", () => {
it("returns cart", () => {
expect(subject.body._id).toEqual(IdMap.getId("regionCart"))
expect(subject.body.decorated).toEqual(true)
})
})
@@ -0,0 +1,87 @@
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/:id", () => {
describe("successfully creates a line item", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/store/carts/${IdMap.getId("emptyCart")}/line-items`,
{
payload: {
variant_id: IdMap.getId("testVariant"),
quantity: 3,
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls CartService create", () => {
expect(CartServiceMock.addLineItem).toHaveBeenCalledTimes(1)
})
it("calls LineItemService generate", () => {
expect(LineItemServiceMock.generate).toHaveBeenCalledTimes(1)
expect(LineItemServiceMock.generate).toHaveBeenCalledWith(
IdMap.getId("testVariant"),
3,
IdMap.getId("testRegion")
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns the cart", () => {
expect(subject.body._id).toEqual(IdMap.getId("emptyCart"))
expect(subject.body.decorated).toEqual(true)
})
})
describe("handles unsuccessful line item generation", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/store/carts/${IdMap.getId("emptyCart")}/line-items`,
{
payload: {
variant_id: IdMap.getId("fail"),
quantity: 3,
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls LineItemService generate", () => {
expect(LineItemServiceMock.generate).toHaveBeenCalledTimes(1)
expect(LineItemServiceMock.generate).toHaveBeenCalledWith(
IdMap.getId("fail"),
3,
IdMap.getId("testRegion")
)
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns error", () => {
expect(subject.body.message).toEqual("Doesn't exist")
})
})
})
@@ -0,0 +1,33 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { CartServiceMock } from "../../../../../services/__mocks__/cart"
describe("POST /store/carts/:id/payment-sessions", () => {
describe("creates payment sessions", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/store/carts/${IdMap.getId("emptyCart")}/payment-sessions`
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls Cart service set payment sessions", () => {
expect(CartServiceMock.setPaymentSessions).toHaveBeenCalledTimes(1)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns the cart", () => {
expect(subject.body._id).toEqual(IdMap.getId("emptyCart"))
expect(subject.body.decorated).toEqual(true)
})
})
})
@@ -0,0 +1,33 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { CartServiceMock } from "../../../../../services/__mocks__/cart"
describe("POST /store/carts/:id/shipping-options", () => {
describe("creates shipping options", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/store/carts/${IdMap.getId("emptyCart")}/shipping-options`
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls Cart service set shipping options", () => {
expect(CartServiceMock.setShippingOptions).toHaveBeenCalledTimes(1)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns the cart", () => {
expect(subject.body._id).toEqual(IdMap.getId("emptyCart"))
expect(subject.body.decorated).toEqual(true)
})
})
})
@@ -23,6 +23,7 @@ describe("GET /store/carts", () => {
it("returns products", () => {
expect(subject.body._id).toEqual(IdMap.getId("emptyCart"))
expect(subject.body.decorated).toEqual(true)
})
})
@@ -86,6 +86,7 @@ describe("POST /store/carts/:id", () => {
it("returns cart", () => {
expect(subject.body._id).toEqual(IdMap.getId("emptyCart"))
expect(subject.body.decorated).toEqual(true)
})
})
@@ -0,0 +1,92 @@
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/:id/line-items/:line_id", () => {
describe("successfully updates a line item", () => {
let subject
beforeAll(async () => {
const cartId = IdMap.getId("emptyCart")
const lineId = IdMap.getId("existingLine")
subject = await request(
"POST",
`/store/carts/${cartId}/line-items/${lineId}`,
{
payload: {
variant_id: IdMap.getId("can-cover"),
quantity: 3,
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls CartService create", () => {
expect(CartServiceMock.updateLineItem).toHaveBeenCalledTimes(1)
})
it("calls LineItemService generate", () => {
expect(LineItemServiceMock.generate).toHaveBeenCalledTimes(1)
expect(LineItemServiceMock.generate).toHaveBeenCalledWith(
IdMap.getId("can-cover"),
3,
IdMap.getId("testRegion")
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns the cart", () => {
expect(subject.body._id).toEqual(IdMap.getId("emptyCart"))
expect(subject.body.decorated).toEqual(true)
})
})
describe("handles unsuccessful line item generation", () => {
let subject
beforeAll(async () => {
const cartId = IdMap.getId("emptyCart")
const lineId = IdMap.getId("existingLine")
subject = await request(
"POST",
`/store/carts/${cartId}/line-items/${lineId}`,
{
payload: {
variant_id: IdMap.getId("fail"),
quantity: 3,
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls LineItemService generate", () => {
expect(LineItemServiceMock.generate).toHaveBeenCalledTimes(1)
expect(LineItemServiceMock.generate).toHaveBeenCalledWith(
IdMap.getId("fail"),
3,
IdMap.getId("testRegion")
)
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns error", () => {
expect(subject.body.message).toEqual("Doesn't exist")
})
})
})
@@ -0,0 +1,99 @@
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/:id/payment-method", () => {
describe("successfully sets the payment method", () => {
let subject
beforeAll(async () => {
const cartId = IdMap.getId("cartWithPaySessions")
subject = await request("POST", `/store/carts/${cartId}/payment-method`, {
payload: {
provider_id: "default_provider",
},
})
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls CartService retrievePaymentSession", () => {
expect(CartServiceMock.retrievePaymentSession).toHaveBeenCalledTimes(1)
expect(CartServiceMock.retrievePaymentSession).toHaveBeenCalledWith(
IdMap.getId("cartWithPaySessions"),
"default_provider"
)
})
it("calls CartService setPaymentMethod", () => {
expect(CartServiceMock.setPaymentMethod).toHaveBeenCalledTimes(1)
expect(CartServiceMock.setPaymentMethod).toHaveBeenCalledWith(
IdMap.getId("cartWithPaySessions"),
{
provider_id: "default_provider",
data: {
money_id: "success",
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns the cart", () => {
expect(subject.body._id).toEqual(IdMap.getId("cartWithPaySessions"))
expect(subject.body.decorated).toEqual(true)
})
})
describe("fails when pay session not authorized", () => {
let subject
beforeAll(async () => {
const cartId = IdMap.getId("cartWithPaySessions")
subject = await request("POST", `/store/carts/${cartId}/payment-method`, {
payload: {
provider_id: "nono",
},
})
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls CartService retrievePaymentSession", () => {
expect(CartServiceMock.retrievePaymentSession).toHaveBeenCalledTimes(1)
expect(CartServiceMock.retrievePaymentSession).toHaveBeenCalledWith(
IdMap.getId("cartWithPaySessions"),
"nono"
)
})
it("calls CartService setPaymentMethod", () => {
expect(CartServiceMock.setPaymentMethod).toHaveBeenCalledTimes(1)
expect(CartServiceMock.setPaymentMethod).toHaveBeenCalledWith(
IdMap.getId("cartWithPaySessions"),
{
provider_id: "nono",
data: {
money_id: "fail",
},
}
)
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns the cart", () => {
expect(subject.body.message).toEqual("Not allowed")
})
})
})
@@ -0,0 +1,39 @@
import _ from "lodash"
import { Validator, MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
option_id: Validator.string().required(),
data: Validator.object().optional(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const cartService = req.scope.resolve("cartService")
const method = await cartService.retrieveShippingOption(id, value.option_id)
// If the option accepts additional data this will be added
if (!_.isEmpty(value.data)) {
method.data = {
...method.data,
...value.data,
}
}
await cartService.addShippingMethod(id, method)
let cart = await cartService.retrieve(id)
cart = await cartService.decorate(cart)
res.status(200).json(cart)
} catch (err) {
throw err
}
}
@@ -35,7 +35,8 @@ export default async (req, res) => {
}
cart = await cartService.retrieve(cart._id)
res.status(201).json(cart)
cart = await cartService.decorate(cart)
res.status(200).json(cart)
} catch (err) {
throw err
}
@@ -0,0 +1,35 @@
import { Validator, MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
variant_id: Validator.string().required(),
quantity: Validator.number().required(),
})
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.retrieve(id)
const lineItem = await lineItemService.generate(
value.variant_id,
value.quantity,
cart.region_id
)
await cartService.addLineItem(cart._id, lineItem)
cart = await cartService.retrieve(cart._id)
cart = await cartService.decorate(cart)
res.status(200).json(cart)
} catch (err) {
throw err
}
}
@@ -0,0 +1,18 @@
export default async (req, res) => {
const { id } = req.params
try {
const cartService = req.scope.resolve("cartService")
// Ask the cart service to set payment sessions
await cartService.setPaymentSessions(id)
// return the updated cart
let cart = await cartService.retrieve(id)
cart = await cartService.decorate(cart)
res.status(200).json(cart)
} catch (err) {
throw err
}
}
@@ -0,0 +1,17 @@
export default async (req, res) => {
const { id } = req.params
try {
const cartService = req.scope.resolve("cartService")
// Ask the cart service to set payment sessions
await cartService.setShippingOptions(id)
// return the updated cart
let cart = await cartService.retrieve(id)
cart = await cartService.decorate(cart)
res.status(200).json(cart)
} catch (err) {
throw err
}
}
@@ -2,12 +2,14 @@ export default async (req, res) => {
const { id } = req.params
const cartService = req.scope.resolve("cartService")
const cart = await cartService.retrieve(id)
let cart = await cartService.retrieve(id)
if (!cart) {
res.sendStatus(404)
return
}
cart = await cartService.decorate(cart)
res.json(cart)
}
@@ -11,5 +11,35 @@ export default app => {
route.post("/", middlewares.wrap(require("./create-cart").default))
route.post("/:id", middlewares.wrap(require("./update-cart").default))
// Line items
route.post(
"/:id/line-items",
middlewares.wrap(require("./create-line-item").default)
)
route.post(
"/:id/line-items/:line_id",
middlewares.wrap(require("./update-line-item").default)
)
// Payment sessions
route.post(
"/:id/payment-sessions",
middlewares.wrap(require("./create-payment-sessions").default)
)
route.post(
"/:id/payment-method",
middlewares.wrap(require("./update-payment-method").default)
)
// Shipping Options
route.post(
"/:id/shipping-options",
middlewares.wrap(require("./create-shipping-options").default)
)
route.post(
"/:id/shipping-methods",
middlewares.wrap(require("./add-shipping-method").default)
)
return app
}
@@ -51,7 +51,8 @@ export default async (req, res) => {
)
}
const newCart = await cartService.retrieve(id)
let newCart = await cartService.retrieve(id)
newCart = await cartService.decorate(newCart)
res.json(newCart)
} catch (err) {
throw err
@@ -0,0 +1,36 @@
import { Validator, MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const { id, line_id } = req.params
const schema = Validator.object().keys({
variant_id: Validator.objectId().required(),
quantity: Validator.number().required(),
})
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.retrieve(id)
const lineItem = await lineItemService.generate(
value.variant_id,
value.quantity,
cart.region_id
)
await cartService.updateLineItem(cart._id, line_id, lineItem)
cart = await cartService.retrieve(cart._id)
cart = await cartService.decorate(cart)
res.status(200).json(cart)
} catch (err) {
throw err
}
}
@@ -0,0 +1,31 @@
import { Validator, MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
provider_id: Validator.string().required(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const cartService = req.scope.resolve("cartService")
const session = await cartService.retrievePaymentSession(
id,
value.provider_id
)
await cartService.setPaymentMethod(id, session)
let cart = await cartService.retrieve(id)
cart = await cartService.decorate(cart)
res.status(200).json(cart)
} catch (err) {
throw err
}
}