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)
184 lines
5.7 KiB
Python
184 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Reassign every Filament resource to the correct navigation group.
|
|
|
|
SIDEBAR STRUCTURE:
|
|
Daily (always open, no collapse)
|
|
├ Donors — "Find a donor, help them"
|
|
├ Donations — "See what came in, investigate issues"
|
|
└ Regular Giving — "Monthly supporters"
|
|
|
|
Fundraising
|
|
├ Review Queue (84 pending badge)
|
|
├ All Fundraisers
|
|
└ Scheduled Campaigns (3 only)
|
|
|
|
Setup (collapsed by default — rarely touched)
|
|
├ Causes (30)
|
|
├ Countries (12)
|
|
├ URL Builder
|
|
├ Settings
|
|
├ Users
|
|
├ Activity Log
|
|
└ (hidden): Snowdon, Campaign Regs, WOH, Engage dims, Fund dims
|
|
"""
|
|
|
|
import re, os
|
|
|
|
BASE = '/home/forge/app.charityright.org.uk'
|
|
|
|
changes = {
|
|
# ── DAILY (top 3 — the only pages staff use every day) ──
|
|
'app/Filament/Resources/CustomerResource.php': {
|
|
'navigationGroup': 'Daily',
|
|
'navigationIcon': 'heroicon-o-user-circle',
|
|
'navigationSort': 1,
|
|
'navigationLabel': 'Donors',
|
|
},
|
|
'app/Filament/Resources/DonationResource.php': {
|
|
'navigationGroup': 'Daily',
|
|
'navigationIcon': 'heroicon-o-banknotes',
|
|
'navigationSort': 2,
|
|
'navigationLabel': 'Donations',
|
|
},
|
|
'app/Filament/Resources/ScheduledGivingDonationResource.php': {
|
|
'navigationGroup': 'Daily',
|
|
'navigationIcon': 'heroicon-o-arrow-path',
|
|
'navigationSort': 3,
|
|
'navigationLabel': 'Regular Giving',
|
|
},
|
|
|
|
# ── FUNDRAISING ──
|
|
'app/Filament/Resources/ApprovalQueueResource.php': {
|
|
'navigationGroup': 'Fundraising',
|
|
'navigationIcon': 'heroicon-o-shield-check',
|
|
'navigationSort': 1,
|
|
'navigationLabel': 'Review Queue',
|
|
},
|
|
'app/Filament/Resources/AppealResource.php': {
|
|
'navigationGroup': 'Fundraising',
|
|
'navigationIcon': 'heroicon-o-hand-raised',
|
|
'navigationSort': 2,
|
|
'navigationLabel': 'All Fundraisers',
|
|
},
|
|
'app/Filament/Resources/ScheduledGivingCampaignResource.php': {
|
|
'navigationGroup': 'Fundraising',
|
|
'navigationIcon': 'heroicon-o-calendar',
|
|
'navigationSort': 3,
|
|
'navigationLabel': 'Giving Campaigns',
|
|
},
|
|
|
|
# ── SETUP (collapsed, rarely used) ──
|
|
'app/Filament/Resources/DonationTypeResource.php': {
|
|
'navigationGroup': 'Setup',
|
|
'navigationIcon': 'heroicon-o-tag',
|
|
'navigationSort': 1,
|
|
'navigationLabel': 'Causes',
|
|
},
|
|
'app/Filament/Resources/DonationCountryResource.php': {
|
|
'navigationGroup': 'Setup',
|
|
'navigationIcon': 'heroicon-o-globe-alt',
|
|
'navigationSort': 2,
|
|
'navigationLabel': 'Countries',
|
|
},
|
|
'app/Filament/Resources/UserResource.php': {
|
|
'navigationGroup': 'Setup',
|
|
'navigationIcon': 'heroicon-o-users',
|
|
'navigationSort': 3,
|
|
'navigationLabel': 'Admin Users',
|
|
},
|
|
'app/Filament/Resources/EventLogResource.php': {
|
|
'navigationGroup': 'Setup',
|
|
'navigationIcon': 'heroicon-o-exclamation-triangle',
|
|
'navigationSort': 4,
|
|
'navigationLabel': 'Activity Log',
|
|
},
|
|
}
|
|
|
|
# Resources to HIDE from navigation entirely (dead data, 0 recent activity)
|
|
hide = [
|
|
'app/Filament/Resources/SnowdonRegistrationResource.php',
|
|
'app/Filament/Resources/CampaignRegistrationResource.php',
|
|
'app/Filament/Resources/WOHMessageResource.php',
|
|
'app/Filament/Resources/EngageAttributionDimensionResource.php',
|
|
'app/Filament/Resources/EngageFundDimensionResource.php',
|
|
]
|
|
|
|
# Pages
|
|
page_changes = {
|
|
'app/Filament/Pages/DonationURLBuilder.php': {
|
|
'navigationGroup': 'Setup',
|
|
'navigationSort': 5,
|
|
'navigationLabel': 'URL Builder',
|
|
},
|
|
'app/Filament/Pages/Settings.php': {
|
|
'navigationGroup': 'Setup',
|
|
'navigationSort': 6,
|
|
},
|
|
}
|
|
|
|
|
|
def update_static_property(content, prop, value):
|
|
"""Replace a static property value in a PHP class."""
|
|
# Match: protected static ?string $prop = 'old_value';
|
|
# or: protected static ?int $prop = 123;
|
|
pattern = rf"(protected\s+static\s+\?(?:string|int)\s+\${prop}\s*=\s*)('[^']*'|\d+)(\s*;)"
|
|
|
|
if isinstance(value, int):
|
|
replacement = rf"\g<1>{value}\3"
|
|
else:
|
|
replacement = rf"\g<1>'{value}'\3"
|
|
|
|
new_content, count = re.subn(pattern, replacement, content)
|
|
return new_content, count
|
|
|
|
|
|
def apply_changes(filepath, props):
|
|
full = os.path.join(BASE, filepath)
|
|
with open(full, 'r') as f:
|
|
content = f.read()
|
|
|
|
for prop, value in props.items():
|
|
content, count = update_static_property(content, prop, value)
|
|
if count == 0:
|
|
print(f" WARNING: {prop} not found in {filepath}")
|
|
|
|
with open(full, 'w') as f:
|
|
f.write(content)
|
|
print(f"Updated: {filepath}")
|
|
|
|
|
|
def hide_from_nav(filepath):
|
|
full = os.path.join(BASE, filepath)
|
|
with open(full, 'r') as f:
|
|
content = f.read()
|
|
|
|
# Add shouldRegisterNavigation = false if not present
|
|
if 'shouldRegisterNavigation' not in content:
|
|
# Insert after the class opening
|
|
content = content.replace(
|
|
'protected static ?string $navigationIcon',
|
|
'protected static bool $shouldRegisterNavigation = false;\n\n protected static ?string $navigationIcon'
|
|
)
|
|
with open(full, 'w') as f:
|
|
f.write(content)
|
|
print(f"Hidden: {filepath}")
|
|
else:
|
|
print(f"Already hidden: {filepath}")
|
|
|
|
|
|
# Apply all changes
|
|
print("=== UPDATING NAVIGATION ===")
|
|
for filepath, props in changes.items():
|
|
apply_changes(filepath, props)
|
|
|
|
print("\n=== HIDING DEAD PAGES ===")
|
|
for filepath in hide:
|
|
hide_from_nav(filepath)
|
|
|
|
print("\n=== UPDATING PAGES ===")
|
|
for filepath, props in page_changes.items():
|
|
apply_changes(filepath, props)
|
|
|
|
print("\nDone!")
|