Commit ea896bb3 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add admin discount override for renewal invoices in collect payment wizard

Super admins can now override the invoice amount directly from step 2
of the collect payment flow. The override updates the invoice total,
stores a full audit trail in invoice metadata (who, when, original
amount, new amount, reason, participant), and logs to the audit channel.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent cc0a1385
......@@ -43,6 +43,11 @@ class CollectPaymentWizard extends Component
public string $payment_method = 'cash';
public string $payment_notes = '';
// Admin price override
public bool $priceOverrideEnabled = false;
public string $priceOverrideInput = '';
public string $priceOverrideReason = '';
// Result
public bool $completed = false;
public ?string $receipt_number = null;
......@@ -219,6 +224,85 @@ private function advanceEnrollmentBillingDate(Enrollment $enrollment): void
$enrollment->update(['next_billing_date' => $next->toDateString()]);
}
public function updatedPriceOverrideEnabled(): void
{
if (!$this->priceOverrideEnabled) {
$this->priceOverrideInput = '';
$this->priceOverrideReason = '';
}
}
public function applyAdminOverride(): void
{
$actor = auth()->user();
if (!$actor->is_super_admin) {
return;
}
$invoice = Invoice::find($this->selected_invoice_id);
if (!$invoice || $this->priceOverrideInput === '') {
return;
}
$newTotalPiasters = (int) round((float) $this->priceOverrideInput * 100);
if ($newTotalPiasters < 0) {
$this->addError('priceOverrideInput', __('المبلغ لا يمكن أن يكون سالبًا'));
return;
}
$originalTotal = $invoice->total_amount;
$originalDue = $invoice->due_amount;
$originalDiscount = $invoice->discount_amount;
// The override sets a new total — the difference becomes admin discount
$adminDiscount = $originalTotal + $originalDiscount - $newTotalPiasters;
$newDueAmount = max(0, $newTotalPiasters - $invoice->paid_amount);
// Store audit trail in metadata
$metadata = $invoice->metadata ?? [];
$metadata['admin_override'] = [
'applied_at' => now()->toIso8601String(),
'applied_by_id' => $actor->id,
'applied_by_name' => $actor->name,
'original_total_piasters' => $originalTotal,
'original_due_piasters' => $originalDue,
'new_total_piasters' => $newTotalPiasters,
'discount_piasters' => $adminDiscount,
'reason' => $this->priceOverrideReason ?: null,
'participant_id' => $this->selected_participant_id,
'participant_name' => $this->selected_participant_name,
];
$invoice->update([
'discount_amount' => $adminDiscount,
'total_amount' => $newTotalPiasters,
'due_amount' => $newDueAmount,
'metadata' => $metadata,
]);
// Audit log
Log::channel('audit')->info('admin_invoice_override', [
'actor_id' => $actor->id,
'actor_name' => $actor->name,
'invoice_id' => $invoice->id,
'invoice_number' => $invoice->number,
'participant_name' => $this->selected_participant_name,
'original_total' => $originalTotal,
'new_total' => $newTotalPiasters,
'reason' => $this->priceOverrideReason,
]);
// Refresh payment amount display
$this->payment_amount_display = number_format($newDueAmount / 100, 2, '.', '');
// Reset override UI
$this->priceOverrideEnabled = false;
$this->priceOverrideInput = '';
$this->priceOverrideReason = '';
session()->flash('success', __('تم تعديل مبلغ الفاتورة بنجاح'));
}
public function updatedSelectedInvoiceId(): void
{
if ($this->selected_invoice_id) {
......@@ -227,6 +311,10 @@ public function updatedSelectedInvoiceId(): void
$this->payment_amount_display = number_format($invoice->due_amount / 100, 2, '.', '');
}
}
// Reset override when changing invoice
$this->priceOverrideEnabled = false;
$this->priceOverrideInput = '';
$this->priceOverrideReason = '';
}
public function selectInvoice(int $id): void
......@@ -405,6 +493,7 @@ public function render()
'invoices' => $invoices,
'upcomingPayments' => $upcomingPayments,
'selectedInvoice' => $selectedInvoice,
'isSuperAdmin' => auth()->user()?->is_super_admin ?? false,
]);
}
}
......@@ -20,6 +20,11 @@ class="inline-flex items-center gap-2 px-4 py-2 text-gray-600 bg-gray-100 rounde
{{ session('error') }}
</div>
@endif
@if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-xl text-green-700 text-sm">
{{ session('success') }}
</div>
@endif
{{-- Step Indicator --}}
@if($currentStep < 5)
......@@ -220,6 +225,66 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-amber-600 text-white
@error('selected_invoice_id') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
{{-- Admin Price Override --}}
@if($isSuperAdmin && $selected_invoice_id)
<div class="mt-4 border border-dashed border-red-300 rounded-xl overflow-hidden" x-data="{ open: @entangle('priceOverrideEnabled') }">
<button type="button" @click="open = !open; $wire.set('priceOverrideEnabled', open)"
class="w-full flex items-center justify-between p-4 text-start hover:bg-red-50 transition-colors">
<div class="flex items-center gap-2">
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
</svg>
<span class="text-sm font-semibold text-red-700">{{ __('تعديل مبلغ الفاتورة (أدمن)') }}</span>
</div>
<svg class="w-4 h-4 text-red-400 transition-transform" :class="open ? 'rotate-180' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div x-show="open" x-cloak x-transition class="p-4 pt-0 space-y-4">
<p class="text-xs text-red-600">{{ __('تحذير: هذا التعديل سيغير مبلغ الفاتورة بشكل نهائي ويتم تسجيل من قام به') }}</p>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('المبلغ الجديد (ج.م)') }}</label>
<input type="number" wire:model="priceOverrideInput" dir="ltr" step="0.01" min="0"
class="w-full max-w-xs px-4 py-3 border border-red-300 rounded-lg focus:ring-2 focus:ring-red-400 focus:border-red-400 text-lg font-mono"
placeholder="0.00">
@error('priceOverrideInput') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
@if($selectedInvoice)
<p class="mt-1 text-xs text-gray-500">{{ __('المبلغ الأصلي') }}: <span dir="ltr">{{ number_format($selectedInvoice->total_amount / 100, 2) }}</span> {{ __('ج.م') }}</p>
@endif
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('سبب التعديل') }}</label>
<input type="text" wire:model="priceOverrideReason"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-400 text-sm"
placeholder="{{ __('مثال: خصم خاص، اتفاقية، تخفيض إداري') }}">
</div>
<button wire:click="applyAdminOverride" wire:loading.attr="disabled"
class="inline-flex items-center gap-2 px-5 py-2.5 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm font-medium transition-colors disabled:opacity-50">
<span wire:loading.remove wire:target="applyAdminOverride">{{ __('تطبيق التعديل') }}</span>
<span wire:loading wire:target="applyAdminOverride">{{ __('جارٍ...') }}</span>
</button>
</div>
</div>
@endif
{{-- Override Applied Badge --}}
@if($selectedInvoice && ($selectedInvoice->metadata['admin_override'] ?? false))
<div class="mt-3 p-3 bg-red-50 border border-red-200 rounded-lg flex items-center gap-3">
<svg class="w-5 h-5 text-red-600 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div class="text-xs text-red-700">
<span class="font-bold">{{ __('تم تعديل المبلغ') }}</span>
{{ __('بواسطة') }} {{ $selectedInvoice->metadata['admin_override']['applied_by_name'] }}
— {{ __('من') }} <span dir="ltr">{{ number_format($selectedInvoice->metadata['admin_override']['original_total_piasters'] / 100, 2) }}</span>
{{ __('إلى') }} <span dir="ltr">{{ number_format($selectedInvoice->metadata['admin_override']['new_total_piasters'] / 100, 2) }}</span> {{ __('ج.م') }}
@if($selectedInvoice->metadata['admin_override']['reason'])
— {{ $selectedInvoice->metadata['admin_override']['reason'] }}
@endif
</div>
</div>
@endif
{{-- Upcoming Installments & Renewals --}}
@if($upcomingPayments->isNotEmpty())
<div class="mt-6 p-4 bg-indigo-50 border border-indigo-200 rounded-xl">
......@@ -298,6 +363,15 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-amber-600 text-white
{{ number_format($selectedInvoice->due_amount / 100, 2) }} {{ __('ج.م') }}
</span>
</div>
@if($selectedInvoice->metadata['admin_override'] ?? false)
<div class="mt-2 pt-2 border-t border-gray-200 flex items-center gap-2 text-xs text-red-600">
<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="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>{{ __('تم تعديل المبلغ بواسطة') }} {{ $selectedInvoice->metadata['admin_override']['applied_by_name'] }}
({{ __('الأصلي') }}: <span dir="ltr">{{ number_format($selectedInvoice->metadata['admin_override']['original_total_piasters'] / 100, 2) }}</span> {{ __('ج.م') }})</span>
</div>
@endif
</div>
@endif
......@@ -450,6 +524,17 @@ class="inline-flex items-center gap-2 px-6 py-3 min-h-16 bg-amber-600 text-white
{{ \App\Domain\Financial\Enums\PaymentMethod::tryFrom($payment_method)?->label() ?? $payment_method }}
</span>
</div>
@if($selectedInvoice && ($selectedInvoice->metadata['admin_override'] ?? false))
<div class="flex items-center justify-between text-red-700">
<span class="text-gray-600">{{ __('خصم إداري') }}</span>
<div class="text-end">
<span class="font-medium text-sm" dir="ltr">
-{{ number_format(($selectedInvoice->metadata['admin_override']['original_total_piasters'] - $selectedInvoice->metadata['admin_override']['new_total_piasters']) / 100, 2) }} {{ __('ج.م') }}
</span>
<p class="text-xs text-red-500">{{ __('بواسطة') }} {{ $selectedInvoice->metadata['admin_override']['applied_by_name'] }}</p>
</div>
</div>
@endif
<div class="border-t border-gray-300 pt-4 flex items-center justify-between">
<span class="text-lg font-bold text-gray-800">{{ __('المبلغ') }}</span>
<span class="text-2xl font-bold text-green-700" dir="ltr">
......
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