Commit 5b48c8ed authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix 5 issues: void reversal, join_date, stepchild removal, form fee + annual sub

1. Payment void now reverts entity status: added payment.voided listener that
   sets dependents back to pending_payment (or reverts member to payment_pending
   if membership payment is voided with no other valid payment)
2. Added join_date column to children and temporary_members tables (migration),
   set on activation. Display in family tree tables.
3. Removed stepchild (ابن/ابنة الزوج) from children relationship dropdown
4. Form fee (570) now includes annual subscription on post-activation additions
   (restored bcadd with getAnnualSubscriptionForAddition)
5. Added join_date to Child and TemporaryMember model fillables
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 1626311b
......@@ -24,6 +24,60 @@ PermissionRegistry::register('cashier', [
'cashier.cancel_request' => ['ar' => 'إلغاء طلب دفع', 'en' => 'Cancel Payment Request'],
]);
// When a payment is voided, revert entity status back to pending_payment
EventBus::listen('payment.voided', function (array $data) {
try {
$db = \App\Core\App::getInstance()->db();
$paymentId = (int) ($data['payment_id'] ?? 0);
$memberId = (int) ($data['member_id'] ?? 0);
$paymentType = $data['payment_type'] ?? '';
if ($paymentId <= 0 || $memberId <= 0) return;
$payment = $db->selectOne("SELECT * FROM payments WHERE id = ?", [$paymentId]);
if (!$payment) return;
$entityType = $payment['related_entity_type'] ?? null;
$entityId = (int) ($payment['related_entity_id'] ?? 0);
if ($paymentType === 'addition_fee' && $entityType && $entityId > 0) {
$validTables = ['spouses', 'children', 'temporary_members'];
if (in_array($entityType, $validTables, true)) {
$db->update($entityType, [
'status' => 'pending_payment',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ? AND is_archived = 0', [$entityId]);
\App\Core\Logger::info("Reverted {$entityType} #{$entityId} to pending_payment after payment void");
}
}
if (in_array($paymentType, ['membership_fee', 'down_payment'], true)) {
$otherPayment = $db->selectOne(
"SELECT id FROM payments WHERE member_id = ? AND payment_type IN ('membership_fee','down_payment') AND is_voided = 0 AND id != ? LIMIT 1",
[$memberId, $paymentId]
);
if (!$otherPayment) {
$member = \App\Modules\Members\Models\Member::find($memberId);
if ($member && $member->status === 'active') {
$member->update([
'status' => 'payment_pending',
'membership_number' => null,
]);
foreach (['spouses', 'children', 'temporary_members'] as $depTable) {
$db->query(
"UPDATE `{$depTable}` SET status = 'pending_payment', updated_at = NOW() WHERE member_id = ? AND status = 'active' AND is_archived = 0",
[$memberId]
);
}
\App\Core\Logger::info("Reverted member #{$memberId} to payment_pending after membership payment void");
}
}
}
} catch (\Throwable $e) {
\App\Core\Logger::error("payment.voided listener failed: " . $e->getMessage(), ['data' => $data]);
}
});
EventBus::listen('payment_request.completed', function (array $data) {
try {
$db = \App\Core\App::getInstance()->db();
......@@ -106,11 +160,9 @@ EventBus::listen('payment_request.completed', function (array $data) {
if (!$hasSeparateRequest) {
$updateData = [
'status' => 'active',
'join_date' => date('Y-m-d'),
'updated_at' => date('Y-m-d H:i:s'),
];
if ($depTable === 'spouses') {
$updateData['join_date'] = date('Y-m-d');
}
$db->update($depTable, $updateData, '`id` = ?', [(int) $dep['id']]);
}
}
......@@ -162,6 +214,7 @@ EventBus::listen('payment_request.completed', function (array $data) {
if ($paymentType === 'addition_fee' && $entityType === 'children') {
$db->update('children', [
'status' => 'active',
'join_date' => date('Y-m-d'),
'fee_receipt_number' => $receiptNumber ?: null,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$entityId]);
......@@ -181,6 +234,7 @@ EventBus::listen('payment_request.completed', function (array $data) {
if ($paymentType === 'addition_fee' && $entityType === 'temporary_members') {
$db->update('temporary_members', [
'status' => 'active',
'join_date' => date('Y-m-d'),
'fee_receipt_number' => $receiptNumber ?: null,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$entityId]);
......
......@@ -21,7 +21,7 @@ class Child extends Model
'age_years', 'age_months', 'gender', 'relationship',
'school_faculty', 'nationality', 'classification',
'addition_fee', 'fee_breakdown_json', 'fee_receipt_number', 'status',
'is_frozen', 'frozen_at', 'frozen_reason', 'photo_path', 'remarks',
'join_date', 'is_frozen', 'frozen_at', 'frozen_reason', 'photo_path', 'remarks',
];
public static function getForMember(int $memberId): array
......
......@@ -31,7 +31,6 @@
<option value="">-- اختر --</option>
<option value="son" <?= old('relationship') === 'son' ? 'selected' : '' ?>>ابن</option>
<option value="daughter" <?= old('relationship') === 'daughter' ? 'selected' : '' ?>>ابنة</option>
<option value="stepchild" <?= old('relationship') === 'stepchild' ? 'selected' : '' ?>>ابن/ابنة الزوج</option>
</select>
</div>
<div class="form-group">
......
......@@ -44,7 +44,10 @@ final class FormFeeService
}
$feeData = RuleEngine::get('FORM_ADDITION_FEE');
return ($feeData && isset($feeData['amount'])) ? $feeData['amount'] : ServicePrice::getPrice('SVC_ADDITION_FORM', '570.00');
$formAmount = ($feeData && isset($feeData['amount'])) ? $feeData['amount'] : ServicePrice::getPrice('SVC_ADDITION_FORM', '570.00');
$annualSub = self::getAnnualSubscriptionForAddition();
return bcadd($formAmount, $annualSub, 2);
}
/**
......
......@@ -685,7 +685,7 @@ $childClassLabels = ['included' => 'تابع مشمول', 'dependent_with_fee' =
<!-- Children -->
<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>
<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 $cIdx => $c): ?>
<?php
$cFee = $c['addition_fee'] ?? '0.00';
......@@ -698,13 +698,7 @@ $childClassLabels = ['included' => 'تابع مشمول', 'dependent_with_fee' =
<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>
<td><?= $c['gender'] === 'male' ? 'ذكر' : 'أنثى' ?></td>
<td><?= (int) ($c['age_years'] ?? 0) ?></td>
<td style="font-size:12px;"><?= match($c['classification'] ?? '') {
'included' => '<span style="color:#059669;">مشمول</span>',
'dependent_with_fee' => '<span style="color:#D97706;">برسوم</span>',
'frozen' => '<span style="color:#6B7280;">مجمد</span>',
'temporary' => '<span style="color:#3B82F6;">مؤقت</span>',
default => e($c['classification'] ?? '—')
} ?></td>
<td style="font-size:12px;"><?= !empty($c['join_date']) ? e($c['join_date']) : '<span style="color:#D97706;">—</span>' ?></td>
<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);
......@@ -721,8 +715,7 @@ $childClassLabels = ['included' => 'تابع مشمول', 'dependent_with_fee' =
<?php endif; ?>
</td>
<td style="white-space:nowrap;">
<a href="/members/<?= (int) $member->id ?>/children/<?= (int) $c['id'] ?>" class="btn btn-sm btn-outline">عرض</a>
</td>
<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;">
......@@ -750,7 +743,7 @@ $childClassLabels = ['included' => 'تابع مشمول', 'dependent_with_fee' =
<!-- Temporary Members -->
<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>
<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 ($temporaries as $tIdx => $t): ?>
<?php
$tFee = $t['addition_fee'] ?? '0.00';
......@@ -763,6 +756,7 @@ $childClassLabels = ['included' => 'تابع مشمول', 'dependent_with_fee' =
<td style="font-size:12px;"><?= $categoryLabels[$t['category']] ?? e($t['category']) ?></td>
<td><?= $t['gender'] === 'male' ? 'ذكر' : 'أنثى' ?></td>
<td><?= (int) ($t['age_years'] ?? 0) ?></td>
<td style="font-size:12px;"><?= !empty($t['join_date']) ? e($t['join_date']) : '<span style="color:#D97706;">—</span>' ?></td>
<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);
......
......@@ -21,7 +21,7 @@ class TemporaryMember extends Model
'relationship_to_member', 'has_championship', 'disability_documentation',
'addition_fee', 'fee_breakdown_json', 'fee_receipt_number',
'can_separate', 'can_get_independent',
'status', 'photo_path', 'notes',
'status', 'join_date', 'photo_path', 'notes',
];
public static function getForMember(int $memberId): array
......
<?php
declare(strict_types=1);
return function (\App\Core\Database $db): void {
$tables = ['children', 'temporary_members'];
foreach ($tables as $table) {
$exists = $db->selectOne(
"SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = 'join_date'",
[$table]
);
if (!$exists) {
$db->raw("ALTER TABLE `{$table}` ADD COLUMN `join_date` DATE NULL AFTER `status`");
}
}
};
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