Commit c492f45a authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix(members): correct installment panel formula and add live preview

- Installment panel in member show page now uses flat simple interest:
  remaining × (rate/100) × (months/12) instead of hardcoded 22% × 30mo
- Live breakdown table updates in real-time as user changes down payment
  or months: shows سعر العضوية, المقدم, المبلغ المتبقي, الفائدة,
  الإجمالي مع الفائدة, القسط الشهري
- Pass installInterestRate and installMaxMonths from RuleEngine to show view
- Fix pay-membership action to respect RuleEngine max months (not hardcoded 30)
Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent b7d24084
......@@ -255,6 +255,9 @@ class MemberController extends Controller
);
}
$instRateData = RuleEngine::get('INSTALLMENT_INTEREST_RATE');
$instMonthsData = RuleEngine::get('INSTALLMENT_MAX_MONTHS');
return $this->view('Members.Views.show', [
'member' => $member,
'branchName' => $branch['name_ar'] ?? '—',
......@@ -276,6 +279,8 @@ class MemberController extends Controller
'installmentPlan' => $member->status === 'pending_cheques'
? $db->selectOne("SELECT id, number_of_months FROM installment_plans WHERE member_id = ? AND status = 'active' ORDER BY id DESC LIMIT 1", [(int) $id])
: null,
'installInterestRate' => (float) ($instRateData['percentage'] ?? 22),
'installMaxMonths' => (int) ($instMonthsData['months'] ?? 30),
]);
}
......@@ -367,7 +372,9 @@ class MemberController extends Controller
if (bccomp($amount, '0.01', 2) < 0) return $this->redirect('/members/' . $id)->withError('المبلغ غير صالح');
$months = ($paymentType === 'down_payment') ? min(30, max(1, (int) $request->post('installment_months', 30))) : null;
$instMaxMonthsData = RuleEngine::get('INSTALLMENT_MAX_MONTHS');
$instMaxMonths = (int) ($instMaxMonthsData['months'] ?? 30);
$months = ($paymentType === 'down_payment') ? min($instMaxMonths, max(1, (int) $request->post('installment_months', $instMaxMonths))) : null;
// Cancel any pending individual addition_fee requests — the collective payment subsumes them
$pendingAdditions = $db->select(
......
......@@ -331,39 +331,104 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false));
</form>
</div>
<?php if ($allowInstallment): ?>
<?php
$instRate = $installInterestRate ?? 22;
$instMaxMos = $installMaxMonths ?? 30;
$minDown = bcdiv(bcmul($bill['total_pending'], '25', 2), '100', 2);
// Default preview: min down, max months
$instRem = bcsub($bill['total_pending'], $minDown, 2);
$instInterest = bcmul(bcmul($instRem, bcdiv((string) $instRate, '100', 8), 8), bcdiv((string) $instMaxMos, '12', 8), 2);
$instTotalWI = bcadd($instRem, $instInterest, 2);
$instMonthly = bcdiv($instTotalWI, (string) $instMaxMos, 2);
?>
<!-- Installment -->
<div style="background:#fff;border:2px solid #0284C7;border-radius:12px;padding:20px;">
<h5 style="margin:0 0 10px;color:#0284C7;">&#x1f4c5; تقسيط</h5>
<p style="font-size:13px;color:#6B7280;margin:0 0 5px;">مقدم 25% على الأقل + باقي على أقساط</p>
<p style="font-size:12px;color:#D97706;margin:0 0 10px;">فائدة 22% سنوياً — حتى 30 شهر</p>
<?php
$minDown = bcdiv(bcmul($bill['total_pending'], '25', 2), '100', 2);
$remaining = bcsub($bill['total_pending'], $minDown, 2);
// Flat simple interest on remaining for 30 months: remaining × (22/100) × (30/12)
$interest = bcmul(bcmul($remaining, bcdiv('22', '100', 8), 8), bcdiv('30', '12', 8), 2);
$totalWithInterest = bcadd($remaining, $interest, 2);
$monthlyEst = bcdiv($totalWithInterest, '30', 2);
?>
<table style="width:100%;font-size:13px;margin-bottom:10px;">
<tr><td style="color:#6B7280;">المقدم (25%)</td><td style="text-align:left;font-weight:600;"><?= money($minDown) ?></td></tr>
<tr><td style="color:#6B7280;">المتبقي</td><td style="text-align:left;"><?= money($remaining) ?></td></tr>
<tr><td style="color:#6B7280;">الفائدة (22% × 30 شهر)</td><td style="text-align:left;color:#D97706;"><?= money($interest) ?></td></tr>
<tr><td style="color:#6B7280;">القسط (~30 شهر)</td><td style="text-align:left;font-weight:600;"><?= money($monthlyEst) ?></td></tr>
</table>
<form method="POST" action="/members/<?= (int) $member->id ?>/pay-membership">
<h5 style="margin:0 0 4px;color:#0284C7;">&#x1f4c5; تقسيط</h5>
<p style="font-size:12px;color:#6B7280;margin:0 0 2px;">مقدم 25% على الأقل + باقي على أقساط</p>
<p style="font-size:11px;color:#D97706;margin:0 0 12px;">فائدة <?= (float) $instRate ?>% سنوياً — حتى <?= $instMaxMos ?> شهر</p>
<!-- Live breakdown (updates with JS) -->
<div style="background:#EFF6FF;border:1px solid #BFDBFE;border-radius:8px;padding:10px 12px;margin-bottom:12px;font-size:12px;">
<table style="width:100%;border-collapse:collapse;">
<tr>
<td style="color:#6B7280;padding:3px 0;">سعر العضوية</td>
<td style="text-align:left;font-weight:600;" id="inst_price_disp"><?= money($bill['total_pending']) ?></td>
</tr>
<tr>
<td style="color:#6B7280;padding:3px 0;">المقدم</td>
<td style="text-align:left;font-weight:600;color:#059669;" id="inst_down_disp"><?= money($minDown) ?></td>
</tr>
<tr style="border-top:1px solid #BFDBFE;">
<td style="color:#374151;font-weight:700;padding:4px 0;">المبلغ المتبقي</td>
<td style="text-align:left;font-weight:800;color:#0284C7;" id="inst_remaining_disp"><?= money($instRem) ?></td>
</tr>
<tr>
<td style="color:#6B7280;padding:3px 0;" id="inst_interest_label">الفائدة (<?= (float) $instRate ?>% × <?= $instMaxMos ?> شهر)</td>
<td style="text-align:left;color:#D97706;font-weight:600;" id="inst_interest_disp"><?= money($instInterest) ?></td>
</tr>
<tr>
<td style="color:#6B7280;padding:3px 0;">الإجمالي مع الفائدة</td>
<td style="text-align:left;font-weight:600;" id="inst_totalwi_disp"><?= money($instTotalWI) ?></td>
</tr>
<tr style="border-top:2px solid #93C5FD;">
<td style="font-weight:700;color:#0D7377;padding:5px 0;" id="inst_monthly_label">القسط الشهري (÷ <?= $instMaxMos ?>)</td>
<td style="text-align:left;font-weight:900;color:#0D7377;font-size:16px;" id="inst_monthly_disp"><?= money($instMonthly) ?></td>
</tr>
</table>
</div>
<form method="POST" action="/members/<?= (int) $member->id ?>/pay-membership" novalidate>
<?= csrf_field() ?>
<input type="hidden" name="payment_type" value="down_payment">
<div class="form-group" style="margin-bottom:10px;">
<div class="form-group" style="margin-bottom:8px;">
<label class="form-label" style="font-size:12px;">المقدم (&#x2265; <?= money($minDown) ?>)</label>
<input type="number" name="amount" value="<?= e($minDown) ?>" min="<?= e($minDown) ?>" step="0.01" class="form-input" style="direction:ltr;text-align:left;" required>
<input type="number" id="inst_down_input" name="amount"
value="<?= e($minDown) ?>" min="<?= e($minDown) ?>" step="0.01"
class="form-input" style="direction:ltr;text-align:left;"
oninput="instRecalc()" required>
</div>
<div class="form-group" style="margin-bottom:10px;">
<label class="form-label" style="font-size:12px;">عدد الأشهر (حتى 30)</label>
<input type="number" name="installment_months" value="30" min="1" max="30" class="form-input" style="direction:ltr;text-align:left;">
<div class="form-group" style="margin-bottom:12px;">
<label class="form-label" style="font-size:12px;">عدد الأشهر (1 – <?= $instMaxMos ?>)</label>
<input type="number" id="inst_months_input" name="installment_months"
value="<?= $instMaxMos ?>" min="1" max="<?= $instMaxMos ?>"
class="form-input" style="direction:ltr;text-align:left;"
oninput="instRecalc()">
</div>
<button type="submit" class="btn btn-primary" style="width:100%;background:#D97706;border-color:#D97706;" onclick="return confirm('إرسال طلب تقسيط للخزينة؟')">&#x1f4e4; إرسال للخزينة</button>
<button type="submit" class="btn btn-primary"
style="width:100%;background:#D97706;border-color:#D97706;"
onclick="return confirm('إرسال طلب تقسيط للخزينة؟')">
&#x1f4e4; إرسال للخزينة
</button>
</form>
</div>
<script>
(function () {
var TOTAL = <?= json_encode((float) $bill['total_pending']) ?>;
var RATE = <?= json_encode((float) $instRate) ?>;
var MIN_DOWN = <?= json_encode((float) $minDown) ?>;
var fmt = function(v) { return v.toLocaleString('ar-EG', {minimumFractionDigits:2, maximumFractionDigits:2}) + ' ج.م'; };
window.instRecalc = function () {
var down = parseFloat(document.getElementById('inst_down_input').value) || MIN_DOWN;
var months = parseInt(document.getElementById('inst_months_input').value,10) || <?= $instMaxMos ?>;
if (down < MIN_DOWN) down = MIN_DOWN;
if (months < 1) months = 1;
var remaining = Math.round((TOTAL - down) * 100) / 100;
var totalInterest = Math.round(remaining * (RATE / 100) * (months / 12) * 100) / 100;
var totalWI = Math.round((remaining + totalInterest) * 100) / 100;
var monthly = Math.round((totalWI / months) * 100) / 100;
document.getElementById('inst_down_disp').textContent = fmt(down);
document.getElementById('inst_remaining_disp').textContent = fmt(remaining);
document.getElementById('inst_interest_disp').textContent = fmt(totalInterest);
document.getElementById('inst_totalwi_disp').textContent = fmt(totalWI);
document.getElementById('inst_monthly_disp').textContent = fmt(monthly);
document.getElementById('inst_interest_label').textContent = 'الفائدة (' + RATE + '% × ' + months + ' شهر)';
document.getElementById('inst_monthly_label').textContent = 'القسط الشهري (÷ ' + months + ')';
};
})();
</script>
<?php endif; ?>
</div>
<div style="margin-top:15px;padding:10px;background:#FEF2F2;border-radius:8px;font-size:12px;color:#DC2626;">
......
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