177 lines
6.3 KiB
PHP
177 lines
6.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use Core\Security;
|
|
use Core\Database;
|
|
use PDO;
|
|
|
|
class QrTokenService
|
|
{
|
|
/**
|
|
* Generates a dynamic QR token valid for 90 seconds.
|
|
*/
|
|
public static function generateToken(int $userId): array
|
|
{
|
|
$pdo = Database::getConnection();
|
|
$stmt = $pdo->prepare('
|
|
SELECT s.id AS sub_id, s.membership_number, s.status, s.qr_seed, s.expires_at, u.full_name
|
|
FROM subscriptions s
|
|
JOIN users u ON u.id = s.user_id
|
|
WHERE s.user_id = :uid AND s.status = "ACTIVE"
|
|
ORDER BY s.id DESC
|
|
LIMIT 1
|
|
');
|
|
$stmt->execute([':uid' => $userId]);
|
|
$sub = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$sub) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'No active membership found for this user.',
|
|
];
|
|
}
|
|
|
|
$appConfig = require __DIR__ . '/../../config/app.php';
|
|
$secConfig = require __DIR__ . '/../../config/security.php';
|
|
|
|
$secret = $appConfig['secret'] . ':' . $sub['qr_seed'];
|
|
$lifetime = (int)($secConfig['qr_token_lifetime'] ?? 90);
|
|
|
|
$token = Security::generateDynamicQrToken(
|
|
$userId,
|
|
$sub['membership_number'],
|
|
$lifetime,
|
|
$secret
|
|
);
|
|
|
|
return [
|
|
'success' => true,
|
|
'token' => $token,
|
|
'membership_number' => $sub['membership_number'],
|
|
'full_name' => $sub['full_name'],
|
|
'expires_in' => $lifetime,
|
|
'valid_until' => time() + $lifetime,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Verifies a QR token scanned by a hospital or hotel receptionist.
|
|
*/
|
|
public static function verifyScannedToken(
|
|
string $token,
|
|
int $partnerId,
|
|
?string $staffName = null,
|
|
float $billAmount = 0.0,
|
|
string $serviceName = 'استخدام خدمات الشريك'
|
|
): array {
|
|
// Decode base64 payload to get user ID without signature first
|
|
$parts = explode('.', $token);
|
|
if (count($parts) !== 2) {
|
|
return ['success' => false, 'message' => 'Invalid QR token format.'];
|
|
}
|
|
|
|
$json = base64_decode(strtr($parts[0], '-_', '+/'));
|
|
$tempPayload = json_decode((string)$json, true);
|
|
|
|
if (!isset($tempPayload['uid'], $tempPayload['mem'])) {
|
|
return ['success' => false, 'message' => 'Corrupted token payload.'];
|
|
}
|
|
|
|
$pdo = Database::getConnection();
|
|
$stmt = $pdo->prepare('
|
|
SELECT s.id AS sub_id, s.membership_number, s.status, s.qr_seed, s.expires_at,
|
|
u.id AS user_id, u.full_name, u.phone, u.avatar_url
|
|
FROM subscriptions s
|
|
JOIN users u ON u.id = s.user_id
|
|
WHERE u.id = :uid AND s.membership_number = :mem
|
|
LIMIT 1
|
|
');
|
|
$stmt->execute([
|
|
':uid' => (int)$tempPayload['uid'],
|
|
':mem' => $tempPayload['mem'],
|
|
]);
|
|
$record = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$record) {
|
|
return ['success' => false, 'message' => 'Membership record not found.'];
|
|
}
|
|
|
|
$appConfig = require __DIR__ . '/../../config/app.php';
|
|
$secret = $appConfig['secret'] . ':' . $record['qr_seed'];
|
|
|
|
$verified = Security::verifyDynamicQrToken($token, $secret);
|
|
if (!$verified) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Token has expired or signature is counterfeit. Please refresh code in app.',
|
|
];
|
|
}
|
|
|
|
// Get partner discount details
|
|
$pStmt = $pdo->prepare('SELECT id, name_ar, type, discount_percentage FROM partners WHERE id = :pid LIMIT 1');
|
|
$pStmt->execute([':pid' => $partnerId]);
|
|
$partner = $pStmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
$discount = $partner ? (float)$partner['discount_percentage'] : 50.00;
|
|
|
|
// Log verification entry for audit & statistics
|
|
$logStmt = $pdo->prepare('
|
|
INSERT INTO verification_logs (subscription_id, partner_id, verifier_staff_name, applied_discount, service_type, scanned_token_signature, verified_at)
|
|
VALUES (:sub_id, :pid, :staff, :discount, :stype, :sig, NOW())
|
|
');
|
|
$logStmt->execute([
|
|
':sub_id' => $record['sub_id'],
|
|
':pid' => $partnerId,
|
|
':staff' => $staffName ?: 'Front Desk',
|
|
':discount' => $discount,
|
|
':stype' => $partner['type'] ?? 'HOSPITAL',
|
|
':sig' => substr($parts[1], 0, 64),
|
|
]);
|
|
|
|
$savedAmount = 0.0;
|
|
$paidAmount = 0.0;
|
|
|
|
// Record savings into savings_ledger if bill amount is provided
|
|
if ($billAmount > 0) {
|
|
$savedAmount = round($billAmount * ($discount / 100.0), 2);
|
|
$paidAmount = round($billAmount - $savedAmount, 2);
|
|
|
|
$saveStmt = $pdo->prepare('
|
|
INSERT INTO savings_ledger
|
|
(subscription_id, user_id, partner_id, service_name, original_amount, discount_percentage, saved_amount, paid_amount, currency, verified_by_staff, created_at)
|
|
VALUES (:sub_id, :uid, :pid, :sname, :orig, :disc, :saved, :paid, "IQD", :staff, NOW())
|
|
');
|
|
$saveStmt->execute([
|
|
':sub_id' => $record['sub_id'],
|
|
':uid' => $record['user_id'],
|
|
':pid' => $partnerId,
|
|
':sname' => $serviceName,
|
|
':orig' => $billAmount,
|
|
':disc' => $discount,
|
|
':saved' => $savedAmount,
|
|
':paid' => $paidAmount,
|
|
':staff' => $staffName ?: 'Front Desk',
|
|
]);
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'is_valid' => true,
|
|
'membership_number' => $record['membership_number'],
|
|
'member_name' => $record['full_name'],
|
|
'member_phone' => $record['phone'],
|
|
'status' => $record['status'],
|
|
'expires_at' => $record['expires_at'],
|
|
'partner_name' => $partner['name_ar'] ?? 'Partner',
|
|
'discount_percentage' => $discount,
|
|
'original_bill_iqd' => $billAmount,
|
|
'saved_amount_iqd' => $savedAmount,
|
|
'paid_amount_iqd' => $paidAmount,
|
|
'verified_at' => date('Y-m-d H:i:s'),
|
|
];
|
|
}
|
|
}
|