Completes Cart Service and store/carts endpoints (#18)
Completes Cart Service to allow shopping and checkout flows.
This commit is contained in:
9
docs/api/store/carts.md
Normal file
9
docs/api/store/carts.md
Normal file
@@ -0,0 +1,9 @@
|
||||
POST /carts
|
||||
GET /carts/:id
|
||||
POST /carts/:id
|
||||
|
||||
POST /carts/:id/line-items
|
||||
POST /carts/:id/line-items/:line_id
|
||||
|
||||
GET /carts/:id/shipping-options
|
||||
POST /carts/:id/payment-sessions
|
||||
@@ -50,7 +50,7 @@ removes the line item with the given line id by calling `removeLineItem`.
|
||||
### Initializing the checkout
|
||||
When the customer is ready to check out they will reach your checkout page. At
|
||||
this point you want to display which payment and shipping options are offered.
|
||||
`POST /cart/payment-providers` and `POST /cart/fulfillment-providers` will
|
||||
`POST /cart/payment-sessions` and `POST /cart/fulfillment-sessions` will
|
||||
initialize payment sessions and shipping methods offered by fulfillment
|
||||
providers. The payment sessions will typically have to be updated if any further
|
||||
changes to the cart total happens (shipping fee, promo codes, user exits
|
||||
@@ -93,3 +93,89 @@ function `setPaymentMethod` which will fetch the cart, search the
|
||||
check that the payment session is authorized. When the authorization is verified
|
||||
the payment method is set and the controller can safely call the Order service
|
||||
function `create`.
|
||||
|
||||
|
||||
### How do payment sessions work?
|
||||
|
||||
When the customer first enters the checkout page you should initialize payment
|
||||
sessions for each of the possible payment providers. This is done with a single
|
||||
call to `POST /cart/payment-sessions`. Calls to `POST /cart/payment-sessions`
|
||||
will either create payment sessions for each payment provider or if the payment
|
||||
sessions have already been initialized ensure that the sessions are up to date (
|
||||
i.e. that the cart amount corresponds to the payment sessions' amounts). When
|
||||
the customer reaches the payment part of the checkout (or alternatively when she
|
||||
decides to use one of the payment providers as a checkout provider) the payment
|
||||
method will be saved once authorized.
|
||||
|
||||
### How do fulfillment sessions work?
|
||||
|
||||
When the customer first enters the checkout page, fulfillment sessions should be
|
||||
initialized. The fulfillment session is responsible for fetching shipping
|
||||
options with a fulfillment provider. E.g. your store has an integration with
|
||||
your 3PL as a fulfillment provider. The 3PL has 4 shipping options: standard,
|
||||
express and fragile shipping as well as a parcel shop service where orders are
|
||||
delivered to a local store.
|
||||
|
||||
The store operator will have set up which shipping options are available in the
|
||||
customer's region. I.e. the store operator may have created a shipping option
|
||||
called Free Shipping, which uses the "Standard Shipping" method from the 3PL
|
||||
integration, and which is free when the order value is above 100 USD. The store
|
||||
operator may have also created an Express Shipping option which uses the
|
||||
"Express Shipping" method from the 3PL integration and which costs 20 USD.
|
||||
The store operator has also created a Fragile Shipping option which uses the
|
||||
"Fragile Shipping" method from the 3PL integration and which has variable
|
||||
pricing depending on the size of the shipment. The variable pricing is
|
||||
calculated by the integration depending on cart. Finally, the store operator has
|
||||
defined a parcel shop option, which uses the 3PL's parcel shop shipping method.
|
||||
The customer needs to provide the ID of the local store that she wants her order
|
||||
delivered to and the shipping method therefore takes some additional input to be
|
||||
a valid shipping method for an order.
|
||||
|
||||
When the customer enters the checkout page the `POST /cart/shipping-options`
|
||||
call will fetch each of the shipping options that the store operator has set up.
|
||||
Extending the above example, an array of shipping options would be stored in the
|
||||
cart in the format:
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
_id: [some-id],
|
||||
provider_id: "3pl_integration",
|
||||
name: "Free Shipping",
|
||||
price: 0,
|
||||
data: {
|
||||
// This will contain data specific to the shipping method, i.e. the
|
||||
// id that the 3PL needs in order to process the order with this shipping
|
||||
// method
|
||||
}
|
||||
},
|
||||
{
|
||||
_id: [some-id],
|
||||
provider_id: "3pl_integration",
|
||||
name: "Express Shipping",
|
||||
price: 20,
|
||||
data: {
|
||||
// This will contain data specific to the shipping method, i.e. the
|
||||
// id that the 3PL needs in order to process the order with this shipping
|
||||
// method
|
||||
}
|
||||
},
|
||||
{
|
||||
_id: [some-id],
|
||||
provider_id: "3pl_integration",
|
||||
name: "Fragile Shipping",
|
||||
price: 120, // Calculated from the cart
|
||||
data: {
|
||||
// This will contain data specific to the shipping method, i.e. the
|
||||
// id that the 3PL needs in order to process the order with this shipping
|
||||
// method
|
||||
}
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
If the customer changes her cart, all shipping options will be recalculated. For
|
||||
example, if the customer removes something from the cart so that they no longer
|
||||
qualify for free shipping, the free shipping method will be removed at the same
|
||||
time the fragile shipping method's price will be updated.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
99
packages/medusa/src/services/__mocks__/shipping-option.js
Normal file
99
packages/medusa/src/services/__mocks__/shipping-option.js
Normal file
@@ -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
|
||||
@@ -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"
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user