Keep OTP phone numbers recoverable for customer-service follow-up

Storing the verification phone as a keyed HMAC made OTP lookups independent
of the encryption mode, but the hash is one-way — and customer service reads
those same rows to chase people who requested a code and never finished
registering. That workflow would have lost the number entirely.

The verification tables now carry both forms: phone_number holds the lookup
key, and a new phone_enc column holds the encrypted number, which is
decryptable when a human needs to call.

The two follow-up queries also compared the verification row against the
driver/passengers tables and the notes tables by matching ciphertext, which
only ever worked because encryption was deterministic. Under GCM every number
would have looked unregistered and every note would have disappeared. Both now
read the number from phone_enc and match on normalised plaintext, so they are
correct under either mode.

Rows written before phone_enc existed are skipped rather than shown without a
number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-25 16:27:29 +03:00
co-authored by Claude Opus 5
parent a1c19b052d
commit 39b5a7fc7f
4 changed files with 130 additions and 80 deletions
@@ -1,56 +1,61 @@
<?php
require_once __DIR__ . '/../connect.php';
/*
المطلوب:
- جلب كل أرقام الهاتف من phone_verification
والتي غير موجودة في جدول driver
- فك تشفير الهاتف والإيميل
- فقط السجلات خلال آخر 5 أيام
*/
/**
* السائقون الذين طلبوا رمز تحقق ولم يُكملوا التسجيل — لمتابعتهم.
*
* نفس علة النسخة السابقة: كانت تربط phone_verification.phone_number بعمود
* phone في جدول driver وبجدول الملاحظات عبر تساوي النص المشفّر، وهو ما يفشل
* لحظة أن يصبح التشفير عشوائياً. الرقم يُقرأ الآن من phone_enc وتتم المطابقة
* على القيم الأصلية.
*/
$sql = "
SELECT
pv.id,
pv.phone_number,
pv.created_at
FROM
phone_verification pv
LEFT JOIN driver d
ON pv.phone_number = d.phone
LEFT JOIN notesForDriverService n
ON pv.phone_number = n.phone
WHERE
d.phone IS NULL -- لم يقم بالتسجيل كسائق بعد
AND n.phone IS NULL -- لم يتم التواصل معه سابقاً (لا يوجد ملاحظات)
AND pv.created_at >= (NOW() - INTERVAL 6 DAY) -- تم الإنشاء خلال آخر 3 أيام
ORDER BY RAND()
LIMIT 1;
";
$sql = "SELECT id, phone_number, phone_enc, created_at
FROM phone_verification
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 5 DAY)
ORDER BY created_at DESC
LIMIT 200";
$stmt = $con->prepare($sql);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($stmt->rowCount() > 0) {
$registered = [];
foreach ($con->query("SELECT phone FROM driver WHERE phone IS NOT NULL")->fetchAll(PDO::FETCH_COLUMN) as $enc) {
$plain = $encryptionHelper->decryptData($enc);
if ($plain) $registered[normalizePhone($plain)] = true;
}
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$contacted = [];
try {
foreach ($con->query("SELECT phone FROM notesForDriverService")->fetchAll(PDO::FETCH_COLUMN) as $enc) {
$plain = $encryptionHelper->decryptData($enc) ?: $enc;
if ($plain) $contacted[normalizePhone($plain)] = true;
}
} catch (PDOException $e) {
error_log('[getDriversPhoneNotComplete] notes unavailable: ' . $e->getMessage());
}
// فك تشفير أرقام الهاتف والإيميل
foreach ($rows as &$r) {
$result = [];
foreach ($rows as $row) {
$phone = $encryptionHelper->decryptData($row['phone_enc'] ?? null);
if (!$phone) continue;
if (isset($r['phone_number']) && $r['phone_number'] != null) {
$r['phone_number'] = $encryptionHelper->decryptData($r['phone_number']);
}
$key = normalizePhone($phone);
if (isset($registered[$key])) continue; // أكمل تسجيله
if (isset($contacted[$key])) continue; // تم التواصل معه سابقاً
if (isset($r['email']) && $r['email'] != null) {
$r['email'] = $encryptionHelper->decryptData($r['email']);
}
}
$result[] = [
'id' => $row['id'],
'phone_number' => $phone,
'created_at' => $row['created_at'],
];
jsonSuccess($rows);
if (count($result) >= 25) break;
}
if ($result) {
jsonSuccess($result);
} else {
jsonError("No phone numbers found in the last 5 days");
jsonError("No records found");
}
?>