-
-
Notifications
You must be signed in to change notification settings - Fork 35.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(api): add charge-stripe endpoint #54545
base: main
Are you sure you want to change the base?
Changes from all commits
c127075
84d0625
e272590
515fded
edb42f9
e166e67
59c6306
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,12 +1,10 @@ | ||||||||||||||||
import { | ||||||||||||||||
Type, | ||||||||||||||||
type FastifyPluginCallbackTypebox | ||||||||||||||||
} from '@fastify/type-provider-typebox'; | ||||||||||||||||
import { type FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox'; | ||||||||||||||||
import Stripe from 'stripe'; | ||||||||||||||||
|
||||||||||||||||
import isEmail from 'validator/lib/isEmail'; | ||||||||||||||||
import { donationSubscriptionConfig } from '../../../shared/config/donation-settings'; | ||||||||||||||||
import { schemas } from '../schemas'; | ||||||||||||||||
import { STRIPE_SECRET_KEY } from '../utils/env'; | ||||||||||||||||
import { findOrCreateUser } from './helpers/auth-helpers'; | ||||||||||||||||
|
||||||||||||||||
/** | ||||||||||||||||
* Plugin for the donation endpoints. | ||||||||||||||||
|
@@ -35,22 +33,7 @@ export const donateRoutes: FastifyPluginCallbackTypebox = ( | |||||||||||||||
fastify.post( | ||||||||||||||||
'/donate/add-donation', | ||||||||||||||||
{ | ||||||||||||||||
schema: { | ||||||||||||||||
body: Type.Object({}), | ||||||||||||||||
response: { | ||||||||||||||||
200: Type.Object({ | ||||||||||||||||
isDonating: Type.Boolean() | ||||||||||||||||
}), | ||||||||||||||||
400: Type.Object({ | ||||||||||||||||
message: Type.Literal('User is already donating.'), | ||||||||||||||||
type: Type.Literal('info') | ||||||||||||||||
}), | ||||||||||||||||
500: Type.Object({ | ||||||||||||||||
message: Type.Literal('Something went wrong.'), | ||||||||||||||||
type: Type.Literal('danger') | ||||||||||||||||
}) | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
schema: schemas.addDonation | ||||||||||||||||
}, | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a clean up. |
||||||||||||||||
async (req, reply) => { | ||||||||||||||||
try { | ||||||||||||||||
|
@@ -214,3 +197,119 @@ export const donateRoutes: FastifyPluginCallbackTypebox = ( | |||||||||||||||
|
||||||||||||||||
done(); | ||||||||||||||||
}; | ||||||||||||||||
|
||||||||||||||||
/** | ||||||||||||||||
* Plugin for the donation endpoints. | ||||||||||||||||
* | ||||||||||||||||
* @param fastify The Fastify instance. | ||||||||||||||||
* @param _options Options passed to the plugin via `fastify.register(plugin, options)`. | ||||||||||||||||
* @param done The callback to signal that the plugin is ready. | ||||||||||||||||
*/ | ||||||||||||||||
export const chargeStripeRoute: FastifyPluginCallbackTypebox = ( | ||||||||||||||||
fastify, | ||||||||||||||||
_options, | ||||||||||||||||
done | ||||||||||||||||
) => { | ||||||||||||||||
// Stripe plugin | ||||||||||||||||
const stripe = new Stripe(STRIPE_SECRET_KEY, { | ||||||||||||||||
apiVersion: '2020-08-27', | ||||||||||||||||
typescript: true | ||||||||||||||||
}); | ||||||||||||||||
|
||||||||||||||||
// @ts-expect-error - @fastify/csrf-protection needs to update their types | ||||||||||||||||
// eslint-disable-next-line @typescript-eslint/unbound-method | ||||||||||||||||
fastify.addHook('onRequest', fastify.csrfProtection); | ||||||||||||||||
fastify.addHook('onRequest', fastify.addUserIfAuthorized); | ||||||||||||||||
fastify.post( | ||||||||||||||||
'/donate/charge-stripe', | ||||||||||||||||
{ | ||||||||||||||||
schema: schemas.chargeStripe | ||||||||||||||||
}, | ||||||||||||||||
async (req, reply) => { | ||||||||||||||||
try { | ||||||||||||||||
const id = req.user?.id; | ||||||||||||||||
const { email, name, token, amount, duration } = req.body; | ||||||||||||||||
|
||||||||||||||||
// verify the parameters | ||||||||||||||||
if ( | ||||||||||||||||
!isEmail(email) || | ||||||||||||||||
!donationSubscriptionConfig.plans[duration].includes(amount) | ||||||||||||||||
) { | ||||||||||||||||
void reply.code(500); | ||||||||||||||||
Comment on lines
+234
to
+237
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improved the validation slightly to check existing for plan. |
||||||||||||||||
return { | ||||||||||||||||
error: 'The donation form had invalid values for this submission.' | ||||||||||||||||
} as const; | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// TODO(Post-MVP) new users should not be created if user is not found | ||||||||||||||||
const user = id | ||||||||||||||||
? await fastify.prisma.user.findUniqueOrThrow({ where: { id } }) | ||||||||||||||||
: await findOrCreateUser(fastify, email); | ||||||||||||||||
|
||||||||||||||||
const { id: customerId } = await stripe.customers.create({ | ||||||||||||||||
email, | ||||||||||||||||
name | ||||||||||||||||
}); | ||||||||||||||||
|
||||||||||||||||
// TODO(Post-MVP) stripe has moved to a paymentintent flow, the create call should be updated to reflect this | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should figure out if this might benefit from being done before creating the customer.
Suggested change
|
||||||||||||||||
const paymentMethod = await stripe.paymentMethods.attach(token.id, { | ||||||||||||||||
customer: customerId | ||||||||||||||||
}); | ||||||||||||||||
|
||||||||||||||||
Comment on lines
+249
to
+257
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the current api, card has been passed directly when creating the customer. The current api works but the card definition has been removed from the ar types. As a result, I create the customer and then attach the payment method to it. |
||||||||||||||||
await stripe.customers.update(customerId, { | ||||||||||||||||
invoice_settings: { | ||||||||||||||||
default_payment_method: paymentMethod.id | ||||||||||||||||
} | ||||||||||||||||
}); | ||||||||||||||||
|
||||||||||||||||
const plan = `${donationSubscriptionConfig.duration[ | ||||||||||||||||
duration | ||||||||||||||||
].toLowerCase()}-donation-${amount}`; | ||||||||||||||||
|
||||||||||||||||
const { id: subscriptionId } = await stripe.subscriptions.create({ | ||||||||||||||||
customer: customerId, | ||||||||||||||||
items: [{ plan }] | ||||||||||||||||
}); | ||||||||||||||||
|
||||||||||||||||
const donation = { | ||||||||||||||||
userId: user.id, | ||||||||||||||||
email, | ||||||||||||||||
amount, | ||||||||||||||||
duration, | ||||||||||||||||
provider: 'stripe', | ||||||||||||||||
subscriptionId, | ||||||||||||||||
customerId, | ||||||||||||||||
// TODO(Post-MVP) migrate to startDate: new Date() | ||||||||||||||||
startDate: { | ||||||||||||||||
date: new Date().toISOString(), | ||||||||||||||||
when: new Date().toISOString().replace(/.$/, '+00:00') | ||||||||||||||||
} | ||||||||||||||||
}; | ||||||||||||||||
|
||||||||||||||||
await fastify.prisma.donation.create({ | ||||||||||||||||
data: donation | ||||||||||||||||
}); | ||||||||||||||||
|
||||||||||||||||
await fastify.prisma.user.update({ | ||||||||||||||||
where: { id: user.id }, | ||||||||||||||||
data: { | ||||||||||||||||
isDonating: true | ||||||||||||||||
} | ||||||||||||||||
}); | ||||||||||||||||
|
||||||||||||||||
return reply.send({ | ||||||||||||||||
type: 'success', | ||||||||||||||||
isDonating: true | ||||||||||||||||
}); | ||||||||||||||||
Comment on lines
+300
to
+302
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current api returns the whole subscription, but that is not used in the client. |
||||||||||||||||
} catch (error) { | ||||||||||||||||
fastify.log.error(error); | ||||||||||||||||
void reply.code(500); | ||||||||||||||||
return { | ||||||||||||||||
error: 'Donation failed due to a server error.' | ||||||||||||||||
} as const; | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
); | ||||||||||||||||
|
||||||||||||||||
done(); | ||||||||||||||||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { Type } from '@fastify/type-provider-typebox'; | ||
|
||
export const addDonation = { | ||
body: Type.Object({}), | ||
response: { | ||
200: Type.Object({ | ||
isDonating: Type.Boolean() | ||
}), | ||
400: Type.Object({ | ||
message: Type.Literal('User is already donating.'), | ||
type: Type.Literal('info') | ||
}), | ||
500: Type.Object({ | ||
message: Type.Literal('Something went wrong.'), | ||
type: Type.Literal('danger') | ||
}) | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if I need to return an error or not here since we are just checking if user is authorized and pass the user object.