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:
183
temp_files/v4/nav_changes.py
Normal file
183
temp_files/v4/nav_changes.py
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/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!")
|
||||
Reference in New Issue
Block a user