diff --git a/.changeset/heavy-moles-check.md b/.changeset/heavy-moles-check.md new file mode 100644 index 0000000000..a4c63dcc9d --- /dev/null +++ b/.changeset/heavy-moles-check.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": minor +--- + +feat(medusa): Respond with order when cart is already completed diff --git a/integration-tests/api/__tests__/store/cart/__snapshots__/cart.js.snap b/integration-tests/api/__tests__/store/cart/__snapshots__/cart.js.snap index 6df7864b25..cb8267f0ed 100644 --- a/integration-tests/api/__tests__/store/cart/__snapshots__/cart.js.snap +++ b/integration-tests/api/__tests__/store/cart/__snapshots__/cart.js.snap @@ -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, diff --git a/integration-tests/api/__tests__/store/cart/cart.js b/integration-tests/api/__tests__/store/cart/cart.js index e71863d023..839dd3bacd 100644 --- a/integration-tests/api/__tests__/store/cart/cart.js +++ b/integration-tests/api/__tests__/store/cart/cart.js @@ -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 () => { @@ -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" diff --git a/integration-tests/api/__tests__/store/orders.js b/integration-tests/api/__tests__/store/orders.js index 2018bd02ab..44828e5776 100644 --- a/integration-tests/api/__tests__/store/orders.js +++ b/integration-tests/api/__tests__/store/orders.js @@ -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 @@ -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") }) }) diff --git a/packages/medusa/src/strategies/__tests__/cart-completion.js b/packages/medusa/src/strategies/__tests__/cart-completion.js index dd83c10989..ebfacc2653 100644 --- a/packages/medusa/src/strategies/__tests__/cart-completion.js +++ b/packages/medusa/src/strategies/__tests__/cart-completion.js @@ -88,7 +88,7 @@ const toTest = [ }, ], [ - "returns 409", + "succeeds", { cart: { id: "test-cart", @@ -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"], }) }, }, @@ -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 = { diff --git a/packages/medusa/src/strategies/cart-completion.ts b/packages/medusa/src/strategies/cart-completion.ts index 3752518aed..d769bc9369 100644 --- a/packages/medusa/src/strategies/cart-completion.ts +++ b/packages/medusa/src/strategies/cart-completion.ts @@ -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 { EntityManager } from "typeorm" -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 { MedusaError } from "medusa-core-utils" +import { EntityManager } from "typeorm" +import CartService from "../services/cart" +import IdempotencyKeyService from "../services/idempotency-key" +import SwapService from "../services/swap" +import { RequestContext } from "../types/request" type InjectedDependencies = { productVariantInventoryService: ProductVariantInventoryService @@ -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" }, } } @@ -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 {