#!/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!')