Feat/stripe#40
Conversation
…er schema to merge into main branch
RLee64
left a comment
There was a problem hiding this comment.
Great work!
Instead of associating user to stripePaymentIntentId, I think it should actually be the other way around. That is, we'll want a new collection that stores Payment objects (probably with stripePaymentIntentId as the id value, and an associated user ID as an attribute). This can then extend to logging transactions for events as well.
When you check for duplicate stripePaymentIntentId, it's then best to check this new Payment collection. Users shouldn't normally be deleted, but it won't be as reputable as a dedicated location. That said, if there's some sort of way to indicate to stripe that a stripePaymentIntent has somehow been 'used' to grant some sort of privilege, that'd be even better.
Also, just to verify my understanding, the client_secret is what associates a payment intent with a specific client right? Is there anything extra that needs to be done to verify that the paymentIntentId is coming from the initiating user (i.e. prevent malicious user from using my paymentIntentId)? Or should that be enough to make things secure?
Flagging this as requiring a re-review before merging since it's important payment-related code.
|
Also merge main and resolve conflicts :) |
…id in payment intent metadata
…euid metadata check
… payment records accordingly
RLee64
left a comment
There was a problem hiding this comment.
Nice work! Just one last step before we can merge this in, you'll need to merge main and update the admin auth code to work with this new payment system (since role payment plays a key role in determining roles). Unfortunately, the hasPaid/membershipPaid field is still necessary for our system as there may be multiple membership payments throughout the lifetime of an account (i.e. users paying for 2025 vs 2026).
Perhaps we try to automate this system a bit, and store the latest calender year a user has paid for 🤔 . This would be better than a boolean field that admins have to reset by manually press a button to reset at the end of each year. This also lets us get a bit more granular (if the clients wanted), where if a user paid in Dec 2025, maybe we set their paid for year to 2026 (since nothing happens in Dec anyways).
In a future ticket, we'll need to create a recovery flow to handle successful payments without actual accounts (as you've acknowledged, this PR is long enough). Some ideas for when we get around to it:
- Hold sign-up info in a temporary location between submission and payment (and then fetching when entering the sign-up page to see if there's an almost complete account in process). This is a relatively more user-friendly method.
- Make users re-enter their information, but check if there's an existing successful payment ID to use for account creation. This is more robust since backend crashes won't cause issues.
… membership year in authRoutes and userController
PR Review 3 (Current)
New Changes
mainchanges into this branch.user.tsby replacing thehasPaidBoolean field with alatestMembershipYearNumber field.authRoutes.tssoderiveRole()determinesmemberrole usinglatestMembershipYear.getMembershipYear()helper inauthRoutes.tsanduserController.tsto determine the correct membership year.Updated Verification
MongoDB
After successful payment and signup:
Userdocument should be created.latestMembershipYearfield should be set to2026.PR Review 2
New Changes
Backend
Paymentmodel to track Stripe payments separately from user accounts.paymentController.tsnow creates a localPaymentrecord when the Stripe Payment Intent is created.status: "pending"withuserId: null.webhookController.tsnow updates existing payment records based on Stripe events.status: "succeeded".status: "failed".userController.tsnow links the created user to an existing successful payment instead of storing payment fields directly on theUsermodel.Frontend
Signup.tsxnow checks that the Stripe Payment Intent succeeded before calling the signup endpoint.Updated Payment Flow
Paymentrecord withstatus: "pending"anduserId: null.Stripe.js.paymentIntentId.payment_intent.succeededtowebhookController.ts, which updates the localPaymentrecord tostatus: "succeeded".userController.tsalso verifies the Payment Intent directly with Stripe before creating the user, so it does not rely only on the webhook.Paymentrecord is updated withuserId.Note:
webhookController.tsanduserController.tsrun asynchronously, so either may update the payment status first.Updated Security
Paymentrecords, where:status: "succeeded"userId: nullPaymentrecord already has a linkeduserId.Updated Verification
MongoDB
After successful payment and signup:
Userdocument should be created.Paymentdocument should exist with:status: "succeeded"userIdlinked to the created userstripePaymentIntentIdpopulatedgoogleUidpopulatedpaidAtpopulatedIf payment succeeds but account creation does not complete, the
Paymentdocument should remain as:Additional Notes
Stripe CLI
For local development, Stripe webhooks will only reach the backend if Stripe CLI is forwarding events to the local server:
Incomplete payment records
Because the
Paymentrecord is now created before payment confirmation, MongoDB may also containpendingorfailedrecords whereuserId: null.This is expected. The tradeoff allows us to detect the more important edge case where a payment succeeds but account creation does not complete, such as if the user loses network connection before the signup request finishes. Stripe webhook events will update records to
succeededorfailedwhere possible.PR Review 1
Changes
Backend
Stripe SDK v21to avoid latest-version TypeScript compatibility issuespaymentController.ts: creates a Stripe Payment Intent server-side; membership price is hardcoded at$5 NZD(not client-controlled)webhookController.ts: listens for Stripe events and logspayment_intent.succeeded; structured for future event ticket supportuserController.ts: verifies payment with Stripe before creating a user account; setsmembershipPaid: trueandpaidAton creationpaymentRoutes.ts/webhookRoutes.ts: registers/api/payments/create-payment-intentand/api/payments/webhookindex.ts: webhook route mounted beforeexpress.json()to preserve raw body for Stripe signature verificationuser.ts: addedmembershipPaid,stripePaymentIntentId(unique,sparse), andpaidAtfields to the existing User modelFrontend
@stripe/react-stripe-jsand@stripe/stripe-jsSignup.tsx: integrated StripeCardElementinto the existing sign up form; payment is processed inline before account creationPayment Flow
/signup$5 NZDStripe.jsconfirms the card payment client-sidemembershipPaid: true, and stores thestripePaymentIntentIdandpaidAtfields.Security
type, never the amount. In future, this can be moved into a custom admin panel so admins can manage pricing, events, and related settings without code changes.stripe.paymentIntents.retrieve()to confirmstatus === "succeeded"before creating any accountpaymentIntentId; prevents one payment being used to create multiple accountsSTRIPE_WEBHOOK_SECRETbefore processingStripe.jsis loading.env
Add the following to your
.envfiles:How to Test
Steps
.envvariables/signup4242 4242 4242 4242, Expiry: any future date, CVC: any 3 digits./Verify
MongoDB: user document created withmembershipPaid: true,paidAtset, andstripePaymentIntentIdpopulatedAdditional Notes
Rare Edge Case
STRIPE_WEBHOOK_SECRET
STRIPE_WEBHOOK_SECRETby running:whsec_...value from the terminal and add it toserver/.env:STRIPE_WEBHOOK_SECRET="whsec_..."