feat(api): add update-stripe-card endpoint (#55548)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>pull/55764/head
parent
65dfc044e8
commit
a1c12847e4
|
@ -4,7 +4,8 @@ import {
|
||||||
devLogin,
|
devLogin,
|
||||||
setupServer,
|
setupServer,
|
||||||
superRequest,
|
superRequest,
|
||||||
defaultUserEmail
|
defaultUserEmail,
|
||||||
|
defaultUserId
|
||||||
} from '../../jest.utils';
|
} from '../../jest.utils';
|
||||||
import { createUserInput } from '../utils/create-user';
|
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 = {
|
const sharedDonationReqBody = {
|
||||||
amount: 500,
|
amount: 500,
|
||||||
duration: 'month'
|
duration: 'month'
|
||||||
|
@ -93,6 +109,9 @@ const mockSubRetrieveObj = {
|
||||||
status: 'active'
|
status: 'active'
|
||||||
};
|
};
|
||||||
const mockSubRetrieve = jest.fn(() => Promise.resolve(mockSubRetrieveObj));
|
const mockSubRetrieve = jest.fn(() => Promise.resolve(mockSubRetrieveObj));
|
||||||
|
const mockCheckoutSessionCreate = jest.fn(() =>
|
||||||
|
Promise.resolve({ id: 'checkout_session_id' })
|
||||||
|
);
|
||||||
const mockCustomerUpdate = jest.fn();
|
const mockCustomerUpdate = jest.fn();
|
||||||
const generateMockSubCreate = (status: string) => () =>
|
const generateMockSubCreate = (status: string) => () =>
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
|
@ -120,15 +139,22 @@ jest.mock('stripe', () => {
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
create: mockSubCreate,
|
create: mockSubCreate,
|
||||||
retrieve: mockSubRetrieve
|
retrieve: mockSubRetrieve
|
||||||
|
},
|
||||||
|
checkout: {
|
||||||
|
sessions: {
|
||||||
|
create: mockCheckoutSessionCreate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Donate', () => {
|
describe('Donate', () => {
|
||||||
|
let setCookies: string[];
|
||||||
setupServer();
|
setupServer();
|
||||||
describe('Authenticated User', () => {
|
describe('Authenticated User', () => {
|
||||||
let superPost: ReturnType<typeof createSuperRequest>;
|
let superPost: ReturnType<typeof createSuperRequest>;
|
||||||
|
let superPut: ReturnType<typeof createSuperRequest>;
|
||||||
const verifyUpdatedUserAndNewDonation = async (email: string) => {
|
const verifyUpdatedUserAndNewDonation = async (email: string) => {
|
||||||
const user = await fastifyTestInstance.prisma.user.findFirst({
|
const user = await fastifyTestInstance.prisma.user.findFirst({
|
||||||
where: { email }
|
where: { email }
|
||||||
|
@ -162,8 +188,9 @@ describe('Donate', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const setCookies = await devLogin();
|
setCookies = await devLogin();
|
||||||
superPost = createSuperRequest({ method: 'POST', setCookies });
|
superPost = createSuperRequest({ method: 'POST', setCookies });
|
||||||
|
superPut = createSuperRequest({ method: 'PUT', setCookies });
|
||||||
await fastifyTestInstance.prisma.user.updateMany({
|
await fastifyTestInstance.prisma.user.updateMany({
|
||||||
where: { email: userWithProgress.email },
|
where: { email: userWithProgress.email },
|
||||||
data: userWithProgress
|
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', () => {
|
describe('POST /donate/create-stripe-payment-intent', () => {
|
||||||
it('should return 200 and call stripe api properly', async () => {
|
it('should return 200 and call stripe api properly', async () => {
|
||||||
mockSubCreate.mockImplementationOnce(
|
mockSubCreate.mockImplementationOnce(
|
||||||
|
@ -432,16 +492,16 @@ describe('Donate', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Unauthenticated User', () => {
|
describe('Unauthenticated User', () => {
|
||||||
let setCookies: string[];
|
|
||||||
// Get the CSRF cookies from an unprotected route
|
// Get the CSRF cookies from an unprotected route
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const res = await superRequest('/status/ping', { method: 'GET' });
|
const res = await superRequest('/status/ping', { method: 'GET' });
|
||||||
setCookies = res.get('Set-Cookie');
|
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/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 }) => {
|
endpoints.forEach(({ path, method }) => {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
allStripeProductIdsArray
|
allStripeProductIdsArray
|
||||||
} from '../../../shared/config/donation-settings';
|
} from '../../../shared/config/donation-settings';
|
||||||
import * as schemas from '../schemas';
|
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 { inLastFiveMinutes } from '../utils/validate-donation';
|
||||||
import { findOrCreateUser } from './helpers/auth-helpers';
|
import { findOrCreateUser } from './helpers/auth-helpers';
|
||||||
|
|
||||||
|
@ -30,6 +30,35 @@ export const donateRoutes: FastifyPluginCallbackTypebox = (
|
||||||
typescript: true
|
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(
|
fastify.post(
|
||||||
'/donate/add-donation',
|
'/donate/add-donation',
|
||||||
{
|
{
|
||||||
|
|
|
@ -14,6 +14,7 @@ export { deprecatedEndpoints } from './schemas/deprecated';
|
||||||
export { chargeStripeCard } from './schemas/donate/charge-stripe-card';
|
export { chargeStripeCard } from './schemas/donate/charge-stripe-card';
|
||||||
export { chargeStripe } from './schemas/donate/charge-stripe';
|
export { chargeStripe } from './schemas/donate/charge-stripe';
|
||||||
export { createStripePaymentIntent } from './schemas/donate/create-stripe-payment-intent';
|
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 { resubscribe } from './schemas/email-subscription/resubscribe';
|
||||||
export { unsubscribe } from './schemas/email-subscription/unsubscribe';
|
export { unsubscribe } from './schemas/email-subscription/unsubscribe';
|
||||||
export { updateMyAbout } from './schemas/settings/update-my-about';
|
export { updateMyAbout } from './schemas/settings/update-my-about';
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
};
|
|
@ -243,7 +243,7 @@ export function addDonation(body: Donation): Promise<ResponseWithData<void>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateStripeCard() {
|
export function updateStripeCard() {
|
||||||
return put('/donate/update-stripe-card');
|
return put('/donate/update-stripe-card', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function postChargeStripe(
|
export function postChargeStripe(
|
||||||
|
|
Loading…
Reference in New Issue