Commit 32e8dda5 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix membership payment sync, auto-send post-activation additions to cashier,...

Fix membership payment sync, auto-send post-activation additions to cashier, and add fee calculation transparency

- Fix critical bug in Cashier bootstrap where children/temporary_members activation
  failed silently due to non-existent join_date column (only spouses have it)
- Add self-healing sync in MemberController::show() to fix historically stuck dependents
- Auto-create payment_request when adding dependents to active members (post-activation)
- Add pending additions UI section for active members showing cashier queue status
- Store fee calculation breakdowns in new fee_breakdown_json column on dependent tables
- Display expandable fee breakdowns in family tree for full calculation transparency
- Add migration Phase_65_014 for fee_breakdown_json column
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 715fac64
......@@ -99,20 +99,24 @@ EventBus::listen('payment_request.completed', function (array $data) {
[$memberId]
);
foreach ($pendingDeps as $dep) {
// Only activate if no separate pending payment request exists
$hasSeparateRequest = $db->selectOne(
"SELECT id FROM payment_requests WHERE member_id = ? AND payment_type = 'addition_fee' AND related_entity_type = ? AND related_entity_id = ? AND status IN ('pending','processing') AND is_voided = 0 LIMIT 1",
[$memberId, $depTable, (int) $dep['id']]
);
if (!$hasSeparateRequest) {
$db->update($depTable, [
$updateData = [
'status' => 'active',
'join_date' => date('Y-m-d'),
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $dep['id']]);
];
if ($depTable === 'spouses') {
$updateData['join_date'] = date('Y-m-d');
}
$db->update($depTable, $updateData, '`id` = ?', [(int) $dep['id']]);
}
}
} catch (\Throwable $e) {}
} catch (\Throwable $e) {
\App\Core\Logger::error("Failed to activate {$depTable} for member {$memberId}: " . $e->getMessage());
}
}
EventBus::dispatch('member.activated', ['member_id' => $memberId, 'membership_number' => $membershipNumber]);
......
......@@ -13,6 +13,7 @@ use App\Modules\Children\Services\ChildFeeCalculator;
use App\Modules\Members\Services\NationalIdParser;
use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Forms\Services\FormBridge;
use App\Modules\Cashier\Services\PaymentRequestService;
class ChildController extends Controller
......@@ -134,6 +135,8 @@ class ChildController extends Controller
$totalFee = $feeCalc['total_fee'] ?? $feeCalc['fee'] ?? '0.00';
$hasFee = bccomp($totalFee, '0', 2) > 0;
$breakdownJson = !empty($feeCalc['breakdown']) ? json_encode($feeCalc['breakdown'], JSON_UNESCAPED_UNICODE) : null;
$child = Child::create([
'member_id' => (int) $memberId,
'child_order' => $childOrder,
......@@ -150,6 +153,7 @@ class ChildController extends Controller
'nationality' => $data['nationality'] ?? 'مصري',
'classification' => $classification,
'addition_fee' => $totalFee,
'fee_breakdown_json' => $breakdownJson,
'status' => $hasFee ? 'pending_payment' : 'active',
'remarks' => $data['remarks'] ?? null,
]);
......@@ -165,6 +169,22 @@ class ChildController extends Controller
'fee' => $totalFee,
]);
if ($hasFee && !empty($member['membership_number'])) {
$breakdown = $feeCalc['breakdown'] ?? [];
$childLabel = ($data['gender'] ?? '') === 'male' ? 'ابن' : 'ابنة';
PaymentRequestService::createRequest([
'member_id' => (int) $memberId,
'amount' => $totalFee,
'payment_type' => 'addition_fee',
'related_entity_type' => 'children',
'related_entity_id' => (int) $child->id,
'description_ar' => 'رسوم إضافة ' . $childLabel . ' — ' . trim($data['full_name_ar']),
'notes' => json_encode(['fee_breakdown' => $breakdown], JSON_UNESCAPED_UNICODE),
]);
return $this->redirect("/members/{$memberId}")
->withSuccess('تم إضافة الابن/الابنة — التصنيف: ' . $child->getClassificationLabel() . ' — الرسوم: ' . money($totalFee) . ' — تم إرسالها للخزينة تلقائياً');
}
if ($hasFee) {
return $this->redirect("/members/{$memberId}")
->withSuccess('تم إضافة الابن/الابنة — التصنيف: ' . $child->getClassificationLabel() . ' — الرسوم: ' . money($totalFee) . ' — سيتم تحصيلها ضمن الفاتورة المجمعة');
......
......@@ -20,7 +20,7 @@ class Child extends Model
'national_id', 'birth_certificate_number', 'date_of_birth',
'age_years', 'age_months', 'gender', 'relationship',
'school_faculty', 'nationality', 'classification',
'addition_fee', 'fee_receipt_number', 'status',
'addition_fee', 'fee_breakdown_json', 'fee_receipt_number', 'status',
'is_frozen', 'frozen_at', 'frozen_reason', 'photo_path', 'remarks',
];
......
......@@ -142,6 +142,41 @@ class MemberController extends Controller
try { $children = $db->select("SELECT * FROM children WHERE member_id = ? AND is_archived = 0 ORDER BY child_order", [(int) $id]); } catch (\Throwable $e) {}
try { $temporaries = $db->select("SELECT * FROM temporary_members WHERE member_id = ? AND is_archived = 0 ORDER BY id", [(int) $id]); } catch (\Throwable $e) {}
// Self-healing: fix dependents stuck at pending_payment after membership was paid
if ($member->status === 'active') {
$membershipPaid = $db->selectOne(
"SELECT id FROM payments WHERE member_id = ? AND payment_type IN ('membership_fee','down_payment') AND is_voided = 0 LIMIT 1",
[(int) $id]
);
if ($membershipPaid) {
$fixedAny = false;
foreach (['spouses', 'children', 'temporary_members'] as $tbl) {
$stuck = $db->select(
"SELECT id FROM `{$tbl}` WHERE member_id = ? AND status = 'pending_payment' AND is_archived = 0",
[(int) $id]
);
foreach ($stuck as $dep) {
$hasPending = $db->selectOne(
"SELECT id FROM payment_requests WHERE member_id = ? AND payment_type = 'addition_fee' AND related_entity_type = ? AND related_entity_id = ? AND status IN ('pending','processing') AND is_voided = 0 LIMIT 1",
[(int) $id, $tbl, (int) $dep['id']]
);
if (!$hasPending) {
$upd = ['status' => 'active', 'updated_at' => date('Y-m-d H:i:s')];
if ($tbl === 'spouses') $upd['join_date'] = date('Y-m-d');
$db->update($tbl, $upd, '`id` = ?', [(int) $dep['id']]);
$fixedAny = true;
}
}
}
if ($fixedAny) {
try { $spouses = $db->select("SELECT * FROM spouses WHERE member_id = ? AND is_archived = 0 ORDER BY spouse_order", [(int) $id]); } catch (\Throwable $e) {}
try { $children = $db->select("SELECT * FROM children WHERE member_id = ? AND is_archived = 0 ORDER BY child_order", [(int) $id]); } catch (\Throwable $e) {}
try { $temporaries = $db->select("SELECT * FROM temporary_members WHERE member_id = ? AND is_archived = 0 ORDER BY id", [(int) $id]); } catch (\Throwable $e) {}
Logger::info("Fixed stuck pending_payment dependants", ['member_id' => (int) $id]);
}
}
}
$bill = BillingService::getMemberBill((int) $id);
$formFilled = ($member->qualification_id !== null && $member->qualification_id > 0);
......
......@@ -133,6 +133,12 @@ final class BillingService
$fee = $s['addition_fee'] ?? '0.00';
$isFirstFree = ($order === 1 && bccomp($fee, '0', 2) <= 0);
$breakdown = null;
if (!empty($s['fee_breakdown_json'])) {
$breakdown = json_decode($s['fee_breakdown_json'], true);
if (!is_array($breakdown)) $breakdown = null;
}
$items[] = [
'type' => 'spouse_fee',
'label' => 'زوجة #' . $order . ' — ' . $s['full_name_ar'],
......@@ -143,6 +149,7 @@ final class BillingService
'entity_type' => 'spouses',
'entity_id' => (int) $s['id'],
'category' => 'addition',
'breakdown' => $breakdown,
];
}
......@@ -168,6 +175,12 @@ final class BillingService
default => $classification,
};
$breakdown = null;
if (!empty($c['fee_breakdown_json'])) {
$breakdown = json_decode($c['fee_breakdown_json'], true);
if (!is_array($breakdown)) $breakdown = null;
}
$items[] = [
'type' => 'child_fee',
'label' => ($c['gender'] === 'male' ? 'ابن' : 'ابنة') . ' #' . $order . ' — ' . $c['full_name_ar'] . ' (' . (int) ($c['age_years'] ?? 0) . ' سنة)',
......@@ -178,6 +191,7 @@ final class BillingService
'entity_type' => 'children',
'entity_id' => (int) $c['id'],
'category' => 'addition',
'breakdown' => $breakdown,
];
}
......@@ -188,6 +202,12 @@ final class BillingService
[$memberId]
);
foreach ($temps as $t) {
$breakdown = null;
if (!empty($t['fee_breakdown_json'])) {
$breakdown = json_decode($t['fee_breakdown_json'], true);
if (!is_array($breakdown)) $breakdown = null;
}
$items[] = [
'type' => 'temp_fee',
'label' => 'عضو مؤقت — ' . $t['full_name_ar'] . ' (' . $t['category'] . ')',
......@@ -197,6 +217,7 @@ final class BillingService
'entity_type' => 'temporary_members',
'entity_id' => (int) $t['id'],
'category' => 'addition',
'breakdown' => $breakdown,
];
}
} catch (\Throwable $e) {}
......
......@@ -302,6 +302,68 @@ $pendingAdditions ??= [];
</div>
<?php endif; ?>
<!-- ═══════════════════════════════════════════════ -->
<!-- POST-ACTIVATION PENDING ADDITIONS (for active members) -->
<!-- ═══════════════════════════════════════════════ -->
<?php if ($isActive && !empty($pendingAdditions)): ?>
<div class="card" style="margin-bottom:20px;border:2px solid #F59E0B;">
<div style="padding:15px 20px;background:#FFFBEB;border-bottom:1px solid #FDE68A;">
<h3 style="margin:0;color:#D97706;font-size:16px;">&#x23f3; إضافات في انتظار السداد</h3>
</div>
<div style="padding:20px;">
<table style="width:100%;font-size:14px;border-collapse:collapse;">
<thead>
<tr style="border-bottom:2px solid #E5E7EB;">
<th style="padding:10px;text-align:right;color:#6B7280;">البند</th>
<th style="padding:10px;text-align:left;color:#6B7280;width:120px;">المبلغ</th>
<th style="padding:10px;text-align:center;color:#6B7280;width:150px;">رقم الطلب</th>
<th style="padding:10px;text-align:center;color:#6B7280;width:100px;">الحالة</th>
</tr>
</thead>
<tbody>
<?php foreach ($pendingAdditions as $paKey => $pa): ?>
<?php
$paNotes = $pa['notes'] ? json_decode($pa['notes'], true) : null;
$paBreakdown = $paNotes['fee_breakdown'] ?? null;
$paIdx = 'pa-' . (int) $pa['id'];
?>
<tr style="border-bottom:1px solid #F3F4F6;<?= !empty($paBreakdown) ? 'cursor:pointer;' : '' ?>" <?= !empty($paBreakdown) ? 'onclick="toggleBillDetail(\'' . $paIdx . '\')"' : '' ?>>
<td style="padding:10px;">
<?= e($pa['description_ar'] ?? 'رسوم إضافة') ?>
<?php if (!empty($paBreakdown)): ?>
<small style="display:block;color:#0D7377;font-size:11px;">&#x25BC; اضغط لعرض التفاصيل</small>
<?php endif; ?>
</td>
<td style="padding:10px;text-align:left;font-weight:700;direction:ltr;color:#DC2626;"><?= money($pa['amount']) ?></td>
<td style="padding:10px;text-align:center;direction:ltr;font-size:12px;color:#6B7280;"><?= e($pa['request_number']) ?></td>
<td style="padding:10px;text-align:center;">
<span style="color:#D97706;font-weight:600;font-size:12px;">&#x1f4b3; في الخزينة</span>
</td>
</tr>
<?php if (!empty($paBreakdown)): ?>
<tr id="bill-detail-<?= $paIdx ?>" style="display:none;">
<td colspan="4" style="padding:0 10px 10px;">
<div style="background:#F9FAFB;border:1px solid #E5E7EB;border-radius:8px;padding:10px 14px;font-size:12px;">
<?php foreach ($paBreakdown as $line): ?>
<?php if (str_contains($line, '═══')): ?>
<hr style="border:0;border-top:1px solid #D1D5DB;margin:4px 0;">
<?php elseif (str_starts_with($line, '💵')): ?>
<div style="font-weight:700;color:#059669;font-size:13px;"><?= e($line) ?></div>
<?php else: ?>
<div style="color:#374151;padding:1px 0;"><?= e($line) ?></div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</td>
</tr>
<?php endif; ?>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<!-- ═══════════════════════════════════════════════ -->
<!-- MISSING STEPS — shows what still needs to happen -->
<!-- ═══════════════════════════════════════════════ -->
......@@ -553,12 +615,17 @@ $childClassLabels = ['included' => 'تابع مشمول', 'dependent_with_fee' =
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<div style="font-size:12px;color:#0D7377;font-weight:700;margin-bottom:10px;text-transform:uppercase;">&#x1f48d; الزوجات (<?= count($spouses) ?>)</div>
<div class="table-responsive"><table class="data-table" style="margin:0;"><thead><tr><th>#</th><th>الاسم</th><th>نوع العضوية</th><th>الرقم القومي</th><th>تاريخ الالتحاق</th><th>الرسوم</th><th>الحالة</th><th></th></tr></thead><tbody>
<?php foreach ($spouses as $s): ?>
<tr>
<?php foreach ($spouses as $sIdx => $s): ?>
<?php
$sFee = $s['addition_fee'] ?? '0.00';
$sHasBreakdown = !empty($s['fee_breakdown_json']);
$sBreakdown = $sHasBreakdown ? json_decode($s['fee_breakdown_json'], true) : null;
if (!is_array($sBreakdown)) $sBreakdown = null;
?>
<tr <?= $sBreakdown ? 'style="cursor:pointer;" onclick="toggleBillDetail(\'spouse-' . $sIdx . '\')"' : '' ?>>
<td><?= (int) $s['spouse_order'] ?></td>
<td style="font-weight:600;"><?= e($s['full_name_ar']) ?></td>
<td><?php
$sFee = $s['addition_fee'] ?? '0.00';
$sTypeLabel = (bccomp($sFee, '0', 2) <= 0 && (int) $s['spouse_order'] === 1) ? 'تابع مشمول' : 'تابع برسوم';
?><span style="background:#E0F2FE;color:#0369A1;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;"><?= $sTypeLabel ?></span></td>
<td style="direction:ltr;text-align:right;font-size:12px;"><?= e($s['national_id'] ?? '—') ?></td>
......@@ -567,6 +634,7 @@ $childClassLabels = ['included' => 'تابع مشمول', 'dependent_with_fee' =
$fee = $s['addition_fee'] ?? '0.00';
if (bccomp($fee, '0', 2) <= 0 && (int) $s['spouse_order'] === 1) echo '<span style="color:#059669;">مشمولة</span>';
else echo money($fee);
if ($sBreakdown) echo ' <small style="color:#0D7377;font-size:10px;">&#x25BC;</small>';
?></td>
<td><?php
$sKey = 'spouses:' . (int) $s['id'];
......@@ -582,6 +650,23 @@ $childClassLabels = ['included' => 'تابع مشمول', 'dependent_with_fee' =
<a href="/members/<?= (int) $member->id ?>/spouses/<?= (int) $s['id'] ?>" class="btn btn-sm btn-outline">عرض</a>
</td>
</tr>
<?php if ($sBreakdown): ?>
<tr id="bill-detail-spouse-<?= $sIdx ?>" style="display:none;">
<td colspan="8" style="padding:5px 10px 10px;">
<div style="background:#F9FAFB;border:1px solid #E5E7EB;border-radius:8px;padding:10px 14px;font-size:12px;">
<?php foreach ($sBreakdown as $line): ?>
<?php if (str_contains($line, '═══')): ?>
<hr style="border:0;border-top:1px solid #D1D5DB;margin:4px 0;">
<?php elseif (str_starts_with($line, '💵')): ?>
<div style="font-weight:700;color:#059669;font-size:13px;"><?= e($line) ?></div>
<?php else: ?>
<div style="color:#374151;padding:1px 0;"><?= e($line) ?></div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</td>
</tr>
<?php endif; ?>
<?php endforeach; ?>
</tbody></table></div>
</div>
......@@ -592,8 +677,13 @@ $childClassLabels = ['included' => 'تابع مشمول', 'dependent_with_fee' =
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<div style="font-size:12px;color:#0D7377;font-weight:700;margin-bottom:10px;text-transform:uppercase;">&#x1f476; الأبناء (<?= count($children) ?>)</div>
<div class="table-responsive"><table class="data-table" style="margin:0;"><thead><tr><th>#</th><th>الاسم</th><th>نوع العضوية</th><th>النوع</th><th>السن</th><th>التصنيف</th><th>الرسوم</th><th>الحالة</th><th></th></tr></thead><tbody>
<?php foreach ($children as $c): ?>
<tr>
<?php foreach ($children as $cIdx => $c): ?>
<?php
$cFee = $c['addition_fee'] ?? '0.00';
$cBreakdown = !empty($c['fee_breakdown_json']) ? json_decode($c['fee_breakdown_json'], true) : null;
if (!is_array($cBreakdown)) $cBreakdown = null;
?>
<tr <?= $cBreakdown ? 'style="cursor:pointer;" onclick="toggleBillDetail(\'child-' . $cIdx . '\')"' : '' ?>>
<td><?= (int) $c['child_order'] ?></td>
<td style="font-weight:600;"><?= e($c['full_name_ar']) ?></td>
<td><span style="background:#E0F2FE;color:#0369A1;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;"><?= $childClassLabels[$c['classification'] ?? 'included'] ?? 'تابع' ?></span></td>
......@@ -609,6 +699,7 @@ $childClassLabels = ['included' => 'تابع مشمول', 'dependent_with_fee' =
<td style="font-weight:600;"><?php
$fee = $c['addition_fee'] ?? '0.00';
echo bccomp($fee, '0', 2) <= 0 ? '<span style="color:#059669;">مشمول</span>' : money($fee);
if ($cBreakdown) echo ' <small style="color:#0D7377;font-size:10px;">&#x25BC;</small>';
?></td>
<td><?php
$cKey = 'children:' . (int) $c['id'];
......@@ -624,6 +715,23 @@ $childClassLabels = ['included' => 'تابع مشمول', 'dependent_with_fee' =
<a href="/members/<?= (int) $member->id ?>/children/<?= (int) $c['id'] ?>" class="btn btn-sm btn-outline">عرض</a>
</td>
</tr>
<?php if ($cBreakdown): ?>
<tr id="bill-detail-child-<?= $cIdx ?>" style="display:none;">
<td colspan="9" style="padding:5px 10px 10px;">
<div style="background:#F9FAFB;border:1px solid #E5E7EB;border-radius:8px;padding:10px 14px;font-size:12px;">
<?php foreach ($cBreakdown as $line): ?>
<?php if (str_contains($line, '═══')): ?>
<hr style="border:0;border-top:1px solid #D1D5DB;margin:4px 0;">
<?php elseif (str_starts_with($line, '💵')): ?>
<div style="font-weight:700;color:#059669;font-size:13px;"><?= e($line) ?></div>
<?php else: ?>
<div style="color:#374151;padding:1px 0;"><?= e($line) ?></div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</td>
</tr>
<?php endif; ?>
<?php endforeach; ?>
</tbody></table></div>
</div>
......@@ -634,8 +742,13 @@ $childClassLabels = ['included' => 'تابع مشمول', 'dependent_with_fee' =
<div style="padding:15px 20px;">
<div style="font-size:12px;color:#0D7377;font-weight:700;margin-bottom:10px;text-transform:uppercase;">&#x1f464; الأعضاء المؤقتون (<?= count($temporaries) ?>)</div>
<div class="table-responsive"><table class="data-table" style="margin:0;"><thead><tr><th>الاسم</th><th>نوع العضوية</th><th>الصلة</th><th>النوع</th><th>السن</th><th>الرسوم</th><th>الحالة</th><th></th></tr></thead><tbody>
<?php foreach ($temporaries as $t): ?>
<tr>
<?php foreach ($temporaries as $tIdx => $t): ?>
<?php
$tFee = $t['addition_fee'] ?? '0.00';
$tBreakdown = !empty($t['fee_breakdown_json']) ? json_decode($t['fee_breakdown_json'], true) : null;
if (!is_array($tBreakdown)) $tBreakdown = null;
?>
<tr <?= $tBreakdown ? 'style="cursor:pointer;" onclick="toggleBillDetail(\'temp-' . $tIdx . '\')"' : '' ?>>
<td style="font-weight:600;"><?= e($t['full_name_ar']) ?></td>
<td><span style="background:#E0F2FE;color:#0369A1;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;">تابع مؤقت</span></td>
<td style="font-size:12px;"><?= $categoryLabels[$t['category']] ?? e($t['category']) ?></td>
......@@ -644,6 +757,7 @@ $childClassLabels = ['included' => 'تابع مشمول', 'dependent_with_fee' =
<td style="font-weight:600;"><?php
$fee = $t['addition_fee'] ?? '0.00';
echo bccomp($fee, '0', 2) <= 0 ? '<span style="color:#059669;">مشمول</span>' : money($fee);
if ($tBreakdown) echo ' <small style="color:#0D7377;font-size:10px;">&#x25BC;</small>';
?></td>
<td><?php
$tKey = 'temporary_members:' . (int) $t['id'];
......@@ -659,6 +773,23 @@ $childClassLabels = ['included' => 'تابع مشمول', 'dependent_with_fee' =
<a href="/members/<?= (int) $member->id ?>/temporary/<?= (int) $t['id'] ?>" class="btn btn-sm btn-outline">عرض</a>
</td>
</tr>
<?php if ($tBreakdown): ?>
<tr id="bill-detail-temp-<?= $tIdx ?>" style="display:none;">
<td colspan="8" style="padding:5px 10px 10px;">
<div style="background:#F9FAFB;border:1px solid #E5E7EB;border-radius:8px;padding:10px 14px;font-size:12px;">
<?php foreach ($tBreakdown as $line): ?>
<?php if (str_contains($line, '═══')): ?>
<hr style="border:0;border-top:1px solid #D1D5DB;margin:4px 0;">
<?php elseif (str_starts_with($line, '💵')): ?>
<div style="font-weight:700;color:#059669;font-size:13px;"><?= e($line) ?></div>
<?php else: ?>
<div style="color:#374151;padding:1px 0;"><?= e($line) ?></div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</td>
</tr>
<?php endif; ?>
<?php endforeach; ?>
</tbody></table></div>
</div>
......
......@@ -12,6 +12,7 @@ use App\Modules\Spouses\Models\Spouse;
use App\Modules\Spouses\Services\SpouseFeeCalculator;
use App\Modules\Members\Services\NationalIdParser;
use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Cashier\Services\PaymentRequestService;
class SpouseController extends Controller
......@@ -150,6 +151,8 @@ class SpouseController extends Controller
$totalFee = $feeCalc['total_fee'] ?? '0.00';
$hasFee = bccomp($totalFee, '0', 2) > 0;
$breakdownJson = !empty($feeCalc['breakdown']) ? json_encode($feeCalc['breakdown'], JSON_UNESCAPED_UNICODE) : null;
$spouse = Spouse::create([
'member_id' => (int) $memberId,
'spouse_order' => $spouseOrder,
......@@ -172,6 +175,7 @@ class SpouseController extends Controller
'join_date' => date('Y-m-d'),
'classification' => 'working',
'addition_fee' => $totalFee,
'fee_breakdown_json' => $breakdownJson,
'status' => $hasFee ? 'pending_payment' : 'active',
]);
......@@ -184,6 +188,21 @@ class SpouseController extends Controller
$genderWord = $requiredGender === 'male' ? 'الزوج' : 'الزوجة';
if ($hasFee && !empty($member['membership_number'])) {
$breakdown = $feeCalc['breakdown'] ?? [];
PaymentRequestService::createRequest([
'member_id' => (int) $memberId,
'amount' => $totalFee,
'payment_type' => 'addition_fee',
'related_entity_type' => 'spouses',
'related_entity_id' => (int) $spouse->id,
'description_ar' => 'رسوم إضافة ' . $genderWord . ' — ' . trim($data['full_name_ar']),
'notes' => json_encode(['fee_breakdown' => $breakdown], JSON_UNESCAPED_UNICODE),
]);
return $this->redirect("/members/{$memberId}")
->withSuccess("تم إضافة {$genderWord} — الرسوم: " . money($totalFee) . ' — تم إرسالها للخزينة تلقائياً');
}
if ($hasFee) {
return $this->redirect("/members/{$memberId}")
->withSuccess("تم إضافة {$genderWord} — الرسوم: " . money($totalFee) . ' — سيتم تحصيلها ضمن الفاتورة المجمعة');
......
......@@ -21,7 +21,7 @@ class Spouse extends Model
'age_years', 'age_months', 'gender', 'nationality', 'religion',
'qualification_id', 'occupation', 'work_address', 'work_phone', 'mobile',
'marriage_date', 'join_date', 'classification', 'addition_fee',
'fee_receipt_number', 'status', 'photo_path',
'fee_breakdown_json', 'fee_receipt_number', 'status', 'photo_path',
];
public static function getForMember(int $memberId): array
......
......@@ -11,6 +11,7 @@ use App\Core\EventBus;
use App\Modules\Temporary\Models\TemporaryMember;
use App\Modules\Temporary\Services\TemporaryFeeCalculator;
use App\Modules\Members\Services\NationalIdParser;
use App\Modules\Cashier\Services\PaymentRequestService;
class TemporaryController extends Controller
......@@ -133,6 +134,8 @@ class TemporaryController extends Controller
$totalFee = $feeCalc['total_fee'] ?? $feeCalc['fee'] ?? '0.00';
$hasFee = bccomp($totalFee, '0', 2) > 0;
$breakdownJson = !empty($feeCalc['breakdown']) ? json_encode($feeCalc['breakdown'], JSON_UNESCAPED_UNICODE) : null;
$temp = TemporaryMember::create([
'member_id' => (int) $memberId,
'category' => $category,
......@@ -149,6 +152,7 @@ class TemporaryController extends Controller
'has_championship' => !empty($data['has_championship']) ? 1 : 0,
'disability_documentation' => !empty($data['disability_documentation']) ? 1 : 0,
'addition_fee' => $totalFee,
'fee_breakdown_json' => $breakdownJson,
'can_separate' => TemporaryFeeCalculator::canSeparate($category) ? 1 : 0,
'can_get_independent' => TemporaryFeeCalculator::canGetIndependent($category) ? 1 : 0,
'status' => $hasFee ? 'pending_payment' : 'active',
......@@ -162,6 +166,21 @@ class TemporaryController extends Controller
'fee' => $totalFee,
]);
if ($hasFee && !empty($member['membership_number'])) {
$breakdown = $feeCalc['breakdown'] ?? [];
PaymentRequestService::createRequest([
'member_id' => (int) $memberId,
'amount' => $totalFee,
'payment_type' => 'addition_fee',
'related_entity_type' => 'temporary_members',
'related_entity_id' => (int) $temp->id,
'description_ar' => 'رسوم إضافة عضو مؤقت — ' . trim($data['full_name_ar']),
'notes' => json_encode(['fee_breakdown' => $breakdown], JSON_UNESCAPED_UNICODE),
]);
return $this->redirect("/members/{$memberId}")
->withSuccess('تم إضافة العضو المؤقت — الرسوم: ' . money($totalFee) . ' — تم إرسالها للخزينة تلقائياً');
}
if ($hasFee) {
return $this->redirect("/members/{$memberId}")
->withSuccess('تم إضافة العضو المؤقت — الرسوم: ' . money($totalFee) . ' — سيتم تحصيلها ضمن الفاتورة المجمعة');
......
......@@ -19,7 +19,8 @@ class TemporaryMember extends Model
'national_id', 'passport_number', 'date_of_birth',
'age_years', 'age_months', 'gender', 'nationality',
'relationship_to_member', 'has_championship', 'disability_documentation',
'addition_fee', 'fee_receipt_number', 'can_separate', 'can_get_independent',
'addition_fee', 'fee_breakdown_json', 'fee_receipt_number',
'can_separate', 'can_get_independent',
'status', 'photo_path', 'notes',
];
......
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE spouses ADD COLUMN fee_breakdown_json TEXT NULL AFTER addition_fee;
ALTER TABLE children ADD COLUMN fee_breakdown_json TEXT NULL AFTER addition_fee;
ALTER TABLE temporary_members ADD COLUMN fee_breakdown_json TEXT NULL AFTER addition_fee
",
'down' => "
ALTER TABLE spouses DROP COLUMN fee_breakdown_json;
ALTER TABLE children DROP COLUMN fee_breakdown_json;
ALTER TABLE temporary_members DROP COLUMN fee_breakdown_json
",
];
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