Update: 2026-08-08 12:58:16

This commit is contained in:
Hamza-Ayed
2026-08-08 12:58:17 +03:00
parent 611b5e616d
commit fae0e4a38a
55 changed files with 5771 additions and 1820 deletions
+16 -12
View File
@@ -9,6 +9,7 @@
require_once __DIR__ . '/../connect.php';
require_once __DIR__ . '/eligibility.php';
require_once __DIR__ . '/../obligations/functions.php';
$driverId = $user_id ?? '';
if (empty($driverId) || ($role ?? '') !== 'driver') {
@@ -22,7 +23,12 @@ if (!$policy) {
$reason = filterRequest('reason') ?: 'بطلب السائق';
// ‏الوثيقة والالتزام يُغلقان معاً. التزام يبقى نشطاً بعد إلغاء وثيقته
// ‏يعني قسطاً يُقيَّد كل ليلة على سائقٍ بلا تغطية — عكس الثغرة السابقة
// ‏تماماً، وأسوأ منها: هناك مال يضيع على الشركة، وهنا مال يُؤخذ ظلماً.
try {
$con->beginTransaction();
// ‏الشرط على driver_id إلى جانب المعرّف: الوثيقة أتت من استعلام
// ‏مقيَّد بالسائق أصلاً، لكن الشرط هنا يجعل الاستعلام آمناً بذاته
// ‏لو أعيد استعماله يوماً في سياق آخر.
@@ -34,24 +40,22 @@ try {
$st->execute([mb_substr($reason, 0, 255), $policy['id'], $driverId]);
if ($st->rowCount() === 0) {
$con->rollBack();
jsonError('تعذّر إلغاء الوثيقة');
}
} catch (PDOException $e) {
obligationClose($con, $driverId, $policy['code'], $reason);
$con->commit();
} catch (Throwable $e) {
if ($con->inTransaction()) $con->rollBack();
error_log('[assurance/cancel] ' . $e->getMessage());
jsonError('Server error');
}
$pending = 0.0;
try {
$st = $con->prepare("
SELECT COALESCE(SUM(amount), 0) FROM insurance_premium_ledger
WHERE policy_id = ? AND status = 'pending'
");
$st->execute([$policy['id']]);
$pending = (float) $st->fetchColumn();
} catch (PDOException $e) {
error_log('[assurance/cancel] تعذّرت قراءة المستحق: ' . $e->getMessage());
}
// ‏المستحق يُقرأ من الدفتر الموحّد لا من دفتر التأمين القديم: القديم
// ‏مجمَّد بعد الترحيل، وقراءته تعني رقماً لا يتغيّر مهما سدّد السائق.
$pending = obligationPendingTotal($con, $driverId, $policy['code']);
error_log("[assurance] أُلغيت وثيقة #{$policy['id']} للسائق $driverId");
+19 -12
View File
@@ -32,28 +32,35 @@ $summary = ['pending_total' => 0.0, 'settled_total' => 0.0,
'currency' => $policy['currency']];
$entries = [];
// ‏المصدر هو الدفتر الموحّد لا دفتر التأمين القديم (المجمَّد بعد ترحيل
// ‏2026_08_08). المفتاح صار (السائق + رمز المنتج) بدل رقم الوثيقة:
// ‏الدفتر الموحّد لا يعرف الوثائق، يعرف التزامات.
try {
// ‏المستحق من `amount_remaining` والمحصَّل من `amount_collected`:
// ‏قيدٌ حُصِّل نصفه يظهر في الرقمين معاً بحصّته الصحيحة، بينما جمع
// ‏`amount` حسب الحالة كان سيعدّه كاملاً في أحدهما ويكذب في كليهما.
$st = $con->prepare("
SELECT status, SUM(amount) AS total
FROM insurance_premium_ledger
WHERE policy_id = ?
GROUP BY status
SELECT COALESCE(SUM(amount_remaining), 0) AS pending_total,
COALESCE(SUM(amount_collected), 0) AS settled_total
FROM obligation_ledger
WHERE driver_id = ? AND product_code = ?
");
$st->execute([$policy['id']]);
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $row) {
if ($row['status'] === 'pending') $summary['pending_total'] = (float) $row['total'];
if ($row['status'] === 'settled') $summary['settled_total'] = (float) $row['total'];
$st->execute([$driverId, $policy['code']]);
if ($row = $st->fetch(PDO::FETCH_ASSOC)) {
$summary['pending_total'] = (float) $row['pending_total'];
$summary['settled_total'] = (float) $row['settled_total'];
}
// ‏آخر ثلاثين قيداً: كافية لشهر يومي، وتمنع رداً ينمو بلا حد.
$st = $con->prepare("
SELECT charge_date, amount, currency, status, note
FROM insurance_premium_ledger
WHERE policy_id = ?
SELECT charge_date, amount, amount_collected, amount_remaining,
currency, status, note
FROM obligation_ledger
WHERE driver_id = ? AND product_code = ?
ORDER BY charge_date DESC
LIMIT 30
");
$st->execute([$policy['id']]);
$st->execute([$driverId, $policy['code']]);
$entries = $st->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log('[assurance/get] تعذّرت قراءة الدفتر: ' . $e->getMessage());
+20
View File
@@ -10,6 +10,7 @@
require_once __DIR__ . '/../connect.php';
require_once __DIR__ . '/eligibility.php';
require_once __DIR__ . '/../obligations/functions.php';
// ‏الهوية من الـJWT لا من الطلب — الأهلية معلومة عن السائق نفسه،
// ‏وقراءتها بمعرّف مُرسَل تكشف أرقام غيره.
@@ -37,10 +38,29 @@ try {
$current = assuranceActivePolicy($con, $driverId);
// ‏نداء واحد للمحفظة لكل الشاشة: الملخّص عن السائق لا عن الخطة.
// ‏بدونه كانت الشاشة تعرض «مؤهَّل» ثم يُرفض الطلب عند الضغط — وهو أسوأ
// ‏من الرفض المبكر: يعد السائق بشيء ثم يسحبه.
$incomeSummary = obligationWalletIncomeSummary($driverId);
$out = [];
foreach ($plans as $plan) {
$check = assuranceCheckEligibility($con, $driverId, $plan);
// ‏بوابة الائتمان تُقاس على منتج الالتزام المقابل للخطة. غيابه يعني
// ‏خطة لم تُرحَّل بعد — لا نحجبها ولا ندّعي أهليةً لها، بل نتركها
// ‏لحكم شروط الخطة وحدها ونسجّل الحالة.
$product = obligationProduct($con, $plan['code']);
if ($product) {
$gate = obligationCheckWalletIncome($product, $driverId, $incomeSummary);
if (!$gate['eligible']) {
$check['eligible'] = false;
$check['reasons'] = array_merge($check['reasons'], $gate['reasons']);
}
} else {
error_log("[assurance/plans] لا منتج التزام للخطة {$plan['code']}");
}
$out[] = [
'id' => (int) $plan['id'],
'code' => $plan['code'],
+34 -1
View File
@@ -9,6 +9,7 @@
require_once __DIR__ . '/../connect.php';
require_once __DIR__ . '/eligibility.php';
require_once __DIR__ . '/../obligations/functions.php';
$driverId = $user_id ?? '';
if (empty($driverId) || ($role ?? '') !== 'driver') {
@@ -49,7 +50,30 @@ if (!$check['eligible']) {
jsonError('لم تستوفِ شروط هذه الخطة بعد: ' . implode('، ', $check['reasons']));
}
// ‏بوابة الائتمان تُفحص هنا صراحةً رغم أن `obligationOpen` تفحصها أيضاً:
// ‏الفحص هناك حارسٌ أخير يرمي استثناءً، ورسالته تصل للسائق كخطأ عام.
// ‏السائق الذي يُرفض طلبه يستحق أن يعرف السبب وما يفعله ليستوفيه.
$product = obligationProduct($con, $plan['code']);
if (!$product) {
error_log("[assurance/subscribe] منتج الالتزام مفقود للخطة {$plan['code']}");
jsonError('تعذّر تفعيل الوثيقة حالياً');
}
$walletGate = obligationCheckWalletIncome($product, $driverId);
if (!$walletGate['eligible']) {
jsonError(
'التأمين يُخصم قسطه من أرباحك في المحفظة، ولذلك يشترط أن تمرّ أرباحك بها: '
. implode('، ', $walletGate['reasons'])
);
}
// ‏كتابتان في معاملة واحدة: الوثيقة (السجل التأميني الذي يقرأه التطبيق)
// ‏والالتزام (ما يراه محرك القيد والتسوية). وثيقة بلا التزام تعني تغطية
// ‏تُمنح ولا يُحصَّل قسطها أبداً — وهي بالضبط الثغرة التي جاء هذا المحرك
// ‏ليغلقها، فلا يجوز أن نعيد فتحها من باب الاشتراكات الجديدة.
try {
$con->beginTransaction();
$con->prepare("
INSERT INTO driver_insurance_policies
(driver_id, plan_id, status, started_at, rides_at_signup, rating_at_signup)
@@ -57,7 +81,16 @@ try {
")->execute([$driverId, $planId, $check['rides'], $check['rating']]);
$policyId = (int) $con->lastInsertId();
} catch (PDOException $e) {
obligationOpen($con, $driverId, $plan['code'], [
'cycle_amount' => (float) $plan['premium'],
'rides' => $check['rides'],
'rating' => $check['rating'],
]);
$con->commit();
} catch (Throwable $e) {
if ($con->inTransaction()) $con->rollBack();
error_log('[assurance/subscribe] تعذّر إنشاء الوثيقة: ' . $e->getMessage());
jsonError('تعذّر تفعيل الوثيقة حالياً');
}