Commit 2d63c655 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Implement unified family payment: single button pays entire year

- New payYear() controller action: processes all unpaid subscriptions for a
  member's financial year in one transaction with a single receipt
- Route: POST /members/{memberId}/subscriptions/{year}/pay
- View: replaced per-row pay buttons with a single "سداد اشتراك بالكامل" button
  at the bottom of each year section showing total due
- Itemized table remains for transparency (shows each person's breakdown)
- Added discount_amount column to the table display
- FIFO still enforced: only oldest unpaid year's button is active
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent cd3c2724
......@@ -94,6 +94,75 @@ class SubscriptionController extends Controller
]);
}
public function payYear(Request $request, string $memberId, string $year): Response
{
$db = App::getInstance()->db();
$financialYear = str_replace('-', '/', $year);
$member = $db->selectOne("SELECT id, full_name_ar FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]);
if (!$member) return $this->redirect('/members')->withError('العضو غير موجود');
$olderUnpaid = $db->selectOne(
"SELECT financial_year FROM subscriptions WHERE member_id = ? AND financial_year < ? AND status IN ('pending','overdue') LIMIT 1",
[(int) $memberId, $financialYear]
);
if ($olderUnpaid) {
return $this->redirect("/members/{$memberId}/subscriptions")
->withError('يجب سداد اشتراكات السنة الأقدم أولاً (' . $olderUnpaid['financial_year'] . ')');
}
$rows = $db->select(
"SELECT id, total_amount, fine_amount, paid_amount, person_name, person_type
FROM subscriptions WHERE member_id = ? AND financial_year = ? AND status IN ('pending','overdue')",
[(int) $memberId, $financialYear]
);
if (empty($rows)) {
return $this->redirect("/members/{$memberId}/subscriptions")->withError('لا توجد اشتراكات مستحقة لهذه السنة');
}
$totalAmount = '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);
}
$data = $request->all();
$data['member_id'] = (int) $memberId;
$data['amount'] = $totalAmount;
$data['payment_type'] = 'annual_subscription';
$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'] . ' (عائلة كاملة)';
$result = PaymentService::processPayment($data);
if (!$result['success']) {
return $this->redirect("/members/{$memberId}/subscriptions")->withError($result['error']);
}
$ts = date('Y-m-d H:i:s');
foreach ($rows as $r) {
$remaining = bcsub(bcadd($r['total_amount'], $r['fine_amount'], 2), $r['paid_amount'], 2);
$db->update('subscriptions', [
'paid_amount' => bcadd($r['paid_amount'], $remaining, 2),
'payment_id' => $result['payment_id'],
'status' => 'paid',
'paid_at' => $ts,
'updated_at' => $ts,
], '`id` = ?', [(int) $r['id']]);
}
EventBus::dispatch('subscription.paid', [
'member_id' => (int) $memberId,
'year' => $financialYear,
'amount' => $totalAmount,
'count' => count($rows),
]);
return $this->redirect("/members/{$memberId}/subscriptions")
->withSuccess('تم سداد اشتراك ' . $financialYear . ' بالكامل (' . count($rows) . ' فرد) — إيصال: ' . $result['receipt_number']);
}
public function pay(Request $request, string $id): Response
{
$db = App::getInstance()->db();
......
......@@ -6,6 +6,7 @@ return [
['GET', '/subscriptions/batch-generate', 'Subscriptions\Controllers\SubscriptionController@batchGenerateForm', ['auth'], 'subscription.generate_batch'],
['POST', '/subscriptions/batch-generate', 'Subscriptions\Controllers\SubscriptionController@batchGenerate', ['auth', 'csrf'], 'subscription.generate_batch'],
['GET', '/members/{memberId}/subscriptions', 'Subscriptions\Controllers\SubscriptionController@memberSubscriptions',['auth'], 'subscription.view'],
['POST', '/members/{memberId}/subscriptions/{year}/pay', 'Subscriptions\Controllers\SubscriptionController@payYear', ['auth', 'csrf'], 'subscription.collect'],
['POST', '/subscriptions/{id}/pay', 'Subscriptions\Controllers\SubscriptionController@pay', ['auth', 'csrf'], 'subscription.collect'],
['POST', '/subscriptions/{id}/exempt', 'Subscriptions\Controllers\SubscriptionController@exempt', ['auth', 'csrf'], 'subscription.exempt'],
];
\ No newline at end of file
......@@ -126,53 +126,29 @@ foreach ($lateFine['details'] ?? [] as $d) {
<th>النوع</th>
<th>الاسم</th>
<th>المبلغ</th>
<th>خصم</th>
<th>تنمية</th>
<th>الإجمالي</th>
<th>غرامة</th>
<th>المدفوع</th>
<th>الحالة</th>
<th>الإجراءات</th>
</tr></thead>
<tbody>
<?php foreach ($rows as $s):
$personLabel = match($s['person_type']) { 'member' => 'عضو', 'spouse' => 'زوجة', 'child' => 'ابن', 'temporary' => 'مؤقت', default => $s['person_type'] };
$rowStatusColor = match($s['status']) { 'paid' => '#059669', 'exempt' => '#0284C7', 'overdue' => '#DC2626', default => '#D97706' };
$rowStatusLabel = match($s['status']) { 'paid' => 'مدفوع', 'pending' => 'معلق', 'overdue' => 'متأخر', 'exempt' => 'معفى', default => $s['status'] };
$rowPayable = $isPayable && in_array($s['status'], ['pending', 'overdue']);
?>
<tr>
<td><span style="font-weight:600;"><?= $personLabel ?></span></td>
<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>
<td>
<?php if (in_array($s['status'], ['pending', 'overdue'])): ?>
<div style="display:flex;gap:5px;">
<?php if (can('subscription.collect')): ?>
<?php if ($rowPayable): ?>
<form method="POST" action="/subscriptions/<?= (int) $s['id'] ?>/pay" style="display:inline;">
<?= csrf_field() ?>
<input type="hidden" name="payment_method" value="cash">
<button type="submit" class="btn btn-sm btn-primary" onclick="return confirm('تسجيل دفع اشتراك <?= e($s['person_name'] ?? '') ?><?= money(bcsub(bcadd($s['total_amount'], $s['fine_amount'] ?? '0', 2), $s['paid_amount'] ?? '0', 2)) ?>؟')">دفع</button>
</form>
<?php else: ?>
<button class="btn btn-sm btn-outline" disabled title="يجب سداد اشتراكات <?= e($oldestUnpaidYear ?? '') ?> أولاً" style="opacity:0.5;cursor:not-allowed;">دفع</button>
<?php endif; ?>
<?php endif; ?>
<?php if (can('subscription.exempt')): ?>
<form method="POST" action="/subscriptions/<?= (int) $s['id'] ?>/exempt" style="display:inline;">
<?= csrf_field() ?>
<input type="hidden" name="exemption_reason" value="قرار مجلس">
<button type="submit" class="btn btn-sm btn-outline" onclick="return confirm('إعفاء من الاشتراك؟')">إعفاء</button>
</form>
<?php endif; ?>
</div>
<?php else: ?><?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
......@@ -180,14 +156,42 @@ foreach ($lateFine['details'] ?? [] as $d) {
<tr style="background:#F9FAFB;font-weight:700;">
<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 colspan="2"></td>
<td></td>
</tr>
</tfoot>
</table>
<?php if (!$isPaid): ?>
<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>
<div style="display:flex;gap:8px;align-items:center;">
<?php if (can('subscription.collect')): ?>
<?php if ($isPayable): ?>
<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متأكد؟')">
💰 سداد اشتراك <?= e($fy) ?> بالكامل
</button>
</form>
<?php else: ?>
<button class="btn btn-outline" disabled style="opacity:0.5;cursor:not-allowed;font-size:14px;padding:10px 24px;" title="يجب سداد اشتراكات <?= e($oldestUnpaidYear ?? '') ?> أولاً">
🔒 يجب سداد <?= e($oldestUnpaidYear ?? '') ?> أولاً
</button>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
......
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