Set Up Recurring Donations#133
Open
aaronashby wants to merge 11 commits into
Open
Conversation
Add nullable stripeSubscriptionId and stripeCustomerId to the Donation entity so recurring donations can be linked to their Stripe subscription (for renewals and future management). Includes a migration that also adds the previously entity-only feeAmount column (guarded with IF NOT EXISTS), and registers the migration in data-source.ts (migrations are statically imported for Webpack bundling).
Accept and persist stripeSubscriptionId/stripeCustomerId on donation creation, and add recordRenewalCharge() which writes each successful (or failed) subscription renewal as its own donation row. Renewals are matched to the original donation via stripeSubscriptionId, are idempotent on the renewal PaymentIntent id, and — being succeeded rows — count toward the goal total automatically.
Replace the createSubscription stub with a full flow: find-or-create the Stripe customer, resolve/create the donation product, create the subscription with inline price_data and payment_behavior 'default_incomplete', and return the first PaymentIntent's id + client secret (derived from latest_invoice.confirmation_secret) so the frontend confirms it exactly like a one-time payment. Adds POST /payments/subscription and its response DTO/mapper, and webhook handling for invoice.paid / invoice.payment_failed that records subscription-cycle renewals. Co-authored resolution helper getPaymentIntentIdForInvoice reads the expanded invoice.payments list since invoice.payment_intent no longer exists on this Stripe API version.
Add apiClient.createSubscription and branch the checkout flow: recurring
donations call the subscription endpoint, one-time donations keep using the
payment intent endpoint. Both return { id, clientSecret }, so the existing
confirmCardPayment flow is unchanged. The subscription's customer and
subscription ids are threaded into createDonation so they persist on the
donation row.
Rewrite the createSubscription service tests for the new signature (customer reuse/creation, product resolution, interval mapping, confirmation_secret parsing) and add controller tests for the subscription endpoint and the invoice.paid / invoice.payment_failed renewal branches. Add donations-service tests for Stripe-id persistence and recordRenewalCharge (idempotency, template cloning). Also fix breakage that predated this work: provide EmailsService in the donations-service spec, backfill the new nullable columns in Donation test fixtures, and wrap DonationForm renders in MemoryRouter (the form now uses useSearchParams).
The "cover fees" toggle was cosmetic: it updated only Step 2's local state, so the fee was shown but never charged, and formData.coverFees was dead. Make formData the single source of truth — DonationSummary is now controlled (coverFees + onCoverFeesChange), and a shared calculateChargeAmount helper (gross-up: (base + fixed) / (1 - rate), rounded to cents) drives the Step 2 total, the Stripe charge for both one-time and recurring donations, and the recorded donation amount. Rewrite the stale DonationSummary spec to cover the helpers and the controlled toggle.
…nity/fcc into recurring-donations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Implements fully functional recurring donations via Stripe Subscriptions, replacing the previous mocked stub. Recurring selections now create a real Stripe Subscription (billed on the donor's chosen interval) while keeping the on-site card-confirmation UX identical to one-time gifts. Also fixes a pre-existing bug where the "cover fees" toggle was shown to donors but never actually charged.
Changes Made
Recurring subscriptions
PaymentsService.createSubscriptionto create a Stripe customer (find-or-create by email), resolve/create the donation product, and create a subscription withpayment_behavior: 'default_incomplete'. Returns the first invoice's PaymentIntent id + client secret (derived fromlatest_invoice.confirmation_secret, sinceinvoice.payment_intentno longer exists on our Stripe API version), so the frontend confirms it exactly like a one-time payment.POST /payments/subscriptionendpoint + response DTO/mapper.stripeSubscriptionId/stripeCustomerIdcolumns ondonations(migration also backfills the entity-onlyfeeAmountcolumn that shipped without one).invoice.paid/invoice.payment_failed: eachsubscription_cyclerenewal is recorded as its own succeeded/failed donation row (matched viastripeSubscriptionId, idempotent on the renewal PaymentIntent id) so renewals count toward the goal.Fee coverage fix
formData.coverFeeswas dead. MadeformDatathe single source of truth:DonationSummaryis now controlled, and a sharedcalculateChargeAmounthelper (gross-up formula, rounded to cents) drives the Step 2 total, the Stripe charge (one-time and recurring), and the recorded amount.Testing & Verification
Verification Steps:
npx jest --config apps/backend/jest.config.ts --testPathIgnorePatterns=e2e). Rewrote thecreateSubscriptionservice tests (customer reuse/creation, product resolution, all 5 interval mappings,confirmation_secretparsing); added controller tests for the subscription endpoint and theinvoice.paid/invoice.payment_failedrenewal branches; addeddonations.servicetests for Stripe-id persistence andrecordRenewalCharge(idempotency, template cloning). Also fixed pre-existing spec breakage:EmailsServicewas missing from the donations-service test module,Donationfixtures lacked the new/feeAmountcolumns, andDonationFormrenders needed aMemoryRouterwrapper.DonationSummaryspec (now 7 passing) covering the gross-up math, cent-rounding, and the controlled toggle.Screenshots (if relevant)
Future Improvements/Notes
invoice.paid+invoice.payment_failedon the Stripe webhook endpoint, and optionally setSTRIPE_DONATION_PRODUCT_IDin the ECS task def (leave blank to auto-create on first use — it logs the id). Migrations auto-run on startup (migrationsRun: true), so no manual RDS step.DONATION_FEE_RATE/DONATION_FIXED_FEEare hardcoded to Stripe's standard US card pricing (2.9% + $0.30); update if the new Stripe account has different (e.g. nonprofit) rates.feeAmountreflects Stripe's actual cut, which differs slightly from the client-side estimate.stripeSubscriptionIdis the hook for it later.donations.e2e-spec.tsrequires a live Postgres; it passes with the DB up but fails locally without one (environmental).Related Issues
Closes #