From 9e0cb1212023d7035165ddd269edab3efc7ebe29 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Wed, 24 Aug 2022 16:25:40 +0200 Subject: [PATCH] fix(medusa): remove unique cart on payments to allow canceled payments to exist (#1854) Fixes CORE-321 Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com> Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> --- .changeset/many-pumpkins-enjoy.md | 5 + .../api/__tests__/store/orders.js | 160 +++++++++++++++++- .../1661345741249-multi_payment_cart.ts | 25 +++ packages/medusa/src/models/payment.ts | 9 +- 4 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 .changeset/many-pumpkins-enjoy.md create mode 100644 packages/medusa/src/migrations/1661345741249-multi_payment_cart.ts diff --git a/.changeset/many-pumpkins-enjoy.md b/.changeset/many-pumpkins-enjoy.md new file mode 100644 index 0000000000..2f879986a4 --- /dev/null +++ b/.changeset/many-pumpkins-enjoy.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Fixes issue where failed cart completion attempts could not be retried without 500 error diff --git a/integration-tests/api/__tests__/store/orders.js b/integration-tests/api/__tests__/store/orders.js index c6b6922d32..307063f5a0 100644 --- a/integration-tests/api/__tests__/store/orders.js +++ b/integration-tests/api/__tests__/store/orders.js @@ -7,14 +7,14 @@ const { Product, ProductVariant, LineItem, + Payment, } = require("@medusajs/medusa") const setupServer = require("../../../helpers/setup-server") const { useApi } = require("../../../helpers/use-api") const { initDb, useDb } = require("../../../helpers/use-db") - -const swapSeeder = require("../../helpers/swap-seeder") -const cartSeeder = require("../../helpers/cart-seeder") +const { simpleRegionFactory, simpleProductFactory } = require("../../factories") +const { MedusaError } = require("medusa-core-utils") jest.setTimeout(30000) @@ -25,7 +25,7 @@ describe("/store/carts", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) - medusaProcess = await setupServer({ cwd }) + medusaProcess = await setupServer({ cwd, verbose: false }) }) afterAll(async () => { @@ -147,4 +147,156 @@ describe("/store/carts", () => { expect(response.status).toEqual(404) }) }) + + describe("Cart Completion with INSUFFICIENT_INVENTORY", () => { + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("recovers from failed completion", async () => { + const api = useApi() + + const region = await simpleRegionFactory(dbConnection) + const product = await simpleProductFactory(dbConnection) + + const cartRes = await api + .post("/store/carts", { + region_id: region.id, + }) + .catch((err) => { + return err.response + }) + + const cartId = cartRes.data.cart.id + + await api.post(`/store/carts/${cartId}/line-items`, { + variant_id: product.variants[0].id, + quantity: 1, + }) + await api.post(`/store/carts/${cartId}`, { + email: "testmailer@medusajs.com", + }) + await api.post(`/store/carts/${cartId}/payment-sessions`) + + const manager = dbConnection.manager + await manager.update( + ProductVariant, + { id: product.variants[0].id }, + { + inventory_quantity: 0, + } + ) + + const responseFail = await api + .post(`/store/carts/${cartId}/complete`) + .catch((err) => { + return err.response + }) + + expect(responseFail.status).toEqual(409) + expect(responseFail.data.type).toEqual("not_allowed") + expect(responseFail.data.code).toEqual( + MedusaError.Codes.INSUFFICIENT_INVENTORY + ) + + let payments = await manager.find(Payment, { cart_id: cartId }) + expect(payments).toHaveLength(1) + expect(payments).toContainEqual( + expect.objectContaining({ + canceled_at: expect.any(Date), + }) + ) + + await manager.update( + ProductVariant, + { id: product.variants[0].id }, + { + inventory_quantity: 1, + } + ) + + const responseSuccess = await api + .post(`/store/carts/${cartId}/complete`) + .catch((err) => { + return err.response + }) + + expect(responseSuccess.status).toEqual(200) + expect(responseSuccess.data.type).toEqual("order") + + payments = await manager.find(Payment, { cart_id: cartId }) + expect(payments).toHaveLength(2) + expect(payments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + canceled_at: null, + }), + ]) + ) + }) + }) + + describe("Cart consecutive completion", () => { + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should fails on cart already completed", async () => { + const api = useApi() + const manager = dbConnection.manager + + const region = await simpleRegionFactory(dbConnection) + const product = await simpleProductFactory(dbConnection) + + const cartRes = await api + .post("/store/carts", { + region_id: region.id, + }) + .catch((err) => { + return err.response + }) + + const cartId = cartRes.data.cart.id + + await api.post(`/store/carts/${cartId}/line-items`, { + variant_id: product.variants[0].id, + quantity: 1, + }) + await api.post(`/store/carts/${cartId}`, { + email: "testmailer@medusajs.com", + }) + await api.post(`/store/carts/${cartId}/payment-sessions`) + + const responseSuccess = await api + .post(`/store/carts/${cartId}/complete`) + .catch((err) => { + return err.response + }) + + expect(responseSuccess.status).toEqual(200) + expect(responseSuccess.data.type).toEqual("order") + + const payments = await manager.find(Payment, { cart_id: cartId }) + expect(payments).toHaveLength(1) + expect(payments).toContainEqual( + expect.objectContaining({ + canceled_at: null, + }) + ) + + const responseFail = await api + .post(`/store/carts/${cartId}/complete`) + .catch((err) => { + return err.response + }) + + expect(responseFail.status).toEqual(409) + expect(responseFail.data.code).toEqual("cart_incompatible_state") + expect(responseFail.data.message).toEqual( + "Cart has already been completed" + ) + }) + }) }) diff --git a/packages/medusa/src/migrations/1661345741249-multi_payment_cart.ts b/packages/medusa/src/migrations/1661345741249-multi_payment_cart.ts new file mode 100644 index 0000000000..2ada304469 --- /dev/null +++ b/packages/medusa/src/migrations/1661345741249-multi_payment_cart.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class multiPaymentCart1661345741249 implements MigrationInterface { + name = "multiPaymentCart1661345741249" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "payment" DROP CONSTRAINT "REL_4665f17abc1e81dd58330e5854"` + ) + await queryRunner.query( + `CREATE UNIQUE INDEX "UniquePaymentActive" ON "payment" ("cart_id") WHERE canceled_at IS NULL` + ) + await queryRunner.query( + `CREATE INDEX "IDX_aac4855eadda71aa1e4b6d7684" ON "payment" ("cart_id") WHERE canceled_at IS NOT NULL` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_aac4855eadda71aa1e4b6d7684"`) + await queryRunner.query(`DROP INDEX "UniquePaymentActive"`) + await queryRunner.query( + `ALTER TABLE "payment" ADD CONSTRAINT "REL_4665f17abc1e81dd58330e5854" UNIQUE ("cart_id")` + ) + } +} diff --git a/packages/medusa/src/models/payment.ts b/packages/medusa/src/models/payment.ts index 1fe31488db..456c86156d 100644 --- a/packages/medusa/src/models/payment.ts +++ b/packages/medusa/src/models/payment.ts @@ -16,6 +16,8 @@ import { Order } from "./order" import { Swap } from "./swap" import { generateEntityId } from "../utils/generate-entity-id" +@Index(["cart_id"], { where: "canceled_at IS NOT NULL" }) +@Index("UniquePaymentActive", ["cart_id"], { where: "canceled_at IS NULL", unique: true, }) @Entity() export class Payment extends BaseEntity { @Index() @@ -30,7 +32,7 @@ export class Payment extends BaseEntity { @Column({ nullable: true }) cart_id: string - @OneToOne(() => Cart) + @ManyToOne(() => Cart) @JoinColumn({ name: "cart_id" }) cart: Cart @@ -38,7 +40,10 @@ export class Payment extends BaseEntity { @Column({ nullable: true }) order_id: string - @ManyToOne(() => Order, (order) => order.payments) + @ManyToOne( + () => Order, + (order) => order.payments + ) @JoinColumn({ name: "order_id" }) order: Order