Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(medusa): Respond with order when cart is already completed #5766

Merged
merged 10 commits into from
Dec 1, 2023
5 changes: 5 additions & 0 deletions .changeset/heavy-moles-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": minor
---

feat(medusa): Respond with order when cart is already completed
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,6 @@ Object {
}
`;

exports[`/store/carts POST /store/carts/:id returns early, if cart is already completed 1`] = `
Object {
"code": "cart_incompatible_state",
"message": "Cart has already been completed",
"type": "not_allowed",
}
`;

exports[`/store/carts shipping address + region updates updates region only - single to multiple countries 1`] = `
Object {
"address_1": null,
Expand Down
59 changes: 44 additions & 15 deletions integration-tests/api/__tests__/store/cart/cart.js
Original file line number Diff line number Diff line change
Expand Up @@ -2234,24 +2234,27 @@ describe("/store/carts", () => {
expect(createdOrder.status).toEqual(200)
})

it("returns early, if cart is already completed", async () => {
const manager = dbConnection.manager
it("should return early, if cart is already completed", async () => {
const api = useApi()
await manager.query(
`UPDATE "cart"
SET completed_at=current_timestamp
WHERE id = 'test-cart-2'`

const completedCart = await api.post(
`/store/carts/test-cart-2/complete-cart`
)
try {
await api.post(`/store/carts/test-cart-2/complete-cart`)
} catch (error) {
expect(error.response.data).toMatchSnapshot({
type: "not_allowed",
message: "Cart has already been completed",
code: "cart_incompatible_state",

expect(completedCart.status).toEqual(200)

const alreadyCompletedCart = await api.post(
`/store/carts/test-cart-2/complete-cart`
)

expect(alreadyCompletedCart.data.data).toEqual(
expect.objectContaining({
cart_id: "test-cart-2",
id: expect.any(String),
})
expect(error.response.status).toEqual(409)
}
)
expect(alreadyCompletedCart.data.type).toEqual("order")
expect(alreadyCompletedCart.status).toEqual(200)
})

it("fails to complete cart with items inventory not/partially covered", async () => {
Expand Down Expand Up @@ -2354,6 +2357,32 @@ describe("/store/carts", () => {
expect(res.data.cart.completed_at).not.toBe(null)
})

it("should return the swap when cart is already completed", async () => {
const manager = dbConnection.manager
await manager.query(
"UPDATE swap SET cart_id='swap-cart' where id='test-swap'"
)

await manager.query("DELETE FROM payment where swap_id='test-swap'")

const api = useApi()

await api.post(`/store/carts/swap-cart/complete-cart`)

const alreadyCompletedCart = await api.post(
`/store/carts/swap-cart/complete-cart`
)

expect(alreadyCompletedCart.data.data).toEqual(
expect.objectContaining({
cart_id: "swap-cart",
id: expect.any(String),
})
)
expect(alreadyCompletedCart.data.type).toEqual("swap")
expect(alreadyCompletedCart.status).toEqual(200)
})

it("completes cart with a non-customer and for a customer with the same email created later the order doesn't show up", async () => {
const api = useApi()
const customerEmail = "test-email-for-non-existent-customer@test.com"
Expand Down
19 changes: 9 additions & 10 deletions integration-tests/api/__tests__/store/orders.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ describe("/store/carts", () => {
await db.teardown()
})

it("should fails on cart already completed", async () => {
it("should return an order on cart already completed", async () => {
const api = useApi()
const manager = dbConnection.manager

Expand Down Expand Up @@ -501,17 +501,16 @@ describe("/store/carts", () => {
})
)

const responseFail = await api
.post(`/store/carts/${cartId}/complete`)
.catch((err) => {
return err.response
})
const successRes = await api.post(`/store/carts/${cartId}/complete`)

expect(responseFail.status).toEqual(409)
expect(responseFail.data.code).toEqual("cart_incompatible_state")
expect(responseFail.data.message).toEqual(
"Cart has already been completed"
expect(successRes.status).toEqual(200)
expect(successRes.data.data).toEqual(
expect.objectContaining({
cart_id: cartId,
id: expect.any(String),
})
)
expect(successRes.data.type).toEqual("order")
})
})

Expand Down
18 changes: 11 additions & 7 deletions packages/medusa/src/strategies/__tests__/cart-completion.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const toTest = [
},
],
[
"returns 409",
"succeeds",
{
cart: {
id: "test-cart",
Expand All @@ -104,12 +104,15 @@ const toTest = [
idempotency_key: "ikey",
recovery_point: "started",
},
validate: function (value) {
expect(value.response_code).toEqual(409)
expect(value.response_body).toEqual({
code: "cart_incompatible_state",
message: "Cart has already been completed",
type: "not_allowed",
validate: function (value, { orderServiceMock }) {
expect(value.response_code).toEqual(200)
expect(
orderServiceMock.retrieveByCartIdWithTotals
).toHaveBeenCalledTimes(1)
expect(
orderServiceMock.retrieveByCartIdWithTotals
).toHaveBeenCalledWith("test-cart", {
relations: ["shipping_address", "items", "payments"],
})
},
},
Expand Down Expand Up @@ -204,6 +207,7 @@ describe("CartCompletionStrategy", () => {
createFromCart: jest.fn(() => Promise.resolve(cart)),
retrieve: jest.fn(() => Promise.resolve({})),
retrieveWithTotals: jest.fn(() => Promise.resolve({})),
retrieveByCartIdWithTotals: jest.fn(() => Promise.resolve({})),
newTotalsService: newTotalsServiceMock,
}
const swapServiceMock = {
Expand Down
54 changes: 35 additions & 19 deletions packages/medusa/src/strategies/cart-completion.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import {
AbstractCartCompletionStrategy,
CartCompletionResponse,
} from "../interfaces"
import {
IEventBusService,
IInventoryService,
ReservationItemDTO,
} from "@medusajs/types"
import {
AbstractCartCompletionStrategy,
CartCompletionResponse,
} from "../interfaces"
import { IdempotencyKey, Order } from "../models"
import OrderService, {
ORDER_CART_ALREADY_EXISTS_ERROR,
} from "../services/order"
import {
PaymentProviderService,
ProductVariantInventoryService,
} from "../services"
import OrderService, {
ORDER_CART_ALREADY_EXISTS_ERROR,
} from "../services/order"

import CartService from "../services/cart"
import { promiseAll } from "@medusajs/utils"
import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import CartService from "../services/cart"
import IdempotencyKeyService from "../services/idempotency-key"
import { MedusaError } from "medusa-core-utils"
import { RequestContext } from "../types/request"
import SwapService from "../services/swap"
import { promiseAll } from "@medusajs/utils"
import { RequestContext } from "../types/request"

type InjectedDependencies = {
productVariantInventoryService: ProductVariantInventoryService
Expand Down Expand Up @@ -201,13 +201,29 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
})

if (cart.completed_at) {
if (cart.type === "swap") {
const swapId = cart.metadata?.swap_id as string
const swapServiceTx = this.swapService_.withTransaction(manager)

const swap = await swapServiceTx.retrieve(swapId, {
relations: ["shipping_address"],
})

return {
response_code: 200,
response_body: { data: swap, type: "swap" },
}
}

const order = await this.orderService_
.withTransaction(manager)
.retrieveByCartIdWithTotals(id, {
relations: ["shipping_address", "items", "payments"],
})

return {
response_code: 409,
response_body: {
code: MedusaError.Codes.CART_INCOMPATIBLE_STATE,
message: "Cart has already been completed",
type: MedusaError.Types.NOT_ALLOWED,
},
response_code: 200,
response_body: { data: order, type: "order" },
olivermrbl marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -449,8 +465,8 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
await this.removeReservations(reservations)

if (error && error.message === ORDER_CART_ALREADY_EXISTS_ERROR) {
order = await orderServiceTx.retrieveByCartId(id, {
relations: ["shipping_address", "payments"],
order = await orderServiceTx.retrieveByCartIdWithTotals(id, {
relations: ["shipping_address", "items", "payments"],
})

return {
Expand Down