diff --git a/src/operations/carts.ts b/src/operations/carts.ts index 60ec11da..f870fe49 100644 --- a/src/operations/carts.ts +++ b/src/operations/carts.ts @@ -15,18 +15,29 @@ import nanoid from '../utils/nanoid'; import { fetchUserItems } from './item'; import { fetchTeam, lockTeam, unlockTeam } from './team'; import { fetchTournament } from './tournament'; +import { stripe } from '../utils/stripe'; -export const checkForExpiredCarts = () => - database.$transaction([ +export const checkForExpiredCarts = async () => { + const carts = await database.cart.findMany({ + where: { + createdAt: { + lt: new Date(Date.now() - env.api.cartLifespan * 1000), + }, + transactionState: TransactionState.pending, + }, + }); + for (const cart of carts) { + if (cart.transactionId) { + await stripe.paymentIntents.cancel(cart.transactionId); + } + } + await database.$transaction([ database.cart.updateMany({ data: { transactionState: TransactionState.expired, }, where: { - createdAt: { - lt: new Date(Date.now() - env.api.cartLifespan * 1000), - }, - transactionState: TransactionState.pending, + id: { in: carts.map((item) => item.id) }, }, }), database.user.deleteMany({ @@ -42,6 +53,7 @@ export const checkForExpiredCarts = () => }, }), ]); +}; export const fetchCart = (cartId: string): Promise => database.cart.findUnique({ diff --git a/src/utils/stripe.ts b/src/utils/stripe.ts index ee5a4659..270b0e8c 100644 --- a/src/utils/stripe.ts +++ b/src/utils/stripe.ts @@ -9,10 +9,6 @@ import { fetchCartFromTransactionId } from '../operations/carts'; export const stripe = new Stripe(env.stripe.token); -export function clampString(string_: string) { - return string_.length > 37 ? `${string_.slice(0, 37)}...` : string_; -} - export function paymentIntentWebhookMiddleware(eventType: 'processing' | 'canceled' | 'succeeded') { return [ validateBody( diff --git a/tests/stripe.ts b/tests/stripe.ts index 6341995d..2467d885 100644 --- a/tests/stripe.ts +++ b/tests/stripe.ts @@ -113,7 +113,7 @@ export function generateStripeSession(email: string, amount: number) { return session; } -export function generatePaymentIntent(amount: number) { +export function generateStripePaymentIntent(amount: number) { const paymentIntentId = id('pi'); const paymentIntent: StripePaymentIntent = { id: paymentIntentId, @@ -343,7 +343,17 @@ function listen() { if (amount <= 0) { return [500, 'Price is negative']; } - return [200, generatePaymentIntent(amount)]; + return [200, generateStripePaymentIntent(amount)]; + }) + + .post(/\/payment_intents\/.*\/cancel$/) + .reply((uri) => { + const paymentIntentId = uri.match(/payment_intents\/(.*)\/cancel$/)[1]; + const paymentIntentIndex = stripePaymentIntents.findIndex((pi) => pi.id === paymentIntentId); + if (!paymentIntentIndex) { + return [500, 'Payment intent was not found']; + } + return [200, stripePaymentIntents.splice(paymentIntentIndex, 1)[0]]; }); } diff --git a/tests/stripe/paymentCanceledWebhook.test.ts b/tests/stripe/paymentCanceledWebhook.test.ts index 06493e52..8d3f709c 100644 --- a/tests/stripe/paymentCanceledWebhook.test.ts +++ b/tests/stripe/paymentCanceledWebhook.test.ts @@ -8,7 +8,7 @@ import database from '../../src/services/database'; import { Error, User, Cart } from '../../src/types'; import { createFakeUser, createFakeCart } from '../utils'; import { updateCart } from '../../src/operations/carts'; -import { generatePaymentIntent, resetFakeStripeApi, StripePaymentIntent } from '../stripe'; +import { generateStripePaymentIntent, resetFakeStripeApi, StripePaymentIntent } from '../stripe'; describe('POST /stripe/canceled', () => { let user: User; @@ -18,7 +18,7 @@ describe('POST /stripe/canceled', () => { before(async () => { user = await createFakeUser(); cart = await createFakeCart({ userId: user.id, items: [] }); - paymentIntent = generatePaymentIntent(120); + paymentIntent = generateStripePaymentIntent(120); await updateCart(cart.id, { transactionId: paymentIntent.id, transactionState: TransactionState.processing }); }); diff --git a/tests/stripe/paymentProcessingWebhook.test.ts b/tests/stripe/paymentProcessingWebhook.test.ts index 1ee81c1c..ba7ef911 100644 --- a/tests/stripe/paymentProcessingWebhook.test.ts +++ b/tests/stripe/paymentProcessingWebhook.test.ts @@ -8,7 +8,7 @@ import database from '../../src/services/database'; import { Error, User, Cart } from '../../src/types'; import { createFakeUser, createFakeCart } from '../utils'; import { updateCart } from '../../src/operations/carts'; -import { generatePaymentIntent, resetFakeStripeApi, StripePaymentIntent } from '../stripe'; +import { generateStripePaymentIntent, resetFakeStripeApi, StripePaymentIntent } from '../stripe'; describe('POST /stripe/processing', () => { let user: User; @@ -18,7 +18,7 @@ describe('POST /stripe/processing', () => { before(async () => { user = await createFakeUser(); cart = await createFakeCart({ userId: user.id, items: [] }); - paymentIntent = generatePaymentIntent(120); + paymentIntent = generateStripePaymentIntent(120); await updateCart(cart.id, { transactionId: paymentIntent.id, transactionState: TransactionState.processing }); }); diff --git a/tests/stripe/paymentSucceededWebhook.test.ts b/tests/stripe/paymentSucceededWebhook.test.ts index c1940f24..4fa98160 100644 --- a/tests/stripe/paymentSucceededWebhook.test.ts +++ b/tests/stripe/paymentSucceededWebhook.test.ts @@ -8,7 +8,7 @@ import database from '../../src/services/database'; import { Error, User, Cart } from '../../src/types'; import { createFakeUser, createFakeCart } from '../utils'; import { updateCart } from '../../src/operations/carts'; -import { generatePaymentIntent, resetFakeStripeApi, StripePaymentIntent } from '../stripe'; +import { generateStripePaymentIntent, resetFakeStripeApi, StripePaymentIntent } from '../stripe'; describe('POST /stripe/succeeded', () => { let user: User; @@ -18,7 +18,7 @@ describe('POST /stripe/succeeded', () => { before(async () => { user = await createFakeUser(); cart = await createFakeCart({ userId: user.id, items: [] }); - paymentIntent = generatePaymentIntent(120); + paymentIntent = generateStripePaymentIntent(120); await updateCart(cart.id, { transactionId: paymentIntent.id, transactionState: TransactionState.processing }); }); diff --git a/tests/users/createCart.test.ts b/tests/users/createCart.test.ts index 1d46930d..39203b4a 100644 --- a/tests/users/createCart.test.ts +++ b/tests/users/createCart.test.ts @@ -14,7 +14,7 @@ import { setShopAllowed } from '../../src/operations/settings'; import { getCaptain } from '../../src/utils/teams'; import { createAttendant, deleteUser, updateAdminUser } from '../../src/operations/user'; import { joinTeam } from '../../src/operations/team'; -import { resetFakeStripeApi, stripePaymentIntents } from '../stripe'; +import { generateStripePaymentIntent, resetFakeStripeApi, stripePaymentIntents } from '../stripe'; import { fetchItem } from '../../src/operations/item'; describe('POST /users/current/carts', () => { @@ -628,6 +628,7 @@ describe('POST /users/current/carts', () => { // We use that unit for a spectator and force-expire his cart const expiredSpectator = await createFakeUser({ type: UserType.spectator }); const expiredSpectatorCart = await cartOperations.forcePay(expiredSpectator); + const paymentIntent = generateStripePaymentIntent(spectatorTicket.price); await database.cart.update({ where: { id: expiredSpectatorCart.id, @@ -636,6 +637,7 @@ describe('POST /users/current/carts', () => { createdAt: new Date(Date.now() - 6e6), updatedAt: new Date(Date.now() - 6e6), transactionState: 'pending', + transactionId: paymentIntent.id, cartItems: { updateMany: { data: { @@ -677,6 +679,7 @@ describe('POST /users/current/carts', () => { expect(spectatorTickets).to.have.lengthOf(2); expect(stripePaymentIntents.at(-1).amount).to.be.equal(spectatorTicket.price); + expect(stripePaymentIntents.some((pi) => pi.id === paymentIntent.id)).to.be.false; // Restore actual stock return database.item.update({