99 lines
4.2 KiB
PHP
99 lines
4.2 KiB
PHP
<?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";
|