Files
calvana/temp_files/v4/ListCustomers.php
Omair Saleh 3b46222118 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)
2026-03-04 22:46:08 +08:00

63 lines
2.0 KiB
PHP

<?php
namespace App\Filament\Resources\CustomerResource\Pages;
use App\Filament\Resources\CustomerResource;
use App\Models\Customer;
use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords;
use Illuminate\Database\Eloquent\Builder;
class ListCustomers extends ListRecords
{
protected static string $resource = CustomerResource::class;
public function getHeading(): string
{
return 'Donors';
}
public function getSubheading(): string
{
return 'Search by name, email, or phone number. Click a donor to see their full history.';
}
public function getTabs(): array
{
return [
'all' => Tab::make('All Donors')
->icon('heroicon-o-users'),
'monthly' => Tab::make('Monthly Supporters')
->icon('heroicon-o-arrow-path')
->modifyQueryUsing(fn (Builder $query) => $query
->whereHas('scheduledGivingDonations', fn ($q) => $q->where('is_active', true))
),
'major' => Tab::make('Major Donors')
->icon('heroicon-o-star')
->modifyQueryUsing(fn (Builder $query) => $query
->whereIn('id', function ($sub) {
$sub->select('customer_id')
->from('donations')
->join('donation_confirmations', 'donations.id', '=', 'donation_confirmations.donation_id')
->whereNotNull('donation_confirmations.confirmed_at')
->groupBy('customer_id')
->havingRaw('SUM(donations.amount) >= 100000');
})
),
'recent' => Tab::make('New (30 days)')
->icon('heroicon-o-sparkles')
->modifyQueryUsing(fn (Builder $query) => $query
->where('created_at', '>=', now()->subDays(30))
),
];
}
protected function getHeaderActions(): array
{
return [];
}
}