Commit f00b5c97 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add 3% platform fee to wizard invoice + printable invoice on success

- Invoice now includes service_fee_amount via PlatformFeeService (3% env-configurable)
- Review step and payment step both show fee breakdown (subtotal + service fee = total)
- Payment records the full total (including fee), not just the program price
- Success screen shows a proper printable invoice with items table, totals breakdown,
  payment status badge, and a print button that hides nav/sidebar
- Stores invoiceId for the computed invoiceForPrint property
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent c8ba2296
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
namespace App\Livewire\Receptionist; namespace App\Livewire\Receptionist;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Services\InvoiceService; use App\Domain\Financial\Services\InvoiceService;
use App\Domain\Financial\Services\PaymentService; use App\Domain\Financial\Services\PaymentService;
use App\Domain\Identity\Models\Branch; use App\Domain\Identity\Models\Branch;
...@@ -13,6 +14,7 @@ ...@@ -13,6 +14,7 @@
use App\Domain\Participant\Services\ParticipantService; use App\Domain\Participant\Services\ParticipantService;
use App\Domain\Pricing\Models\BasePrice; use App\Domain\Pricing\Models\BasePrice;
use App\Domain\Shared\Exceptions\DomainException; use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Services\PlatformFeeService;
use App\Domain\Shared\Traits\UsesBranchScope; use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\Activity; use App\Domain\Training\Models\Activity;
use App\Domain\Training\Models\TrainingProgram; use App\Domain\Training\Models\TrainingProgram;
...@@ -70,6 +72,7 @@ class NewRegistrationWizard extends Component ...@@ -70,6 +72,7 @@ class NewRegistrationWizard extends Component
public ?string $enrollment_summary = null; public ?string $enrollment_summary = null;
public ?int $invoice_amount = null; public ?int $invoice_amount = null;
public ?string $invoice_number = null; public ?string $invoice_number = null;
public ?int $invoiceId = null;
public bool $payment_recorded = false; public bool $payment_recorded = false;
// Pre-flight errors — blocks wizard from starting // Pre-flight errors — blocks wizard from starting
...@@ -351,8 +354,11 @@ public function confirm(): void ...@@ -351,8 +354,11 @@ public function confirm(): void
$actor $actor
); );
// 7. Look up the price for this program // 7. Look up the price for this program + calculate platform fee
$feeAmount = $this->resolveProgramFee($program); $feeAmount = $this->resolveProgramFee($program);
$platformFeeService = app(PlatformFeeService::class);
$serviceFee = $platformFeeService->calculate($feeAmount);
$totalWithFee = $feeAmount + $serviceFee;
// 8. Create invoice if there's a fee // 8. Create invoice if there's a fee
$invoice = null; $invoice = null;
...@@ -368,7 +374,8 @@ public function confirm(): void ...@@ -368,7 +374,8 @@ public function confirm(): void
'subtotal_amount' => $feeAmount, 'subtotal_amount' => $feeAmount,
'discount_amount' => 0, 'discount_amount' => 0,
'tax_amount' => 0, 'tax_amount' => 0,
'total_amount' => $feeAmount, 'service_fee_amount' => $serviceFee,
'total_amount' => $totalWithFee,
'currency' => 'EGP', 'currency' => 'EGP',
'issue_date' => now()->toDateString(), 'issue_date' => now()->toDateString(),
'due_date' => now()->addDays(7)->toDateString(), 'due_date' => now()->addDays(7)->toDateString(),
...@@ -389,8 +396,9 @@ public function confirm(): void ...@@ -389,8 +396,9 @@ public function confirm(): void
// Link enrollment to invoice // Link enrollment to invoice
$enrollment->update(['invoice_id' => $invoice->id]); $enrollment->update(['invoice_id' => $invoice->id]);
$this->invoice_amount = $feeAmount; $this->invoice_amount = $invoice->total_amount;
$this->invoice_number = $invoice->number; $this->invoice_number = $invoice->number;
$this->invoiceId = $invoice->id;
// 9. Record payment if paying now // 9. Record payment if paying now
if ($this->pay_now) { if ($this->pay_now) {
...@@ -403,7 +411,7 @@ public function confirm(): void ...@@ -403,7 +411,7 @@ public function confirm(): void
'method' => $this->payment_method, 'method' => $this->payment_method,
'payer_type' => Participant::class, 'payer_type' => Participant::class,
'payer_id' => $participant->id, 'payer_id' => $participant->id,
'amount' => $feeAmount, 'amount' => $invoice->total_amount,
'currency' => 'EGP', 'currency' => 'EGP',
'payment_date' => now()->toDateString(), 'payment_date' => now()->toDateString(),
'notes' => 'دفع اشتراك: ' . $program->name_ar, 'notes' => 'دفع اشتراك: ' . $program->name_ar,
...@@ -479,6 +487,24 @@ public function getSelectedProgramFeeProperty(): int ...@@ -479,6 +487,24 @@ public function getSelectedProgramFeeProperty(): int
return $program ? $this->resolveProgramFee($program) : 0; return $program ? $this->resolveProgramFee($program) : 0;
} }
public function getPlatformFeeProperty(): int
{
return app(PlatformFeeService::class)->calculate($this->selectedProgramFee);
}
public function getTotalWithFeeProperty(): int
{
return $this->selectedProgramFee + $this->platformFee;
}
public function getInvoiceForPrintProperty(): ?Invoice
{
if (!$this->invoiceId) {
return null;
}
return Invoice::with('items')->find($this->invoiceId);
}
public function render() public function render()
{ {
$activities = Activity::where('is_active', true)->orderBy('name_ar')->get(); $activities = Activity::where('is_active', true)->orderBy('name_ar')->get();
......
<div> <div>
<style>
@media print {
nav, aside, .sidebar, header, [data-topbar], .print\:hidden { display: none !important; }
main { margin: 0 !important; padding: 0 !important; width: 100% !important; }
body { background: white !important; }
#printable-invoice { border: none !important; box-shadow: none !important; border-radius: 0 !important; padding: 0 !important; max-width: 100% !important; }
}
</style>
{{-- Header --}} {{-- Header --}}
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6 print:hidden">
<div> <div>
<h1 class="text-2xl font-bold text-gray-800">{{ __('تسجيل جديد') }}</h1> <h1 class="text-2xl font-bold text-gray-800">{{ __('تسجيل جديد') }}</h1>
<p class="text-sm text-gray-500 mt-1">{{ __('تسجيل مشترك جديد مع ولي الأمر والبرنامج') }}</p> <p class="text-sm text-gray-500 mt-1">{{ __('تسجيل مشترك جديد مع ولي الأمر والبرنامج') }}</p>
...@@ -518,12 +527,27 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-blue-600 text-white ...@@ -518,12 +527,27 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-blue-600 text-white
<p class="text-gray-500 mt-1">{{ __('النشاط') }}: {{ $this->selectedProgram->activity->name_ar }}</p> <p class="text-gray-500 mt-1">{{ __('النشاط') }}: {{ $this->selectedProgram->activity->name_ar }}</p>
@endif @endif
@if($this->selectedProgramFee > 0) @if($this->selectedProgramFee > 0)
<p class="mt-2"> <div class="mt-3 space-y-1 text-sm">
<span class="text-gray-500">{{ __('الرسوم') }}:</span> <div class="flex items-center justify-between">
<span class="font-bold text-green-700 ms-1" dir="ltr"> <span class="text-gray-500">{{ __('رسوم البرنامج') }}:</span>
{{ number_format($this->selectedProgramFee / 100, 2) }} {{ __('ج.م') }} <span class="font-medium text-gray-800" dir="ltr">{{ number_format($this->selectedProgramFee / 100, 2) }} {{ __('ج.م') }}</span>
</span> </div>
</p> @if($this->platformFee > 0)
<div class="flex items-center justify-between">
<span class="text-gray-500">{{ __('رسوم الخدمة (3%)') }}:</span>
<span class="font-medium text-gray-800" dir="ltr">{{ number_format($this->platformFee / 100, 2) }} {{ __('ج.م') }}</span>
</div>
<div class="flex items-center justify-between pt-2 border-t border-gray-200">
<span class="font-semibold text-gray-700">{{ __('الإجمالي') }}:</span>
<span class="font-bold text-green-700" dir="ltr">{{ number_format($this->totalWithFee / 100, 2) }} {{ __('ج.م') }}</span>
</div>
@else
<div class="flex items-center justify-between">
<span class="font-semibold text-gray-700">{{ __('الإجمالي') }}:</span>
<span class="font-bold text-green-700" dir="ltr">{{ number_format($this->selectedProgramFee / 100, 2) }} {{ __('ج.م') }}</span>
</div>
@endif
</div>
@else @else
<p class="mt-2 text-gray-500">{{ __('بدون رسوم') }}</p> <p class="mt-2 text-gray-500">{{ __('بدون رسوم') }}</p>
@endif @endif
...@@ -558,11 +582,21 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-blue-600 text-white ...@@ -558,11 +582,21 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-blue-600 text-white
<h2 class="text-lg font-semibold text-gray-800 mb-6">{{ __('الدفع') }}</h2> <h2 class="text-lg font-semibold text-gray-800 mb-6">{{ __('الدفع') }}</h2>
@if($this->selectedProgramFee > 0) @if($this->selectedProgramFee > 0)
<div class="p-4 bg-blue-50 border border-blue-200 rounded-xl mb-6"> <div class="p-4 bg-blue-50 border border-blue-200 rounded-xl mb-6 space-y-2">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between text-sm">
<span class="text-blue-700 font-medium">{{ __('المبلغ المطلوب') }}</span> <span class="text-blue-600">{{ __('رسوم البرنامج') }}</span>
<span class="font-medium text-blue-700" dir="ltr">{{ number_format($this->selectedProgramFee / 100, 2) }} {{ __('ج.م') }}</span>
</div>
@if($this->platformFee > 0)
<div class="flex items-center justify-between text-sm">
<span class="text-blue-600">{{ __('رسوم الخدمة (3%)') }}</span>
<span class="font-medium text-blue-700" dir="ltr">{{ number_format($this->platformFee / 100, 2) }} {{ __('ج.م') }}</span>
</div>
@endif
<div class="flex items-center justify-between pt-2 border-t border-blue-200">
<span class="text-blue-700 font-semibold">{{ __('الإجمالي المطلوب') }}</span>
<span class="text-xl font-bold text-blue-800" dir="ltr"> <span class="text-xl font-bold text-blue-800" dir="ltr">
{{ number_format($this->selectedProgramFee / 100, 2) }} {{ __('ج.م') }} {{ number_format($this->totalWithFee / 100, 2) }} {{ __('ج.م') }}
</span> </span>
</div> </div>
</div> </div>
...@@ -653,65 +687,171 @@ class="inline-flex items-center gap-2 px-8 py-3 min-h-16 bg-green-600 text-white ...@@ -653,65 +687,171 @@ class="inline-flex items-center gap-2 px-8 py-3 min-h-16 bg-green-600 text-white
</div> </div>
@endif @endif
{{-- Step 6: Success --}} {{-- Step 6: Success + Printable Invoice --}}
@if($currentStep === 6) @if($currentStep === 6)
<div class="text-center py-8"> <div>
<div class="w-20 h-20 mx-auto bg-green-100 rounded-full flex items-center justify-center mb-6"> {{-- Success Banner --}}
<svg class="w-10 h-10 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <div class="text-center py-6 mb-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/> <div class="w-16 h-16 mx-auto bg-green-100 rounded-full flex items-center justify-center mb-4">
</svg> <svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</div> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<h2 class="text-2xl font-bold text-green-700 mb-2">{{ __('تم التسجيل بنجاح!') }}</h2>
<p class="text-gray-600 mb-4">{{ __('تم تسجيل المشترك وإنشاء الاشتراك بنجاح') }}</p>
{{-- Summary cards --}}
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 max-w-lg mx-auto mb-6 text-start">
@if($participant_number)
<div class="p-4 bg-gray-50 rounded-xl border border-gray-200">
<p class="text-xs text-gray-500 mb-1">{{ __('رقم المشترك') }}</p>
<p class="text-lg font-bold text-gray-800" dir="ltr">{{ $participant_number }}</p>
</div> </div>
<h2 class="text-xl font-bold text-green-700 mb-1">{{ __('تم التسجيل بنجاح!') }}</h2>
<p class="text-gray-500 text-sm">{{ __('رقم المشترك') }}: <strong dir="ltr">{{ $participant_number }}</strong></p>
@if($enrollment_summary)
<p class="text-gray-500 text-sm mt-1">{{ $enrollment_summary }}</p>
@endif @endif
</div>
@if($enrollment_summary) {{-- Printable Invoice --}}
<div class="p-4 bg-blue-50 rounded-xl border border-blue-200"> @if($this->invoiceForPrint)
<p class="text-xs text-blue-600 mb-1">{{ __('البرنامج') }}</p> @php $inv = $this->invoiceForPrint; @endphp
<p class="text-sm font-bold text-blue-800">{{ $enrollment_summary }}</p> <div id="printable-invoice" class="bg-white border border-gray-300 rounded-xl p-6 max-w-2xl mx-auto print:border-0 print:shadow-none print:rounded-none print:p-0 print:max-w-full">
{{-- Invoice Header --}}
<div class="flex items-start justify-between border-b border-gray-200 pb-4 mb-4">
<div>
<h3 class="text-lg font-bold text-gray-800">{{ __('فاتورة') }}</h3>
<p class="text-sm text-gray-500 mt-1" dir="ltr">#{{ $inv->number }}</p>
</div>
<div class="text-end text-sm">
<p class="font-semibold text-gray-700">{{ app('current_academy')->name_ar ?? '' }}</p>
<p class="text-gray-500 mt-1">{{ __('التاريخ') }}: {{ $inv->issue_date?->format('Y/m/d') ?? now()->format('Y/m/d') }}</p>
<p class="text-gray-500">{{ __('الاستحقاق') }}: {{ $inv->due_date?->format('Y/m/d') ?? now()->addDays(7)->format('Y/m/d') }}</p>
</div>
</div> </div>
@endif
@if($invoice_number) {{-- Customer Info --}}
<div class="p-4 bg-amber-50 rounded-xl border border-amber-200"> <div class="grid grid-cols-2 gap-4 text-sm mb-6">
<p class="text-xs text-amber-600 mb-1">{{ __('الفاتورة') }}</p> <div>
<p class="text-sm font-bold text-amber-800" dir="ltr">{{ $invoice_number }}</p> <p class="text-gray-500 text-xs mb-1">{{ __('المشترك') }}</p>
<p class="text-sm text-amber-700 mt-1" dir="ltr">{{ number_format($invoice_amount / 100, 2) }} {{ __('ج.م') }}</p> <p class="font-semibold text-gray-800">{{ $participant_name_ar }}</p>
@if($participant_number)
<p class="text-gray-500" dir="ltr">{{ $participant_number }}</p>
@endif
</div>
<div class="text-end">
<p class="text-gray-500 text-xs mb-1">{{ __('ولي الأمر') }}</p>
<p class="font-semibold text-gray-800">{{ $inv->contact_name }}</p>
<p class="text-gray-500" dir="ltr">{{ $inv->contact_phone }}</p>
</div>
</div> </div>
@endif
@if($payment_recorded) {{-- Items Table --}}
<div class="p-4 bg-green-50 rounded-xl border border-green-200"> <table class="w-full text-sm mb-4">
<p class="text-xs text-green-600 mb-1">{{ __('الدفع') }}</p> <thead>
<p class="text-sm font-bold text-green-800">{{ __('تم الدفع') }} ✓</p> <tr class="border-b border-gray-200">
<th class="text-start py-2 font-medium text-gray-600">{{ __('البند') }}</th>
<th class="text-center py-2 font-medium text-gray-600 w-16">{{ __('الكمية') }}</th>
<th class="text-end py-2 font-medium text-gray-600 w-28">{{ __('السعر') }}</th>
<th class="text-end py-2 font-medium text-gray-600 w-28">{{ __('المجموع') }}</th>
</tr>
</thead>
<tbody>
@foreach($inv->items as $item)
<tr class="border-b border-gray-100">
<td class="py-3">{{ $item->description }}</td>
<td class="py-3 text-center">{{ $item->quantity }}</td>
<td class="py-3 text-end" dir="ltr">{{ number_format($item->unit_price / 100, 2) }}</td>
<td class="py-3 text-end" dir="ltr">{{ number_format($item->total_amount / 100, 2) }}</td>
</tr>
@endforeach
</tbody>
</table>
{{-- Totals --}}
<div class="border-t border-gray-200 pt-3 space-y-2 text-sm max-w-xs ms-auto">
<div class="flex justify-between">
<span class="text-gray-500">{{ __('المجموع الفرعي') }}</span>
<span class="font-medium" dir="ltr">{{ number_format($inv->subtotal_amount / 100, 2) }} {{ __('ج.م') }}</span>
</div>
@if($inv->discount_amount > 0)
<div class="flex justify-between text-green-600">
<span>{{ __('الخصم') }}</span>
<span dir="ltr">-{{ number_format($inv->discount_amount / 100, 2) }}</span>
</div>
@endif
@if($inv->service_fee_amount > 0)
<div class="flex justify-between">
<span class="text-gray-500">{{ __('رسوم الخدمة (3%)') }}</span>
<span class="font-medium" dir="ltr">{{ number_format($inv->service_fee_amount / 100, 2) }} {{ __('ج.م') }}</span>
</div>
@endif
@if($inv->tax_amount > 0)
<div class="flex justify-between">
<span class="text-gray-500">{{ __('الضريبة') }}</span>
<span class="font-medium" dir="ltr">{{ number_format($inv->tax_amount / 100, 2) }}</span>
</div>
@endif
<div class="flex justify-between pt-2 border-t border-gray-300 text-base">
<span class="font-bold text-gray-800">{{ __('الإجمالي') }}</span>
<span class="font-bold text-gray-800" dir="ltr">{{ number_format($inv->total_amount / 100, 2) }} {{ __('ج.م') }}</span>
</div>
@if($payment_recorded)
<div class="flex justify-between text-green-700">
<span class="font-semibold">{{ __('المدفوع') }}</span>
<span class="font-semibold" dir="ltr">{{ number_format($inv->total_amount / 100, 2) }} {{ __('ج.م') }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">{{ __('المتبقي') }}</span>
<span class="font-bold text-green-700" dir="ltr">0.00 {{ __('ج.م') }}</span>
</div>
@else
<div class="flex justify-between text-red-700">
<span class="font-semibold">{{ __('المتبقي') }}</span>
<span class="font-bold" dir="ltr">{{ number_format($inv->due_amount / 100, 2) }} {{ __('ج.م') }}</span>
</div>
@endif
</div> </div>
@elseif($invoice_number)
<div class="p-4 bg-red-50 rounded-xl border border-red-200"> {{-- Payment status badge --}}
<p class="text-xs text-red-600 mb-1">{{ __('الدفع') }}</p> <div class="mt-4 text-center">
<p class="text-sm font-bold text-red-800">{{ __('مستحق — لم يتم الدفع') }}</p> @if($payment_recorded)
<span class="inline-flex items-center gap-1 px-4 py-2 bg-green-100 text-green-800 rounded-full text-sm font-bold">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
{{ __('تم الدفع') }}
</span>
@else
<span class="inline-flex items-center gap-1 px-4 py-2 bg-amber-100 text-amber-800 rounded-full text-sm font-bold">
{{ __('مستحق الدفع') }}
</span>
@endif
</div> </div>
@endif
</div> </div>
<div class="flex items-center justify-center gap-4 mt-6"> {{-- Actions --}}
<div class="flex items-center justify-center gap-4 mt-6 print:hidden">
<button onclick="window.print()"
class="inline-flex items-center gap-2 px-6 py-3 bg-gray-800 text-white rounded-lg hover:bg-gray-900 text-base font-medium transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
{{ __('طباعة الفاتورة') }}
</button>
<a href="{{ route('receptionist.new-registration') }}" wire:navigate
class="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-base font-medium transition-colors">
{{ __('تسجيل آخر') }}
</a>
<a href="{{ route('receptionist.dashboard') }}" wire:navigate <a href="{{ route('receptionist.dashboard') }}" wire:navigate
class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 text-base font-medium transition-colors"> class="inline-flex items-center gap-2 px-6 py-3 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 text-base font-medium transition-colors">
{{ __('العودة للاستقبال') }} {{ __('العودة للاستقبال') }}
</a> </a>
</div>
@else
{{-- No invoice (free program) --}}
<div class="flex items-center justify-center gap-4 mt-6">
<a href="{{ route('receptionist.new-registration') }}" wire:navigate <a href="{{ route('receptionist.new-registration') }}" wire:navigate
class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-base font-medium transition-colors"> class="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-base font-medium transition-colors">
{{ __('تسجيل آخر') }} {{ __('تسجيل آخر') }}
</a> </a>
<a href="{{ route('receptionist.dashboard') }}" wire:navigate
class="inline-flex items-center gap-2 px-6 py-3 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 text-base font-medium transition-colors">
{{ __('العودة للاستقبال') }}
</a>
</div> </div>
@endif
</div> </div>
@endif @endif
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment