feat(api): add update-stripe-card endpoint (#55548)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
pull/55764/head
Ahmad Abdolsaheb 2024-08-06 02:52:03 +03:00 committed by GitHub
parent 65dfc044e8
commit a1c12847e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 109 additions and 7 deletions

View File

@ -4,7 +4,8 @@ import {
devLogin,
setupServer,
superRequest,
defaultUserEmail
defaultUserEmail,
defaultUserId
} from '../../jest.utils';
import { createUserInput } from '../utils/create-user';
@ -42,6 +43,21 @@ const userWithProgress: Prisma.userCreateInput = {
}
]
};
const donationMock = {
endDate: null,
startDate: {
date: '2024-07-17T10:20:56.076Z',
when: '2024-07-17T10:20:56.076+00:00'
},
id: '66979a414748aa2f3ba36d41',
amount: 500,
customerId: 'cust_test_id',
duration: 'month',
email: 'foo@bar.com',
provider: 'stripe',
subscriptionId: 'sub_test_id',
userId: defaultUserId
};
const sharedDonationReqBody = {
amount: 500,
duration: 'month'
@ -93,6 +109,9 @@ const mockSubRetrieveObj = {
status: 'active'
};
const mockSubRetrieve = jest.fn(() => Promise.resolve(mockSubRetrieveObj));
const mockCheckoutSessionCreate = jest.fn(() =>
Promise.resolve({ id: 'checkout_session_id' })
);
const mockCustomerUpdate = jest.fn();
const generateMockSubCreate = (status: string) => () =>
Promise.resolve({
@ -120,15 +139,22 @@ jest.mock('stripe', () => {
subscriptions: {
create: mockSubCreate,
retrieve: mockSubRetrieve
},
checkout: {
sessions: {
create: mockCheckoutSessionCreate
}
}
};
});
});
describe('Donate', () => {
let setCookies: string[];
setupServer();
describe('Authenticated User', () => {
let superPost: ReturnType<typeof createSuperRequest>;
let superPut: ReturnType<typeof createSuperRequest>;
const verifyUpdatedUserAndNewDonation = async (email: string) => {
const user = await fastifyTestInstance.prisma.user.findFirst({
where: { email }
@ -162,8 +188,9 @@ describe('Donate', () => {
};
beforeEach(async () => {
const setCookies = await devLogin();
setCookies = await devLogin();
superPost = createSuperRequest({ method: 'POST', setCookies });
superPut = createSuperRequest({ method: 'PUT', setCookies });
await fastifyTestInstance.prisma.user.updateMany({
where: { email: userWithProgress.email },
data: userWithProgress
@ -302,6 +329,39 @@ describe('Donate', () => {
});
});
describe('PUT /donate/update-stripe-card', () => {
it('should return 200 and return session id', async () => {
await fastifyTestInstance.prisma.donation.create({
data: donationMock
});
const response = await superPut('/donate/update-stripe-card').send({});
expect(mockCheckoutSessionCreate).toHaveBeenCalledWith({
cancel_url: 'http://localhost:8000/update-stripe-card',
customer: 'cust_test_id',
mode: 'setup',
payment_method_types: ['card'],
setup_intent_data: {
metadata: {
customer_id: 'cust_test_id',
subscription_id: 'sub_test_id'
}
},
success_url:
'http://localhost:8000/update-stripe-card?session_id={CHECKOUT_SESSION_ID}'
});
expect(response.body).toEqual({ sessionId: 'checkout_session_id' });
expect(response.status).toBe(200);
});
it('should return 500 if there is no donation record', async () => {
const response = await superPut('/donate/update-stripe-card').send({});
expect(response.body).toEqual({
message: 'flash.generic-error',
type: 'danger'
});
expect(response.status).toBe(500);
});
});
describe('POST /donate/create-stripe-payment-intent', () => {
it('should return 200 and call stripe api properly', async () => {
mockSubCreate.mockImplementationOnce(
@ -432,16 +492,16 @@ describe('Donate', () => {
});
describe('Unauthenticated User', () => {
let setCookies: string[];
// Get the CSRF cookies from an unprotected route
beforeAll(async () => {
const res = await superRequest('/status/ping', { method: 'GET' });
setCookies = res.get('Set-Cookie');
});
const endpoints: { path: string; method: 'POST' }[] = [
const endpoints: { path: string; method: 'POST' | 'PUT' }[] = [
{ path: '/donate/add-donation', method: 'POST' },
{ path: '/donate/charge-stripe-card', method: 'POST' }
{ path: '/donate/charge-stripe-card', method: 'POST' },
{ path: '/donate/update-stripe-card', method: 'PUT' }
];
endpoints.forEach(({ path, method }) => {

View File

@ -8,7 +8,7 @@ import {
allStripeProductIdsArray
} from '../../../shared/config/donation-settings';
import * as schemas from '../schemas';
import { STRIPE_SECRET_KEY } from '../utils/env';
import { STRIPE_SECRET_KEY, HOME_LOCATION } from '../utils/env';
import { inLastFiveMinutes } from '../utils/validate-donation';
import { findOrCreateUser } from './helpers/auth-helpers';
@ -30,6 +30,35 @@ export const donateRoutes: FastifyPluginCallbackTypebox = (
typescript: true
});
fastify.put(
'/donate/update-stripe-card',
{
schema: schemas.updateStripeCard
},
async req => {
const donation = await fastify.prisma.donation.findFirst({
where: { userId: req.user?.id, provider: 'stripe' }
});
if (!donation)
throw Error(`Stripe donation record not found: ${req.user?.id}`);
const { customerId, subscriptionId } = donation;
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'setup',
customer: customerId,
setup_intent_data: {
metadata: {
customer_id: customerId,
subscription_id: subscriptionId
}
},
success_url: `${HOME_LOCATION}/update-stripe-card?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${HOME_LOCATION}/update-stripe-card`
});
return { sessionId: session.id } as const;
}
);
fastify.post(
'/donate/add-donation',
{

View File

@ -14,6 +14,7 @@ export { deprecatedEndpoints } from './schemas/deprecated';
export { chargeStripeCard } from './schemas/donate/charge-stripe-card';
export { chargeStripe } from './schemas/donate/charge-stripe';
export { createStripePaymentIntent } from './schemas/donate/create-stripe-payment-intent';
export { updateStripeCard } from './schemas/donate/update-stripe-card';
export { resubscribe } from './schemas/email-subscription/resubscribe';
export { unsubscribe } from './schemas/email-subscription/unsubscribe';
export { updateMyAbout } from './schemas/settings/update-my-about';

View File

@ -0,0 +1,12 @@
import { Type } from '@fastify/type-provider-typebox';
import { genericError } from '../types';
export const updateStripeCard = {
body: Type.Object({}),
response: {
200: Type.Object({
sessionId: Type.String()
}),
default: genericError
}
};

View File

@ -243,7 +243,7 @@ export function addDonation(body: Donation): Promise<ResponseWithData<void>> {
}
export function updateStripeCard() {
return put('/donate/update-stripe-card');
return put('/donate/update-stripe-card', {});
}
export function postChargeStripe(