Commit b01d4577 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix registration wizard: step-skip bug + actual invoice/payment creation

Bugs fixed:
- Step 2 was skipped: checkDuplicates() advanced step AND nextStep() advanced
  again after it returned. Now nextStep() returns early after checkDuplicates()
- No invoice was ever created: confirm() now creates a real invoice via
  InvoiceService with the price resolved from base_prices table
- No payment was ever recorded: now uses PaymentService.recordPayment() which
  creates double-entry transaction and updates invoice paid_amount
- fee_amount doesn't exist on training_programs — price comes from base_prices
  (polymorphic). Added resolveProgramFee() + selectedProgramFee computed property
- Success screen now shows: participant number, program, invoice number/amount,
  payment status (paid vs outstanding)
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent e8798d13
......@@ -2,12 +2,15 @@
namespace App\Livewire\Receptionist;
use App\Domain\Financial\Services\InvoiceService;
use App\Domain\Financial\Services\PaymentService;
use App\Domain\Identity\Models\Guardian;
use App\Domain\Identity\Models\Person;
use App\Domain\Identity\Services\PersonService;
use App\Domain\Participant\Models\Participant;
use App\Domain\Participant\Services\DuplicateDetectionService;
use App\Domain\Participant\Services\ParticipantService;
use App\Domain\Pricing\Models\BasePrice;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\Activity;
......@@ -52,7 +55,6 @@ class NewRegistrationWizard extends Component
// Step 5: Payment
public bool $pay_now = false;
public string $payment_method = 'cash';
public int $payment_amount = 0;
// Duplicate detection
public array $potentialDuplicates = [];
......@@ -63,6 +65,9 @@ class NewRegistrationWizard extends Component
public bool $completed = false;
public ?string $participant_number = null;
public ?string $enrollment_summary = null;
public ?int $invoice_amount = null;
public ?string $invoice_number = null;
public bool $payment_recorded = false;
public function mount(): void
{
......@@ -78,11 +83,11 @@ public function nextStep(): void
$this->validate($rules, $this->messages());
}
// Duplicate check on step 1 — if duplicates found, stay on step 1
if ($this->currentStep === 1 && !$this->duplicateCheckDone) {
$this->checkDuplicates();
if (!empty($this->potentialDuplicates)) {
return;
}
// checkDuplicates will advance step if no duplicates
return;
}
$this->currentStep = min($this->currentStep + 1, $this->totalSteps);
......@@ -220,6 +225,8 @@ public function confirm(): void
$personService = app(PersonService::class);
$participantService = app(ParticipantService::class);
$enrollmentService = app(EnrollmentService::class);
$invoiceService = app(InvoiceService::class);
$paymentService = app(PaymentService::class);
// 1. Create or reuse guardian's Person record
if ($this->useExistingPersonId) {
......@@ -283,10 +290,72 @@ public function confirm(): void
$enrollment = $enrollmentService->enrollInProgram(
$participant,
$program,
$actor,
['payment_status' => $this->pay_now ? 'paid' : 'pending']
$actor
);
// 7. Look up the price for this program
$feeAmount = $this->resolveProgramFee($program);
// 8. Create invoice if there's a fee
$invoice = null;
if ($feeAmount > 0) {
$invoice = $invoiceService->create([
'academy_id' => app('current_academy')->id,
'number' => $invoiceService->generateNumber(app('current_academy')->id),
'type' => 'enrollment',
'billable_type' => Participant::class,
'billable_id' => $participant->id,
'contact_name' => $this->guardian_name_ar,
'contact_phone' => $this->guardian_phone,
'subtotal_amount' => $feeAmount,
'discount_amount' => 0,
'tax_amount' => 0,
'total_amount' => $feeAmount,
'currency' => 'EGP',
'issue_date' => now()->toDateString(),
'due_date' => now()->addDays(7)->toDateString(),
'notes' => 'اشتراك: ' . $program->name_ar,
], [
[
'description' => $program->name_ar,
'quantity' => 1,
'unit_price' => $feeAmount,
'discount_amount' => 0,
'tax_amount' => 0,
],
], $actor);
// Send the invoice (mark as sent)
$invoice->update(['status' => 'sent']);
// Link enrollment to invoice
$enrollment->update(['invoice_id' => $invoice->id]);
$this->invoice_amount = $feeAmount;
$this->invoice_number = $invoice->number;
// 9. Record payment if paying now
if ($this->pay_now) {
$paymentService->recordPayment([
'academy_id' => app('current_academy')->id,
'branch_id' => $this->branchId,
'invoice_id' => $invoice->id,
'reference' => 'PAY-' . now()->format('YmdHis') . '-' . $participant->id,
'direction' => 'inbound',
'method' => $this->payment_method,
'payer_type' => Participant::class,
'payer_id' => $participant->id,
'amount' => $feeAmount,
'currency' => 'EGP',
'payment_date' => now()->toDateString(),
'notes' => 'دفع اشتراك: ' . $program->name_ar,
], $actor);
$this->payment_recorded = true;
$enrollment->update(['payment_status' => 'paid']);
}
}
$this->completed = true;
$this->participant_number = $participant->participant_number;
$this->enrollment_summary = $program->name_ar . ' — ' . ($enrollment->group?->name_ar ?? '');
......@@ -301,6 +370,20 @@ public function confirm(): void
}
}
private function resolveProgramFee(TrainingProgram $program): int
{
$basePrice = BasePrice::where('priceable_type', TrainingProgram::class)
->where('priceable_id', $program->id)
->where('is_active', true)
->where('effective_from', '<=', now())
->where(fn ($q) => $q->whereNull('effective_to')->orWhere('effective_to', '>=', now()))
->forBranch($this->branchId)
->orderByDesc('priority')
->first();
return $basePrice?->amount ?? 0;
}
public function getSelectedProgramProperty(): ?TrainingProgram
{
if (!$this->selected_program_id) {
......@@ -310,6 +393,20 @@ public function getSelectedProgramProperty(): ?TrainingProgram
return TrainingProgram::with('activity')->find($this->selected_program_id);
}
public function getSelectedProgramFeeProperty(): int
{
if (!$this->selected_program_id) {
return 0;
}
$program = TrainingProgram::find($this->selected_program_id);
if (!$program) {
return 0;
}
return $this->resolveProgramFee($program);
}
public function render()
{
$activities = Activity::where('is_active', true)->orderBy('name_ar')->get();
......
......@@ -329,13 +329,6 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-blue-600 text-white
@if($program->description)
<p class="text-sm text-gray-600 mt-2">{{ Str::limit($program->description, 80) }}</p>
@endif
@if($program->fee_amount)
<div class="mt-3 flex items-center gap-2">
<span class="px-2 py-1 bg-green-100 text-green-700 rounded text-sm font-bold" dir="ltr">
{{ number_format($program->fee_amount / 100, 2) }} {{ __('ج.م') }}
</span>
</div>
@endif
</div>
</label>
@endforeach
......@@ -451,13 +444,15 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-blue-600 text-white
@if($this->selectedProgram->activity)
<p class="text-gray-500 mt-1">{{ __('النشاط') }}: {{ $this->selectedProgram->activity->name_ar }}</p>
@endif
@if($this->selectedProgram->fee_amount)
@if($this->selectedProgramFee > 0)
<p class="mt-2">
<span class="text-gray-500">{{ __('الرسوم') }}:</span>
<span class="font-bold text-green-700 ms-1" dir="ltr">
{{ number_format($this->selectedProgram->fee_amount / 100, 2) }} {{ __('ج.م') }}
{{ number_format($this->selectedProgramFee / 100, 2) }} {{ __('ج.م') }}
</span>
</p>
@else
<p class="mt-2 text-gray-500">{{ __('بدون رسوم') }}</p>
@endif
</div>
@endif
......@@ -489,15 +484,19 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-blue-600 text-white
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-6">{{ __('الدفع') }}</h2>
@if($this->selectedProgram && $this->selectedProgram->fee_amount)
@if($this->selectedProgramFee > 0)
<div class="p-4 bg-blue-50 border border-blue-200 rounded-xl mb-6">
<div class="flex items-center justify-between">
<span class="text-blue-700 font-medium">{{ __('المبلغ المطلوب') }}</span>
<span class="text-xl font-bold text-blue-800" dir="ltr">
{{ number_format($this->selectedProgram->fee_amount / 100, 2) }} {{ __('ج.م') }}
{{ number_format($this->selectedProgramFee / 100, 2) }} {{ __('ج.م') }}
</span>
</div>
</div>
@else
<div class="p-4 bg-gray-50 border border-gray-200 rounded-xl mb-6">
<p class="text-gray-600 text-sm">{{ __('لا يوجد سعر محدد لهذا البرنامج حالياً — سيتم التسجيل بدون فاتورة.') }}</p>
</div>
@endif
<div class="space-y-6">
......@@ -584,21 +583,51 @@ class="inline-flex items-center gap-2 px-8 py-3 min-h-16 bg-green-600 text-white
{{-- Step 6: Success --}}
@if($currentStep === 6)
<div class="text-center py-8">
<div class="w-20 h-20 mx-auto bg-green-100 rounded-full flex items-center justify-center mb-6 animate-bounce">
<div class="w-20 h-20 mx-auto bg-green-100 rounded-full flex items-center justify-center mb-6">
<svg class="w-10 h-10 text-green-600" 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>
</div>
<h2 class="text-2xl font-bold text-green-700 mb-2">{{ __('تم التسجيل بنجاح!') }}</h2>
<p class="text-gray-600 mb-6">{{ __('تم تسجيل المشترك وإنشاء الاشتراك بنجاح') }}</p>
<p class="text-gray-600 mb-4">{{ __('تم تسجيل المشترك وإنشاء الاشتراك بنجاح') }}</p>
@if($participant_number)
<div class="inline-block px-6 py-3 bg-gray-100 rounded-xl mb-6">
<p class="text-sm text-gray-500">{{ __('رقم المشترك') }}</p>
<p class="text-xl font-bold text-gray-800" dir="ltr">{{ $participant_number }}</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>
@endif
@if($enrollment_summary)
<div class="p-4 bg-blue-50 rounded-xl border border-blue-200">
<p class="text-xs text-blue-600 mb-1">{{ __('البرنامج') }}</p>
<p class="text-sm font-bold text-blue-800">{{ $enrollment_summary }}</p>
</div>
@endif
@if($invoice_number)
<div class="p-4 bg-amber-50 rounded-xl border border-amber-200">
<p class="text-xs text-amber-600 mb-1">{{ __('الفاتورة') }}</p>
<p class="text-sm font-bold text-amber-800" dir="ltr">{{ $invoice_number }}</p>
<p class="text-sm text-amber-700 mt-1" dir="ltr">{{ number_format($invoice_amount / 100, 2) }} {{ __('ج.م') }}</p>
</div>
@endif
@if($payment_recorded)
<div class="p-4 bg-green-50 rounded-xl border border-green-200">
<p class="text-xs text-green-600 mb-1">{{ __('الدفع') }}</p>
<p class="text-sm font-bold text-green-800">{{ __('تم الدفع') }} ✓</p>
</div>
@elseif($invoice_number)
<div class="p-4 bg-red-50 rounded-xl border border-red-200">
<p class="text-xs text-red-600 mb-1">{{ __('الدفع') }}</p>
<p class="text-sm font-bold text-red-800">{{ __('مستحق — لم يتم الدفع') }}</p>
</div>
@endif
</div>
@endif
<div class="flex items-center justify-center gap-4 mt-6">
<a href="{{ route('receptionist.dashboard') }}" wire:navigate
......
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