Commit a64e13fa authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(installments): add early settlement — full principal-only payoff with interest waiver

- InstallmentCalculator: calculateEarlySettlement() sums all pending principal and interest separately; settlement_amount = principal only, all interest waived
- InstallmentController: earlySettlement() (confirmation page), processEarlySettlement() (executes: single PaymentService call, zeros interest on each settled row, marks plan completed + is_cash_settled=1, dispatches installment.early_settled), settlementReceipt() (print view)
- Routes: GET/POST /installments/{id}/early-settlement, GET /installments/{id}/settlement-receipt/{receiptId}
- show.php:  تسوية مبكرة button (requires installment.pay permission, only when active + pending > 0)
- early_settlement.php: breakdown table (original due / interest waived / principal to pay), pending items preview, mandatory confirmation checkbox, submit disabled until checked
- settlement_receipt.php: print-ready receipt showing original balance, interest waived, amount paid, settled items list, amount in words, stamp/signature area
- PaymentService: early_settlement payment type label
- Architecture Map: section 5.6, new route rows, new event row
Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent 69f09d80
......@@ -286,6 +286,186 @@ class InstallmentController extends Controller
);
}
/**
* GET /installments/{id}/early-settlement
* Show the early-settlement confirmation page with the breakdown.
*/
public function earlySettlement(Request $request, string $id): Response
{
$this->authorize('installment.pay');
$db = App::getInstance()->db();
$plan = $db->selectOne(
"SELECT ip.*, m.full_name_ar as member_name, m.membership_number, m.form_number
FROM installment_plans ip JOIN members m ON m.id = ip.member_id
WHERE ip.id = ?",
[(int) $id]
);
if (!$plan) {
return $this->redirect('/installments')->withError('الخطة غير موجودة');
}
if ($plan['status'] !== 'active') {
return $this->redirect("/installments/{$id}")->withError('لا يمكن التسوية المبكرة — الخطة ليست نشطة');
}
$calc = InstallmentCalculator::calculateEarlySettlement((int) $id);
if (!($calc['success'] ?? false)) {
return $this->redirect("/installments/{$id}")->withError($calc['error'] ?? 'خطأ في الحساب');
}
return $this->view('Installments.Views.early_settlement', [
'plan' => $plan,
'calc' => $calc,
]);
}
/**
* POST /installments/{id}/early-settlement
* Execute the early settlement: charge principal only, waive all remaining interest.
*/
public function processEarlySettlement(Request $request, string $id): Response
{
$this->authorize('installment.pay');
$db = App::getInstance()->db();
$plan = $db->selectOne(
"SELECT * FROM installment_plans WHERE id = ? AND status = 'active'",
[(int) $id]
);
if (!$plan) {
return $this->redirect('/installments')->withError('الخطة غير موجودة أو غير نشطة');
}
$calc = InstallmentCalculator::calculateEarlySettlement((int) $id);
if (!($calc['success'] ?? false)) {
return $this->redirect("/installments/{$id}")->withError($calc['error'] ?? 'خطأ في الحساب');
}
$paymentMethod = trim((string) $request->post('payment_method', 'cash'));
$settlementAmount = $calc['settlement_amount'];
// Single payment for the entire remaining principal
$result = PaymentService::processPayment([
'member_id' => (int) $plan['member_id'],
'amount' => $settlementAmount,
'payment_type' => 'early_settlement',
'payment_method' => $paymentMethod,
'related_entity_type' => 'installment_plan',
'related_entity_id' => (int) $id,
'description' => 'تسوية مبكرة لخطة التقسيط #' . $id
. ' — ' . $calc['items_count'] . ' قسط — أصل الدين فقط بدون فوائد',
]);
if (!$result['success']) {
return $this->redirect("/installments/{$id}/early-settlement")->withError($result['error']);
}
$ts = date('Y-m-d H:i:s');
$db->beginTransaction();
try {
foreach ($calc['pending_items'] as $item) {
$db->update('installment_schedule', [
'interest' => '0.00',
'amount' => $item['principal'],
'paid_amount' => $item['principal'],
'payment_id' => $result['payment_id'],
'status' => 'paid',
'paid_at' => $ts,
'updated_at' => $ts,
], '`id` = ?', [(int) $item['id']]);
}
$db->update('installment_plans', [
'status' => 'completed',
'is_cash_settled' => 1,
'updated_at' => $ts,
], '`id` = ?', [(int) $id]);
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
return $this->redirect("/installments/{$id}/early-settlement")
->withError('خطأ أثناء تسجيل التسوية: ' . $e->getMessage());
}
EventBus::dispatch('installment.early_settled', [
'plan_id' => (int) $id,
'member_id' => (int) $plan['member_id'],
'payment_id' => $result['payment_id'],
'receipt_id' => $result['receipt_id'],
'settlement_amount' => $settlementAmount,
'interest_waived' => $calc['total_interest_waived'],
'items_settled' => $calc['items_count'],
]);
return $this->redirect("/installments/{$id}/settlement-receipt/{$result['receipt_id']}")
->withSuccess('تمت التسوية المبكرة بنجاح — إيصال: ' . $result['receipt_number']);
}
/**
* GET /installments/{id}/settlement-receipt/{receiptId}
* Print-friendly early settlement receipt.
*/
public function settlementReceipt(Request $request, string $id, string $receiptId): Response
{
$this->authorize('installment.view');
$db = App::getInstance()->db();
$plan = $db->selectOne(
"SELECT ip.*, m.full_name_ar as member_name, m.form_number, m.membership_number
FROM installment_plans ip JOIN members m ON m.id = ip.member_id
WHERE ip.id = ?",
[(int) $id]
);
if (!$plan) {
return $this->redirect('/installments')->withError('الخطة غير موجودة');
}
$receipt = $db->selectOne(
"SELECT r.*, p.payment_method, p.amount as payment_amount,
e.full_name_ar as cashier_name
FROM receipts r
LEFT JOIN payments p ON p.id = r.payment_id
LEFT JOIN employees e ON e.id = r.issued_by_employee_id
WHERE r.id = ? AND r.member_id = ?",
[(int) $receiptId, (int) $plan['member_id']]
);
if (!$receipt) {
return $this->redirect("/installments/{$id}")->withError('الإيصال غير موجود');
}
// Reconstruct settlement breakdown from the paid items linked to this payment
$settledItems = $db->select(
"SELECT * FROM installment_schedule
WHERE installment_plan_id = ? AND payment_id = ?
ORDER BY installment_number ASC",
[(int) $id, (int) $receipt['payment_id']]
);
$totalPrincipal = array_reduce(
$settledItems,
fn ($carry, $item) => bcadd($carry, (string) $item['paid_amount'], 2),
'0.00'
);
// Reconstruct waived interest: we stored interest=0 but can derive from plan header
// We compute from the original plan totals vs what was already paid before settlement
$totalWaived = $db->selectOne(
"SELECT p2.amount as settlement_amt
FROM payments p2 WHERE p2.id = ?",
[(int) $receipt['payment_id']]
);
return $this->view('Installments.Views.settlement_receipt', [
'plan' => $plan,
'receipt' => $receipt,
'settledItems' => $settledItems,
'totalPrincipal' => $totalPrincipal,
'itemsCount' => count($settledItems),
]);
}
public function payInstallment(Request $request, string $planId, string $scheduleId): Response
{
$db = App::getInstance()->db();
......
......@@ -9,6 +9,11 @@ return [
['POST', '/installments/{planId}/pay/{scheduleId}', 'Installments\Controllers\InstallmentController@payInstallment', ['auth', 'csrf'], 'installment.pay'],
['POST', '/installments/{id}/recalculate', 'Installments\Controllers\InstallmentController@recalculate', ['auth', 'csrf'], 'installment.create_plan'],
// Early settlement
['GET', '/installments/{id}/early-settlement', 'Installments\Controllers\InstallmentController@earlySettlement', ['auth'], 'installment.pay'],
['POST', '/installments/{id}/early-settlement', 'Installments\Controllers\InstallmentController@processEarlySettlement', ['auth', 'csrf'], 'installment.pay'],
['GET', '/installments/{id}/settlement-receipt/{receiptId}','Installments\Controllers\InstallmentController@settlementReceipt', ['auth'], 'installment.view'],
// Cheque uploads
['GET', '/installments/{planId}/cheques', 'Installments\Controllers\ChequeController@index', ['auth'], 'installment.view'],
['POST', '/installments/{planId}/cheques', 'Installments\Controllers\ChequeController@store', ['auth', 'csrf'], 'installment.create_plan'],
......
......@@ -151,4 +151,57 @@ final class InstallmentCalculator
),
];
}
/**
* Calculate early settlement: member pays ALL remaining principal now, ALL future interest is waived.
*
* Business rule: interest waiver is only valid when every single pending/overdue installment
* is being settled in the same transaction. Partial settlement is not allowed here.
*
* Returns:
* - pending_items : the unpaid schedule rows
* - items_count : how many will be settled
* - total_original_due : sum of amount (principal+interest) across all pending rows
* - remaining_principal : sum of principal only — what the member actually pays
* - total_interest_waived: sum of interest across all pending rows — fully waived
* - settlement_amount : equals remaining_principal (what is charged)
*/
public static function calculateEarlySettlement(int $planId): array
{
$db = \App\Core\App::getInstance()->db();
$items = $db->select(
"SELECT * FROM installment_schedule
WHERE installment_plan_id = ? AND status IN ('pending','overdue')
ORDER BY installment_number ASC",
[$planId]
);
if (empty($items)) {
return [
'success' => false,
'error' => 'لا توجد أقساط معلقة لهذه الخطة',
];
}
$totalOriginalDue = '0.00';
$remainingPrincipal = '0.00';
$totalInterestWaived = '0.00';
foreach ($items as $item) {
$totalOriginalDue = bcadd($totalOriginalDue, (string) $item['amount'], 2);
$remainingPrincipal = bcadd($remainingPrincipal, (string) $item['principal'], 2);
$totalInterestWaived = bcadd($totalInterestWaived, (string) $item['interest'], 2);
}
return [
'success' => true,
'pending_items' => $items,
'items_count' => count($items),
'total_original_due' => $totalOriginalDue,
'remaining_principal' => $remainingPrincipal,
'total_interest_waived'=> $totalInterestWaived,
'settlement_amount' => $remainingPrincipal,
];
}
}
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تسوية مبكرة — خطة #<?= (int) $plan['id'] ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/installments/<?= (int) $plan['id'] ?>" class="btn btn-outline">← خطة التقسيط</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$remaining = (float) $calc['remaining_principal'];
$waived = (float) $calc['total_interest_waived'];
$originalDue = (float) $calc['total_original_due'];
$itemsCount = (int) $calc['items_count'];
$savingsPct = $originalDue > 0 ? round($waived / $originalDue * 100, 1) : 0;
?>
<!-- ═══ Warning Banner ════════════════════════════════════════════════════════ -->
<div style="background:#FEF3C7;border:2px solid #F59E0B;border-radius:10px;padding:18px 22px;margin-bottom:20px;display:flex;align-items:flex-start;gap:14px;">
<i data-lucide="alert-triangle" style="width:28px;height:28px;color:#D97706;flex-shrink:0;margin-top:2px;"></i>
<div>
<div style="font-size:15px;font-weight:700;color:#92400E;margin-bottom:4px;">تسوية مبكرة — إجراء لا يمكن التراجع عنه</div>
<div style="font-size:13px;color:#78350F;">
ستقوم بتسوية <strong><?= $itemsCount ?> قسط</strong> دفعةً واحدة. سيتم إسقاط جميع الفوائد المستقبلية
(<strong><?= money($waived) ?></strong>) وتحصيل أصل الدين فقط (<strong><?= money($remaining) ?></strong>).
لا يمكن التراجع عن هذا الإجراء بعد التأكيد.
</div>
</div>
</div>
<!-- ═══ Member & Plan Identity ═══════════════════════════════════════════════ -->
<div class="card" style="padding:16px 22px;margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:20px;flex-wrap:wrap;">
<div>
<div style="font-size:11px;color:#6B7280;">العضو</div>
<div style="font-size:16px;font-weight:700;"><?= e($plan['member_name']) ?></div>
<div style="font-size:12px;color:#6B7280;">استمارة: <?= e($plan['form_number'] ?? '—') ?></div>
</div>
<div style="width:1px;height:40px;background:#E5E7EB;"></div>
<div>
<div style="font-size:11px;color:#6B7280;">خطة التقسيط</div>
<div style="font-size:16px;font-weight:700;">#<?= (int) $plan['id'] ?></div>
<div style="font-size:12px;color:#6B7280;"><?= (int) $plan['number_of_months'] ?> شهر — <?= $plan['interest_rate'] ?>% سنوياً</div>
</div>
<div style="width:1px;height:40px;background:#E5E7EB;"></div>
<div>
<div style="font-size:11px;color:#6B7280;">الأقساط المراد تسويتها</div>
<div style="font-size:22px;font-weight:800;color:#7C3AED;"><?= $itemsCount ?> قسط</div>
</div>
</div>
</div>
<!-- ═══ Financial Breakdown ═══════════════════════════════════════════════════ -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;margin-bottom:20px;">
<div class="card" style="padding:18px 20px;text-align:center;border-top:4px solid #DC2626;">
<div style="font-size:11px;color:#6B7280;margin-bottom:6px;">الرصيد الأصلي المستحق</div>
<div style="font-size:22px;font-weight:800;color:#DC2626;"><?= money($originalDue) ?></div>
<div style="font-size:11px;color:#9CA3AF;margin-top:4px;"><?= $itemsCount ?> قسط × أصل + فائدة</div>
</div>
<div class="card" style="padding:18px 20px;text-align:center;border-top:4px solid #059669;">
<div style="font-size:11px;color:#6B7280;margin-bottom:6px;">إجمالي الفائدة الساقطة ✂️</div>
<div style="font-size:22px;font-weight:800;color:#059669;"><?= money($waived) ?></div>
<div style="font-size:11px;color:#9CA3AF;margin-top:4px;">وفر <?= $savingsPct ?>% من الرصيد الأصلي</div>
</div>
<div class="card" style="padding:18px 20px;text-align:center;border-top:4px solid #7C3AED;background:linear-gradient(135deg,#F5F3FF,#EDE9FE);">
<div style="font-size:11px;color:#6B7280;margin-bottom:6px;">المبلغ الواجب الدفع</div>
<div style="font-size:26px;font-weight:900;color:#7C3AED;"><?= money($remaining) ?></div>
<div style="font-size:11px;color:#7C3AED;margin-top:4px;font-weight:600;">أصل الدين فقط — بدون فوائد</div>
</div>
</div>
<!-- ═══ Formula Ribbon ════════════════════════════════════════════════════════ -->
<div class="card" style="padding:12px 22px;margin-bottom:20px;background:#F0FDF4;border:1px solid #86EFAC;">
<div style="display:flex;flex-wrap:wrap;gap:16px;align-items:center;justify-content:center;font-size:14px;">
<span style="color:#6B7280;">الرصيد الأصلي</span>
<strong style="color:#DC2626;"><?= money($originalDue) ?></strong>
<span style="color:#6B7280;"></span>
<span style="color:#6B7280;">الفائدة الساقطة</span>
<strong style="color:#059669;"><?= money($waived) ?></strong>
<span style="color:#6B7280;">=</span>
<span style="color:#6B7280;">يدفع العضو</span>
<strong style="color:#7C3AED;font-size:17px;"><?= money($remaining) ?></strong>
</div>
</div>
<!-- ═══ Pending Items Preview ═════════════════════════════════════════════════ -->
<div class="card" style="margin-bottom:24px;">
<div style="padding:12px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="list" style="width:16px;height:16px;color:#7C3AED;"></i>
<h3 style="margin:0;color:#7C3AED;font-size:14px;">الأقساط التي ستُسوَّى (<?= $itemsCount ?>)</h3>
</div>
<div class="table-responsive" style="max-height:320px;overflow-y:auto;">
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead style="position:sticky;top:0;background:#F9FAFB;">
<tr style="border-bottom:2px solid #E5E7EB;">
<th style="padding:8px 12px;text-align:right;color:#6B7280;">#</th>
<th style="padding:8px 12px;text-align:right;color:#6B7280;">تاريخ الاستحقاق</th>
<th style="padding:8px 12px;text-align:right;color:#6B7280;">المبلغ الأصلي</th>
<th style="padding:8px 12px;text-align:right;color:#6B7280;">أصل الدين</th>
<th style="padding:8px 12px;text-align:right;color:#D97706;">الفائدة الساقطة</th>
<th style="padding:8px 12px;text-align:right;color:#7C3AED;">يُدفع</th>
</tr>
</thead>
<tbody>
<?php foreach ($calc['pending_items'] as $item): ?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:7px 12px;font-weight:600;"><?= (int) $item['installment_number'] ?></td>
<td style="padding:7px 12px;color:<?= $item['due_date'] < date('Y-m-d') ? '#DC2626' : '#374151' ?>;font-weight:<?= $item['due_date'] < date('Y-m-d') ? '700' : '400' ?>;">
<?= e($item['due_date']) ?>
<?php if ($item['due_date'] < date('Y-m-d')): ?><span style="font-size:11px;margin-right:4px;">⚠️</span><?php endif; ?>
</td>
<td style="padding:7px 12px;color:#6B7280;"><?= money($item['amount']) ?></td>
<td style="padding:7px 12px;font-weight:600;"><?= money($item['principal']) ?></td>
<td style="padding:7px 12px;color:#D97706;">✂️ <?= money($item['interest']) ?></td>
<td style="padding:7px 12px;font-weight:700;color:#7C3AED;"><?= money($item['principal']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr style="background:#F0F9FF;font-weight:700;border-top:2px solid #E5E7EB;">
<td colspan="2" style="padding:8px 12px;color:#374151;">الإجمالي</td>
<td style="padding:8px 12px;color:#DC2626;"><?= money($originalDue) ?></td>
<td style="padding:8px 12px;"><?= money($remaining) ?></td>
<td style="padding:8px 12px;color:#D97706;">✂️ <?= money($waived) ?></td>
<td style="padding:8px 12px;color:#7C3AED;font-size:15px;"><?= money($remaining) ?></td>
</tr>
</tfoot>
</table>
</div>
</div>
<!-- ═══ Confirmation Form ═════════════════════════════════════════════════════ -->
<div class="card" style="padding:22px;border:2px solid #7C3AED;">
<h3 style="margin:0 0 16px;color:#7C3AED;font-size:15px;">
✅ تأكيد التسوية المبكرة
</h3>
<form method="POST" action="/installments/<?= (int) $plan['id'] ?>/early-settlement" id="settlementForm">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;">
<div class="form-group">
<label class="form-label">طريقة الدفع <span style="color:#DC2626;">*</span></label>
<select name="payment_method" class="form-select" required>
<option value="cash">نقدي</option>
<option value="check">شيك</option>
<option value="visa">بطاقة فيزا</option>
<option value="bank_transfer">تحويل بنكي</option>
</select>
</div>
<div style="display:flex;align-items:center;background:#F5F3FF;border:1px solid #DDD6FE;border-radius:8px;padding:14px 18px;">
<div>
<div style="font-size:11px;color:#7C3AED;margin-bottom:2px;">المبلغ الذي سيُحصَّل</div>
<div style="font-size:24px;font-weight:900;color:#7C3AED;"><?= money($remaining) ?></div>
</div>
</div>
</div>
<!-- Confirmation checkbox -->
<div style="background:#FEF9C3;border:1px solid #FDE68A;border-radius:8px;padding:12px 16px;margin-bottom:18px;display:flex;align-items:flex-start;gap:10px;">
<input type="checkbox" id="confirmCheck" name="confirmed" value="1" required
style="margin-top:3px;accent-color:#7C3AED;width:16px;height:16px;flex-shrink:0;">
<label for="confirmCheck" style="font-size:13px;color:#78350F;cursor:pointer;">
أؤكد أن العضو <strong><?= e($plan['member_name']) ?></strong> يرغب في تسوية جميع الأقساط المتبقية
(<strong><?= $itemsCount ?> قسط</strong>) بمبلغ <strong><?= money($remaining) ?></strong> (أصل الدين فقط بدون فوائد).
وأفهم أن الفائدة الساقطة (<strong><?= money($waived) ?></strong>) لن تُسترد وهذا الإجراء نهائي.
</label>
</div>
<div style="display:flex;gap:12px;">
<button type="submit" id="submitBtn" class="btn btn-primary"
style="background:#7C3AED;border-color:#7C3AED;font-size:15px;padding:10px 28px;"
disabled
onclick="return confirm('تأكيد التسوية المبكرة — سيُحصَّل <?= e(money($remaining)) ?> ولن يمكن التراجع؟')">
⚡ تأكيد التسوية المبكرة — <?= money($remaining) ?>
</button>
<a href="/installments/<?= (int) $plan['id'] ?>" class="btn btn-outline">إلغاء</a>
</div>
</form>
</div>
<script>
document.getElementById('confirmCheck').addEventListener('change', function () {
document.getElementById('submitBtn').disabled = !this.checked;
});
document.addEventListener('DOMContentLoaded', function () {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إيصال التسوية المبكرة — <?= e($receipt['receipt_number']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<button type="button" onclick="window.print()" class="btn btn-primary">
<i data-lucide="printer" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طباعة
</button>
<a href="/installments/<?= (int) $plan['id'] ?>" class="btn btn-outline">← خطة التقسيط</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$paymentMethodLabel = match ($receipt['payment_method'] ?? 'cash') {
'cash' => 'نقدي',
'check' => 'شيك',
'visa' => 'بطاقة فيزا',
'bank_transfer' => 'تحويل بنكي',
default => e($receipt['payment_method'] ?? ''),
};
$totalPrincipalFloat = (float) $totalPrincipal;
$settlementAmount = (float) $receipt['payment_amount'];
// Waived = original due - what was actually paid
// Sum the original amounts (principal = amount since we zeroed interest on save)
// Derive waived from plan data: original per-item amount stored in amount column before we changed it
// We stored paid_amount = principal, so waived = (originalDue that we can infer)
// Safer: re-derive from plan totals
$paidScheduleInterest = (float) \App\Core\App::getInstance()->db()->selectOne(
"SELECT COALESCE(SUM(interest), 0) as s FROM installment_schedule WHERE installment_plan_id = ? AND status = 'paid'",
[(int) $plan['id']]
)['s'];
// Items settled in this receipt: their interest is now 0, original interest was wiped.
// We'll just show settlement_amount vs total_with_interest remaining at the time.
// The description_ar on the receipt contains the full text, which is the source of truth.
?>
<!-- Print styles -->
<style>
@media print {
.btn, nav, aside, header, .page-actions, .sidebar { display:none !important; }
body { background:#fff !important; }
.receipt-wrap { box-shadow:none !important; border:none !important; }
}
</style>
<div class="receipt-wrap" style="max-width:680px;margin:0 auto;background:#fff;border:1px solid #E5E7EB;border-radius:12px;overflow:hidden;box-shadow:0 2px 12px rgba(0,0,0,.06);">
<!-- Header -->
<div style="background:linear-gradient(135deg,#7C3AED,#5B21B6);padding:28px 32px;color:#fff;text-align:center;">
<div style="font-size:11px;letter-spacing:2px;opacity:.8;margin-bottom:6px;">النادي الرياضي</div>
<div style="font-size:22px;font-weight:800;margin-bottom:4px;">إيصال تسوية مبكرة</div>
<div style="font-size:13px;opacity:.85;">Early Settlement Receipt</div>
<div style="margin-top:14px;background:rgba(255,255,255,.15);display:inline-block;padding:6px 20px;border-radius:99px;font-size:15px;font-weight:700;letter-spacing:1px;">
<?= e($receipt['receipt_number']) ?>
</div>
</div>
<!-- Settlement badge -->
<div style="background:#F0FDF4;border-bottom:1px solid #86EFAC;padding:12px 32px;display:flex;align-items:center;gap:10px;justify-content:center;">
<i data-lucide="check-circle" style="width:20px;height:20px;color:#059669;"></i>
<span style="font-size:13px;font-weight:700;color:#15803D;">تسوية مبكرة لجميع الأقساط المتبقية — تمت بنجاح</span>
</div>
<div style="padding:28px 32px;">
<!-- Member & receipt meta -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0;border:1px solid #E5E7EB;border-radius:8px;overflow:hidden;margin-bottom:24px;font-size:13px;">
<div style="padding:11px 16px;border-left:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;">
<div style="color:#9CA3AF;font-size:11px;margin-bottom:2px;">العضو</div>
<div style="font-weight:700;"><?= e($plan['member_name']) ?></div>
</div>
<div style="padding:11px 16px;border-bottom:1px solid #E5E7EB;">
<div style="color:#9CA3AF;font-size:11px;margin-bottom:2px;">رقم الاستمارة</div>
<div style="font-weight:700;"><?= e($plan['form_number'] ?? '—') ?></div>
</div>
<div style="padding:11px 16px;border-left:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;">
<div style="color:#9CA3AF;font-size:11px;margin-bottom:2px;">رقم العضوية</div>
<div style="font-weight:700;"><?= e($plan['membership_number'] ?? '—') ?></div>
</div>
<div style="padding:11px 16px;border-bottom:1px solid #E5E7EB;">
<div style="color:#9CA3AF;font-size:11px;margin-bottom:2px;">تاريخ الإيصال</div>
<div style="font-weight:700;"><?= e(substr($receipt['issued_at'] ?? '', 0, 10)) ?></div>
</div>
<div style="padding:11px 16px;border-left:1px solid #E5E7EB;">
<div style="color:#9CA3AF;font-size:11px;margin-bottom:2px;">طريقة الدفع</div>
<div style="font-weight:700;"><?= $paymentMethodLabel ?></div>
</div>
<div style="padding:11px 16px;">
<div style="color:#9CA3AF;font-size:11px;margin-bottom:2px;">المحصِّل</div>
<div style="font-weight:700;"><?= e($receipt['cashier_name'] ?? '—') ?></div>
</div>
</div>
<!-- Financial breakdown -->
<div style="border:1px solid #E5E7EB;border-radius:8px;overflow:hidden;margin-bottom:24px;">
<div style="padding:10px 16px;background:#F9FAFB;border-bottom:1px solid #E5E7EB;font-size:12px;font-weight:700;color:#374151;">
تفاصيل التسوية
</div>
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 16px;color:#6B7280;">عدد الأقساط المسوَّاة</td>
<td style="padding:10px 16px;font-weight:700;text-align:left;"><?= $itemsCount ?> قسط</td>
</tr>
<?php
// Derive original total due = principal + original interest
// Since we zero'd interest on save, we need another source.
// The receipt description has it; or we can compute from plan:
// original pending total = totalPrincipal + waivedInterest
// We don't have waived stored separately, but receipt amount = principal paid
// So waived = whatever the original amounts were minus principal.
// Best approach: query the original totals from plan header.
$planTotalWI = (float) $plan['total_with_interest'];
$planTotalInt = (float) $plan['total_interest'];
// Already-paid interest before this settlement
$alreadyPaidInt = $paidScheduleInterest - 0; // settled items had interest zeroed so paid int = only previously paid
// Actually paidScheduleInterest now includes settled items (all interest=0).
// We need the original interest of the settled items.
// Simplest: original_total_due = settlement_amount + waived_interest
// waived_interest = total_interest_of_plan - interest_paid_before_this_settlement
// But we don't have that cleanly. Use plan totals instead:
// remaining_principal at settlement time = settlement_amount (what we charged)
// total original due for pending = remaining_balance on plan at settlement
$planRemainingBalance = (float) $plan['remaining_balance'];
// remaining_balance in plan was updated... Let's just use what we know:
// The receipt description_ar contains the exact breakdown text — use that as the narrative
// For the table show: settlement_amount as principal, derive waived from receipt amount vs original
// Simple: we have settlement_amount and items; original per-item interest was stored in schedule
// but now zeroed. Compute from plan: total_interest - sum(interest of paid-before-settlement rows)
$originalPendingInterest = 0.0; // will compute below
foreach ($settledItems as $si) {
// interest is now 0; original = we can't recover exactly unless we stored it
// We know: settlement_amount = sum(principal). We know total_interest from plan.
// Best we can do: original_pending_interest = total_interest - already_paid_before
// already_paid_before = sum of interest on rows paid BEFORE this settlement
$originalPendingInterest = 0.0; // will do single query below
}
$prevPaidInterest = (float) \App\Core\App::getInstance()->db()->selectOne(
"SELECT COALESCE(SUM(is2.interest), 0) as s
FROM installment_schedule is2
WHERE is2.installment_plan_id = ?
AND is2.status = 'paid'
AND is2.payment_id != ?",
[(int) $plan['id'], (int) $receipt['payment_id']]
)['s'];
$originalPendingInterest = round($planTotalInt - $prevPaidInterest, 2);
$originalPendingTotal = round($totalPrincipalFloat + $originalPendingInterest, 2);
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 16px;color:#6B7280;">الرصيد الأصلي المستحق</td>
<td style="padding:10px 16px;font-weight:700;text-align:left;color:#DC2626;"><?= money($originalPendingTotal) ?></td>
</tr>
<tr style="border-bottom:1px solid #F3F4F6;background:#FFF7ED;">
<td style="padding:10px 16px;color:#D97706;font-weight:600;">✂️ الفوائد الساقطة (خصم التسوية المبكرة)</td>
<td style="padding:10px 16px;font-weight:700;text-align:left;color:#059669;"><?= money($originalPendingInterest) ?></td>
</tr>
<tr style="background:#F5F3FF;">
<td style="padding:12px 16px;font-weight:800;color:#7C3AED;font-size:14px;">المبلغ المحصَّل (أصل الدين فقط)</td>
<td style="padding:12px 16px;font-weight:900;text-align:left;color:#7C3AED;font-size:18px;"><?= money($settlementAmount) ?></td>
</tr>
</table>
</div>
<!-- Settled items list -->
<div style="border:1px solid #E5E7EB;border-radius:8px;overflow:hidden;margin-bottom:24px;">
<div style="padding:10px 16px;background:#F9FAFB;border-bottom:1px solid #E5E7EB;font-size:12px;font-weight:700;color:#374151;">
الأقساط المسوَّاة (<?= $itemsCount ?>)
</div>
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="background:#F9FAFB;border-bottom:1px solid #E5E7EB;">
<th style="padding:7px 12px;text-align:right;color:#6B7280;">#</th>
<th style="padding:7px 12px;text-align:right;color:#6B7280;">تاريخ الاستحقاق</th>
<th style="padding:7px 12px;text-align:right;color:#6B7280;">المبلغ المسدد</th>
<th style="padding:7px 12px;text-align:right;color:#6B7280;">الحالة</th>
</tr>
</thead>
<tbody>
<?php foreach ($settledItems as $si): ?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:6px 12px;font-weight:600;"><?= (int) $si['installment_number'] ?></td>
<td style="padding:6px 12px;"><?= e($si['due_date']) ?></td>
<td style="padding:6px 12px;font-weight:600;"><?= money($si['paid_amount']) ?></td>
<td style="padding:6px 12px;color:#059669;font-weight:700;">✅ مسدد</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Amount in words -->
<div style="background:#F5F3FF;border:1px solid #DDD6FE;border-radius:8px;padding:14px 18px;margin-bottom:24px;text-align:center;">
<div style="font-size:11px;color:#7C3AED;margin-bottom:4px;">المبلغ كتابةً</div>
<div style="font-size:14px;font-weight:700;color:#4C1D95;"><?= e($receipt['amount_in_words_ar'] ?? '—') ?></div>
</div>
<!-- Stamp area -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:8px;">
<div style="border:1px dashed #D1D5DB;border-radius:8px;padding:20px;text-align:center;">
<div style="font-size:11px;color:#9CA3AF;margin-bottom:30px;">توقيع المحصِّل</div>
<div style="border-top:1px solid #D1D5DB;padding-top:6px;font-size:11px;color:#6B7280;"><?= e($receipt['cashier_name'] ?? '—') ?></div>
</div>
<div style="border:1px dashed #D1D5DB;border-radius:8px;padding:20px;text-align:center;">
<div style="font-size:11px;color:#9CA3AF;margin-bottom:30px;">ختم النادي</div>
<div style="border-top:1px solid #D1D5DB;padding-top:6px;font-size:11px;color:#6B7280;">&nbsp;</div>
</div>
</div>
</div>
<!-- Footer -->
<div style="background:#F9FAFB;border-top:1px solid #E5E7EB;padding:12px 32px;text-align:center;font-size:11px;color:#9CA3AF;">
هذا الإيصال يُثبت التسوية المبكرة لخطة التقسيط #<?= (int) $plan['id'] ?><?= e($receipt['issued_at'] ?? '') ?>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>خطة تقسيط #<?= (int) $plan['id'] ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if (can('installment.pay') && $plan['status'] === 'active' && $pendingCount > 0): ?>
<a href="/installments/<?= (int) $plan['id'] ?>/early-settlement" class="btn btn-primary"
style="background:#7C3AED;border-color:#7C3AED;">
⚡ تسوية مبكرة
</a>
<?php endif; ?>
<?php if (can('installment.create_plan') && $plan['status'] === 'active'): ?>
<form method="POST" action="/installments/<?= (int) $plan['id'] ?>/recalculate" style="display:inline;">
<?= csrf_field() ?>
......
......@@ -313,6 +313,7 @@ final class PaymentService
'development_fee' => 'رسوم تنمية',
'down_payment' => 'مقدم تقسيط',
'installment' => 'قسط شهري',
'early_settlement' => 'تسوية مبكرة للأقساط',
'fine' => 'غرامة',
'separation_fee' => 'رسوم فصل',
'divorce_fee' => 'رسوم طلاق',
......
......@@ -134,8 +134,12 @@ app/Modules/Installments/
| POST | /installments/store/{memberId} | InstallmentController@store | auth, csrf | installment.create_plan |
| GET | /installments/{id} | InstallmentController@show | auth | installment.view |
| POST | /installments/{planId}/pay/{scheduleId} | InstallmentController@payInstallment | auth, csrf | installment.pay |
| GET | /installments/{id}/early-settlement | InstallmentController@earlySettlement | auth | installment.pay |
| POST | /installments/{id}/early-settlement | InstallmentController@processEarlySettlement | auth, csrf | installment.pay |
| GET | /installments/{id}/settlement-receipt/{receiptId} | InstallmentController@settlementReceipt | auth | installment.view |
| GET | /installments/{planId}/cheques | ChequeController@index | auth | installment.view |
| POST | /installments/{planId}/cheques | ChequeController@store | auth, csrf | installment.create_plan |
| POST | /installments/{planId}/cheques/batch | ChequeController@storeBatch | auth, csrf | installment.create_plan |
---
......@@ -219,6 +223,40 @@ app/Modules/Installments/
- Return: total_early_payoff, interest_saved
```
### 5.6 Early Settlement (Full — all pending items at once)
```
GET /installments/{id}/early-settlement
→ InstallmentController::earlySettlement()
→ InstallmentCalculator::calculateEarlySettlement($planId)
* Loads all pending/overdue schedule rows
* Computes: remaining_principal (sum of principal), total_interest_waived (sum of interest)
* settlement_amount = remaining_principal (interest fully waived)
→ renders early_settlement.php confirmation page with breakdown + checkbox
POST /installments/{id}/early-settlement
→ InstallmentController::processEarlySettlement()
1. Re-run calculateEarlySettlement() for freshness
2. PaymentService::processPayment(payment_type='early_settlement', amount=settlement_amount)
3. DB transaction:
a. For each pending item: set interest=0, amount=principal, paid_amount=principal,
payment_id=result.payment_id, status='paid', paid_at=now
b. Plan: status='completed', is_cash_settled=1
4. Dispatch installment.early_settled event
5. Redirect to settlement_receipt
GET /installments/{id}/settlement-receipt/{receiptId}
→ InstallmentController::settlementReceipt()
→ renders settlement_receipt.php (print-friendly)
Business rules enforced:
- Only available when plan.status='active' AND pending count > 0
- ALL pending rows are settled together — no partial interest waiver
- Confirmation checkbox required before submit button activates
- Single payment record covers all settled rows (linked via payment_id)
- interest_waived derived on receipt by querying paid rows with different payment_id
```
---
## 6. Interest Calculation Method
......@@ -249,6 +287,7 @@ For each month i:
|-------|---------|-----------------|
| `installment_plan.created` | `{plan_id, member_id}` | InstallmentController::store |
| `installment.paid` | `{plan_id, schedule_id, member_id}` | InstallmentController::payInstallment |
| `installment.early_settled` | `{plan_id, member_id, payment_id, receipt_id, settlement_amount, interest_waived, items_settled}` | InstallmentController::processEarlySettlement |
| `member.activated` | `{member_id, membership_number, payment_method:'installment'}` | ChequeService::activateMemberAfterCheques |
| `member.dropped` | `{member_id, reason, installment_plan_id}` | DefaultChecker, InstallmentDefaultJob |
......
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