Skip to content

Commit

Permalink
Implement Comprehensive Payment Refund Functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
sweep-ai[bot] authored Dec 24, 2024
1 parent 9cb574d commit e747b34
Show file tree
Hide file tree
Showing 3 changed files with 299 additions and 103 deletions.
151 changes: 131 additions & 20 deletions app/Filament/App/Resources/RefundResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,55 +12,166 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use App\Services\RefundService;
use Filament\Notifications\Notification;
use Closure;

class RefundResource extends Resource
{
protected static ?string $model = Payment::class;

protected static ?string $navigationIcon = 'heroicon-o-circle-stack';
protected static ?string $navigationIcon = 'heroicon-o-arrow-path';

protected static ?string $navigationLabel = 'Refunds';

protected static ?string $modelLabel = 'Refund';

public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Select::make('payment_id')
->label('Payment')
->options(function () {
return Payment::whereIn('refund_status', ['none', 'partial'])
->with('invoice')
->get()
->mapWithKeys(function ($payment) {
return [
$payment->id => "Payment #{$payment->id} - Invoice #{$payment->invoice->invoice_number} ({$payment->amount} {$payment->currency})"
];
});
})
->required()
->searchable()
->reactive()
->afterStateUpdated(function ($state, callable $set) {
if ($state) {
$payment = Payment::find($state);
$set('max_refund_amount', $payment->getRemainingRefundableAmount());
$set('currency', $payment->currency);
}
}),

Forms\Components\TextInput::make('amount')
->required()
->numeric()
->label('Refund Amount'),
Forms\Components\Select::make('payment_id')
->label('Payment')
->options(Payment::where('refund_status', 'none')->pluck('id', 'id'))
->label('Refund Amount')
->hint(fn ($state, $record) => $record ? "Maximum refundable amount: {$record->getRemainingRefundableAmount()} {$record->currency}" : '')
->rules([
'required',
'numeric',
'min:0.01',
function (string $attribute, $value, Closure $fail) {
$payment = Payment::find(request()->input('data.payment_id'));
if ($payment && $value > $payment->getRemainingRefundableAmount()) {
$fail("The refund amount cannot exceed the remaining refundable amount of {$payment->getRemainingRefundableAmount()} {$payment->currency}");
}
},
]),

Forms\Components\Textarea::make('reason')
->label('Refund Reason')
->required()
->searchable(),
->maxLength(1000),

Forms\Components\Hidden::make('currency'),
]);
}

public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('id')->sortable(),
Tables\Columns\TextColumn::make('invoice.invoice_number')->sortable(),
Tables\Columns\TextColumn::make('amount')->sortable(),
Tables\Columns\TextColumn::make('refunded_amount')->sortable(),
Tables\Columns\TextColumn::make('refund_status')->sortable(),
Tables\Columns\TextColumn::make('id')
->label('Payment ID')
->sortable(),
Tables\Columns\TextColumn::make('invoice.invoice_number')
->label('Invoice Number')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('amount')
->label('Original Amount')
->money(fn ($record) => $record->currency)
->sortable(),
Tables\Columns\TextColumn::make('refunded_amount')
->label('Refunded Amount')
->money(fn ($record) => $record->currency)
->sortable(),
Tables\Columns\BadgeColumn::make('refund_status')
->colors([
'danger' => 'none',
'warning' => 'partial',
'success' => 'full',
]),
Tables\Columns\TextColumn::make('created_at')
->label('Payment Date')
->dateTime()
->sortable(),
])
->filters([
//
Tables\Filters\SelectFilter::make('refund_status')
->options([
'none' => 'No Refund',
'partial' => 'Partially Refunded',
'full' => 'Fully Refunded',
]),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\Action::make('refund')
->visible(fn (Payment $record) => $record->isRefundable())
->form([
Forms\Components\TextInput::make('amount')
->required()
->numeric()
->label('Refund Amount')
->rules([
'required',
'numeric',
'min:0.01',
function (string $attribute, $value, Closure $fail) use ($record) {
if ($value > $record->getRemainingRefundableAmount()) {
$fail("Cannot refund more than {$record->getRemainingRefundableAmount()} {$record->currency}");
}
},
]),
Forms\Components\Textarea::make('reason')
->required()
->label('Refund Reason'),
])
->action(function (array $data, Payment $record) {
$refundService = app(RefundService::class);

try {
$result = $refundService->processRefund($record, $data['amount']);

if ($result['success']) {
Notification::make()
->title('Refund processed successfully')
->success()
->send();
} else {
Notification::make()
->title('Refund failed')
->body($result['message'])
->danger()
->send();
}
} catch (\Exception $e) {
Notification::make()
->title('Error processing refund')
->body($e->getMessage())
->danger()
->send();
}
}),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
]);
->bulkActions([])
->defaultSort('created_at', 'desc');
}

public static function getRelations(): array
{
return [
//
];
return [];
}

public static function getPages(): array
Expand All @@ -75,7 +186,7 @@ public static function getPages(): array
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->whereIn('refund_status', ['none', 'partial'])
->with(['invoice', 'paymentGateway'])
->latest();
}
}
45 changes: 45 additions & 0 deletions app/Models/Payment.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ class Payment extends Model
'refunded_amount',
'affiliate_id',
'affiliate_commission',
'refund_reason',
];

protected $casts = [
'amount' => 'float',
'refunded_amount' => 'float',
'payment_date' => 'datetime',
];

public function invoice()
Expand Down Expand Up @@ -54,4 +61,42 @@ public function getRemainingRefundableAmount()
{
return $this->amount - ($this->refunded_amount ?? 0);
}

public function processRefund(float $amount, string $reason = null)
{
if (!$this->isRefundable()) {
throw new \Exception('This payment is not eligible for refund');
}

if ($amount > $this->getRemainingRefundableAmount()) {
throw new \Exception('Refund amount exceeds remaining refundable amount');
}

$this->refunded_amount = ($this->refunded_amount ?? 0) + $amount;
$this->refund_status = $this->refunded_amount >= $this->amount ? 'full' : 'partial';
$this->refund_reason = $reason;
$this->save();

return true;
}

public function getFormattedAmount()
{
return number_format($this->amount, 2) . ' ' . $this->currency;
}

public function getFormattedRefundedAmount()
{
return number_format($this->refunded_amount ?? 0, 2) . ' ' . $this->currency;
}

public function getRefundStatusBadgeAttribute()
{
return match($this->refund_status) {
'none' => '<span class="badge badge-danger">No Refund</span>',
'partial' => '<span class="badge badge-warning">Partial Refund</span>',
'full' => '<span class="badge badge-success">Full Refund</span>',
default => '<span class="badge badge-secondary">Unknown</span>',
};
}
}
Loading

0 comments on commit e747b34

Please sign in to comment.