Commit 35e90bcb authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix membership status not reverting on payment queue cancellation

- Add payment_request.cancelled listener that reverts member/dependent to
  payment_pending when their payment is cancelled from the queue
- Allow cancellation of completed payment requests (voids linked payment)
- Add cancel button for completed items in queue UI
- Fix self-healing logic to not re-activate dependents whose payment was
  intentionally cancelled (checks for cancelled requests without a valid
  completed replacement)
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 16c0f194
......@@ -174,11 +174,20 @@ final class PaymentRequestService
if (!$request) {
return ['success' => false, 'error' => 'طلب الدفع غير موجود'];
}
if ($request['status'] === 'completed') {
return ['success' => false, 'error' => 'لا يمكن إلغاء طلب مكتمل'];
}
$now = date('Y-m-d H:i:s');
// If completed, also void the linked payment
if ($request['status'] === 'completed' && !empty($request['payment_id'])) {
$paymentResult = \App\Modules\Payments\Services\PaymentService::voidPayment(
(int) $request['payment_id'],
$reason ?: 'إلغاء طلب الدفع من الخزينة'
);
if (!$paymentResult['success']) {
return ['success' => false, 'error' => 'فشل إلغاء الدفعة المرتبطة: ' . ($paymentResult['error'] ?? '')];
}
}
$db->update('payment_requests', [
'status' => 'cancelled',
'is_voided' => 1,
......@@ -189,9 +198,11 @@ final class PaymentRequestService
], '`id` = ?', [$requestId]);
EventBus::dispatch('payment_request.cancelled', [
'request_id' => $requestId,
'member_id' => (int) $request['member_id'],
'payment_type' => $request['payment_type'],
'request_id' => $requestId,
'member_id' => (int) $request['member_id'],
'payment_type' => $request['payment_type'],
'related_entity_type' => $request['related_entity_type'] ?? null,
'related_entity_id' => (int) ($request['related_entity_id'] ?? 0),
]);
return ['success' => true];
......
......@@ -143,6 +143,12 @@
<?php if (!empty($r['receipt_id'])): ?>
<a href="/receipts/<?= (int)$r['receipt_id'] ?>/print" target="_blank" class="btn btn-sm btn-outline" style="font-size:11px;margin-top:4px;">&#x1f5a8; طباعة</a>
<?php endif; ?>
<form method="POST" action="/cashier/<?= (int)$r['id'] ?>/cancel" style="display:inline;margin-top:4px;">
<?= csrf_field() ?>
<input type="hidden" name="reason" value="">
<button type="submit" class="btn btn-sm" style="font-size:11px;color:#DC2626;border:1px solid #DC2626;background:transparent;"
onclick="var r=prompt('سبب إلغاء الدفعة:');if(!r)return false;this.form.reason.value=r;return confirm('سيتم إلغاء الدفعة وإرجاع حالة العضوية. متأكد؟');">إلغاء الدفعة</button>
</form>
<?php endif; ?>
</td>
</tr>
......
......@@ -78,6 +78,79 @@ EventBus::listen('payment.voided', function (array $data) {
}
});
// When a payment request is cancelled from the queue, revert member/dependent status
EventBus::listen('payment_request.cancelled', function (array $data) {
try {
$db = \App\Core\App::getInstance()->db();
$memberId = (int) ($data['member_id'] ?? 0);
$paymentType = $data['payment_type'] ?? '';
$entityType = $data['related_entity_type'] ?? null;
$entityId = (int) ($data['related_entity_id'] ?? 0);
if ($memberId <= 0) return;
// Membership fee cancellation: revert member if no other valid payment exists
if (in_array($paymentType, ['membership_fee', 'down_payment'], true)) {
$otherValidPayment = $db->selectOne(
"SELECT id FROM payments WHERE member_id = ? AND payment_type IN ('membership_fee','down_payment') AND is_voided = 0 LIMIT 1",
[$memberId]
);
if (!$otherValidPayment) {
$member = \App\Modules\Members\Models\Member::find($memberId);
if ($member && in_array($member->status, ['active', 'frozen', 'suspended'], true)) {
$member->update([
'status' => 'payment_pending',
'membership_number' => null,
]);
foreach (['spouses', 'children', 'temporary_members'] as $depTable) {
$db->query(
"UPDATE `{$depTable}` SET status = 'pending_payment', join_date = NULL, updated_at = NOW() WHERE member_id = ? AND status IN ('active','frozen') AND is_archived = 0",
[$memberId]
);
}
\App\Core\Logger::info("Reverted member #{$memberId} to payment_pending after payment request cancellation");
}
}
}
// Addition fee cancellation: revert the specific dependent
if ($paymentType === 'addition_fee' && $entityType && $entityId > 0) {
$validTables = ['spouses', 'children', 'temporary_members'];
if (in_array($entityType, $validTables, true)) {
$otherValidRequest = $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 = 'completed' AND is_voided = 0 LIMIT 1",
[$memberId, $entityType, $entityId]
);
if (!$otherValidRequest) {
$db->update($entityType, [
'status' => 'pending_payment',
'join_date' => null,
'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 request cancellation");
}
}
}
// Separation/divorce/death/waiver fee cancellation: revert entity status
$entityRevertMap = [
'separation_fee' => 'transfer_requests',
'divorce_fee' => 'divorce_cases',
'death_fee' => 'death_cases',
'waiver_fee' => 'waiver_requests',
];
if (isset($entityRevertMap[$paymentType]) && $entityId > 0) {
$table = $entityRevertMap[$paymentType];
$db->update($table, [
'status' => 'requested',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ? AND status = ?', [$entityId, 'fee_paid']);
}
} catch (\Throwable $e) {
\App\Core\Logger::error("payment_request.cancelled listener failed: " . $e->getMessage(), ['data' => $data]);
}
});
EventBus::listen('payment_request.completed', function (array $data) {
try {
$db = \App\Core\App::getInstance()->db();
......
......@@ -163,7 +163,15 @@ class MemberController extends Controller
"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) {
$hasCompletedPayment = $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 = 'completed' AND is_voided = 0 LIMIT 1",
[(int) $id, $tbl, (int) $dep['id']]
);
$hasCancelled = !$hasCompletedPayment ? $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 = 'cancelled' ORDER BY id DESC LIMIT 1",
[(int) $id, $tbl, (int) $dep['id']]
) : null;
if (!$hasPending && !$hasCancelled) {
$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']]);
......
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