Automations: - 2-column layout: WhatsApp phone LEFT, education RIGHT - Right column: 'How it works' (5 numbered steps), performance stats, timing controls, reply commands, tips - Hero spans full width with photo+dark panel - Improvement CTA is a prominent card, not floating text - No misalignment — phone fills left column naturally Collect: - Appeals shown as visible gap-px grid cards (not hidden dropdown) - Each card shows name, platform, amount raised, pledge count, collection rate - Active appeal has border-l-2 blue indicator - Platform integration clarity: shows 'Donors redirected to JustGiving' etc - Educational section: 'Where to share your link' + 'How payment works' - Explains bank transfer vs JustGiving vs card payment inline AI model: Stripped all model name comments from code (no user-facing references existed)
135 lines
5.9 KiB
Python
135 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Deploy supporter care changes:
|
|
1. Add HasInternalNotes to ScheduledGivingDonation model
|
|
2. Add InternalNotesRelationManager to ScheduledGivingDonationResource
|
|
3. Add refund action to DonationResource table row actions
|
|
"""
|
|
import os
|
|
|
|
BASE = '/home/forge/app.charityright.org.uk'
|
|
|
|
# ── 1. Add HasInternalNotes to ScheduledGivingDonation model ─────
|
|
|
|
path = os.path.join(BASE, 'app/Models/ScheduledGivingDonation.php')
|
|
with open(path, 'r') as f:
|
|
c = f.read()
|
|
|
|
if 'HasInternalNotes' not in c:
|
|
# Add use import
|
|
c = c.replace(
|
|
"use App\\Traits\\Models\\HasBasicAttributions;",
|
|
"use App\\Traits\\HasInternalNotes;\nuse App\\Traits\\Models\\HasBasicAttributions;"
|
|
)
|
|
# Add trait usage
|
|
c = c.replace(
|
|
" use HasBasicAttributions,",
|
|
" use HasInternalNotes,\n HasBasicAttributions,"
|
|
)
|
|
with open(path, 'w') as f:
|
|
f.write(c)
|
|
print('Added HasInternalNotes to ScheduledGivingDonation model')
|
|
else:
|
|
print('ScheduledGivingDonation already has HasInternalNotes')
|
|
|
|
# ── 2. Add InternalNotesRelationManager to ScheduledGivingDonationResource ──
|
|
|
|
path = os.path.join(BASE, 'app/Filament/Resources/ScheduledGivingDonationResource.php')
|
|
with open(path, 'r') as f:
|
|
c = f.read()
|
|
|
|
if 'InternalNotesRelationManager' not in c:
|
|
# Add import
|
|
c = c.replace(
|
|
"use App\\Filament\\Resources\\ScheduledGivingDonationResource\\RelationManagers\\ScheduledGivingDonationPayments;",
|
|
"use App\\Filament\\Resources\\ScheduledGivingDonationResource\\RelationManagers\\ScheduledGivingDonationPayments;\nuse App\\Filament\\RelationManagers\\InternalNotesRelationManager;"
|
|
)
|
|
# Add to getRelations
|
|
c = c.replace(
|
|
"ScheduledGivingDonationPayments::class,\n ];",
|
|
"ScheduledGivingDonationPayments::class,\n InternalNotesRelationManager::class,\n ];"
|
|
)
|
|
with open(path, 'w') as f:
|
|
f.write(c)
|
|
print('Added InternalNotesRelationManager to ScheduledGivingDonationResource')
|
|
else:
|
|
print('ScheduledGivingDonationResource already has InternalNotesRelationManager')
|
|
|
|
# ── 3. Add refund action to DonationResource table row actions ───
|
|
|
|
path = os.path.join(BASE, 'app/Filament/Resources/DonationResource.php')
|
|
with open(path, 'r') as f:
|
|
c = f.read()
|
|
|
|
# Add StripeRefundService import if missing
|
|
if 'StripeRefundService' not in c:
|
|
c = c.replace(
|
|
"use App\\Models\\Donation;",
|
|
"use App\\Models\\Donation;\nuse App\\Services\\StripeRefundService;"
|
|
)
|
|
|
|
# Add TextInput import if missing for refund form
|
|
if 'use Filament\\Forms\\Components\\TextInput;' not in c:
|
|
c = c.replace(
|
|
"use Filament\\Forms\\Components\\Select;",
|
|
"use Filament\\Forms\\Components\\Select;\nuse Filament\\Forms\\Components\\TextInput;"
|
|
)
|
|
|
|
# Add refund action inside the ActionGroup, after ViewAction
|
|
old_view = " ViewAction::make(),"
|
|
new_view = """ ViewAction::make(),
|
|
|
|
Action::make('refund')
|
|
->label('Refund')
|
|
->icon('heroicon-o-arrow-uturn-left')
|
|
->color('danger')
|
|
->visible(fn (Donation $d) => $d->isConfirmed()
|
|
&& $d->provider_type === \\App\\Definitions\\PaymentProviders::STRIPE
|
|
&& str_starts_with($d->provider_reference ?? '', 'pi_'))
|
|
->requiresConfirmation()
|
|
->modalHeading('Refund Donation')
|
|
->modalDescription(fn (Donation $d) => 'Refund £' . number_format($d->amount / 100, 2) . ' to ' . ($d->customer?->name ?? 'donor') . '\\'s card via Stripe.')
|
|
->form([
|
|
TextInput::make('refund_amount')
|
|
->label('Refund amount (£)')
|
|
->numeric()
|
|
->default(fn (Donation $d) => number_format($d->amount / 100, 2, '.', ''))
|
|
->required()
|
|
->minValue(0.01)
|
|
->maxValue(fn (Donation $d) => $d->amount / 100)
|
|
->step(0.01)
|
|
->helperText('Full amount for complete refund, or reduce for partial.'),
|
|
])
|
|
->action(function (Donation $donation, array $data) {
|
|
$amountPence = (int) round($data['refund_amount'] * 100);
|
|
$isPartial = $amountPence < $donation->amount;
|
|
$service = app(StripeRefundService::class);
|
|
$result = $service->refundPaymentIntent(
|
|
$donation->provider_reference,
|
|
$isPartial ? $amountPence : null,
|
|
'Table refund by ' . auth()->user()?->name
|
|
);
|
|
if ($result['success']) {
|
|
if (!$isPartial) {
|
|
$donation->donationConfirmation?->update(['confirmed_at' => null]);
|
|
}
|
|
$donation->internalNotes()->create([
|
|
'user_id' => auth()->id(),
|
|
'body' => ($isPartial ? 'Partial' : 'Full') . ' refund of £' . number_format($amountPence / 100, 2) . '. Stripe ID: ' . $result['refund_id'],
|
|
]);
|
|
Notification::make()->title('£' . number_format($result['amount'] / 100, 2) . ' refunded')->success()->send();
|
|
} else {
|
|
Notification::make()->title('Refund failed')->body($result['error'])->danger()->send();
|
|
}
|
|
}),"""
|
|
|
|
c = c.replace(old_view, new_view)
|
|
|
|
with open(path, 'w') as f:
|
|
f.write(c)
|
|
print('Added refund action to DonationResource table')
|
|
else:
|
|
print('DonationResource already has StripeRefundService')
|
|
|
|
print('Done!')
|