We need to add a Payment Plan system to community.givehub.com, starting with Custom Forms.
Goal: allow organizations to create custom forms where the submitter can either:
-
Enroll in a simple recurring payment plan
-
Choose between Pay in Full or Installments inside the form checkout
This should work for fees, tuition, camps, retreats, sponsorships, program payments, mission trips, pledges, etc.
Phase 1: Database
Create migrations for the following tables.
1. payment_plan_templates
Stores reusable payment plan settings tied to a custom form field/block.
CREATE TABLE payment_plan_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
form_id UUID NULL,
form_field_id UUID NULL,
name TEXT NOT NULL,
description TEXT NULL,
total_amount_cents INTEGER NOT NULL,
currency TEXT NOT NULL DEFAULT 'USD',
allow_pay_in_full BOOLEAN NOT NULL DEFAULT true,
allow_payment_plan BOOLEAN NOT NULL DEFAULT true,
down_payment_required BOOLEAN NOT NULL DEFAULT false,
down_payment_amount_cents INTEGER NULL,
installment_count INTEGER NOT NULL,
frequency TEXT NOT NULL DEFAULT 'monthly',
start_timing TEXT NOT NULL DEFAULT 'immediate',
start_date DATE NULL,
fund_id UUID NULL,
category_id UUID NULL,
allow_card BOOLEAN NOT NULL DEFAULT true,
allow_ach BOOLEAN NOT NULL DEFAULT true,
auto_charge BOOLEAN NOT NULL DEFAULT true,
reminder_days_before INTEGER NOT NULL DEFAULT 3,
retry_failed_payments BOOLEAN NOT NULL DEFAULT true,
max_retry_attempts INTEGER NOT NULL DEFAULT 3,
authorization_text TEXT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
2. payment_plans
Represents an individual person’s enrolled plan.
CREATE TABLE payment_plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
template_id UUID NULL REFERENCES payment_plan_templates(id) ON DELETE SET NULL,
form_id UUID NULL,
form_submission_id UUID NULL,
person_id UUID NULL,
donor_name TEXT NULL,
donor_email TEXT NOT NULL,
donor_phone TEXT NULL,
name TEXT NOT NULL,
total_amount_cents INTEGER NOT NULL,
down_payment_amount_cents INTEGER NOT NULL DEFAULT 0,
installment_amount_cents INTEGER NOT NULL,
installment_count INTEGER NOT NULL,
frequency TEXT NOT NULL DEFAULT 'monthly',
currency TEXT NOT NULL DEFAULT 'USD',
fund_id UUID NULL,
category_id UUID NULL,
payment_method_id UUID NULL,
gateway_provider TEXT NULL,
status TEXT NOT NULL DEFAULT 'active',
/*
active
completed
paused
canceled
failed
*/
next_charge_date DATE NULL,
completed_at TIMESTAMPTZ NULL,
canceled_at TIMESTAMPTZ NULL,
cancel_reason TEXT NULL,
authorization_accepted BOOLEAN NOT NULL DEFAULT false,
authorization_text TEXT NULL,
authorization_accepted_at TIMESTAMPTZ NULL,
authorization_ip TEXT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
3. payment_plan_installments
Each scheduled payment.
CREATE TABLE payment_plan_installments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
payment_plan_id UUID NOT NULL REFERENCES payment_plans(id) ON DELETE CASCADE,
installment_number INTEGER NOT NULL,
amount_cents INTEGER NOT NULL,
due_date DATE NOT NULL,
status TEXT NOT NULL DEFAULT 'scheduled',
/*
scheduled
processing
paid
failed
skipped
canceled
*/
payment_id UUID NULL,
contribution_id UUID NULL,
attempted_at TIMESTAMPTZ NULL,
paid_at TIMESTAMPTZ NULL,
failed_at TIMESTAMPTZ NULL,
retry_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(payment_plan_id, installment_number)
);
Phase 2: Custom Form Builder
Add a new field/block type:
payment_plan
In the custom form builder, org admins should be able to add a Payment Plan block.
Admin configuration fields
The Payment Plan block should allow:
-
Plan name
-
Description
-
Total amount
-
Allow Pay in Full: yes/no
-
Allow Payment Plan: yes/no
-
Number of installments
-
Frequency:
-
weekly
-
biweekly
-
monthly
-
quarterly
-
-
Down payment required: yes/no
-
Down payment amount
-
Start timing:
-
immediately after form submission
-
specific date
-
first day of next month
-
-
Fund
-
Category
-
Allow card
-
Allow ACH
-
Auto-charge installments
-
Reminder days before charge
-
Failed payment retry attempts
-
Custom authorization text
Default authorization text:
I authorize this organization and GiveHub to charge my selected payment method according to the payment schedule shown above. I understand that I may contact the organization with questions about this payment plan.
Phase 3: Public Form Experience
When a public user fills out a form that contains a Payment Plan block, show a clear checkout section.
If both payment options are enabled
Display:
Choose payment option:
[ ] Pay in full today
[ ] Payment plan
Pay in full
Charge the full amount immediately using existing GiveHub payment flow.
Create a normal contribution/payment record.
Do not create an active payment plan unless needed for reporting. If easier, create a payment plan with status completed, but do not schedule installments.
Payment plan
Show:
-
Total amount
-
Down payment today, if required
-
Number of installments
-
Installment amount
-
Frequency
-
First charge date
-
Final estimated charge date
-
Payment method section
-
Authorization checkbox
Example:
Total: $1,200.00
Due today: $100.00
Remaining balance: $1,100.00
Plan: 11 monthly payments of $100.00
First scheduled payment: May 28, 2026
Require checkbox:
I authorize this organization and GiveHub to charge my selected payment method according to the schedule shown above.
Do not allow submission unless accepted.
Phase 4: Payment Calculation Logic
Add shared utility functions.
type Frequency = "weekly" | "biweekly" | "monthly" | "quarterly";
function calculatePaymentPlanSchedule(input: {
totalAmountCents: number;
downPaymentAmountCents?: number;
installmentCount: number;
frequency: Frequency;
startDate: Date;
}): {
downPaymentAmountCents: number;
installmentAmountCents: number;
installments: {
installmentNumber: number;
amountCents: number;
dueDate: Date;
}[];
}
Rules:
-
Remaining balance = total - down payment.
-
Split remaining balance across installment count.
-
Handle rounding in cents.
-
Any rounding remainder should be added to the final installment.
-
Do not allow total amount <= 0.
-
Do not allow installment count <= 0.
-
Do not allow down payment greater than total.
-
If pay in full, skip installment generation.
Phase 5: Form Submission Flow
When public form is submitted:
Case A: Pay in full
-
Validate form.
-
Process payment immediately.
-
Save form submission.
-
Save contribution/payment.
-
Send normal receipt.
Case B: Payment plan
-
Validate form.
-
Validate payment plan selection.
-
Tokenize/save payment method using existing saved payment method flow.
-
If down payment is required, charge down payment immediately.
-
Create payment_plans record.
-
Create payment_plan_installments records.
-
Save authorization acceptance details.
-
Save form submission.
-
Send confirmation email with payment schedule.
Important: if down payment charge fails, do not create active payment plan.
Phase 6: Scheduled Auto-Charging Job
Create backend scheduled job:
processDuePaymentPlanInstallments
Runs daily.
Find installments where:
status = 'scheduled'
AND due_date <= CURRENT_DATE
AND payment_plan.status = 'active'
AND payment_plan.auto_charge = true
For each installment:
-
Mark installment processing.
-
Charge saved payment method.
-
On success:
-
create contribution/payment record
-
mark installment paid
-
link payment_id / contribution_id
-
update payment_plans.next_charge_date
-
if all installments paid, mark plan completed
-
send receipt
-
-
On failure:
-
mark installment failed
-
increment retry count
-
store gateway error
-
send failed payment email with update-payment link
-
if max retries exceeded, mark plan failed
-
Use existing gateway abstraction where possible.
Phase 7: Reminder Emails
Create reminder job:
sendPaymentPlanReminders
Runs daily.
Find scheduled installments due in template.reminder_days_before.
Send email:
Subject:
Upcoming payment reminder
Body should include:
-
Org name
-
Plan name
-
Amount
-
Due date
-
Payment method last 4 if available
-
Link to update payment method
Phase 8: Failed Payment Recovery
When an installment fails, email the donor.
Subject:
Action needed: payment failed
Include:
-
Plan name
-
Failed amount
-
Due date
-
Reason if safe to show
-
Secure update-payment link
We can reuse the same general saved payment update flow used by pledges/recurring gifts, but make sure links use production FRONTEND_URL, not preview/dev URLs.
Phase 9: Admin UI
Add admin pages or sections.
Payment Plans list
Route suggestion:
/org-dashboard/payment-plans
Show:
-
Donor name
-
Email
-
Plan name
-
Total amount
-
Paid amount
-
Remaining amount
-
Status
-
Next charge date
-
Source form
-
Created date
Filters:
-
Active
-
Failed
-
Completed
-
Canceled
-
Form
-
Fund
Payment Plan detail page
Show:
-
Donor info
-
Form submission link
-
Plan summary
-
Authorization accepted timestamp
-
Payment method
-
Installment schedule table
Installment table columns:
-
-
Due date
-
Amount
-
Status
-
Paid date
-
Retry count
-
Error
Admin actions:
-
Pause plan
-
Resume plan
-
Cancel plan
-
Retry failed payment
-
Update next charge date
-
Change payment method link / resend update link
Phase 10: Reporting
Add payment plan activity to reports.
At minimum:
-
Payment plans created
-
Active plans
-
Failed plans
-
Completed plans
-
Installments collected
-
Future scheduled installments
-
Total outstanding balance
Contribution records from installments should still appear in normal giving/payment reports.
Add metadata to contributions/payments:
source_type: "payment_plan"
source_id: paymentPlanId
installment_id: installmentId
form_submission_id: formSubmissionId
Phase 11: Permissions
Add permissions:
payment_plans.view
payment_plans.create
payment_plans.edit
payment_plans.cancel
payment_plans.retry
payment_plan_templates.manage
Admins should have these by default.
Phase 12: Audit / Safety
Log key actions:
-
Plan created
-
Authorization accepted
-
Installment charged
-
Installment failed
-
Plan paused
-
Plan resumed
-
Plan canceled
-
Payment method update link sent
-
Manual retry attempted
MVP Scope
Build this first:
-
Payment Plan block in Custom Forms
-
Pay in full vs payment plan checkout
-
Card payment method support first
-
Optional ACH if existing ACH saved-payment flow is already stable
-
Down payment support
-
Monthly/weekly/biweekly/quarterly schedules
-
Daily charge job
-
Reminder emails
-
Failed payment email
-
Admin list/detail view
Do not overbuild initially:
-
No interest
-
No late fees
-
No partial manual payments yet
-
No complex proration
-
No donor self-service cancellation yet
-
No variable installment amounts yet
Important implementation notes
Please reuse existing GiveHub payment infrastructure as much as possible.
Do not create a separate payment gateway system.
Use existing:
-
organization payment gateway config
-
saved payment method logic
-
contribution/payment creation
-
receipt email system
-
update-payment method flow
-
fund/category reporting
-
form submission records
The payment plan system should be a layer on top of existing payments, not a replacement.
Critical: make sure public URLs in payment plan emails use the production frontend URL for the environment, not preview/dev URLs.
