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
}
}
+143 -11
View File
@@ -6,8 +6,117 @@ export const carts = {
title: "test",
region_id: IdMap.getId("testRegion"),
items: [],
shippingAddress: {},
billingAddress: {},
shipping_address: {},
billing_address: {},
discounts: [],
customer_id: "",
},
withShippingOptions: {
_id: IdMap.getId("withShippingOptions"),
title: "test",
region_id: IdMap.getId("region-france"),
items: [],
shipping_options: [
{
_id: IdMap.getId("freeShipping"),
name: "Free Shipping",
region_id: IdMap.getId("testRegion"),
price: 10,
provider_id: "test_shipper",
},
{
_id: IdMap.getId("expensiveShipping"),
name: "Expensive Shipping",
region_id: IdMap.getId("testRegion"),
price: 100,
provider_id: "test_shipper",
},
],
shipping_address: {},
billing_address: {},
discounts: [],
customer_id: "",
},
cartWithPaySessionsDifRegion: {
_id: IdMap.getId("cartWithPaySessionsDifRegion"),
region_id: IdMap.getId("region-france"),
items: [
{
_id: IdMap.getId("existingLine"),
title: "merge 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,
},
],
payment_sessions: [
{
provider_id: "default_provider",
data: {
id: "default_provider_session",
},
},
{
provider_id: "unregistered",
data: {
id: "unregistered_session",
},
},
],
shipping_address: {},
billing_address: {},
discounts: [],
customer_id: "",
},
cartWithPaySessions: {
_id: IdMap.getId("cartWithPaySessions"),
region_id: IdMap.getId("testRegion"),
shipping_methods: [],
items: [
{
_id: IdMap.getId("existingLine"),
title: "merge 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,
},
],
payment_sessions: [
{
provider_id: "default_provider",
data: {
id: "default_provider_session",
},
},
{
provider_id: "unregistered",
data: {
id: "unregistered_session",
},
},
],
shipping_address: {},
billing_address: {},
discounts: [],
customer_id: "",
},
@@ -34,8 +143,8 @@ export const carts = {
quantity: 10,
},
],
shippingAddress: {},
billingAddress: {},
shipping_address: {},
billing_address: {},
discounts: [],
customer_id: "",
},
@@ -50,12 +159,14 @@ export const carts = {
yes: "sir",
},
},
shipping_method: {
provider_id: "gls",
data: {
yes: "sir",
shipping_methods: [
{
provider_id: "gls",
data: {
yes: "sir",
},
},
},
],
shipping_address: {
first_name: "hi",
last_name: "you",
@@ -127,8 +238,20 @@ export const carts = {
quantity: 10,
},
],
shippingAddress: {},
billingAddress: {},
shipping_methods: [
{
_id: IdMap.getId("freeShipping"),
profile_id: "default_profile",
},
],
shipping_options: [
{
_id: IdMap.getId("freeShipping"),
profile_id: "default_profile",
},
],
shipping_address: {},
billing_address: {},
discounts: [],
customer_id: "",
},
@@ -141,6 +264,15 @@ export const CartModelMock = {
}),
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
findOne: jest.fn().mockImplementation(query => {
if (query._id === IdMap.getId("withShippingOptions")) {
return Promise.resolve(carts.withShippingOptions)
}
if (query._id === IdMap.getId("cartWithPaySessionsDifRegion")) {
return Promise.resolve(carts.cartWithPaySessionsDifRegion)
}
if (query._id === IdMap.getId("cartWithPaySessions")) {
return Promise.resolve(carts.cartWithPaySessions)
}
if (query._id === IdMap.getId("emptyCart")) {
return Promise.resolve(carts.emptyCart)
}
+2
View File
@@ -20,6 +20,8 @@ class CartModel extends BaseModel {
region_id: { type: String, required: true },
discounts: { type: [String], default: [] },
customer_id: { type: String, default: "" },
payment_sessions: { type: [PaymentMethodSchema], default: [] },
shipping_options: { type: [ShippingMethodSchema], default: [] },
payment_method: { type: PaymentMethodSchema, default: {} },
shipping_methods: { type: [ShippingMethodSchema], default: [] },
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
@@ -5,12 +5,132 @@ export const carts = {
emptyCart: {
_id: IdMap.getId("emptyCart"),
items: [],
region_id: IdMap.getId("testRegion"),
shipping_options: [
{
_id: IdMap.getId("freeShipping"),
profile_id: "default_profile",
data: {
some_data: "yes",
},
},
],
},
regionCart: {
_id: IdMap.getId("regionCart"),
name: "Product 1",
region_id: IdMap.getId("testRegion"),
},
frCart: {
_id: IdMap.getId("fr-cart"),
title: "test",
region_id: IdMap.getId("region-france"),
items: [
{
_id: IdMap.getId("line"),
title: "merge line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
content: [
{
unit_price: 8,
variant: {
_id: IdMap.getId("eur-8-us-10"),
},
product: {
_id: IdMap.getId("product"),
},
quantity: 1,
},
{
unit_price: 10,
variant: {
_id: IdMap.getId("eur-10-us-12"),
},
product: {
_id: IdMap.getId("product"),
},
quantity: 1,
},
],
quantity: 10,
},
{
_id: IdMap.getId("existingLine"),
title: "merge line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
content: {
unit_price: 10,
variant: {
_id: IdMap.getId("eur-10-us-12"),
},
product: {
_id: IdMap.getId("product"),
},
quantity: 1,
},
quantity: 10,
},
],
shipping_methods: [
{
_id: IdMap.getId("freeShipping"),
profile_id: "default_profile",
},
],
shipping_options: [
{
_id: IdMap.getId("freeShipping"),
profile_id: "default_profile",
},
],
shipping_address: {},
billing_address: {},
discounts: [],
customer_id: "",
},
cartWithPaySessions: {
_id: IdMap.getId("cartWithPaySessions"),
region_id: IdMap.getId("testRegion"),
items: [
{
_id: IdMap.getId("existingLine"),
title: "merge 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,
},
],
payment_sessions: [
{
provider_id: "default_provider",
data: {
id: "default_provider_session",
},
},
{
provider_id: "unregistered",
data: {
id: "unregistered_session",
},
},
],
shipping_address: {},
billing_address: {},
discounts: [],
customer_id: "",
},
}
export const CartServiceMock = {
@@ -23,17 +143,33 @@ export const CartServiceMock = {
}
}),
retrieve: jest.fn().mockImplementation(cartId => {
if (cartId === IdMap.getId("fr-cart")) {
return Promise.resolve(carts.frCart)
}
if (cartId === IdMap.getId("regionCart")) {
return Promise.resolve(carts.regionCart)
}
if (cartId === IdMap.getId("emptyCart")) {
return Promise.resolve(carts.emptyCart)
}
if (cartId === IdMap.getId("cartWithPaySessions")) {
return Promise.resolve(carts.cartWithPaySessions)
}
return Promise.resolve(undefined)
}),
addLineItem: jest.fn().mockImplementation((cartId, lineItem) => {
return Promise.resolve()
}),
setPaymentMethod: jest.fn().mockImplementation((cartId, method) => {
if (method.provider_id === "default_provider") {
return Promise.resolve()
}
throw new MedusaError(MedusaError.Types.NOT_ALLOWED, "Not allowed")
}),
updateLineItem: jest.fn().mockImplementation((cartId, lineItem) => {
return Promise.resolve()
}),
setRegion: jest.fn().mockImplementation((cartId, regionId) => {
if (regionId === IdMap.getId("fail")) {
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Region not found")
@@ -52,6 +188,55 @@ export const CartServiceMock = {
applyPromoCode: jest.fn().mockImplementation((cartId, code) => {
return Promise.resolve()
}),
setPaymentSessions: jest.fn().mockImplementation(cartId => {
return Promise.resolve()
}),
setShippingOptions: jest.fn().mockImplementation(cartId => {
return Promise.resolve()
}),
decorate: jest.fn().mockImplementation(cart => {
cart.decorated = true
return cart
}),
addShippingMethod: jest.fn().mockImplementation(cartId => {
return Promise.resolve()
}),
retrieveShippingOption: jest.fn().mockImplementation((cartId, optionId) => {
if (optionId === IdMap.getId("freeShipping")) {
return {
_id: IdMap.getId("freeShipping"),
profile_id: "default_profile",
}
}
if (optionId === IdMap.getId("withData")) {
return {
_id: IdMap.getId("withData"),
profile_id: "default_profile",
data: {
some_data: "yes",
},
}
}
}),
retrievePaymentSession: jest.fn().mockImplementation((cartId, providerId) => {
if (providerId === "default_provider") {
return {
provider_id: "default_provider",
data: {
money_id: "success",
},
}
}
if (providerId === "nono") {
return {
provider_id: "nono",
data: {
money_id: "fail",
},
}
}
}),
}
const mock = jest.fn().mockImplementation(() => {
@@ -1,4 +1,5 @@
import { IdMap } from "medusa-test-utils"
import { MedusaError } from "medusa-core-utils"
export const LineItemServiceMock = {
validate: jest.fn().mockImplementation(data => {
@@ -8,6 +9,10 @@ export const LineItemServiceMock = {
return data
}),
generate: jest.fn().mockImplementation((variantId, quantity, regionId) => {
if (variantId === IdMap.getId("fail") || regionId === IdMap.getId("fail")) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, "Doesn't exist")
}
return Promise.resolve({
content: {
variant: {
@@ -13,6 +13,24 @@ export const DefaultProviderMock = {
}
export const PaymentProviderServiceMock = {
updateSession: jest.fn().mockImplementation((session, cart) => {
return Promise.resolve({
provider_id: session.provider_id,
data: {
...session.data,
id: `${session.data.id}_updated`,
},
})
}),
createSession: jest.fn().mockImplementation((providerId, cart) => {
return Promise.resolve({
provider_id: providerId,
data: {
id: `${providerId}_session`,
cartId: cart._id,
},
})
}),
retrieveProvider: jest.fn().mockImplementation(providerId => {
if (providerId === "default_provider") {
return DefaultProviderMock
@@ -14,6 +14,7 @@ export const regions = {
_id: IdMap.getId("region-france"),
name: "France",
countries: ["FR"],
payment_providers: ["default_provider", "france-provider"],
currency_code: "eur",
},
regionUs: {
@@ -0,0 +1,99 @@
import { IdMap } from "medusa-test-utils"
export const shippingOptions = {
freeShipping: {
_id: IdMap.getId("freeShipping"),
name: "Free Shipping",
region_id: IdMap.getId("testRegion"),
profile_id: IdMap.getId("default-profile"),
data: {
id: "fs",
},
price: {
type: "flat_rate",
amount: 10,
},
provider_id: "test_shipper",
},
expensiveShipping: {
_id: IdMap.getId("expensiveShipping"),
name: "Expensive Shipping",
profile_id: IdMap.getId("fragile-profile"),
region_id: IdMap.getId("testRegion"),
data: {
id: "es",
},
price: {
type: "flat_rate",
amount: 100,
},
provider_id: "test_shipper",
},
franceShipping: {
_id: IdMap.getId("franceShipping"),
name: "FR Shipping",
profile_id: IdMap.getId("default-profile"),
region_id: IdMap.getId("region-france"),
data: {
id: "bonjour",
},
price: {
type: "flat_rate",
amount: 20,
},
provider_id: "test_shipper",
},
}
export const ShippingOptionServiceMock = {
retrieve: jest.fn().mockImplementation(optionId => {
if (optionId === IdMap.getId("freeShipping")) {
return Promise.resolve(shippingOptions.freeShipping)
}
return Promise.resolve(undefined)
}),
list: jest.fn().mockImplementation(data => {
if (data.region_id === IdMap.getId("region-france")) {
return Promise.resolve([shippingOptions.franceShipping])
}
if (data.region_id === IdMap.getId("testRegion")) {
return Promise.resolve([
shippingOptions.freeShipping,
shippingOptions.expensiveShipping,
])
}
}),
validateCartOption: jest.fn().mockImplementation((method, cart) => {
if (method._id === IdMap.getId("freeShipping")) {
return Promise.resolve(true)
}
if (method._id === IdMap.getId("franceShipping")) {
return Promise.resolve(true)
}
if (method._id === IdMap.getId("fail")) {
return Promise.resolve(false)
}
}),
fetchCartOptions: jest.fn().mockImplementation(cart => {
if (cart._id === IdMap.getId("cartWithLine")) {
return Promise.resolve([
{
_id: IdMap.getId("freeShipping"),
name: "Free Shipping",
region_id: IdMap.getId("testRegion"),
price: 10,
data: {
id: "fs",
},
provider_id: "test_shipper",
},
])
}
}),
}
const mock = jest.fn().mockImplementation(() => {
return ShippingOptionServiceMock
})
export default mock
+569 -15
View File
@@ -7,6 +7,7 @@ import {
} from "../__mocks__/payment-provider"
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
import { RegionServiceMock } from "../__mocks__/region"
import { ShippingOptionServiceMock } from "../__mocks__/shipping-option"
import { CartModelMock, carts } from "../../models/__mocks__/cart"
import { LineItemServiceMock } from "../__mocks__/line-item"
@@ -204,20 +205,6 @@ describe("CartService", () => {
)
})
it("throws if line item not validated", async () => {
const lineItem = {
title: "invalid lineitem",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
}
try {
await cartService.addLineItem(IdMap.getId("cartWithLine"), lineItem)
} catch (err) {
expect(err.message).toEqual(`"content" is required`)
}
})
it("throws if inventory isn't covered", async () => {
const lineItem = {
title: "merge line",
@@ -285,6 +272,85 @@ describe("CartService", () => {
})
})
describe("updateLineItem", () => {
const cartService = new CartService({
cartModel: CartModelMock,
productVariantService: ProductVariantServiceMock,
lineItemService: LineItemServiceMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("successfully updates existing line item", async () => {
const lineItem = {
title: "update line",
description: "This is a new line",
thumbnail: "https://test-img-yeah.com/thumb",
content: {
unit_price: 123,
variant: {
_id: IdMap.getId("can-cover"),
},
product: {
_id: IdMap.getId("product"),
},
quantity: 1,
},
quantity: 2,
}
await cartService.updateLineItem(
IdMap.getId("cartWithLine"),
IdMap.getId("existingLine"),
lineItem
)
expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(CartModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("cartWithLine"),
"items._id": IdMap.getId("existingLine"),
},
{
$set: { "items.$": lineItem },
}
)
})
it("throws if inventory isn't covered", async () => {
const lineItem = {
title: "merge line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
quantity: 1,
content: {
variant: {
_id: IdMap.getId("cannot-cover"),
},
product: {
_id: IdMap.getId("product"),
},
quantity: 1,
unit_price: 1234,
},
}
try {
await cartService.updateLineItem(
IdMap.getId("cartWithLine"),
IdMap.getId("existingLine"),
lineItem
)
} catch (err) {
expect(err.message).toEqual(
`Inventory doesn't cover the desired quantity`
)
}
})
})
describe("updateEmail", () => {
const cartService = new CartService({
cartModel: CartModelMock,
@@ -459,6 +525,7 @@ describe("CartService", () => {
{
$set: {
region_id: IdMap.getId("region-us"),
shipping_methods: [],
items: [
{
_id: IdMap.getId("line"),
@@ -526,7 +593,7 @@ describe("CartService", () => {
{
$set: {
region_id: IdMap.getId("region-us"),
shipping_method: undefined,
shipping_methods: [],
payment_method: undefined,
shipping_address: {
first_name: "hi",
@@ -711,4 +778,491 @@ describe("CartService", () => {
}
})
})
describe("setPaymentSessions", () => {
const cartService = new CartService({
cartModel: CartModelMock,
regionService: RegionServiceMock,
paymentProviderService: PaymentProviderServiceMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("initializes payment sessions for each of the providers", async () => {
await cartService.setPaymentSessions(IdMap.getId("cartWithLine"))
expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(2)
expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledWith(
"default_provider",
carts.cartWithLine
)
expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledWith(
"unregistered",
carts.cartWithLine
)
expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(CartModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("cartWithLine"),
},
{
$set: {
payment_sessions: [
{
provider_id: "default_provider",
data: {
id: "default_provider_session",
cartId: IdMap.getId("cartWithLine"),
},
},
{
provider_id: "unregistered",
data: {
id: "unregistered_session",
cartId: IdMap.getId("cartWithLine"),
},
},
],
},
}
)
})
it("updates payment sessions for existing sessions", async () => {
await cartService.setPaymentSessions(IdMap.getId("cartWithPaySessions"))
expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(0)
expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(2)
expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledWith(
{
provider_id: "default_provider",
data: {
id: "default_provider_session",
},
},
carts.cartWithPaySessions
)
expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledWith(
{
provider_id: "unregistered",
data: {
id: "unregistered_session",
},
},
carts.cartWithPaySessions
)
expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(CartModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("cartWithPaySessions"),
},
{
$set: {
payment_sessions: [
{
provider_id: "default_provider",
data: {
id: "default_provider_session_updated",
},
},
{
provider_id: "unregistered",
data: {
id: "unregistered_session_updated",
},
},
],
},
}
)
})
it("filters sessions not available in the region", async () => {
await cartService.setPaymentSessions(
IdMap.getId("cartWithPaySessionsDifRegion")
)
expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledTimes(1)
expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledTimes(1)
expect(PaymentProviderServiceMock.updateSession).toHaveBeenCalledWith(
{
provider_id: "default_provider",
data: {
id: "default_provider_session",
},
},
carts.cartWithPaySessionsDifRegion
)
expect(PaymentProviderServiceMock.createSession).toHaveBeenCalledWith(
"france-provider",
carts.cartWithPaySessionsDifRegion
)
expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(CartModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("cartWithPaySessionsDifRegion"),
},
{
$set: {
payment_sessions: [
{
provider_id: "default_provider",
data: {
id: "default_provider_session_updated",
},
},
{
provider_id: "france-provider",
data: {
id: "france-provider_session",
cartId: IdMap.getId("cartWithPaySessionsDifRegion"),
},
},
],
},
}
)
})
})
describe("setShippingOptions", () => {
const cartService = new CartService({
cartModel: CartModelMock,
regionService: RegionServiceMock,
shippingOptionService: ShippingOptionServiceMock,
})
describe("gets shipping options from the cart's regions", () => {
beforeAll(async () => {
jest.clearAllMocks()
await cartService.setShippingOptions(IdMap.getId("cartWithLine"))
})
it("gets shipping options from region", () => {
expect(
ShippingOptionServiceMock.fetchCartOptions
).toHaveBeenCalledTimes(1)
expect(ShippingOptionServiceMock.fetchCartOptions).toHaveBeenCalledWith(
carts.cartWithLine
)
})
it("updates cart", () => {
expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(CartModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("cartWithLine"),
},
{
$set: {
shipping_options: [
{
_id: IdMap.getId("freeShipping"),
name: "Free Shipping",
region_id: IdMap.getId("testRegion"),
price: 10,
data: {
id: "fs",
},
provider_id: "test_shipper",
},
],
},
}
)
})
})
})
describe("retrievePaymentSession", () => {
const cartService = new CartService({
cartModel: CartModelMock,
})
let res
describe("it retrieves the correct payment session", () => {
beforeAll(async () => {
jest.clearAllMocks()
res = await cartService.retrievePaymentSession(
IdMap.getId("cartWithPaySessions"),
"default_provider"
)
})
it("retrieves the cart", () => {
expect(CartModelMock.findOne).toHaveBeenCalledTimes(1)
expect(CartModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("cartWithPaySessions"),
})
})
it("finds the correct payment session", () => {
expect(res.provider_id).toEqual("default_provider")
expect(res.data).toEqual({
id: "default_provider_session",
})
})
})
describe("it fails when provider doesn't match open session", () => {
beforeAll(async () => {
jest.clearAllMocks()
try {
await cartService.retrievePaymentSession(
IdMap.getId("cartWithPaySessions"),
"nono"
)
} catch (err) {
res = err
}
})
it("retrieves the cart", () => {
expect(CartModelMock.findOne).toHaveBeenCalledTimes(1)
expect(CartModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("cartWithPaySessions"),
})
})
it("throws invalid data errro", () => {
expect(res.message).toEqual(
"The provider_id did not match any open payment sessions"
)
})
})
})
describe("addShippingMethod", () => {
const cartService = new CartService({
cartModel: CartModelMock,
shippingOptionService: ShippingOptionServiceMock,
})
describe("successfully adds the shipping method", () => {
const method = {
_id: IdMap.getId("freeShipping"),
provider_id: "test_shipper",
profile_id: "default_profile",
price: 20,
region_id: IdMap.getId("testRegion"),
data: {
id: "testshipperid",
},
products: [IdMap.getId("product")],
}
beforeAll(async () => {
jest.clearAllMocks()
const cartId = IdMap.getId("cartWithPaySessions")
await cartService.addShippingMethod(cartId, method)
})
it("checks availability", () => {
expect(
ShippingOptionServiceMock.validateCartOption
).toHaveBeenCalledTimes(1)
expect(
ShippingOptionServiceMock.validateCartOption
).toHaveBeenCalledWith(method, carts.cartWithPaySessions)
})
it("updates cart", () => {
expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(CartModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("cartWithPaySessions"),
},
{
$set: { shipping_methods: [method] },
}
)
})
})
describe("successfully overrides existing profile shipping method", () => {
const method = {
_id: IdMap.getId("freeShipping"),
provider_id: "test_shipper",
profile_id: "default_profile",
price: 20,
region_id: IdMap.getId("testRegion"),
data: {
id: "testshipperid",
},
products: [IdMap.getId("product")],
}
beforeAll(async () => {
jest.clearAllMocks()
const cartId = IdMap.getId("fr-cart")
await cartService.addShippingMethod(cartId, method)
})
it("checks availability", () => {
expect(
ShippingOptionServiceMock.validateCartOption
).toHaveBeenCalledTimes(1)
expect(
ShippingOptionServiceMock.validateCartOption
).toHaveBeenCalledWith(method, carts.frCart)
})
it("updates cart", () => {
expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(CartModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("fr-cart"),
},
{
$set: { shipping_methods: [method] },
}
)
})
})
describe("successfully adds additional shipping method", () => {
const method = {
_id: IdMap.getId("freeShipping"),
provider_id: "test_shipper",
profile_id: "additional_profile",
price: 20,
region_id: IdMap.getId("testRegion"),
data: {
id: "testshipperid",
},
products: [IdMap.getId("product")],
}
beforeAll(async () => {
jest.clearAllMocks()
const cartId = IdMap.getId("fr-cart")
await cartService.addShippingMethod(cartId, method)
})
it("checks availability", () => {
expect(
ShippingOptionServiceMock.validateCartOption
).toHaveBeenCalledTimes(1)
expect(
ShippingOptionServiceMock.validateCartOption
).toHaveBeenCalledWith(method, carts.frCart)
})
it("updates cart", () => {
expect(CartModelMock.updateOne).toHaveBeenCalledTimes(1)
expect(CartModelMock.updateOne).toHaveBeenCalledWith(
{
_id: IdMap.getId("fr-cart"),
},
{
$set: {
shipping_methods: [
{
_id: IdMap.getId("freeShipping"),
profile_id: "default_profile",
},
method,
],
},
}
)
})
})
describe("throws error on no availability", () => {
const method = {
_id: IdMap.getId("fail"),
}
let res
beforeAll(async () => {
jest.clearAllMocks()
const cartId = IdMap.getId("fr-cart")
try {
await cartService.addShippingMethod(cartId, method)
} catch (err) {
res = err
}
})
it("checks availability", () => {
expect(
ShippingOptionServiceMock.validateCartOption
).toHaveBeenCalledTimes(1)
expect(
ShippingOptionServiceMock.validateCartOption
).toHaveBeenCalledWith(method, carts.frCart)
})
it("throw error", () => {
expect(res.message).toEqual(
"The selected shipping method cannot be applied to the cart"
)
})
})
})
describe("retrieveShippingOption", () => {
const cartService = new CartService({
cartModel: CartModelMock,
})
let res
describe("it retrieves the correct payment session", () => {
beforeAll(async () => {
jest.clearAllMocks()
res = await cartService.retrieveShippingOption(
IdMap.getId("fr-cart"),
IdMap.getId("freeShipping")
)
})
it("retrieves the cart", () => {
expect(CartModelMock.findOne).toHaveBeenCalledTimes(1)
expect(CartModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("fr-cart"),
})
})
it("finds the correct payment session", () => {
expect(res._id).toEqual(IdMap.getId("freeShipping"))
})
})
describe("it fails when provider doesn't match open session", () => {
beforeAll(async () => {
jest.clearAllMocks()
try {
await cartService.retrieveShippingOption(
IdMap.getId("fr-cart"),
"nono"
)
} catch (err) {
res = err
}
})
it("retrieves the cart", () => {
expect(CartModelMock.findOne).toHaveBeenCalledTimes(1)
expect(CartModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("fr-cart"),
})
})
it("throws invalid data errro", () => {
expect(res.message).toEqual(
"The option id doesn't match any available shipping options"
)
})
})
})
})
+241 -11
View File
@@ -15,6 +15,7 @@ class CartService extends BaseService {
productVariantService,
regionService,
lineItemService,
shippingOptionService,
}) {
super()
@@ -38,6 +39,9 @@ class CartService extends BaseService {
/** @private @const {PaymentProviderService} */
this.paymentProviderService_ = paymentProviderService
/** @private @const {ShippingOptionsService} */
this.shippingOptionService_ = shippingOptionService
}
/**
@@ -200,9 +204,7 @@ class CartService extends BaseService {
* @return {Cart} return the decorated cart.
*/
async decorate(cart, fields, expandFields = []) {
const requiredFields = ["_id", "metadata"]
const decorated = _.pick(cart, fields.concat(requiredFields))
return decorated
return cart
}
/**
@@ -213,9 +215,7 @@ class CartService extends BaseService {
*/
async addLineItem(cartId, lineItem) {
const validatedLineItem = this.lineItemService_.validate(lineItem)
const cart = await this.retrieve(cartId)
const currentItem = cart.items.find(line =>
_.isEqual(line.content, validatedLineItem.content)
)
@@ -275,6 +275,61 @@ class CartService extends BaseService {
)
}
/**
* Updates a cart's existing line item.
* @param {string} cartId - the id of the cart to update
* @param {string} lineItemId - the id of the line item to update.
* @param {LineItem} lineItem - the line item to update. Must include an _id
* field.
* @return {Promise} the result of the update operation
*/
async updateLineItem(cartId, lineItemId, lineItem) {
const cart = await this.retrieve(cartId)
const validatedLineItem = this.lineItemService_.validate(lineItem)
if (!lineItemId) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Line Item must have an _id corresponding to an existing line item id"
)
}
// Ensure that the line item exists in the cart
const lineItemExists = cart.items.find(i => i._id === lineItemId)
if (!lineItemExists) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"A line item with the provided id doesn't exist in the cart"
)
}
// Ensure that inventory covers the request
const hasInventory = await this.confirmInventory_(
validatedLineItem.content,
validatedLineItem.quantity
)
if (!hasInventory) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Inventory doesn't cover the desired quantity"
)
}
// Update the line item
return this.cartModel_.updateOne(
{
_id: cartId,
"items._id": lineItemId,
},
{
$set: {
"items.$": validatedLineItem,
},
}
)
}
/**
* Sets the email of a cart
* @param {string} cartId - the id of the cart to add email to
@@ -283,7 +338,6 @@ class CartService extends BaseService {
*/
async updateEmail(cartId, email) {
const cart = await this.retrieve(cartId)
const schema = Validator.string()
.email()
.required()
@@ -313,7 +367,6 @@ class CartService extends BaseService {
*/
async updateBillingAddress(cartId, address) {
const cart = await this.retrieve(cartId)
const { value, error } = Validator.address().validate(address)
if (error) {
throw new MedusaError(
@@ -340,7 +393,6 @@ class CartService extends BaseService {
*/
async updateShippingAddress(cartId, address) {
const cart = await this.retrieve(cartId)
const { value, error } = Validator.address().validate(address)
if (error) {
throw new MedusaError(
@@ -360,12 +412,37 @@ class CartService extends BaseService {
}
/**
* A payment method represents a way for the customer to pay. The payment
* method will typically come from one of the payment sessions.
* @typedef {object} PaymentMethod
* @property {string} provider_id - the identifier of the payment method's
* provider
* @property {object} data - the data associated with the payment method
*/
/**
* Retrieves an open payment session from the list of payment sessions
* stored in the cart. If none is an INVALID_DATA error is thrown.
* @param {string} cartId - the id of the cart to retrieve the session from
* @param {string} providerId - the id of the provider the session belongs to
* @return {PaymentMethod} the session
*/
async retrievePaymentSession(cartId, providerId) {
const cart = await this.retrieve(cartId)
const session = cart.payment_sessions.find(
({ provider_id }) => provider_id === providerId
)
if (!session) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The provider_id did not match any open payment sessions`
)
}
return session
}
/**
* Sets a payment method for a cart.
* @param {string} cartId - the id of the cart to add payment method to
@@ -389,7 +466,8 @@ class CartService extends BaseService {
)
}
// Check if the payment method has been authorized.
// The provider service will be able to perform operations on the
// session we are trying to set as the payment method.
const provider = this.paymentProviderService_.retrieveProvider(
paymentMethod.provider_id
)
@@ -413,6 +491,158 @@ class CartService extends BaseService {
)
}
/**
* Creates, updates and sets payment sessions associated with the cart. The
* first time the method is called payment sessions will be created for each
* provider. Additional calls will ensure that payment sessions have correct
* amounts, currencies, etc. as well as make sure to filter payment sessions
* that are not available for the cart's region.
* @param {string} cartId - the id of the cart to set payment session for
* @returns {Promise} the result of the update operation.
*/
async setPaymentSessions(cartId) {
const cart = await this.retrieve(cartId)
const region = await this.regionService_.retrieve(cart.region_id)
// If there are existing payment sessions ensure that these are up to date
let sessions = []
if (cart.payment_sessions && cart.payment_sessions.length) {
sessions = await Promise.all(
cart.payment_sessions.map(async pSession => {
if (!region.payment_providers.includes(pSession.provider_id)) {
return null
}
return this.paymentProviderService_.updateSession(pSession, cart)
})
)
}
// Filter all null sessions
sessions = sessions.filter(s => !!s)
// For all the payment providers in the region make sure to either skip them
// if they already exist or create them if they don't yet exist.
let newSessions = await Promise.all(
region.payment_providers.map(async pId => {
if (sessions.find(s => s.provider_id === pId)) {
return null
}
return this.paymentProviderService_.createSession(pId, cart)
})
)
// Filter null sessions
newSessions = newSessions.filter(s => !!s)
// Update the payment sessions with the concatenated array of updated and
// newly created payment sessions
return this.cartModel_.updateOne(
{
_id: cart._id,
},
{
$set: { payment_sessions: sessions.concat(newSessions) },
}
)
}
/**
* Retrieves one of the open shipping options for the cart.
* @param {string} cartId - the id of the cart to retrieve the option from
* @param {string} optionId - the id of the option to retrieve
* @return {ShippingOption} the option that was found
*/
async retrieveShippingOption(cartId, optionId) {
const cart = await this.retrieve(cartId)
const option = cart.shipping_options.find(({ _id }) => _id === optionId)
if (!option) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The option id doesn't match any available shipping options`
)
}
return option
}
/**
* Adds the shipping method to the list of shipping methods associated with
* the cart.
* @param {string} cartId - the id of the cart to add shipping method to
* @param {ShippingOption} method - the shipping method to add to the cart
* @return {Promise} the result of the update operation
*/
async addShippingMethod(cartId, method) {
const cart = await this.retrieve(cartId)
const { shipping_methods } = cart
const isValid = await this.shippingOptionService_.validateCartOption(
method,
cart
)
if (!isValid) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"The selected shipping method cannot be applied to the cart"
)
}
// Go through all existing selected shipping methods and update the one
// that has the same profile as the selected shipping method.
let exists = false
const newMethods = shipping_methods.map(sm => {
if (sm.profile_id === method.profile_id) {
exists = true
return method
}
return sm
})
// If none of the selected methods are for the same profile as the new
// shipping method the exists flag will be false. Therefore we push the new
// method.
if (!exists) {
newMethods.push(method)
}
return this.cartModel_.updateOne(
{
_id: cart._id,
},
{
$set: { shipping_methods: newMethods },
}
)
}
/**
* Finds all shipping options that are available to the cart and stores them
* in shipping_options. The shipping options are retrieved from the shipping
* option service.
* @param {string} cartId - the id of the cart
* @return {Promse} the result of the update operation
*/
async setShippingOptions(cartId) {
const cart = await this.retrieve(cartId)
// Get the shipping options available in the region
const cartOptions = await this.shippingOptionService_.fetchCartOptions(cart)
return this.cartModel_.updateOne(
{
_id: cart._id,
},
{
$set: { shipping_options: cartOptions },
}
)
}
/**
* Set's the region of a cart.
* @param {string} cartId - the id of the cart to set region on
@@ -463,8 +693,8 @@ class CartService extends BaseService {
// Shipping methods are determined by region so the user needs to find a
// new shipping method
if (!_.isEmpty(cart.shipping_method)) {
update.shipping_method = undefined
if (cart.shipping_methods && cart.shipping_methods.length) {
update.shipping_methods = []
}
// Payment methods are region specific so the user needs to find a