Commit e381d4e9 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Separate dev fee from subscription total: single 35 EGP added at invoice level

Business rule: مصاريف تنمية is a single flat 35 EGP fee per family per year,
NOT per person, NOT included in discountable base, added AFTER all calculations.

Changes:
- Generator: total_amount = base - discount (dev fee stored separately, not in total)
- Migration: subtracts dev_fee from total_amount for existing unpaid rows
- payYear(): adds single dev fee on top of subscription+fine totals at payment
- View: dev fee shown as separate line below the subscription subtotal, before
  the invoice grand total. Table columns simplified (no per-row dev fee column)

Calculation order:
1. Sum all family subscription bases
2. Apply year discount (e.g. 50% for 2023/2024) to subscription amounts
3. Add single 35 EGP dev fee as standalone charge at the end
4. Add fines (on subscription only, not dev fee)
= Final invoice total
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 2d63c655
......@@ -112,7 +112,7 @@ class SubscriptionController extends Controller
}
$rows = $db->select(
"SELECT id, total_amount, fine_amount, paid_amount, person_name, person_type
"SELECT id, total_amount, fine_amount, paid_amount, development_fee, person_name, person_type
FROM subscriptions WHERE member_id = ? AND financial_year = ? AND status IN ('pending','overdue')",
[(int) $memberId, $financialYear]
);
......@@ -120,11 +120,16 @@ class SubscriptionController extends Controller
return $this->redirect("/members/{$memberId}/subscriptions")->withError('لا توجد اشتراكات مستحقة لهذه السنة');
}
$totalAmount = '0.00';
$subscriptionTotal = '0.00';
$devFee = '0.00';
foreach ($rows as $r) {
$remaining = bcsub(bcadd($r['total_amount'], $r['fine_amount'], 2), $r['paid_amount'], 2);
$totalAmount = bcadd($totalAmount, $remaining, 2);
$subscriptionTotal = bcadd($subscriptionTotal, $remaining, 2);
if (bccomp($r['development_fee'], '0', 2) > 0) {
$devFee = $r['development_fee'];
}
}
$totalAmount = bcadd($subscriptionTotal, $devFee, 2);
$data = $request->all();
$data['member_id'] = (int) $memberId;
......@@ -133,7 +138,7 @@ class SubscriptionController extends Controller
$data['payment_method'] = $data['payment_method'] ?? 'cash';
$data['related_entity_type'] = 'subscriptions';
$data['related_entity_id'] = (int) $rows[0]['id'];
$data['description'] = 'اشتراك سنوي ' . $financialYear . ' — ' . $member['full_name_ar'] . ' (عائلة كاملة)';
$data['description'] = 'اشتراك سنوي ' . $financialYear . ' — ' . $member['full_name_ar'] . ' (عائلة كاملة' . (bccomp($devFee, '0', 2) > 0 ? ' + مصاريف تنمية' : '') . ')';
$result = PaymentService::processPayment($data);
if (!$result['success']) {
......
......@@ -73,9 +73,9 @@ final class SubscriptionGenerator
);
if ($existing) { $skipped++; continue; }
// Member subscription — discount on base only, dev fee added after
// Member subscription — discount on base only; dev fee is separate non-discountable charge
$discount = $discountPct ? bcdiv(bcmul($memberRate, $discountPct, 4), '100', 2) : '0.00';
$total = bcadd(bcsub($memberRate, $discount, 2), $devFee, 2);
$total = bcsub($memberRate, $discount, 2);
$db->insert('subscriptions', [
'member_id' => $memberId,
'financial_year' => $financialYear,
......
......@@ -5,9 +5,10 @@
<?php
$totalDebt = '0.00';
foreach ($yearTotals as $yt) {
foreach ($yearTotals as $yf => $yt) {
if ($yt['year_status'] !== 'paid') {
$remaining = bcsub(bcadd($yt['total_amount'], $yt['total_fine'], 2), $yt['total_paid'], 2);
$remaining = bcadd($remaining, $yt['total_dev'], 2);
$totalDebt = bcadd($totalDebt, $remaining, 2);
}
}
......@@ -65,7 +66,7 @@ foreach ($lateFine['details'] ?? [] as $d) {
$isPayable = ($oldestUnpaidYear === $fy);
$isPaid = ($yt['year_status'] === 'paid');
$fineDetail = $fineDetailsByYear[$fy] ?? null;
$yearRemaining = bcsub(bcadd($yt['total_amount'], $yt['total_fine'], 2), $yt['total_paid'], 2);
$yearRemaining = bcsub(bcadd(bcadd($yt['total_amount'], $yt['total_fine'], 2), $yt['total_dev'], 2), $yt['total_paid'], 2);
$borderColor = $isPaid ? '#059669' : ($isPayable ? '#D97706' : '#D1D5DB');
$headerBg = $isPaid ? '#F0FDF4' : ($isPayable ? '#FFF7ED' : '#F9FAFB');
......@@ -121,16 +122,26 @@ foreach ($lateFine['details'] ?? [] as $d) {
<?php endif; ?>
<div style="padding:0 20px 16px;">
<?php
$yearDevFee = '0.00';
foreach ($rows as $r) {
if (bccomp($r['development_fee'] ?? '0', '0', 2) > 0) {
$yearDevFee = $r['development_fee'];
break;
}
}
$invoiceTotal = bcadd($yt['total_amount'], $yearDevFee, 2);
$invoiceGrand = bcadd(bcadd($invoiceTotal, $yt['total_fine'], 2), '0', 2);
$invoiceRemaining = bcsub($invoiceGrand, $yt['total_paid'], 2);
?>
<table class="data-table" style="margin-top:12px;">
<thead><tr>
<th>النوع</th>
<th>الاسم</th>
<th>المبلغ</th>
<th>الاشتراك</th>
<th>خصم</th>
<th>تنمية</th>
<th>الإجمالي</th>
<th>الصافي</th>
<th>غرامة</th>
<th>المدفوع</th>
<th>الحالة</th>
</tr></thead>
<tbody>
......@@ -144,25 +155,33 @@ foreach ($lateFine['details'] ?? [] as $d) {
<td style="font-size:13px;"><?= e($s['person_name'] ?? '—') ?></td>
<td><?= money($s['base_amount']) ?></td>
<td style="font-size:12px;color:#7C3AED;"><?= bccomp($s['discount_amount'] ?? '0', '0', 2) > 0 ? '-' . money($s['discount_amount']) : '—' ?></td>
<td style="font-size:12px;color:#6B7280;"><?= bccomp($s['development_fee'] ?? '0', '0', 2) > 0 ? money($s['development_fee']) : '—' ?></td>
<td style="font-weight:600;"><?= money($s['total_amount']) ?></td>
<td style="color:#DC2626;"><?= bccomp($s['fine_amount'] ?? '0', '0', 2) > 0 ? money($s['fine_amount']) : '—' ?></td>
<td style="color:#059669;"><?= bccomp($s['paid_amount'] ?? '0', '0', 2) > 0 ? money($s['paid_amount']) : '—' ?></td>
<td><span style="color:<?= $rowStatusColor ?>;font-weight:600;"><?= $rowStatusLabel ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr style="background:#F9FAFB;font-weight:700;">
<td colspan="2" style="text-align:center;">إجمالي السنة</td>
<tr style="background:#F9FAFB;font-weight:600;">
<td colspan="2" style="text-align:center;">إجمالي الاشتراكات</td>
<td><?= money(array_reduce($rows, fn($c, $r) => bcadd($c, $r['base_amount'], 2), '0.00')) ?></td>
<td style="font-size:12px;color:#7C3AED;"><?php $totalDisc = array_reduce($rows, fn($c, $r) => bcadd($c, $r['discount_amount'] ?? '0', 2), '0.00'); echo bccomp($totalDisc, '0', 2) > 0 ? '-' . money($totalDisc) : '—'; ?></td>
<td style="font-size:12px;"><?= bccomp($yt['total_dev'], '0', 2) > 0 ? money($yt['total_dev']) : '—' ?></td>
<td><?= money($yt['total_amount']) ?></td>
<td style="color:#DC2626;"><?= bccomp($yt['total_fine'], '0', 2) > 0 ? money($yt['total_fine']) : '—' ?></td>
<td style="color:#059669;"><?= bccomp($yt['total_paid'], '0', 2) > 0 ? money($yt['total_paid']) : '—' ?></td>
<td></td>
</tr>
<?php if (bccomp($yearDevFee, '0', 2) > 0): ?>
<tr style="background:#F0F9FF;font-weight:600;">
<td colspan="4" style="text-align:center;color:#0369A1;">مصاريف تنمية (رسم ثابت)</td>
<td style="color:#0369A1;"><?= money($yearDevFee) ?></td>
<td colspan="2"></td>
</tr>
<?php endif; ?>
<tr style="background:#1E293B;color:#fff;font-weight:700;">
<td colspan="4" style="text-align:center;font-size:14px;">إجمالي الفاتورة</td>
<td style="font-size:14px;"><?= money($invoiceGrand) ?></td>
<td colspan="2" style="font-size:12px;"><?= bccomp($yt['total_paid'], '0', 2) > 0 ? 'المدفوع: ' . money($yt['total_paid']) : '' ?></td>
</tr>
</tfoot>
</table>
......@@ -170,8 +189,13 @@ foreach ($lateFine['details'] ?? [] as $d) {
<div style="margin-top:16px;padding:16px;background:#F8FAFC;border:1px solid #E2E8F0;border-radius:8px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px;">
<div>
<div style="font-size:13px;color:#64748B;">المطلوب لسداد اشتراك <?= e($fy) ?> بالكامل:</div>
<div style="font-size:24px;font-weight:700;color:#1E293B;margin-top:4px;"><?= money($yearRemaining) ?></div>
<div style="font-size:11px;color:#94A3B8;margin-top:2px;">(<?= (int) $yt['count'] ?> فرد: اشتراك <?= money($yt['total_amount']) ?><?= bccomp($yt['total_fine'], '0', 2) > 0 ? ' + غرامة ' . money($yt['total_fine']) : '' ?><?= bccomp($yt['total_paid'], '0', 2) > 0 ? ' − مدفوع ' . money($yt['total_paid']) : '' ?>)</div>
<div style="font-size:24px;font-weight:700;color:#1E293B;margin-top:4px;"><?= money($invoiceRemaining) ?></div>
<div style="font-size:11px;color:#94A3B8;margin-top:2px;">
(اشتراكات: <?= money($yt['total_amount']) ?>
<?= bccomp($yearDevFee, '0', 2) > 0 ? ' + تنمية: ' . money($yearDevFee) : '' ?>
<?= bccomp($yt['total_fine'], '0', 2) > 0 ? ' + غرامة: ' . money($yt['total_fine']) : '' ?>
<?= bccomp($yt['total_paid'], '0', 2) > 0 ? ' − مدفوع: ' . money($yt['total_paid']) : '' ?>)
</div>
</div>
<div style="display:flex;gap:8px;align-items:center;">
<?php if (can('subscription.collect')): ?>
......@@ -179,7 +203,7 @@ foreach ($lateFine['details'] ?? [] as $d) {
<form method="POST" action="/members/<?= (int) $member['id'] ?>/subscriptions/<?= str_replace('/', '-', $fy) ?>/pay">
<?= csrf_field() ?>
<input type="hidden" name="payment_method" value="cash">
<button type="submit" class="btn btn-primary" style="font-size:15px;padding:10px 28px;" onclick="return confirm('تسجيل سداد اشتراك <?= e($fy) ?> بالكامل\n\nالمبلغ: <?= money($yearRemaining) ?>\nعدد الأفراد: <?= (int) $yt['count'] ?>\n\nمتأكد؟')">
<button type="submit" class="btn btn-primary" style="font-size:15px;padding:10px 28px;" onclick="return confirm('تسجيل سداد اشتراك <?= e($fy) ?> بالكامل\n\nالمبلغ: <?= money($invoiceRemaining) ?>\nعدد الأفراد: <?= (int) $yt['count'] ?>\n\nمتأكد؟')">
💰 سداد اشتراك <?= e($fy) ?> بالكامل
</button>
</form>
......
<?php
declare(strict_types=1);
/**
* Separate development fee from total_amount.
*
* Business rule: مصاريف تنمية (35 EGP) is a single flat fee per membership per year,
* added AFTER all subscription calculations and discounts. It must NOT be part of
* total_amount (which represents only the subscription charge per person).
*
* This migration subtracts development_fee from total_amount for all rows where
* dev fee was incorrectly embedded. Only affects unpaid rows.
*/
return function (\App\Core\Database $db): void {
$rows = $db->select(
"SELECT id, total_amount, development_fee
FROM subscriptions
WHERE development_fee > 0 AND status IN ('pending', 'overdue')"
);
foreach ($rows as $row) {
$newTotal = bcsub($row['total_amount'], $row['development_fee'], 2);
if (bccomp($newTotal, '0', 2) < 0) continue;
$db->query(
"UPDATE subscriptions SET total_amount = ?, updated_at = NOW() WHERE id = ?",
[$newTotal, (int) $row['id']]
);
}
};
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