Payment System Integration Guide

📄 General
← Back to Documentation
# Payment System Integration Guide ## Unified Payment Transactions for Reservations & Bookings ## Overview The system now uses a **unified `payment_transactions` table** for both: - ✅ **Reservations** (existing functionality) - ✅ **Bookings** (new functionality with deposit/check-in/check-out flow) ## Database Structure ### Payment Transactions Table Single table handling all payment types: ```sql payment_transactions ├── id ├── reservation_id (nullable) - For reservation payments ├── booking_id (nullable) - For booking payments ├── user_id - Staff who processed payment ├── client_id - Customer who made payment ├── amount ├── currency ├── payment_method (cash, card_online, card_inplace, bank_transfer, paypal, stripe, mypos) ├── payment_gateway (stripe, paypal, mypos, etc.) ├── payment_stage (deposit, check_in, check_out, full, partial, refund) ├── transaction_type (deposit, withdrawal, full_transfer) ├── status (pending, completed, failed, refunded, canceled) ├── transaction_id - Unique transaction identifier ├── reference_number - Gateway reference ├── receipt_number ├── fiscal_receipt_number ├── notes ├── metadata (JSON) ├── paid_at ├── company_id ├── workspace_id └── timestamps ``` ## Admin Panel Integration ### 1. Booking Resource - Payment Tab **File:** `app/Filament/Resources/BookingResource.php` Add a "Payments" relation manager to show all payments for a booking: ```php use App\Filament\Resources\BookingResource\RelationManagers\PaymentsRelationManager; public static function getRelations(): array { return [ PaymentsRelationManager::class, ]; } ``` **File:** `app/Filament/Resources/BookingResource/RelationManagers/PaymentsRelationManager.php` ```php <?php namespace App\Filament\Resources\BookingResource\RelationManagers; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables\Table; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\BadgeColumn; use Filament\Actions\CreateAction; use Filament\Actions\EditAction; use Filament\Actions\DeleteAction; class PaymentsRelationManager extends RelationManager { protected static string $relationship = 'payments'; protected static ?string $title = 'Payment History'; public function table(Table $table): Table { return $table ->columns([ TextColumn::make('paid_at') ->label('Date') ->dateTime('d M Y H:i') ->sortable(), BadgeColumn::make('payment_stage_label') ->label('Stage') ->colors([ 'primary' => 'deposit', 'success' => 'check_in', 'warning' => 'check_out', 'info' => 'full', ]), TextColumn::make('amount') ->money('EUR') ->sortable(), TextColumn::make('payment_method_label') ->label('Method'), BadgeColumn::make('status_label') ->label('Status') ->colors([ 'success' => 'completed', 'warning' => 'pending', 'danger' => 'failed', 'gray' => 'canceled', ]), TextColumn::make('transaction_id') ->label('Transaction ID') ->limit(20) ->copyable(), TextColumn::make('user.name') ->label('Processed By') ->default('System'), TextColumn::make('notes') ->limit(30) ->tooltip(fn ($record) => $record->notes), ]) ->defaultSort('paid_at', 'desc') ->headerActions([ // Add manual payment action ]) ->actions([ EditAction::make(), DeleteAction::make(), ]); } } ``` ### 2. Booking List - Payment Status Column **File:** `app/Filament/Resources/BookingResource/Tables/BookingsTable.php` ```php use Filament\Tables\Columns\BadgeColumn; use Filament\Tables\Columns\TextColumn; public static function configure(Table $table): Table { return $table->columns([ // ... existing columns TextColumn::make('total_amount') ->label('Total') ->money('EUR') ->sortable(), TextColumn::make('paid_amount') ->label('Paid') ->money('EUR') ->sortable(), TextColumn::make('remaining_amount') ->label('Remaining') ->money('EUR') ->sortable() ->color(fn ($record) => $record->remaining_amount > 0 ? 'warning' : 'success'), BadgeColumn::make('payment_status') ->label('Payment') ->colors([ 'danger' => 'unpaid', 'warning' => 'partially_paid', 'info' => 'deposit_paid', 'success' => 'paid', ]) ->icons([ 'heroicon-o-x-circle' => 'unpaid', 'heroicon-o-clock' => 'partially_paid', 'heroicon-o-check-circle' => 'deposit_paid', 'heroicon-o-check-badge' => 'paid', ]), ]); } ``` ### 3. Booking View - Payment Summary Widget **File:** `app/Filament/Resources/BookingResource/Widgets/PaymentSummaryWidget.php` ```php <?php namespace App\Filament\Resources\BookingResource\Widgets; use App\Models\Booking; use App\Services\BookingPaymentService; use Filament\Widgets\StatsOverviewWidget; use Filament\Widgets\StatsOverviewWidget\Stat; class PaymentSummaryWidget extends StatsOverviewWidget { public ?Booking $record = null; protected function getStats(): array { $paymentService = new BookingPaymentService(); $summary = $paymentService->getPaymentSummary($this->record); return [ Stat::make('Total Amount', '€' . number_format($summary['total'], 2)) ->description('Including tax') ->descriptionIcon('heroicon-o-currency-euro') ->color('primary'), Stat::make('Deposit Required', '€' . number_format($summary['deposit_required'], 2)) ->description($summary['deposit_paid'] ? 'Paid ✓' : 'Pending') ->descriptionIcon($summary['deposit_paid'] ? 'heroicon-o-check-circle' : 'heroicon-o-clock') ->color($summary['deposit_paid'] ? 'success' : 'warning'), Stat::make('Total Paid', '€' . number_format($summary['total_paid'], 2)) ->description('Across all payments') ->descriptionIcon('heroicon-o-banknotes') ->color('success'), Stat::make('Remaining Balance', '€' . number_format($summary['remaining_balance'], 2)) ->description($summary['fully_paid'] ? 'Fully paid' : 'Due') ->descriptionIcon($summary['fully_paid'] ? 'heroicon-o-check-badge' : 'heroicon-o-exclamation-triangle') ->color($summary['fully_paid'] ? 'success' : 'danger'), ]; } } ``` ### 4. Check-In Action with Payment **File:** `app/Filament/Resources/BookingResource/Pages/CheckInBooking.php` ```php <?php namespace App\Filament\Resources\BookingResource\Pages; use App\Filament\Resources\BookingResource; use App\Services\BookingPaymentService; use Filament\Resources\Pages\Page; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Select; use Filament\Forms\Components\Textarea; use Filament\Forms\Form; use Filament\Notifications\Notification; class CheckInBooking extends Page { protected static string $resource = BookingResource::class; protected static string $view = 'filament.resources.booking-resource.pages.check-in-booking'; public ?array $data = []; public $amountDue = 0; public function mount(): void { $paymentService = new BookingPaymentService(); $this->amountDue = $paymentService->calculateCheckInAmount($this->record); $this->form->fill([ 'amount' => $this->amountDue, 'payment_method' => 'cash', ]); } public function form(Form $form): Form { return $form ->schema([ TextInput::make('amount') ->label('Amount Due') ->prefix('€') ->numeric() ->disabled() ->dehydrated(), Select::make('payment_method') ->label('Payment Method') ->options([ 'cash' => 'Cash', 'card_inplace' => 'Card (Terminal)', 'bank_transfer' => 'Bank Transfer', ]) ->required() ->visible(fn () => $this->amountDue > 0), Textarea::make('notes') ->label('Notes') ->rows(3), ]) ->statePath('data'); } public function checkIn(): void { $data = $this->form->getState(); $paymentService = new BookingPaymentService(); // Process payment if amount due if ($this->amountDue > 0) { $paymentService->processCheckInPayment( $this->record, $this->amountDue, $data['payment_method'] ); } // Update booking status $this->record->update([ 'status' => 'checked_in', 'checked_in_at' => now(), ]); Notification::make() ->title('Check-in Successful') ->body('Booking checked in successfully. Payment of €' . number_format($this->amountDue, 2) . ' received.') ->success() ->send(); return redirect()->route('filament.admin.resources.bookings.view', $this->record); } } ``` ### 5. Check-Out Action with Payment **File:** `app/Filament/Resources/BookingResource/Pages/CheckOutBooking.php` ```php <?php namespace App\Filament\Resources\BookingResource\Pages; use App\Filament\Resources\BookingResource; use App\Services\BookingPaymentService; use Filament\Resources\Pages\Page; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Select; use Filament\Forms\Components\Repeater; use Filament\Forms\Form; use Filament\Notifications\Notification; class CheckOutBooking extends Page { protected static string $resource = BookingResource::class; protected static string $view = 'filament.resources.booking-resource.pages.check-out-booking'; public ?array $data = []; public $additionalCharges = 0; public function mount(): void { $paymentService = new BookingPaymentService(); $this->additionalCharges = $paymentService->calculateCheckOutAmount($this->record); $this->form->fill([ 'additional_amount' => $this->additionalCharges, 'payment_method' => 'cash', ]); } public function form(Form $form): Form { return $form ->schema([ Repeater::make('additional_items') ->label('Additional Charges') ->schema([ TextInput::make('description') ->required(), TextInput::make('amount') ->numeric() ->prefix('€') ->required(), ]) ->columns(2) ->addActionLabel('Add Charge'), TextInput::make('additional_amount') ->label('Total Additional Charges') ->prefix('€') ->numeric() ->disabled(), Select::make('payment_method') ->label('Payment Method') ->options([ 'cash' => 'Cash', 'card_inplace' => 'Card (Terminal)', ]) ->required() ->visible(fn () => $this->additionalCharges > 0), ]) ->statePath('data'); } public function checkOut(): void { $data = $this->form->getState(); $paymentService = new BookingPaymentService(); // Process payment for additional charges if ($this->additionalCharges > 0) { $paymentService->processCheckOutPayment( $this->record, $this->additionalCharges, $data['payment_method'] ); } // Update booking status $this->record->update([ 'status' => 'completed', 'checked_out_at' => now(), ]); Notification::make() ->title('Check-out Successful') ->body('Booking completed. Payment of €' . number_format($this->additionalCharges, 2) . ' received.') ->success() ->send(); return redirect()->route('filament.admin.resources.bookings.view', $this->record); } } ``` ## Client UI Integration ### 1. Booking Creation with Deposit Payment **File:** `app/Filament/Client/Pages/CreateBooking.php` ```php protected function afterCreate(): void { $paymentService = new BookingPaymentService(); // Calculate totals $totals = $paymentService->calculateBookingTotal($this->record); $depositAmount = $paymentService->calculateDepositAmount($this->record); // Update booking with calculated amounts $this->record->update([ 'total_amount' => $totals['total'], 'subtotal_amount' => $totals['subtotal'], 'tax_amount' => $totals['tax_amount'], 'deposit_amount' => $depositAmount, 'remaining_amount' => $totals['total'] - $depositAmount, ]); // If deposit required, redirect to payment if ($depositAmount > 0 && financial_setting('require_deposit_for_booking')) { session()->flash('booking_id', $this->record->id); return redirect()->route('client.booking.payment', $this->record); } } ``` ### 2. Client Payment Page **File:** `app/Filament/Client/Pages/BookingPayment.php` ```php <?php namespace App\Filament\Client\Pages; use App\Models\Booking; use App\Services\BookingPaymentService; use Filament\Pages\Page; use Filament\Forms\Components\Select; use Filament\Forms\Components\Hidden; use Filament\Forms\Form; class BookingPayment extends Page { protected static string $view = 'filament.client.pages.booking-payment'; protected static bool $shouldRegisterNavigation = false; public Booking $booking; public ?array $data = []; public $depositAmount = 0; public $paymentSummary = []; public function mount(Booking $booking): void { $this->booking = $booking; $paymentService = new BookingPaymentService(); $this->depositAmount = $paymentService->calculateDepositAmount($booking); $this->paymentSummary = $paymentService->getPaymentSummary($booking); $this->form->fill([ 'payment_method' => 'stripe', ]); } public function form(Form $form): Form { return $form ->schema([ Select::make('payment_method') ->label('Payment Method') ->options([ 'stripe' => 'Credit/Debit Card (Stripe)', 'paypal' => 'PayPal', 'bank_transfer' => 'Bank Transfer', ]) ->required(), Hidden::make('amount') ->default($this->depositAmount), ]) ->statePath('data'); } public function processPayment(): void { $data = $this->form->getState(); // Redirect to payment gateway based on method if ($data['payment_method'] === 'stripe') { return redirect()->route('client.payment.stripe', [ 'booking' => $this->booking, 'amount' => $this->depositAmount, ]); } if ($data['payment_method'] === 'paypal') { return redirect()->route('client.payment.paypal', [ 'booking' => $this->booking, 'amount' => $this->depositAmount, ]); } } } ``` ### 3. Client Booking View - Payment Status **File:** `resources/views/filament/client/pages/booking-details.blade.php` ```blade <div class="space-y-6"> <!-- Payment Summary Card --> <div class="bg-white rounded-lg shadow p-6"> <h3 class="text-lg font-semibold mb-4">Payment Summary</h3> <div class="space-y-3"> <div class="flex justify-between"> <span class="text-gray-600">Subtotal:</span> <span class="font-medium">€{{ number_format($booking->subtotal_amount, 2) }}</span> </div> <div class="flex justify-between"> <span class="text-gray-600">Tax ({{ $taxRate }}%):</span> <span class="font-medium">€{{ number_format($booking->tax_amount, 2) }}</span> </div> <div class="flex justify-between border-t pt-3"> <span class="text-lg font-semibold">Total:</span> <span class="text-lg font-semibold">€{{ number_format($booking->total_amount, 2) }}</span> </div> @if($booking->deposit_amount > 0) <div class="flex justify-between text-blue-600"> <span>Deposit Paid:</span> <span class="font-medium">€{{ number_format($booking->deposit_amount, 2) }}</span> </div> @endif <div class="flex justify-between text-green-600"> <span>Total Paid:</span> <span class="font-semibold">€{{ number_format($booking->paid_amount, 2) }}</span> </div> @if($booking->remaining_amount > 0) <div class="flex justify-between text-orange-600"> <span>Remaining Balance:</span> <span class="font-semibold">€{{ number_format($booking->remaining_amount, 2) }}</span> </div> @endif </div> <!-- Payment Status Badge --> <div class="mt-4"> @if($booking->payment_status === 'paid') <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800"> <svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/> </svg> Fully Paid </span> @elseif($booking->payment_status === 'deposit_paid') <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800"> Deposit Paid - Balance Due at Check-in </span> @else <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-orange-100 text-orange-800"> Payment Pending </span> @endif </div> </div> <!-- Payment History --> <div class="bg-white rounded-lg shadow p-6"> <h3 class="text-lg font-semibold mb-4">Payment History</h3> <div class="space-y-3"> @forelse($booking->payments as $payment) <div class="flex items-center justify-between border-b pb-3"> <div> <div class="font-medium">{{ $payment->payment_stage_label }}</div> <div class="text-sm text-gray-600"> {{ $payment->paid_at->format('d M Y H:i') }} </div> <div class="text-xs text-gray-500"> {{ $payment->payment_method_label }} @if($payment->transaction_id) • ID: {{ $payment->transaction_id }} @endif </div> </div> <div class="text-right"> <div class="font-semibold text-green-600"> €{{ number_format($payment->amount, 2) }} </div> <div class="text-xs"> <span class="px-2 py-1 rounded text-white {{ $payment->status === 'completed' ? 'bg-green-500' : 'bg-gray-500' }}"> {{ $payment->status_label }} </span> </div> </div> </div> @empty <p class="text-gray-500 text-center py-4">No payments yet</p> @endforelse </div> </div> </div> ``` ## Migration Steps 1. **Run the new migration:** ```bash php artisan migrate ``` 2. **Update existing bookings** (if any): ```bash php artisan tinker ``` ```php use App\Services\BookingPaymentService; $service = new BookingPaymentService(); \App\Models\Booking::chunk(100, function($bookings) use ($service) { foreach ($bookings as $booking) { $totals = $service->calculateBookingTotal($booking); $booking->update([ 'total_amount' => $totals['total'], 'subtotal_amount' => $totals['subtotal'], 'tax_amount' => $totals['tax_amount'], ]); } }); ``` 3. **Delete duplicate files** (no longer needed): ```bash rm app/Models/BookingPayment.php rm database/migrations/2025_10_23_150501_create_payments_table.php ``` ## Summary ✅ **Unified payment system** - Single table for reservations & bookings ✅ **Payment stages** - Deposit, check-in, check-out tracking ✅ **Admin visibility** - Payment history, status, and actions ✅ **Client visibility** - Payment summary and history ✅ **Tax calculation** - Automatic based on Financial Settings ✅ **Payment methods** - Cash, card, online, bank transfer ✅ **Receipt tracking** - Regular and fiscal receipts ✅ **Multi-tenant** - Company and workspace scoped The system is now ready for full implementation with UI components!