Update codebase

This commit is contained in:
Hamza-Ayed
2026-08-09 16:56:13 +03:00
parent 95e2e4f35d
commit b64debaa88
1058 changed files with 164327 additions and 113928 deletions
+112
View File
@@ -0,0 +1,112 @@
<?php
/**
* cron_insurance_premiums.php — قيد أقساط التأمين
* ─────────────────────────────────────────────────────────────
* ‏يقيّد قسط كل وثيقة نشطة بلغت دورتها. لا يخصم من رصيد أحد:
*
* ‏رصيد السائق يعيش في payment_server لا في هذه القاعدة، والخصم
* ‏المباشر من هنا كان سيعني كتابةً عابرة للحدود بلا معاملة موحّدة —
* ‏أي خطر رصيد ناقص بلا قيد يقابله، أو قيد بلا خصم. فنقيّد استحقاقاً
* ‏هنا، ويقرأه محرك التسوية `cron_obligation_settlement.php`.
*
* ‏تحديث (٢٠٢٦-٠٨-٠٨): صار هذا الملف **مُصدِر استحقاق** في محرك
* ‏الالتزامات العام، فيكتب في `obligation_ledger` لا في
* ‏`insurance_premium_ledger`. سبب النقل أن الدفتر القديم لم يكن يقرأه
* ‏أحد إطلاقاً — «تقرأه التسوية» كان وعداً بلا منفّذ، وكل قسط قُيّد منذ
* ‏الإطلاق بقي معلّقاً بلا تحصيل. جداول التأمين تبقى كما هي شاهداً
* ‏تاريخياً، وقد رُحّلت بياناتها في
* ‏`migrations/2026_08_08_obligations_engine.sql`.
*
* ‏الحماية من الاحتساب المزدوج طبقتان:
* ١. `last_charged_on` في الالتزام — نفلتر به قبل العمل
* ٢. مفتاح فريد (obligation_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 {
// ‏المصدر الآن الالتزامات لا الوثائق. القيد على `kind = 'recurring'`
// ‏لا على المنتج التأميني تحديداً: أي منتج دوري يُضاف لاحقاً يُقيَّد
// ‏بالمنطق نفسه بلا سطر جديد هنا.
$st = $con->query("
SELECT o.id, o.driver_id, o.last_charged_on, o.cycle_amount,
pr.code AS product_code, pr.currency, pr.billing_cycle
FROM driver_obligations o
JOIN obligation_products pr ON pr.id = o.product_id
WHERE o.status = 'active'
AND pr.kind = 'recurring'
AND pr.billing_cycle <> 'none'
ORDER BY o.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 obligation_ledger
(obligation_id, driver_id, product_code, charge_date,
amount, amount_collected, amount_remaining, currency, status)
VALUES (?, ?, ?, ?, ?, 0, ?, ?, 'pending')
")->execute([
$policy['id'], $policy['driver_id'], $policy['product_code'], $chargeDate,
$policy['cycle_amount'], $policy['cycle_amount'], $policy['currency'],
]);
$con->prepare("
UPDATE driver_obligations 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_obligations 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";