Update: 2026-08-07 16:25:34
This commit is contained in:
@@ -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";
|
||||
Reference in New Issue
Block a user