Update: 2026-08-07 16:25:34

This commit is contained in:
Hamza-Ayed
2026-08-07 16:25:34 +03:00
parent 9338e76894
commit cdd36d4777
16 changed files with 744 additions and 13 deletions
+98
View File
@@ -0,0 +1,98 @@
<?php
/**
* cron_insurance_premiums.php — قيد أقساط التأمين
* ─────────────────────────────────────────────────────────────
* ‏يقيّد قسط كل وثيقة نشطة بلغت دورتها. لا يخصم من رصيد أحد:
*
* ‏رصيد السائق يعيش في payment_server لا في هذه القاعدة، والخصم
* ‏المباشر من هنا كان سيعني كتابةً عابرة للحدود بلا معاملة موحّدة —
* ‏أي خطر رصيد ناقص بلا قيد يقابله، أو قيد بلا خصم. فنقيّد استحقاقاً
* ‏هنا، وتقرأه التسوية.
*
* ‏الحماية من الاحتساب المزدوج طبقتان:
* ١. `last_charged_on` في الوثيقة — نفلتر به قبل العمل
* ٢. مفتاح فريد (policy_id, charge_date) في الدفتر — يحسم السباق
* لو عملت نسختان من الكرون معاً
*
* ‏الأولى وحدها لا تكفي: بين القراءة والكتابة نافذة، والمفتاح الفريد
* ‏هو ما يجعل تكرار القيد مستحيلاً لا نادراً.
*
* جدولة: مرة يومياً بعد منتصف الليل.
* 15 0 * * * docker compose exec -T php php /var/www/backend/bot/cron_insurance_premiums.php
*/
require_once __DIR__ . '/../core/bootstrap.php';
try {
$con = Database::get('main');
} catch (Exception $e) {
fwrite(STDERR, '[insurance] DB unavailable: ' . $e->getMessage() . "\n");
exit(1);
}
$today = date('Y-m-d');
// ‏الشهري يُقيَّد على أول الشهر لا على يوم الاشتراك: دفتر تتوزّع فيه
// ‏تواريخ القيد على ثلاثين يوماً يستحيل تسويته مع الشريك.
$monthStart = date('Y-m-01');
$charged = 0;
$skipped = 0;
try {
$st = $con->query("
SELECT p.id, p.driver_id, p.last_charged_on,
pl.premium, pl.currency, pl.billing_cycle
FROM driver_insurance_policies p
JOIN insurance_plans pl ON pl.id = p.plan_id
WHERE p.status = 'active'
ORDER BY p.id ASC
");
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $policy) {
$chargeDate = $policy['billing_cycle'] === 'monthly' ? $monthStart : $today;
// ‏الفلترة الرخيصة أولاً — تجنّبنا محاولة إدراج لكل وثيقة كل يوم.
if ($policy['last_charged_on'] === $chargeDate) {
$skipped++;
continue;
}
try {
$con->beginTransaction();
$con->prepare("
INSERT INTO insurance_premium_ledger
(policy_id, driver_id, charge_date, amount, currency, status)
VALUES (?, ?, ?, ?, ?, 'pending')
")->execute([
$policy['id'], $policy['driver_id'], $chargeDate,
$policy['premium'], $policy['currency'],
]);
$con->prepare("
UPDATE driver_insurance_policies SET last_charged_on = ? WHERE id = ?
")->execute([$chargeDate, $policy['id']]);
$con->commit();
$charged++;
} catch (PDOException $e) {
if ($con->inTransaction()) $con->rollBack();
// ‏23000 = خرق المفتاح الفريد: نسخة أخرى سبقتنا للقيد نفسه.
// ‏هذه ليست حالة خطأ — هي بالضبط ما صُمّم المفتاح لمنعه.
if ($e->getCode() === '23000') {
$con->prepare("
UPDATE driver_insurance_policies SET last_charged_on = ? WHERE id = ?
")->execute([$chargeDate, $policy['id']]);
$skipped++;
} else {
error_log("[insurance] تعذّر قيد قسط الوثيقة #{$policy['id']}: " . $e->getMessage());
}
}
}
} catch (PDOException $e) {
fwrite(STDERR, '[insurance] فشل المرور: ' . $e->getMessage() . "\n");
exit(1);
}
echo "[insurance] قُيِّد {$charged} قسطاً، تُخطّي {$skipped}\n";