Commit 194da186 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Overhaul subscription display: FIFO enforcement, fine calculations, year grouping

- Add FIFO payment validation: must pay oldest year first before newer years
- Add OverdueFineApplicator::applyForMember() for on-demand fine recalculation
- Rewrite view with year-grouped sections, fine breakdown panels, totals
- Add data migration to fix corrupted rows (paid_amount with no payment_id)
- Show fine calculation details (percentage × base = amount, from rules engine)
- Disable pay buttons for non-oldest years with Arabic tooltip
- Summary cards showing total debt, fines, and years overdue
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 4b76c4b3
...@@ -11,6 +11,7 @@ use App\Core\EventBus; ...@@ -11,6 +11,7 @@ use App\Core\EventBus;
use App\Modules\Subscriptions\Models\Subscription; use App\Modules\Subscriptions\Models\Subscription;
use App\Modules\Subscriptions\Services\SubscriptionGenerator; use App\Modules\Subscriptions\Services\SubscriptionGenerator;
use App\Modules\Subscriptions\Services\SubscriptionCalculator; use App\Modules\Subscriptions\Services\SubscriptionCalculator;
use App\Modules\Subscriptions\Services\OverdueFineApplicator;
use App\Modules\Payments\Services\PaymentService; use App\Modules\Payments\Services\PaymentService;
class SubscriptionController extends Controller class SubscriptionController extends Controller
...@@ -42,15 +43,54 @@ class SubscriptionController extends Controller ...@@ -42,15 +43,54 @@ class SubscriptionController extends Controller
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $memberId]); $member = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $memberId]);
if (!$member) return $this->redirect('/members')->withError('العضو غير موجود'); if (!$member) return $this->redirect('/members')->withError('العضو غير موجود');
$lateFine = OverdueFineApplicator::applyForMember((int) $memberId);
$subscriptions = Subscription::getForMember((int) $memberId); $subscriptions = Subscription::getForMember((int) $memberId);
$unpaidYears = Subscription::getUnpaidYears((int) $memberId); $unpaidYears = Subscription::getUnpaidYears((int) $memberId);
$lateFine = SubscriptionCalculator::calculateLateFine((int) $memberId, self::currentFinancialYear());
$grouped = [];
foreach ($subscriptions as $s) {
$grouped[$s['financial_year']][] = $s;
}
$yearTotals = [];
foreach ($grouped as $fy => $rows) {
$totalAmount = '0.00';
$totalPaid = '0.00';
$totalFine = '0.00';
$totalDev = '0.00';
$allPaid = true;
$anyPaid = false;
foreach ($rows as $r) {
$totalAmount = bcadd($totalAmount, $r['total_amount'], 2);
$totalPaid = bcadd($totalPaid, $r['paid_amount'] ?? '0', 2);
$totalFine = bcadd($totalFine, $r['fine_amount'] ?? '0', 2);
$totalDev = bcadd($totalDev, $r['development_fee'] ?? '0', 2);
if ($r['status'] !== 'paid' && $r['status'] !== 'exempt') $allPaid = false;
if ($r['status'] === 'paid') $anyPaid = true;
}
$yearTotals[$fy] = [
'total_amount' => $totalAmount,
'total_paid' => $totalPaid,
'total_fine' => $totalFine,
'total_dev' => $totalDev,
'year_status' => $allPaid ? 'paid' : ($anyPaid ? 'partial' : 'unpaid'),
'count' => count($rows),
];
}
$oldestUnpaidYear = null;
foreach ($unpaidYears as $uy) {
$oldestUnpaidYear = $uy['financial_year'];
break;
}
return $this->view('Subscriptions.Views.member-subscriptions', [ return $this->view('Subscriptions.Views.member-subscriptions', [
'member' => $member, 'member' => $member,
'subscriptions' => $subscriptions, 'grouped' => $grouped,
'unpaidYears' => $unpaidYears, 'yearTotals' => $yearTotals,
'lateFine' => $lateFine, 'lateFine' => $lateFine,
'oldestUnpaidYear' => $oldestUnpaidYear,
]); ]);
} }
...@@ -61,6 +101,15 @@ class SubscriptionController extends Controller ...@@ -61,6 +101,15 @@ class SubscriptionController extends Controller
if (!$sub) return $this->redirect('/subscriptions')->withError('الاشتراك غير موجود'); if (!$sub) return $this->redirect('/subscriptions')->withError('الاشتراك غير موجود');
if ($sub['status'] === 'paid') return $this->redirect('/subscriptions')->withError('الاشتراك مدفوع بالفعل'); if ($sub['status'] === 'paid') return $this->redirect('/subscriptions')->withError('الاشتراك مدفوع بالفعل');
$olderUnpaid = $db->selectOne(
"SELECT financial_year FROM subscriptions WHERE member_id = ? AND financial_year < ? AND status IN ('pending','overdue') LIMIT 1",
[(int) $sub['member_id'], $sub['financial_year']]
);
if ($olderUnpaid) {
return $this->redirect("/members/{$sub['member_id']}/subscriptions")
->withError('يجب سداد اشتراكات السنة الأقدم أولاً (' . $olderUnpaid['financial_year'] . ')');
}
$amount = bcsub(bcadd($sub['total_amount'], $sub['fine_amount'], 2), $sub['paid_amount'], 2); $amount = bcsub(bcadd($sub['total_amount'], $sub['fine_amount'], 2), $sub['paid_amount'], 2);
$data = $request->all(); $data = $request->all();
......
...@@ -70,6 +70,27 @@ final class OverdueFineApplicator ...@@ -70,6 +70,27 @@ final class OverdueFineApplicator
return $results; return $results;
} }
public static function applyForMember(int $memberId): array
{
$db = App::getInstance()->db();
$currentFY = self::currentFinancialYear();
$calc = SubscriptionCalculator::calculateLateFine($memberId, $currentFY);
foreach ($calc['details'] as $detail) {
if ($detail['in_grace'] ?? false) continue;
if (bccomp($detail['fine_amount'], '0', 2) <= 0) continue;
$db->query(
"UPDATE subscriptions SET fine_amount = ?, status = 'overdue', updated_at = NOW()
WHERE member_id = ? AND financial_year = ? AND status IN ('pending','overdue')",
[$detail['fine_amount'], $memberId, $detail['financial_year']]
);
}
return $calc;
}
public static function expireReinstatements(): array public static function expireReinstatements(): array
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
......
...@@ -3,41 +3,213 @@ ...@@ -3,41 +3,213 @@
<?php $__template->section('page_actions'); ?><a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">← العودة للعضو</a><?php $__template->endSection(); ?> <?php $__template->section('page_actions'); ?><a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">← العودة للعضو</a><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?> <?php $__template->section('content'); ?>
<?php
$totalDebt = '0.00';
foreach ($yearTotals as $yt) {
if ($yt['year_status'] !== 'paid') {
$remaining = bcsub(bcadd($yt['total_amount'], $yt['total_fine'], 2), $yt['total_paid'], 2);
$totalDebt = bcadd($totalDebt, $remaining, 2);
}
}
$fineDetailsByYear = [];
foreach ($lateFine['details'] ?? [] as $d) {
$fineDetailsByYear[$d['financial_year']] = $d;
}
?>
<?php if ($lateFine['should_drop']): ?> <?php if ($lateFine['should_drop']): ?>
<div style="padding:15px;background:#FEF2F2;border:2px solid #DC2626;border-radius:8px;margin-bottom:20px;color:#DC2626;font-weight:600;"> <div style="padding:18px;background:#FEF2F2;border:2px solid #DC2626;border-radius:10px;margin-bottom:20px;">
⚠ هذا العضو متأخر <?= (int) $lateFine['years_unpaid'] ?> سنوات — يجب إسقاط العضوية <div style="display:flex;align-items:center;gap:10px;">
<span style="font-size:28px;"></span>
<div>
<strong style="color:#DC2626;font-size:16px;">تحذير: هذا العضو متأخر <?= (int) $lateFine['years_overdue'] ?> سنوات — يجب إسقاط العضوية</strong>
<p style="margin:4px 0 0;color:#991B1B;font-size:13px;">تجاوز الحد الأقصى للتأخير (<?= (int) $lateFine['max_years'] ?> سنوات)</p>
</div>
</div>
</div>
<?php endif; ?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;margin-bottom:20px;">
<div class="card" style="padding:18px;text-align:center;background:#FFF7ED;border:1px solid #FED7AA;">
<div style="font-size:12px;color:#92400E;margin-bottom:4px;">إجمالي المديونية</div>
<div style="font-size:22px;font-weight:700;color:#D97706;"><?= money($totalDebt) ?></div>
</div>
<div class="card" style="padding:18px;text-align:center;background:#FEF2F2;border:1px solid #FECACA;">
<div style="font-size:12px;color:#991B1B;margin-bottom:4px;">إجمالي الغرامات</div>
<div style="font-size:22px;font-weight:700;color:#DC2626;"><?= money($lateFine['total_fine']) ?></div>
</div>
<div class="card" style="padding:18px;text-align:center;background:#EFF6FF;border:1px solid #BFDBFE;">
<div style="font-size:12px;color:#1E40AF;margin-bottom:4px;">سنوات متأخرة</div>
<div style="font-size:22px;font-weight:700;color:#1D4ED8;"><?= (int) $lateFine['years_unpaid'] ?></div>
</div>
</div> </div>
<?php elseif (!empty($lateFine['details'])): ?>
<div style="padding:15px;background:#FFF7ED;border:1px solid #FED7AA;border-radius:8px;margin-bottom:20px;"> <?php if (!empty($lateFine['details'])): ?>
<strong style="color:#D97706;">غرامات تأخير: <?= money($lateFine['total_fine']) ?></strong><?= (int) $lateFine['years_unpaid'] ?> سنة متأخرة <div class="card" style="padding:12px 18px;margin-bottom:20px;background:#FFFBEB;border:1px solid #FDE68A;">
<div style="font-size:12px;color:#92400E;margin-bottom:6px;font-weight:600;">📐 سلم الغرامات (من محرك القواعد)</div>
<div style="display:flex;gap:12px;flex-wrap:wrap;font-size:12px;color:#78350F;">
<span>السنة 1: <strong>10%</strong></span>
<span>السنة 2: <strong>50%</strong></span>
<span>السنة 3: <strong>100%</strong></span>
<span>السنة 4: <strong>200%</strong></span>
<span>السنة 5: <strong>300%</strong></span>
<span style="color:#DC2626;">بعد 5 سنوات: إسقاط</span>
</div>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div class="card"><div class="table-responsive"><table class="data-table"><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 ($grouped as $fy => $rows):
<?php foreach ($subscriptions as $s): ?> $yt = $yearTotals[$fy] ?? null;
<tr> if (!$yt) continue;
<td style="font-weight:600;"><?= e($s['financial_year']) ?></td> $yearId = str_replace('/', '-', $fy);
<td><?= match($s['person_type']) { 'member' => 'عضو', 'spouse' => 'زوجة', 'child' => 'ابن', 'temporary' => 'مؤقت', default => $s['person_type'] } ?></td> $isPayable = ($oldestUnpaidYear === $fy);
<td style="font-size:13px;"><?= e($s['person_name'] ?? '—') ?></td> $isPaid = ($yt['year_status'] === 'paid');
<td><?= money($s['base_amount']) ?></td> $fineDetail = $fineDetailsByYear[$fy] ?? null;
<td style="font-size:12px;"><?= money($s['development_fee']) ?></td> $yearRemaining = bcsub(bcadd($yt['total_amount'], $yt['total_fine'], 2), $yt['total_paid'], 2);
<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> $borderColor = $isPaid ? '#059669' : ($isPayable ? '#D97706' : '#D1D5DB');
<td><span style="color:<?= match($s['status']) { 'paid' => '#059669', 'exempt' => '#0284C7', 'overdue' => '#DC2626', default => '#D97706' } ?>;font-weight:600;"><?= match($s['status']) { 'paid' => 'مدفوع', 'pending' => 'معلق', 'overdue' => 'متأخر', 'exempt' => 'معفى', default => $s['status'] } ?></span></td> $headerBg = $isPaid ? '#F0FDF4' : ($isPayable ? '#FFF7ED' : '#F9FAFB');
<td> $statusColor = match($yt['year_status']) { 'paid' => '#059669', 'partial' => '#0284C7', default => '#DC2626' };
<?php if (in_array($s['status'], ['pending', 'overdue'])): ?> $statusLabel = match($yt['year_status']) { 'paid' => 'مدفوع بالكامل', 'partial' => 'مدفوع جزئياً', default => 'متأخر' };
<div style="display:flex;gap:5px;"> ?>
<?php if (can('subscription.collect')): ?> <div class="card" style="margin-bottom:16px;border:2px solid <?= $borderColor ?>;border-radius:10px;overflow:hidden;">
<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('تسجيل دفع الاشتراك؟')">دفع</button></form> <div onclick="toggleYear('<?= $yearId ?>')" style="cursor:pointer;padding:16px 20px;background:<?= $headerBg ?>;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;">
<div style="display:flex;align-items:center;gap:10px;">
<span id="year-arrow-<?= $yearId ?>" style="font-size:14px;color:#6B7280;"><?= $isPaid ? '◀' : '▼' ?></span>
<strong style="font-size:16px;color:#1F2937;"><?= e($fy) ?></strong>
<span style="font-size:12px;color:<?= $statusColor ?>;font-weight:600;"><?= $statusLabel ?></span>
<?php if ($isPayable && !$isPaid): ?>
<span style="font-size:11px;background:#FEF3C7;color:#92400E;padding:2px 8px;border-radius:4px;">الأولوية</span>
<?php endif; ?>
</div>
<div style="display:flex;gap:16px;font-size:13px;color:#4B5563;">
<span>المبلغ: <strong><?= money($yt['total_amount']) ?></strong></span>
<?php if (bccomp($yt['total_fine'], '0', 2) > 0): ?>
<span style="color:#DC2626;">الغرامة: <strong><?= money($yt['total_fine']) ?></strong></span>
<?php endif; ?>
<?php if (bccomp($yt['total_paid'], '0', 2) > 0): ?>
<span style="color:#059669;">المدفوع: <strong><?= money($yt['total_paid']) ?></strong></span>
<?php endif; ?> <?php endif; ?>
<?php if (can('subscription.exempt')): ?> <?php if (!$isPaid): ?>
<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> <span style="color:#D97706;">المتبقي: <strong><?= money($yearRemaining) ?></strong></span>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php else: ?><?php endif; ?> </div>
</td>
</tr> <div id="year-body-<?= $yearId ?>" style="<?= $isPaid ? 'display:none;' : '' ?>">
<?php if ($fineDetail && bccomp($fineDetail['fine_amount'], '0', 2) > 0): ?>
<div style="padding:12px 20px;background:#FEF2F2;border-top:1px solid <?= $borderColor ?>;">
<div onclick="toggleFineDetail('<?= $yearId ?>')" style="cursor:pointer;display:flex;align-items:center;gap:8px;">
<span style="font-size:14px;">📊</span>
<span style="font-size:13px;font-weight:600;color:#991B1B;">تفاصيل غرامة هذه السنة</span>
<span style="font-size:11px;color:#6B7280;">(اضغط للتفاصيل)</span>
</div>
<div id="fine-detail-<?= $yearId ?>" style="display:none;margin-top:10px;padding:10px;background:#fff;border-radius:6px;border:1px solid #FECACA;">
<table style="width:100%;font-size:13px;color:#374151;">
<tr><td style="padding:4px 8px;color:#6B7280;">سنوات التأخير:</td><td style="padding:4px 8px;font-weight:600;"><?= (int) $fineDetail['years_overdue'] ?> سنة</td></tr>
<tr><td style="padding:4px 8px;color:#6B7280;">نسبة الغرامة:</td><td style="padding:4px 8px;font-weight:600;"><?= $fineDetail['fine_percentage'] ?>% <span style="font-size:11px;color:#9CA3AF;">(من قيمة الاشتراك الإجمالي)</span></td></tr>
<tr><td style="padding:4px 8px;color:#6B7280;">قيمة الاشتراك:</td><td style="padding:4px 8px;"><?= money($fineDetail['unpaid']) ?></td></tr>
<tr style="background:#FEF2F2;"><td style="padding:4px 8px;color:#991B1B;font-weight:600;">الحساب:</td><td style="padding:4px 8px;font-weight:700;color:#DC2626;"><?= money($fineDetail['unpaid']) ?> × <?= $fineDetail['fine_percentage'] ?>% = <?= money($fineDetail['fine_amount']) ?></td></tr>
<tr><td style="padding:4px 8px;color:#6B7280;">المرجع:</td><td style="padding:4px 8px;font-size:11px;font-family:monospace;">LATE_SUB_FINE_YEAR_<?= (int) $fineDetail['years_overdue'] + 1 ?></td></tr>
</table>
</div>
</div>
<?php elseif ($fineDetail && ($fineDetail['in_grace'] ?? false)): ?>
<div style="padding:10px 20px;background:#F0FDF4;border-top:1px solid <?= $borderColor ?>;font-size:12px;color:#065F46;">
✓ ضمن فترة السماح (<?= (int) $lateFine['grace_months'] ?> أشهر من بداية السنة المالية) — لا توجد غرامة
</div>
<?php endif; ?>
<div style="padding:0 20px 16px;">
<table class="data-table" style="margin-top:12px;">
<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 ($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:#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>
<tfoot>
<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;"><?= 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>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (empty($subscriptions)): ?><tr><td colspan="9" style="text-align:center;padding:30px;color:#6B7280;">لا توجد اشتراكات</td></tr><?php endif; ?>
</tbody></table></div></div> <?php if (empty($grouped)): ?>
<?php $__template->endSection(); ?> <div class="card" style="padding:40px;text-align:center;color:#6B7280;">لا توجد اشتراكات</div>
\ No newline at end of file <?php endif; ?>
<script>
function toggleYear(id) {
var el = document.getElementById('year-body-' + id);
if (!el) return;
var show = el.style.display === 'none';
el.style.display = show ? '' : 'none';
var arrow = document.getElementById('year-arrow-' + id);
if (arrow) arrow.textContent = show ? '▼' : '◀';
}
function toggleFineDetail(id) {
var el = document.getElementById('fine-detail-' + id);
if (!el) return;
el.style.display = el.style.display === 'none' ? '' : 'none';
}
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
return [
'up' => "UPDATE subscriptions SET paid_amount = 0.00, updated_at = NOW() WHERE paid_amount > 0 AND payment_id IS NULL AND status IN ('pending', 'overdue')",
'down' => "SELECT 1",
];
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