Commit 334264d0 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Enforce cheque submission before membership activation on installments

- Members paying via installments (down_payment) now enter 'pending_cheques'
  status instead of being immediately activated
- Membership number is only assigned after ALL cheques are uploaded
  (1 cheque per installment month)
- Dependents (spouses/children) are NOT auto-included in installment
  payments — they must pay addition_fee separately
- New installment_cheques table stores serial, bank, date, scan file
- New ChequeController + ChequeService for upload flow
- Employee uploads cheque scans from installment plan page
- Member show page displays pending_cheques banner with upload link
- Auto-activates member once all required cheques are submitted
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 615c9041
<?php
declare(strict_types=1);
namespace App\Modules\Installments\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Installments\Services\ChequeService;
class ChequeController extends Controller
{
public function index(Request $request, string $planId): Response
{
$this->authorize('installment.view');
$db = App::getInstance()->db();
$plan = $db->selectOne(
"SELECT ip.*, m.full_name_ar as member_name, m.membership_number, m.status as member_status, m.form_number
FROM installment_plans ip
JOIN members m ON m.id = ip.member_id
WHERE ip.id = ? AND ip.status = 'active'",
[(int) $planId]
);
if (!$plan) {
return $this->redirect('/installments')->withError('خطة التقسيط غير موجودة أو غير نشطة');
}
$cheques = $db->select(
"SELECT ic.*, e.full_name_ar as uploaded_by_name
FROM installment_cheques ic
LEFT JOIN employees e ON e.id = ic.uploaded_by
WHERE ic.installment_plan_id = ?
ORDER BY ic.cheque_date ASC",
[(int) $planId]
);
$requiredCount = (int) $plan['number_of_months'];
$uploadedCount = count($cheques);
$remainingCount = max(0, $requiredCount - $uploadedCount);
$allSubmitted = $remainingCount === 0;
return $this->view('Installments.Views.cheques', [
'plan' => $plan,
'cheques' => $cheques,
'requiredCount' => $requiredCount,
'uploadedCount' => $uploadedCount,
'remainingCount' => $remainingCount,
'allSubmitted' => $allSubmitted,
]);
}
public function store(Request $request, string $planId): Response
{
$this->authorize('installment.create_plan');
$db = App::getInstance()->db();
$plan = $db->selectOne(
"SELECT id, member_id, number_of_months FROM installment_plans WHERE id = ? AND status = 'active'",
[(int) $planId]
);
if (!$plan) {
return $this->redirect('/installments')->withError('خطة التقسيط غير موجودة');
}
$chequeNumber = trim((string) $request->post('cheque_number', ''));
$bankName = trim((string) $request->post('bank_name', ''));
$chequeDate = trim((string) $request->post('cheque_date', ''));
$chequeAmount = trim((string) $request->post('cheque_amount', ''));
$notes = trim((string) $request->post('notes', ''));
$errors = [];
if ($chequeNumber === '') {
$errors[] = 'رقم الشيك مطلوب';
} elseif (mb_strlen($chequeNumber) > 50) {
$errors[] = 'رقم الشيك يجب ألا يتجاوز 50 حرف';
}
if ($bankName === '') {
$errors[] = 'اسم البنك مطلوب';
} elseif (mb_strlen($bankName) > 200) {
$errors[] = 'اسم البنك يجب ألا يتجاوز 200 حرف';
}
if ($chequeDate === '') {
$errors[] = 'تاريخ الشيك مطلوب';
}
if ($chequeAmount === '' || (float) $chequeAmount <= 0) {
$errors[] = 'مبلغ الشيك مطلوب ويجب أن يكون أكبر من صفر';
}
// Check uploaded count
$currentCount = (int) $db->selectOne(
"SELECT COUNT(*) as cnt FROM installment_cheques WHERE installment_plan_id = ?",
[(int) $planId]
)['cnt'];
if ($currentCount >= (int) $plan['number_of_months']) {
return $this->redirect("/installments/{$planId}/cheques")->withError('تم رفع جميع الشيكات المطلوبة بالفعل');
}
// File upload validation
$file = $_FILES['scan_file'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
$errors[] = 'صورة/مسح الشيك مطلوبة';
} else {
$allowedMimes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$detectedMime = $finfo->file($file['tmp_name']);
if (!in_array($detectedMime, $allowedMimes, true)) {
$errors[] = 'نوع الملف غير مسموح (يُقبل: JPG, PNG, WebP, PDF)';
}
if ($file['size'] > 10 * 1024 * 1024) {
$errors[] = 'حجم الملف يتجاوز الحد الأقصى (10 ميجابايت)';
}
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$alerts = array_map(fn($msg) => ['type' => 'error', 'message' => $msg], $errors);
$session->flash('_alerts', $alerts);
$session->flash('_old_input', $request->all());
return $this->redirect("/installments/{$planId}/cheques");
}
// Store the file
$uploadDir = App::getInstance()->basePath() . '/storage/uploads/cheques/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$ext = pathinfo($file['name'], PATHINFO_EXTENSION) ?: 'jpg';
$storedFilename = 'chq_' . $planId . '_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$destination = $uploadDir . $storedFilename;
if (!move_uploaded_file($file['tmp_name'], $destination)) {
return $this->redirect("/installments/{$planId}/cheques")->withError('فشل في حفظ الملف');
}
$employeeId = App::getInstance()->session()->get('employee_id');
$db->insert('installment_cheques', [
'installment_plan_id' => (int) $planId,
'cheque_number' => $chequeNumber,
'bank_name' => $bankName,
'cheque_date' => $chequeDate,
'cheque_amount' => (float) $chequeAmount,
'scan_path' => 'storage/uploads/cheques/' . $storedFilename,
'scan_original_name' => $file['name'],
'uploaded_by' => $employeeId ? (int) $employeeId : null,
'notes' => $notes ?: null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
// Check if all cheques now submitted → activate member
if (ChequeService::allChequesSubmitted((int) $planId)) {
$result = ChequeService::activateMemberAfterCheques((int) $planId);
if ($result['success'] && !($result['already_active'] ?? false)) {
return $this->redirect("/installments/{$planId}/cheques")
->withSuccess('تم رفع الشيك وتفعيل العضوية بنجاح — رقم العضوية: ' . ($result['membership_number'] ?? ''));
}
}
$remaining = (int) $plan['number_of_months'] - ($currentCount + 1);
$msg = 'تم رفع الشيك بنجاح';
if ($remaining > 0) {
$msg .= " — المتبقي: {$remaining} شيك";
}
return $this->redirect("/installments/{$planId}/cheques")->withSuccess($msg);
}
}
...@@ -6,4 +6,8 @@ return [ ...@@ -6,4 +6,8 @@ return [
['GET', '/installments/create/{memberId}', 'Installments\Controllers\InstallmentController@create', ['auth'], 'installment.create'], ['GET', '/installments/create/{memberId}', 'Installments\Controllers\InstallmentController@create', ['auth'], 'installment.create'],
['GET', '/installments/{id}', 'Installments\Controllers\InstallmentController@show', ['auth'], 'installment.view'], ['GET', '/installments/{id}', 'Installments\Controllers\InstallmentController@show', ['auth'], 'installment.view'],
['POST', '/installments/{planId}/pay/{scheduleId}', 'Installments\Controllers\InstallmentController@payInstallment', ['auth', 'csrf'], 'installment.pay'], ['POST', '/installments/{planId}/pay/{scheduleId}', 'Installments\Controllers\InstallmentController@payInstallment', ['auth', 'csrf'], 'installment.pay'],
// Cheque uploads
['GET', '/installments/{planId}/cheques', 'Installments\Controllers\ChequeController@index', ['auth'], 'installment.view'],
['POST', '/installments/{planId}/cheques', 'Installments\Controllers\ChequeController@store', ['auth', 'csrf'], 'installment.create_plan'],
]; ];
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Installments\Services;
use App\Core\App;
use App\Core\Logger;
use App\Modules\Members\Services\MemberNumberGenerator;
use App\Core\EventBus;
final class ChequeService
{
/**
* Check if all required cheques have been uploaded for an installment plan.
*/
public static function allChequesSubmitted(int $planId): bool
{
$db = App::getInstance()->db();
$plan = $db->selectOne(
"SELECT number_of_months FROM installment_plans WHERE id = ? AND status = 'active'",
[$planId]
);
if (!$plan) {
return false;
}
$uploadedCount = (int) $db->selectOne(
"SELECT COUNT(*) as cnt FROM installment_cheques WHERE installment_plan_id = ?",
[$planId]
)['cnt'];
return $uploadedCount >= (int) $plan['number_of_months'];
}
/**
* After all cheques are submitted, activate the member and assign membership number.
*/
public static function activateMemberAfterCheques(int $planId): array
{
$db = App::getInstance()->db();
$plan = $db->selectOne(
"SELECT id, member_id FROM installment_plans WHERE id = ? AND status = 'active'",
[$planId]
);
if (!$plan) {
return ['success' => false, 'error' => 'خطة التقسيط غير موجودة'];
}
$memberId = (int) $plan['member_id'];
$member = $db->selectOne(
"SELECT id, status, membership_number FROM members WHERE id = ? AND is_archived = 0",
[$memberId]
);
if (!$member) {
return ['success' => false, 'error' => 'العضو غير موجود'];
}
if ($member['status'] !== 'pending_cheques') {
if ($member['status'] === 'active') {
return ['success' => true, 'already_active' => true];
}
return ['success' => false, 'error' => 'حالة العضو لا تسمح بالتفعيل'];
}
if (!self::allChequesSubmitted($planId)) {
return ['success' => false, 'error' => 'لم يتم رفع جميع الشيكات المطلوبة'];
}
$number = MemberNumberGenerator::assign($memberId);
$db->update('members', [
'status' => 'active',
'activated_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$memberId]);
EventBus::dispatch('member.activated', [
'member_id' => $memberId,
'membership_number' => $number,
'payment_method' => 'installment',
]);
Logger::info("ChequeService: member #{$memberId} activated after all cheques submitted for plan #{$planId}");
return ['success' => true, 'membership_number' => $number];
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شيكات الأقساط — <?= e($plan['member_name']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/installments/<?= (int) $plan['id'] ?>" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> خطة التقسيط</a>
<a href="/members/<?= (int) $plan['member_id'] ?>" class="btn btn-outline"><i data-lucide="user" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> العضو</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Status Banner -->
<?php if ($allSubmitted && $plan['member_status'] === 'active'): ?>
<div style="background:#DCFCE7;border:1px solid #86EFAC;border-radius:8px;padding:15px 20px;margin-bottom:20px;display:flex;align-items:center;gap:10px;">
<i data-lucide="check-circle" style="width:24px;height:24px;color:#16A34A;"></i>
<div>
<div style="font-weight:700;color:#15803D;">تم تسليم جميع الشيكات وتفعيل العضوية</div>
<div style="font-size:13px;color:#166534;">رقم العضوية: <?= e($plan['membership_number'] ?? '—') ?></div>
</div>
</div>
<?php elseif ($plan['member_status'] === 'pending_cheques'): ?>
<div style="background:#FEF3C7;border:1px solid #FCD34D;border-radius:8px;padding:15px 20px;margin-bottom:20px;display:flex;align-items:center;gap:10px;">
<i data-lucide="alert-circle" style="width:24px;height:24px;color:#D97706;"></i>
<div>
<div style="font-weight:700;color:#92400E;">العضوية معلقة — في انتظار تسليم الشيكات</div>
<div style="font-size:13px;color:#78350F;">المطلوب: <?= $requiredCount ?> شيك | تم رفع: <?= $uploadedCount ?> | المتبقي: <strong><?= $remainingCount ?></strong></div>
</div>
</div>
<?php endif; ?>
<!-- Progress -->
<div class="card" style="padding:20px;margin-bottom:20px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<span style="font-size:14px;font-weight:600;color:#374151;">التقدم</span>
<span style="font-size:14px;color:#6B7280;"><?= $uploadedCount ?> / <?= $requiredCount ?></span>
</div>
<div style="background:#E5E7EB;border-radius:999px;height:10px;overflow:hidden;">
<?php $pct = $requiredCount > 0 ? round(($uploadedCount / $requiredCount) * 100) : 0; ?>
<div style="background:<?= $pct >= 100 ? '#16A34A' : '#0D7377' ?>;height:100%;width:<?= $pct ?>%;transition:width 0.3s;border-radius:999px;"></div>
</div>
</div>
<div style="display:grid;grid-template-columns:<?= $remainingCount > 0 ? '1fr 1fr' : '1fr' ?>;gap:20px;">
<!-- Upload Form -->
<?php if ($remainingCount > 0): ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="upload" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">رفع شيك جديد</h3>
</div>
<form method="POST" action="/installments/<?= (int) $plan['id'] ?>/cheques" enctype="multipart/form-data" style="padding:20px;">
<?= csrf_field() ?>
<div class="form-group" style="margin-bottom:15px;">
<label class="form-label">رقم الشيك <span style="color:#DC2626;">*</span></label>
<input type="text" name="cheque_number" class="form-input" required maxlength="50" value="<?= e(old('cheque_number') ?? '') ?>" placeholder="مثال: 123456789">
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="form-label">البنك <span style="color:#DC2626;">*</span></label>
<input type="text" name="bank_name" class="form-input" required maxlength="200" value="<?= e(old('bank_name') ?? '') ?>" placeholder="مثال: البنك الأهلي المصري">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:15px;">
<div class="form-group">
<label class="form-label">تاريخ الشيك <span style="color:#DC2626;">*</span></label>
<input type="date" name="cheque_date" class="form-input" required value="<?= e(old('cheque_date') ?? '') ?>">
</div>
<div class="form-group">
<label class="form-label">مبلغ الشيك <span style="color:#DC2626;">*</span></label>
<input type="number" name="cheque_amount" class="form-input" required step="0.01" min="0.01" value="<?= e(old('cheque_amount') ?? $plan['monthly_payment'] ?? '') ?>" placeholder="0.00">
</div>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="form-label">صورة/مسح الشيك <span style="color:#DC2626;">*</span></label>
<input type="file" name="scan_file" class="form-input" required accept=".jpg,.jpeg,.png,.webp,.pdf">
<small style="color:#6B7280;font-size:12px;">JPG, PNG, WebP, PDF — الحد الأقصى 10 ميجابايت</small>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="form-label">ملاحظات</label>
<input type="text" name="notes" class="form-input" maxlength="500" value="<?= e(old('notes') ?? '') ?>">
</div>
<button type="submit" class="btn btn-primary" style="width:100%;"><i data-lucide="upload" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> رفع الشيك</button>
</form>
</div>
<?php endif; ?>
<!-- Uploaded Cheques -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="file-check" style="width:18px;height:18px;color:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">الشيكات المُسلمة (<?= $uploadedCount ?>)</h3>
</div>
<?php if (!empty($cheques)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr><th>#</th><th>رقم الشيك</th><th>البنك</th><th>التاريخ</th><th>المبلغ</th><th>الملف</th><th>رفع بواسطة</th></tr>
</thead>
<tbody>
<?php foreach ($cheques as $i => $chq): ?>
<tr>
<td><?= $i + 1 ?></td>
<td style="font-weight:600;"><code style="background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($chq['cheque_number']) ?></code></td>
<td><?= e($chq['bank_name']) ?></td>
<td><?= e($chq['cheque_date']) ?></td>
<td style="font-weight:600;color:#059669;"><?= money($chq['cheque_amount']) ?></td>
<td><a href="/<?= e($chq['scan_path']) ?>" target="_blank" style="color:#0D7377;"><i data-lucide="eye" style="width:14px;height:14px;vertical-align:middle;"></i> عرض</a></td>
<td style="font-size:12px;color:#6B7280;"><?= e($chq['uploaded_by_name'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;">
<i data-lucide="file-x" style="width:40px;height:40px;color:#D1D5DB;margin-bottom:10px;"></i>
<p style="color:#6B7280;margin:0;">لم يتم رفع أي شيكات بعد</p>
</div>
<?php endif; ?>
</div>
</div>
<script>document.addEventListener('DOMContentLoaded',function(){if(typeof lucide!=='undefined')lucide.createIcons();});</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?> <?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>خطة تقسيط #<?= (int) $plan['id'] ?><?php $__template->endSection(); ?> <?php $__template->section('title'); ?>خطة تقسيط #<?= (int) $plan['id'] ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?> <?php $__template->section('page_actions'); ?>
<a href="/installments/<?= (int) $plan['id'] ?>/cheques" class="btn btn-primary"><i data-lucide="file-check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> شيكات الأقساط</a>
<a href="/members/<?= (int) $plan['member_id'] ?>" class="btn btn-outline">← العضو</a> <a href="/members/<?= (int) $plan['member_id'] ?>" class="btn btn-outline">← العضو</a>
<a href="/installments" class="btn btn-outline">← كل الأقساط</a> <a href="/installments" class="btn btn-outline">← كل الأقساط</a>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
......
...@@ -234,6 +234,9 @@ class MemberController extends Controller ...@@ -234,6 +234,9 @@ class MemberController extends Controller
'cancelledRequests' => $cancelledRequests, 'cancelledRequests' => $cancelledRequests,
'isSuperAdmin' => self::isSuperAdmin(), 'isSuperAdmin' => self::isSuperAdmin(),
'divorceTransfer' => $divorceTransfer, 'divorceTransfer' => $divorceTransfer,
'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,
]); ]);
} }
......
...@@ -58,6 +58,7 @@ class Member extends Model ...@@ -58,6 +58,7 @@ class Member extends Model
'accepted' => 'مقبول', 'accepted' => 'مقبول',
'rejected' => 'مرفوض', 'rejected' => 'مرفوض',
'payment_pending' => 'في انتظار السداد', 'payment_pending' => 'في انتظار السداد',
'pending_cheques' => 'في انتظار الشيكات',
'active' => 'فعال', 'active' => 'فعال',
'frozen' => 'مجمد', 'frozen' => 'مجمد',
'suspended' => 'موقوف', 'suspended' => 'موقوف',
...@@ -77,6 +78,7 @@ class Member extends Model ...@@ -77,6 +78,7 @@ class Member extends Model
'accepted' => '#059669', 'accepted' => '#059669',
'rejected' => '#DC2626', 'rejected' => '#DC2626',
'payment_pending' => '#D97706', 'payment_pending' => '#D97706',
'pending_cheques' => '#EA580C',
'active' => '#059669', 'active' => '#059669',
'frozen' => '#6B7280', 'frozen' => '#6B7280',
'suspended' => '#DC2626', 'suspended' => '#DC2626',
...@@ -96,6 +98,7 @@ class Member extends Model ...@@ -96,6 +98,7 @@ class Member extends Model
'accepted' => 'مقبول', 'accepted' => 'مقبول',
'rejected' => 'مرفوض', 'rejected' => 'مرفوض',
'payment_pending' => 'في انتظار السداد', 'payment_pending' => 'في انتظار السداد',
'pending_cheques' => 'في انتظار الشيكات',
'active' => 'فعال', 'active' => 'فعال',
'frozen' => 'مجمد', 'frozen' => 'مجمد',
'suspended' => 'موقوف', 'suspended' => 'موقوف',
......
...@@ -32,7 +32,7 @@ final class MembershipPaymentGuard ...@@ -32,7 +32,7 @@ final class MembershipPaymentGuard
return ['success' => false, 'error' => 'العضو غير موجود']; return ['success' => false, 'error' => 'العضو غير موجود'];
} }
$validStatuses = ['potential', 'payment_pending', 'accepted', 'under_review']; $validStatuses = ['potential', 'payment_pending', 'accepted', 'under_review', 'pending_cheques'];
if (!in_array($member['status'], $validStatuses, true)) { if (!in_array($member['status'], $validStatuses, true)) {
if ($member['status'] === 'active') { if ($member['status'] === 'active') {
return ['success' => true, 'already_active' => true]; return ['success' => true, 'already_active' => true];
...@@ -286,6 +286,16 @@ final class MembershipPaymentGuard ...@@ -286,6 +286,16 @@ final class MembershipPaymentGuard
$changes[] = 'activated (had valid payment but was stuck at payment_pending)'; $changes[] = 'activated (had valid payment but was stuck at payment_pending)';
} }
// pending_cheques is valid: member paid down_payment but hasn't submitted cheques yet
// Do NOT auto-activate — cheques must be submitted first via ChequeService
if (!$hasValidPayment && $member['status'] === 'pending_cheques') {
$db->update('members', [
'status' => 'payment_pending',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$memberId]);
$changes[] = 'reverted to payment_pending (was pending_cheques but no valid down_payment found)';
}
if (!$hasValidPayment && $member['status'] === 'active') { if (!$hasValidPayment && $member['status'] === 'active') {
$db->update('members', [ $db->update('members', [
'status' => 'payment_pending', 'status' => 'payment_pending',
......
...@@ -122,6 +122,22 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false)); ...@@ -122,6 +122,22 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false));
</div> </div>
</div> </div>
<!-- ═══════════════════════════════════════════════ -->
<!-- PENDING CHEQUES — member paid down payment but hasn't submitted cheques yet -->
<!-- ═══════════════════════════════════════════════ -->
<?php if ($member->status === 'pending_cheques' && !empty($installmentPlan)): ?>
<div class="card" style="margin-bottom:20px;padding:25px;background:linear-gradient(135deg,#FFF7ED,#FEF3C7);border:2px solid #EA580C;">
<div style="display:flex;align-items:center;gap:15px;">
<div style="font-size:40px;">📝</div>
<div style="flex:1;">
<h3 style="margin:0 0 5px;color:#9A3412;">في انتظار تسليم شيكات الأقساط</h3>
<p style="margin:0;color:#78350F;font-size:14px;">لا يمكن تفعيل العضوية أو إصدار رقم عضوية حتى يتم رفع جميع الشيكات المطلوبة (<?= (int) $installmentPlan['number_of_months'] ?> شيك)</p>
</div>
<a href="/installments/<?= (int) $installmentPlan['id'] ?>/cheques" class="btn btn-primary" style="white-space:nowrap;">رفع الشيكات</a>
</div>
</div>
<?php endif; ?>
<!-- ═══════════════════════════════════════════════ --> <!-- ═══════════════════════════════════════════════ -->
<!-- BILL / INVOICE — visible during initial phase --> <!-- BILL / INVOICE — visible during initial phase -->
<!-- ═══════════════════════════════════════════════ --> <!-- ═══════════════════════════════════════════════ -->
......
...@@ -286,18 +286,11 @@ class PaymentController extends Controller ...@@ -286,18 +286,11 @@ class PaymentController extends Controller
]); ]);
} }
// Assign number and activate // Installment path: member must submit cheques before activation
$number = MemberNumberGenerator::assign($memberId);
$db->update('members', [ $db->update('members', [
'status' => 'active', 'status' => 'pending_cheques',
'updated_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$memberId]); ], '`id` = ?', [$memberId]);
EventBus::dispatch('member.activated', [
'member_id' => $memberId,
'membership_number' => $number,
'payment_method' => 'installment',
]);
} }
} }
......
...@@ -25,10 +25,9 @@ final class PaymentLifecycleService ...@@ -25,10 +25,9 @@ final class PaymentLifecycleService
{ {
$result = MembershipPaymentGuard::activateMember($memberId, $paymentId); $result = MembershipPaymentGuard::activateMember($memberId, $paymentId);
// Always activate included dependents on successful membership payment. // Only auto-activate dependents for full cash payment.
// The collective payment covers all pending dependents regardless of // Installment members must pay addition_fee separately for each dependent.
// whether the member was just activated or was already active. if ($result['success'] && $paymentType !== 'down_payment') {
if ($result['success']) {
MembershipPaymentGuard::activateIncludedDependents($memberId, $paymentId); MembershipPaymentGuard::activateIncludedDependents($memberId, $paymentId);
} }
......
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE `installment_cheques` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`installment_plan_id` BIGINT UNSIGNED NOT NULL,
`cheque_number` VARCHAR(50) NOT NULL,
`bank_name` VARCHAR(200) NOT NULL,
`cheque_date` DATE NOT NULL,
`cheque_amount` DECIMAL(15,2) NOT NULL,
`scan_path` VARCHAR(500) NOT NULL,
`scan_original_name` VARCHAR(300) NULL,
`uploaded_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`uploaded_by` BIGINT UNSIGNED NULL,
`notes` VARCHAR(500) NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_ic_plan` (`installment_plan_id`),
INDEX `idx_ic_cheque_number` (`cheque_number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;",
'down' => "DROP TABLE IF EXISTS `installment_cheques`;",
];
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