Stripe integration: charity connects their own Stripe account
Model: PNPL never touches the money. Each charity connects their own Stripe account by pasting their API key in Settings. When a donor chooses card payment, they're redirected to Stripe Checkout. The money lands in the charity's Stripe balance. ## Schema - Organization.stripeSecretKey (new column) - Organization.stripeWebhookSecret (new column) ## New/rewritten files - src/lib/stripe.ts — getStripeForOrg(secretKey), per-org client - src/app/api/stripe/checkout/route.ts — uses org's key, not env var - src/app/api/stripe/webhook/route.ts — tries all org webhook secrets - src/app/p/[token]/steps/card-payment-step.tsx — redirect to Stripe Checkout (no fake card form — Stripe handles PCI) ## Settings page - New 'Card payments' section between Bank and Charity - Instructions: how to get your Stripe API key - Webhook setup in collapsed <details> (optional, for auto-confirm) - 'Card payments live' green banner when connected - Readiness bar shows Stripe status (5 columns now) ## Pledge flow - PaymentStep shows card option ONLY if org has Stripe configured - hasStripe flag passed from /api/qr/[token] → PaymentStep - Secret key never exposed to frontend (only boolean hasStripe) ## How it works 1. Charity pastes sk_live_... in Settings → Save 2. Donor opens pledge link → sees 'Bank Transfer', 'Direct Debit', 'Card' 3. Donor picks card → enters name + email → redirects to Stripe Checkout 4. Stripe processes payment → money in charity's Stripe balance 5. (Optional) Webhook auto-confirms pledge as paid Payment options: - Bank Transfer: zero fees (default, always available) - Direct Debit via GoCardless: 1% + 20p (if org configured) - Card via Stripe: standard Stripe fees (if org configured)
This commit is contained in:
@@ -34,10 +34,8 @@ use Filament\Tables\Actions\ExportAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Enums\FiltersLayout;
|
||||
use Filament\Tables\Filters\Filter;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -302,7 +300,7 @@ class DonationResource extends Resource
|
||||
->copyable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters(static::getTableFilters(), layout: FiltersLayout::AboveContentCollapsible)
|
||||
->filters(static::getTableFilters())
|
||||
->actions(static::getTableRowActions())
|
||||
->bulkActions(static::getBulkActions())
|
||||
->headerActions([
|
||||
@@ -431,27 +429,11 @@ class DonationResource extends Resource
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── Filters ─────────────────────────────────────────────────
|
||||
// Designed around real questions:
|
||||
// "Show me today's incomplete donations" (investigating failures)
|
||||
// "Show me Zakat donations this month" (reporting)
|
||||
// "Show me donations to a specific cause" (allocation check)
|
||||
// ─── Filters (drill-down within tabs) ───────────────────────
|
||||
|
||||
private static function getTableFilters(): array
|
||||
{
|
||||
return [
|
||||
TernaryFilter::make('confirmed')
|
||||
->label('Payment Status')
|
||||
->placeholder('All')
|
||||
->trueLabel('Confirmed only')
|
||||
->falseLabel('Incomplete only')
|
||||
->queries(
|
||||
true: fn (Builder $q) => $q->whereHas('donationConfirmation', fn ($q2) => $q2->whereNotNull('confirmed_at')),
|
||||
false: fn (Builder $q) => $q->whereDoesntHave('donationConfirmation', fn ($q2) => $q2->whereNotNull('confirmed_at')),
|
||||
blank: fn (Builder $q) => $q,
|
||||
)
|
||||
->default(true),
|
||||
|
||||
SelectFilter::make('donation_type_id')
|
||||
->label('Cause')
|
||||
->options(fn () => DonationType::orderBy('display_name')->pluck('display_name', 'id'))
|
||||
@@ -473,21 +455,6 @@ class DonationResource extends Resource
|
||||
->when($data['to'], fn (Builder $q) => $q->whereDate('created_at', '<=', $data['to']));
|
||||
})
|
||||
->columns(2),
|
||||
|
||||
Filter::make('is_zakat')
|
||||
->label('Zakat')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->whereHas('donationPreferences', fn ($q2) => $q2->where('is_zakat', true))),
|
||||
|
||||
Filter::make('is_gift_aid')
|
||||
->label('Gift Aid')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->whereHas('donationPreferences', fn ($q2) => $q2->where('is_gift_aid', true))),
|
||||
|
||||
Filter::make('has_fundraiser')
|
||||
->label('Via Fundraiser')
|
||||
->toggle()
|
||||
->query(fn (Builder $q) => $q->whereNotNull('appeal_id')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user