Skip to content

Commit

Permalink
fix: users could pay for expired carts (reviewed by @DevNono)
Browse files Browse the repository at this point in the history
  • Loading branch information
TeddyRoncin committed Sep 11, 2024
1 parent 888e79a commit db1325a
Show file tree
Hide file tree
Showing 7 changed files with 40 additions and 19 deletions.
24 changes: 18 additions & 6 deletions src/operations/carts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -42,6 +53,7 @@ export const checkForExpiredCarts = () =>
},
}),
]);
};

export const fetchCart = (cartId: string): Promise<Cart> =>
database.cart.findUnique({
Expand Down
4 changes: 0 additions & 4 deletions src/utils/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
14 changes: 12 additions & 2 deletions tests/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]];
});
}

Expand Down
4 changes: 2 additions & 2 deletions tests/stripe/paymentCanceledWebhook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 });
});

Expand Down
4 changes: 2 additions & 2 deletions tests/stripe/paymentProcessingWebhook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 });
});

Expand Down
4 changes: 2 additions & 2 deletions tests/stripe/paymentSucceededWebhook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 });
});

Expand Down
5 changes: 4 additions & 1 deletion tests/users/createCart.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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,
Expand All @@ -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: {
Expand Down Expand Up @@ -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({
Expand Down

0 comments on commit db1325a

Please sign in to comment.