Initial commit: Uruk Prize Platform architecture, backend core, mobile app, gateway caller & deployment pipeline
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
# OS & System
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment & Secrets
|
||||
.env
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
# PHP Backend
|
||||
backend/vendor/
|
||||
backend/storage/logs/*.log
|
||||
|
||||
# Flutter Mobile Application
|
||||
mobile/.dart_tool/
|
||||
mobile/.packages
|
||||
mobile/.flutter-plugins
|
||||
mobile/.flutter-plugins-dependencies
|
||||
mobile/build/
|
||||
mobile/.idea/
|
||||
mobile/*.iml
|
||||
mobile/ios/.symlinks/
|
||||
mobile/ios/Pods/
|
||||
|
||||
# Android Gateway
|
||||
gateway-caller/build/
|
||||
gateway-caller/.gradle/
|
||||
gateway-caller/.idea/
|
||||
gateway-caller/local.properties
|
||||
gateway-caller/*.iml
|
||||
@@ -0,0 +1,64 @@
|
||||
# منصة جائزة أوروك الدولية (Uruk International Prize Platform)
|
||||
|
||||
منظومة البنية التحتية المتكاملة للتحقق الرقمي، إدارة العضويات الذكية، وأتمتة المدفوعات في جمهورية العراق.
|
||||
|
||||
## معمارية النظام (System Architecture)
|
||||
|
||||
يتكون المشروع من أربعة مكونات رئيسية مترابطة:
|
||||
|
||||
1. **`backend/`**: النواة الخلفية وخادم واجهات برمجة التطبيقات (REST API Core & Admin Engine) مبني بلغة PHP بدون أطر عمل ثقيلة لسرعة استجابة فائقة، مع نظام التوزيع التناوبي للمكالمات وإدارة الفواتير.
|
||||
2. **`mobile/`**: تطبيق الهواتف الذكية متعدد المنصات (Flutter for iOS & Android) بنظام بطاقات العضوية الرقمية المشفرة (Dark Gold Luxury) والبصمة العتادية الأصلية ودعم أجهزة هواوي بدون خدمات جوجل.
|
||||
3. **`gateway-caller/`**: ديمون الأندرويد الأصلي (Native Kotlin Caller Gateway) لإجراء الرنين العكسي الصامت (Flash Call) بتكلفة صفرية ومداورة الشرائح.
|
||||
4. **`docs/`**: التوثيق المعماري، دراسات الجدوى التشغيلية، ومخطط قاعدة البيانات (`04_database_schema.sql`).
|
||||
|
||||
---
|
||||
|
||||
## إعداد السيرفر ونشر المنصة (Server Deployment via CloudPanel)
|
||||
|
||||
- **النطاق المعتمد (Domain):** `https://urukprize.intaleqapp.com`
|
||||
- **مستخدم السيرفر (Site User):** `urukprize`
|
||||
- **مسار المشروع على السيرفر:** `/home/urukprize/htdocs/urukprize.intaleqapp.com`
|
||||
- **مستودع Git البعيد:** `https://git.intaleqapp.com/Hamza/urukprize.git`
|
||||
|
||||
### خطوات التثبيت لأول مرة على السيرفر (Initial Server Setup):
|
||||
|
||||
```bash
|
||||
# 1. الدخول لمجلد الموقع
|
||||
cd /home/urukprize/htdocs/urukprize.intaleqapp.com
|
||||
|
||||
# 2. سحب الكود من المستودع
|
||||
git pull origin main
|
||||
|
||||
# 3. تثبيت حزم الموزع
|
||||
cd backend
|
||||
composer install --no-dev --optimize-autoloader
|
||||
|
||||
# 4. إعداد ملف البيئة
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
|
||||
# 5. استيراد قاعدة البيانات
|
||||
mysql -u [DB_USER] -p [DB_NAME] < ../docs/04_database_schema.sql
|
||||
|
||||
# 6. ضبط الصلاحيات لمجلد التخزين
|
||||
chmod -R 775 storage
|
||||
chown -R urukprize:urukprize .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## التحديث التلقائي (Ongoing Deployments)
|
||||
|
||||
من جهاز التطوير، قم بتشغيل سكريبت النشر المدمج:
|
||||
```bash
|
||||
./deploy.sh "وصف التحديث"
|
||||
```
|
||||
|
||||
وعلى السيرفر، يتم جلب التحديثات فورياً عبر:
|
||||
```bash
|
||||
cd /home/urukprize/htdocs/urukprize.intaleqapp.com && git pull origin main
|
||||
```
|
||||
|
||||
---
|
||||
© 2026 Uruk International Prize. All rights reserved.
|
||||
Developed by: Hamza Ayed (Founding Tech Architect) & Team.
|
||||
@@ -0,0 +1,24 @@
|
||||
APP_NAME="Uruk International Prize"
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://api.urukprize.iq
|
||||
APP_SECRET=your_super_secret_64_char_key_here_for_hmac_and_tokens
|
||||
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=uruk_prize_db
|
||||
DB_USERNAME=uruk_user
|
||||
DB_PASSWORD=uruk_secure_password_2026
|
||||
|
||||
# OTPIQ Service Credentials (Iraq SMS & WhatsApp Gateway)
|
||||
OTPIQ_API_KEY=your_otpiq_api_key
|
||||
OTPIQ_SENDER_ID=URUK-PRIZE
|
||||
OTPIQ_ENDPOINT=https://api.otpiq.com/api/sms
|
||||
|
||||
# SwiftPayIQ Credentials (Aggregator Sandbox / Production)
|
||||
SWIFTPAY_API_KEY=your_swiftpay_key
|
||||
SWIFTPAY_SECRET=your_swiftpay_secret
|
||||
SWIFTPAY_ENDPOINT=https://api.swiftpayiq.com/v1
|
||||
|
||||
# SuperQi / ZainCash Notification Listener Secret
|
||||
NOTIFICATION_INGEST_SECRET=secure_ingest_key_for_android_bridge_listener
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use Core\Database;
|
||||
use PDO;
|
||||
|
||||
class AdminController
|
||||
{
|
||||
/**
|
||||
* POST /api/v1/admin/partners/add
|
||||
* Directly add a new hospital or hotel contract on the fly.
|
||||
*/
|
||||
public function addPartner(Request $request): void
|
||||
{
|
||||
$userId = (int)$request->getHeader('x-user-id');
|
||||
$type = strtoupper(trim((string)$request->get('type', 'HOSPITAL')));
|
||||
$nameAr = trim((string)$request->get('name_ar'));
|
||||
$nameEn = trim((string)$request->get('name_en', ''));
|
||||
$category = trim((string)$request->get('category', 'عام'));
|
||||
$country = trim((string)$request->get('country', 'العراق'));
|
||||
$city = trim((string)$request->get('city', 'بغداد'));
|
||||
$discount = (float)$request->get('discount_percentage', $type === 'HOSPITAL' ? 50.00 : 50.00);
|
||||
$phone = trim((string)$request->get('phone', ''));
|
||||
$address = trim((string)$request->get('address', ''));
|
||||
|
||||
if (empty($nameAr)) {
|
||||
Response::error('Partner Arabic name is required.');
|
||||
}
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
$stmt = $pdo->prepare('
|
||||
INSERT INTO partners (type, name_ar, name_en, category, country, city, discount_percentage, address, phone, is_active, added_by_admin, created_at)
|
||||
VALUES (:type, :name_ar, :name_en, :category, :country, :city, :discount, :address, :phone, 1, :admin_id, NOW())
|
||||
');
|
||||
$stmt->execute([
|
||||
':type' => $type,
|
||||
':name_ar' => $nameAr,
|
||||
':name_en' => $nameEn,
|
||||
':category' => $category,
|
||||
':country' => $country,
|
||||
':city' => $city,
|
||||
':discount' => $discount,
|
||||
':address' => $address,
|
||||
':phone' => $phone,
|
||||
':admin_id' => $userId,
|
||||
]);
|
||||
|
||||
$newId = (int)$pdo->lastInsertId();
|
||||
|
||||
Response::success([
|
||||
'partner_id' => $newId,
|
||||
'name_ar' => $nameAr,
|
||||
'discount' => $discount,
|
||||
'message' => 'تمت إضافة الشريك بنجاح ومتاح فورياً لجميع المشتركين.',
|
||||
], 'Partner created successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/admin/payments/pending
|
||||
*/
|
||||
public function listPendingPayments(Request $request): void
|
||||
{
|
||||
$pdo = Database::getConnection();
|
||||
$stmt = $pdo->query('
|
||||
SELECT t.id, t.subscription_id, t.reference_number, t.amount, t.currency, t.method, t.receipt_image_url, t.created_at,
|
||||
u.id AS user_id, u.full_name, u.phone, s.membership_number
|
||||
FROM transactions t
|
||||
JOIN subscriptions s ON s.id = t.subscription_id
|
||||
JOIN users u ON u.id = t.user_id
|
||||
WHERE t.status = "SUBMITTED"
|
||||
ORDER BY t.id DESC
|
||||
LIMIT 50
|
||||
');
|
||||
$pending = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
Response::success($pending, 'Pending payments list retrieved.');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/admin/payments/approve
|
||||
*/
|
||||
public function approvePayment(Request $request): void
|
||||
{
|
||||
$adminId = (int)$request->getHeader('x-user-id');
|
||||
$transactionId = (int)$request->get('transaction_id');
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare('SELECT id, subscription_id, user_id FROM transactions WHERE id = :id LIMIT 1');
|
||||
$stmt->execute([':id' => $transactionId]);
|
||||
$trans = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$trans) {
|
||||
Response::notFound('Transaction not found.');
|
||||
}
|
||||
|
||||
// Update transaction
|
||||
$updTrans = $pdo->prepare('UPDATE transactions SET status = "VERIFIED_ADMIN", verified_at = NOW() WHERE id = :id');
|
||||
$updTrans->execute([':id' => $transactionId]);
|
||||
|
||||
// Activate subscription
|
||||
$updSub = $pdo->prepare('
|
||||
UPDATE subscriptions
|
||||
SET status = "ACTIVE", starts_at = CURDATE(), expires_at = DATE_ADD(CURDATE(), INTERVAL 2 YEAR), activated_by = :admin_id
|
||||
WHERE id = :sub_id
|
||||
');
|
||||
$updSub->execute([
|
||||
':admin_id' => $adminId,
|
||||
':sub_id' => $trans['subscription_id'],
|
||||
]);
|
||||
|
||||
// Activate user
|
||||
$updUser = $pdo->prepare('UPDATE users SET status = "ACTIVE" WHERE id = :uid');
|
||||
$updUser->execute([':uid' => $trans['user_id']]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
Response::success([
|
||||
'transaction_id' => $transactionId,
|
||||
'subscription_id' => $trans['subscription_id'],
|
||||
'status' => 'ACTIVE',
|
||||
], 'Payment approved and membership activated successfully.');
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
Response::error('Approval failed: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/admin/stats
|
||||
*/
|
||||
public function getStats(Request $request): void
|
||||
{
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
$membersCount = (int)$pdo->query('SELECT COUNT(*) FROM subscriptions WHERE status = "ACTIVE"')->fetchColumn();
|
||||
$pendingCount = (int)$pdo->query('SELECT COUNT(*) FROM subscriptions WHERE status = "PENDING_PAYMENT"')->fetchColumn();
|
||||
$totalRevenue = (float)$pdo->query('SELECT COALESCE(SUM(amount), 0) FROM transactions WHERE status IN ("VERIFIED_AUTO", "VERIFIED_ADMIN")')->fetchColumn();
|
||||
$hospitalsCount = (int)$pdo->query('SELECT COUNT(*) FROM partners WHERE type = "HOSPITAL" AND is_active = 1')->fetchColumn();
|
||||
$hotelsCount = (int)$pdo->query('SELECT COUNT(*) FROM partners WHERE type = "HOTEL" AND is_active = 1')->fetchColumn();
|
||||
|
||||
Response::success([
|
||||
'active_members' => $membersCount,
|
||||
'pending_verifications' => $pendingCount,
|
||||
'total_revenue_iqd' => $totalRevenue,
|
||||
'total_revenue_usd' => round($totalRevenue / 1320, 2),
|
||||
'active_hospitals' => $hospitalsCount,
|
||||
'active_hotels' => $hotelsCount,
|
||||
], 'System statistics retrieved.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use Core\Security;
|
||||
use Core\Database;
|
||||
use App\Services\OtpIqService;
|
||||
use PDO;
|
||||
|
||||
class AuthController
|
||||
{
|
||||
/**
|
||||
* POST /api/v1/auth/request-otp
|
||||
*/
|
||||
public function requestOtp(Request $request): void
|
||||
{
|
||||
$phone = trim((string)$request->get('phone'));
|
||||
if (empty($phone) || strlen($phone) < 8) {
|
||||
Response::error('Phone number is required.');
|
||||
}
|
||||
|
||||
// Generate 6-digit cryptographic OTP code
|
||||
$otp = (string)random_int(100000, 999999);
|
||||
|
||||
// Store OTP temporarily in DB or cache
|
||||
$pdo = Database::getConnection();
|
||||
$stmt = $pdo->prepare('
|
||||
INSERT INTO audit_logs (user_id, action, endpoint, method, ip_address, response_code, payload_snippet, created_at)
|
||||
VALUES (0, "OTP_REQUESTED", "/api/v1/auth/request-otp", "POST", :ip, 200, :payload, NOW())
|
||||
');
|
||||
$stmt->execute([
|
||||
':ip' => $request->getIp(),
|
||||
':payload' => json_encode(['phone' => $phone, 'otp' => $otp]),
|
||||
]);
|
||||
|
||||
$otpService = new OtpIqService();
|
||||
$res = $otpService->sendOtp($phone, $otp);
|
||||
|
||||
Response::success([
|
||||
'phone' => $phone,
|
||||
'channels' => ['whatsapp', 'sms'],
|
||||
'mock_otp' => getenv('APP_ENV') === 'development' ? $otp : null,
|
||||
], 'Verification OTP dispatched via OTPIQ Smart Fallback.');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/verify-otp
|
||||
* Receives phone, otp, device_fingerprint, device_model, and optional full_name.
|
||||
*/
|
||||
public function verifyOtp(Request $request): void
|
||||
{
|
||||
$phone = trim((string)$request->get('phone'));
|
||||
$otp = trim((string)$request->get('otp'));
|
||||
$fingerprint = trim((string)$request->get('device_fingerprint'));
|
||||
$model = trim((string)$request->get('device_model', 'Unknown Device'));
|
||||
$fullName = trim((string)$request->get('full_name', 'عضو جائزة أوروك'));
|
||||
|
||||
if (empty($phone) || empty($otp) || empty($fingerprint)) {
|
||||
Response::error('Phone, OTP, and Device Fingerprint are mandatory.');
|
||||
}
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
// Check or find user
|
||||
$stmt = $pdo->prepare('SELECT id, phone, full_name, role, status FROM users WHERE phone = :phone LIMIT 1');
|
||||
$stmt->execute([':phone' => $phone]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$user) {
|
||||
// Register new user
|
||||
$ins = $pdo->prepare('
|
||||
INSERT INTO users (phone, full_name, role, status, created_at)
|
||||
VALUES (:phone, :name, "MEMBER", "ACTIVE", NOW())
|
||||
');
|
||||
$ins->execute([':phone' => $phone, ':name' => $fullName]);
|
||||
$userId = (int)$pdo->lastInsertId();
|
||||
$role = 'MEMBER';
|
||||
} else {
|
||||
$userId = (int)$user['id'];
|
||||
$role = $user['role'];
|
||||
}
|
||||
|
||||
// Generate per-device secret key and session token
|
||||
$deviceSecret = Security::generateRandomHex(32);
|
||||
$sessionToken = Security::generateRandomHex(32);
|
||||
|
||||
// Bind device fingerprint and store secret in user_security table
|
||||
$secStmt = $pdo->prepare('
|
||||
INSERT INTO user_security (user_id, device_fingerprint, device_secret, device_model, last_token, last_ip, last_active_at)
|
||||
VALUES (:uid, :fp, :secret, :model, :token, :ip, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
device_secret = VALUES(device_secret),
|
||||
device_model = VALUES(device_model),
|
||||
last_token = VALUES(last_token),
|
||||
last_ip = VALUES(last_ip),
|
||||
last_active_at = NOW(),
|
||||
is_locked = 0
|
||||
');
|
||||
$secStmt->execute([
|
||||
':uid' => $userId,
|
||||
':fp' => $fingerprint,
|
||||
':secret' => $deviceSecret,
|
||||
':model' => $model,
|
||||
':token' => $sessionToken,
|
||||
':ip' => $request->getIp(),
|
||||
]);
|
||||
|
||||
Response::success([
|
||||
'user' => [
|
||||
'id' => $userId,
|
||||
'phone' => $phone,
|
||||
'full_name' => $user['full_name'] ?? $fullName,
|
||||
'role' => $role,
|
||||
],
|
||||
'token' => $sessionToken,
|
||||
'device_secret' => $deviceSecret,
|
||||
'device_fingerprint' => $fingerprint,
|
||||
], 'Authentication successful. Device bound and secured.');
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/auth/profile
|
||||
*/
|
||||
public function getProfile(Request $request): void
|
||||
{
|
||||
$userId = (int)$request->getHeader('x-user-id');
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT u.id, u.phone, u.full_name, u.role, u.status, u.created_at,
|
||||
s.membership_number, s.status AS subscription_status, s.expires_at
|
||||
FROM users u
|
||||
LEFT JOIN subscriptions s ON s.user_id = u.id AND s.status = "ACTIVE"
|
||||
WHERE u.id = :uid
|
||||
ORDER BY s.id DESC
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([':uid' => $userId]);
|
||||
$profile = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
Response::success($profile, 'User profile retrieved.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use Core\Database;
|
||||
use PDO;
|
||||
|
||||
class GatewayController
|
||||
{
|
||||
private string $configuredAppKey;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->configuredAppKey = getenv('GATEWAY_APP_KEY') ?: 'uruk_gateway_secret_2026';
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate gateway device app_key
|
||||
*/
|
||||
private function authenticate(string $appKey): bool
|
||||
{
|
||||
return hash_equals($this->configuredAppKey, $appKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/gateway/pending-call or /pending-call.php
|
||||
*/
|
||||
public function pendingCall(Request $request): void
|
||||
{
|
||||
$deviceId = (string)$request->get('device_id');
|
||||
$appKey = (string)$request->get('app_key');
|
||||
|
||||
if (!$this->authenticate($appKey) || empty($deviceId)) {
|
||||
Response::json([
|
||||
'task_id' => null,
|
||||
'phone' => null,
|
||||
'caller_id' => null,
|
||||
'otp' => null,
|
||||
'timeout_seconds' => null,
|
||||
'error' => 'Unauthorized or missing device_id'
|
||||
], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
// Update device heartbeat
|
||||
$this->heartbeat($pdo, $deviceId);
|
||||
|
||||
// Fetch oldest pending FLASH_CALL task
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT id, target_phone, otp_code, timeout_seconds
|
||||
FROM gateway_tasks
|
||||
WHERE task_type = "FLASH_CALL"
|
||||
AND (assigned_device_id = :dev OR assigned_device_id IS NULL)
|
||||
AND status = "PENDING"
|
||||
AND created_at >= NOW() - INTERVAL 60 SECOND
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([':dev' => $deviceId]);
|
||||
$task = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($task) {
|
||||
// Lock task to this device
|
||||
$upd = $pdo->prepare('UPDATE gateway_tasks SET status = "PROCESSING", assigned_device_id = :dev WHERE id = :id');
|
||||
$upd->execute([':dev' => $deviceId, ':id' => $task['id']]);
|
||||
|
||||
Response::json([
|
||||
'task_id' => (int)$task['id'],
|
||||
'phone' => $task['target_phone'],
|
||||
'caller_id' => null,
|
||||
'otp' => $task['otp_code'],
|
||||
'timeout_seconds' => (int)($task['timeout_seconds'] ?: 25),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'task_id' => null,
|
||||
'phone' => null,
|
||||
'caller_id' => null,
|
||||
'otp' => null,
|
||||
'timeout_seconds' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/gateway/pending-sms or /pending-sms.php
|
||||
*/
|
||||
public function pendingSms(Request $request): void
|
||||
{
|
||||
$deviceId = (string)$request->get('device_id');
|
||||
$appKey = (string)$request->get('app_key');
|
||||
|
||||
if (!$this->authenticate($appKey) || empty($deviceId)) {
|
||||
Response::json([
|
||||
'task_id' => null,
|
||||
'phone' => null,
|
||||
'caller_id' => null,
|
||||
'otp' => null,
|
||||
'timeout_seconds' => null,
|
||||
'error' => 'Unauthorized or missing device_id'
|
||||
], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
$this->heartbeat($pdo, $deviceId);
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT id, target_phone, otp_code, timeout_seconds
|
||||
FROM gateway_tasks
|
||||
WHERE task_type = "SMS"
|
||||
AND (assigned_device_id = :dev OR assigned_device_id IS NULL)
|
||||
AND status = "PENDING"
|
||||
AND created_at >= NOW() - INTERVAL 120 SECOND
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([':dev' => $deviceId]);
|
||||
$task = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($task) {
|
||||
$upd = $pdo->prepare('UPDATE gateway_tasks SET status = "PROCESSING", assigned_device_id = :dev WHERE id = :id');
|
||||
$upd->execute([':dev' => $deviceId, ':id' => $task['id']]);
|
||||
|
||||
Response::json([
|
||||
'task_id' => (int)$task['id'],
|
||||
'phone' => $task['target_phone'],
|
||||
'caller_id' => null,
|
||||
'otp' => $task['otp_code'],
|
||||
'timeout_seconds' => (int)($task['timeout_seconds'] ?: 30),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'task_id' => null,
|
||||
'phone' => null,
|
||||
'caller_id' => null,
|
||||
'otp' => null,
|
||||
'timeout_seconds' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/gateway/call-done or /call-done.php
|
||||
*/
|
||||
public function callDone(Request $request): void
|
||||
{
|
||||
$taskId = (int)$request->get('task_id');
|
||||
$deviceId = (string)$request->get('device_id');
|
||||
$appKey = (string)$request->get('app_key');
|
||||
$result = (string)$request->get('result');
|
||||
|
||||
if (!$this->authenticate($appKey)) {
|
||||
Response::json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
$newStatus = (strtoupper($result) === 'SUCCESS' || strtoupper($result) === 'DONE') ? 'COMPLETED' : 'FAILED';
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE gateway_tasks
|
||||
SET status = :st, completed_at = NOW()
|
||||
WHERE id = :id AND assigned_device_id = :dev
|
||||
');
|
||||
$stmt->execute([':st' => $newStatus, ':id' => $taskId, ':dev' => $deviceId]);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'message' => "Call task {$taskId} recorded as {$newStatus}.",
|
||||
'device_id' => $deviceId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/gateway/sms-done or /sms-done.php
|
||||
*/
|
||||
public function smsDone(Request $request): void
|
||||
{
|
||||
$taskId = (int)$request->get('task_id');
|
||||
$deviceId = (string)$request->get('device_id');
|
||||
$appKey = (string)$request->get('app_key');
|
||||
$result = (string)$request->get('result');
|
||||
|
||||
if (!$this->authenticate($appKey)) {
|
||||
Response::json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
$newStatus = (strtoupper($result) === 'SUCCESS' || strtoupper($result) === 'SENT') ? 'COMPLETED' : 'FAILED';
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE gateway_tasks
|
||||
SET status = :st, completed_at = NOW()
|
||||
WHERE id = :id AND assigned_device_id = :dev
|
||||
');
|
||||
$stmt->execute([':st' => $newStatus, ':id' => $taskId, ':dev' => $deviceId]);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'message' => "SMS task {$taskId} recorded as {$newStatus}.",
|
||||
'device_id' => $deviceId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/gateway/register-device or /register-device.php
|
||||
*/
|
||||
public function registerDevice(Request $request): void
|
||||
{
|
||||
$deviceId = (string)$request->get('device_id');
|
||||
$phoneNumber = (string)$request->get('phone_number');
|
||||
$simSlot = (int)$request->get('sim_slot', 1);
|
||||
$appKey = (string)$request->get('app_key');
|
||||
|
||||
if (!$this->authenticate($appKey) || empty($deviceId)) {
|
||||
Response::json(['success' => false, 'message' => 'Unauthorized or missing device_id'], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
$stmt = $pdo->prepare('
|
||||
INSERT INTO gateway_devices (device_id, phone_number, sim_slot, status, last_heartbeat)
|
||||
VALUES (:did, :phone, :slot, "ACTIVE", NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
phone_number = VALUES(phone_number),
|
||||
sim_slot = VALUES(sim_slot),
|
||||
status = "ACTIVE",
|
||||
last_heartbeat = NOW()
|
||||
');
|
||||
$stmt->execute([
|
||||
':did' => $deviceId,
|
||||
':phone' => $phoneNumber,
|
||||
':slot' => $simSlot,
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'message' => 'Gateway device registered and active.',
|
||||
'device_id' => $deviceId,
|
||||
]);
|
||||
}
|
||||
|
||||
private function heartbeat(PDO $pdo, string $deviceId): void
|
||||
{
|
||||
try {
|
||||
$stmt = $pdo->prepare('
|
||||
INSERT INTO gateway_devices (device_id, status, last_heartbeat)
|
||||
VALUES (:did, "ACTIVE", NOW())
|
||||
ON DUPLICATE KEY UPDATE status = "ACTIVE", last_heartbeat = NOW()
|
||||
');
|
||||
$stmt->execute([':did' => $deviceId]);
|
||||
} catch (\Throwable $e) {
|
||||
// Ignore heartbeat failures if table schema is pending
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use Core\Database;
|
||||
use PDO;
|
||||
|
||||
class PartnerController
|
||||
{
|
||||
/**
|
||||
* GET /api/v1/partners
|
||||
* Query params: type (HOSPITAL, HOTEL), country, city, search
|
||||
*/
|
||||
public function listPartners(Request $request): void
|
||||
{
|
||||
$type = strtoupper(trim((string)$request->getQuery('type', '')));
|
||||
$country = trim((string)$request->getQuery('country', ''));
|
||||
$city = trim((string)$request->getQuery('city', ''));
|
||||
$search = trim((string)$request->getQuery('search', ''));
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
$sql = 'SELECT id, type, name_ar, name_en, category, country, city, discount_percentage, address, phone, latitude, longitude
|
||||
FROM partners
|
||||
WHERE is_active = 1';
|
||||
$params = [];
|
||||
|
||||
if (!empty($type)) {
|
||||
$sql .= ' AND type = :type';
|
||||
$params[':type'] = $type;
|
||||
}
|
||||
|
||||
if (!empty($country)) {
|
||||
$sql .= ' AND country = :country';
|
||||
$params[':country'] = $country;
|
||||
}
|
||||
|
||||
if (!empty($city)) {
|
||||
$sql .= ' AND city = :city';
|
||||
$params[':city'] = $city;
|
||||
}
|
||||
|
||||
if (!empty($search)) {
|
||||
$sql .= ' AND (name_ar LIKE :search OR name_en LIKE :search OR category LIKE :search)';
|
||||
$params[':search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
$sql .= ' ORDER BY discount_percentage DESC, id DESC';
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$partners = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
Response::success($partners, 'Partners directory retrieved.');
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/partners/{id}
|
||||
*/
|
||||
public function getPartner(Request $request, array $params): void
|
||||
{
|
||||
$partnerId = (int)($params['id'] ?? 0);
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT * FROM partners WHERE id = :id AND is_active = 1 LIMIT 1');
|
||||
$stmt->execute([':id' => $partnerId]);
|
||||
$partner = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$partner) {
|
||||
Response::notFound('Partner not found.');
|
||||
}
|
||||
|
||||
Response::success($partner, 'Partner details retrieved.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use App\Services\SuperQiParserService;
|
||||
|
||||
class PaymentController
|
||||
{
|
||||
/**
|
||||
* POST /api/v1/payments/ingest-notification
|
||||
* Invoked securely by the Android Bridge Listener app when a SuperQi/ZainCash notification arrives.
|
||||
*/
|
||||
public function ingestNotification(Request $request): void
|
||||
{
|
||||
$secret = $request->getHeader('x-listener-secret');
|
||||
$expectedSecret = getenv('NOTIFICATION_INGEST_SECRET') ?: 'secure_ingest_key_for_android_bridge_listener';
|
||||
|
||||
if ($secret !== $expectedSecret) {
|
||||
Response::forbidden('Invalid listener secret.');
|
||||
}
|
||||
|
||||
$body = $request->getBody();
|
||||
$result = SuperQiParserService::processIngestedNotification($body);
|
||||
|
||||
if (!$result['success']) {
|
||||
Response::error($result['message'] ?? 'Ingestion failed', 400);
|
||||
}
|
||||
|
||||
Response::success($result, 'Notification ingested and processed.');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/payments/swiftpay-webhook
|
||||
*/
|
||||
public function swiftPayWebhook(Request $request): void
|
||||
{
|
||||
// Handle SwiftPayIQ transaction webhook
|
||||
$payload = $request->getBody();
|
||||
Response::success(['status' => 'received'], 'Webhook processed.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use App\Services\QrTokenService;
|
||||
|
||||
class QrController
|
||||
{
|
||||
/**
|
||||
* GET /api/v1/qr/dynamic-token
|
||||
* User's Flutter app requests a rotating QR token.
|
||||
*/
|
||||
public function getDynamicToken(Request $request): void
|
||||
{
|
||||
$userId = (int)$request->getHeader('x-user-id');
|
||||
$res = QrTokenService::generateToken($userId);
|
||||
|
||||
if (!$res['success']) {
|
||||
Response::forbidden($res['message']);
|
||||
}
|
||||
|
||||
Response::success($res, 'Dynamic QR token generated.');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/qr/verify-token
|
||||
* Scanned by hospital/hotel front desk or verification portal.
|
||||
*/
|
||||
public function verifyToken(Request $request): void
|
||||
{
|
||||
$token = trim((string)$request->get('token'));
|
||||
$partnerId = (int)$request->get('partner_id', 1);
|
||||
$staffName = trim((string)$request->get('staff_name', 'موظف الاستقبال'));
|
||||
|
||||
if (empty($token)) {
|
||||
Response::error('QR token is required for verification.');
|
||||
}
|
||||
|
||||
$res = QrTokenService::verifyScannedToken($token, $partnerId, $staffName);
|
||||
|
||||
if (!$res['success']) {
|
||||
Response::error($res['message'], 400);
|
||||
}
|
||||
|
||||
Response::success($res, 'Member verified successfully.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use Core\Security;
|
||||
use Core\Database;
|
||||
use App\Services\SuperQiParserService;
|
||||
use PDO;
|
||||
|
||||
class SubscriptionController
|
||||
{
|
||||
/**
|
||||
* GET /api/v1/subscription/plans
|
||||
*/
|
||||
public function getPlans(Request $request): void
|
||||
{
|
||||
$appConfig = require __DIR__ . '/../../config/app.php';
|
||||
|
||||
$plans = [
|
||||
[
|
||||
'id' => 'uruk-2year-standard',
|
||||
'name' => 'عضوية جائزة أوروك التنفيذية (سنتان)',
|
||||
'duration_years' => 2,
|
||||
'price_usd' => (float)$appConfig['membership_price_usd'],
|
||||
'price_iqd' => (float)$appConfig['membership_price_iqd'],
|
||||
'benefits' => [
|
||||
'خصم 50% في شبكة المستشفيات والمراكز التخصصية في العراق والأردن ومصر ولبنان',
|
||||
'خصومات فندقية وسياحية تتراوح بين 40% و60% في أرقى الفنادق الشريكة',
|
||||
'وصول مجاني لأكثر من 30 تخصصاً تدريبياً معتمداً حضورياً وعبر المنصة الرقمية',
|
||||
'شهادات معتمدة ومسجلة رسمياً باسم المشترك',
|
||||
'هوية رقمية ذكية مع رمز QR مشفر للتحقق الفوري',
|
||||
],
|
||||
'official_activation_date' => '2026-01-01',
|
||||
'early_registration_active' => true,
|
||||
],
|
||||
];
|
||||
|
||||
Response::success($plans, 'Available subscription plans retrieved.');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/subscription/submit
|
||||
* User submits payment details (Method, Ref Number, Receipt).
|
||||
*/
|
||||
public function submitSubscription(Request $request): void
|
||||
{
|
||||
$userId = (int)$request->getHeader('x-user-id');
|
||||
$method = strtoupper(trim((string)$request->get('method', 'SUPER_QI')));
|
||||
$refNumber = trim((string)$request->get('reference_number'));
|
||||
$receiptUrl = trim((string)$request->get('receipt_url', ''));
|
||||
|
||||
if (empty($refNumber)) {
|
||||
Response::error('Payment transaction reference number is required.');
|
||||
}
|
||||
|
||||
$appConfig = require __DIR__ . '/../../config/app.php';
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
// 1. Generate unique membership number (e.g. URUK-2026-XXXX)
|
||||
$memNumber = 'URUK-' . date('Y') . '-' . strtoupper(substr(uniqid(), -5));
|
||||
$qrSeed = Security::generateRandomHex(16);
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
// Create subscription record with PENDING_PAYMENT
|
||||
$subStmt = $pdo->prepare('
|
||||
INSERT INTO subscriptions (user_id, membership_number, plan_name, price_usd, price_local, currency, starts_at, expires_at, status, qr_seed, created_at)
|
||||
VALUES (:uid, :mem, "2-Year Executive Membership", :usd, :iqd, "IQD", CURDATE(), DATE_ADD(CURDATE(), INTERVAL 2 YEAR), "PENDING_PAYMENT", :seed, NOW())
|
||||
');
|
||||
$subStmt->execute([
|
||||
':uid' => $userId,
|
||||
':mem' => $memNumber,
|
||||
':usd' => $appConfig['membership_price_usd'],
|
||||
':iqd' => $appConfig['membership_price_iqd'],
|
||||
':seed' => $qrSeed,
|
||||
]);
|
||||
$subscriptionId = (int)$pdo->lastInsertId();
|
||||
|
||||
// Create transaction record
|
||||
$transStmt = $pdo->prepare('
|
||||
INSERT INTO transactions (subscription_id, user_id, method, reference_number, amount, currency, status, receipt_image_url, created_at)
|
||||
VALUES (:sub_id, :uid, :method, :ref, :amount, "IQD", "SUBMITTED", :receipt, NOW())
|
||||
');
|
||||
$transStmt->execute([
|
||||
':sub_id' => $subscriptionId,
|
||||
':uid' => $userId,
|
||||
':method' => $method,
|
||||
':ref' => $refNumber,
|
||||
':amount' => $appConfig['membership_price_iqd'],
|
||||
':receipt' => $receiptUrl,
|
||||
]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
// 2. Check if a matching SuperQi / ZainCash notification was already ingested by the Android listener
|
||||
$checkStmt = $pdo->prepare('
|
||||
SELECT id, amount, status FROM transactions
|
||||
WHERE reference_number = :ref AND id != :current_id AND status = "VERIFIED_AUTO"
|
||||
LIMIT 1
|
||||
');
|
||||
$checkStmt->execute([':ref' => $refNumber, ':current_id' => (int)$pdo->lastInsertId()]);
|
||||
$preExistingPayment = $checkStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($preExistingPayment) {
|
||||
// Instantly activate!
|
||||
$actSub = $pdo->prepare('UPDATE subscriptions SET status = "ACTIVE" WHERE id = :id');
|
||||
$actSub->execute([':id' => $subscriptionId]);
|
||||
|
||||
$actTrans = $pdo->prepare('UPDATE transactions SET status = "VERIFIED_AUTO", verified_at = NOW() WHERE subscription_id = :id');
|
||||
$actTrans->execute([':id' => $subscriptionId]);
|
||||
|
||||
Response::success([
|
||||
'subscription_id' => $subscriptionId,
|
||||
'membership_number' => $memNumber,
|
||||
'status' => 'ACTIVE',
|
||||
'message' => 'تم التحقق من الحوالة آلياً بنجاح وتفعيل العضوية فورياً!',
|
||||
], 'Membership activated automatically via SuperQi verification.');
|
||||
}
|
||||
|
||||
Response::success([
|
||||
'subscription_id' => $subscriptionId,
|
||||
'membership_number' => $memNumber,
|
||||
'status' => 'PENDING_PAYMENT',
|
||||
'message' => 'تم استلام طلبك بنجاح وجارٍ التحقق من الحوالة وتفعيل بطاقتك.',
|
||||
], 'Subscription request registered. Verification in progress.');
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
Response::error('Failed to submit subscription: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/subscription/my-card
|
||||
*/
|
||||
public function getMyCard(Request $request): void
|
||||
{
|
||||
$userId = (int)$request->getHeader('x-user-id');
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT s.id, s.membership_number, s.plan_name, s.status, s.starts_at, s.expires_at,
|
||||
u.full_name, u.phone, u.avatar_url
|
||||
FROM subscriptions s
|
||||
JOIN users u ON u.id = s.user_id
|
||||
WHERE s.user_id = :uid
|
||||
ORDER BY s.id DESC
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([':uid' => $userId]);
|
||||
$card = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$card) {
|
||||
Response::notFound('No subscription card found for this user.');
|
||||
}
|
||||
|
||||
Response::success($card, 'Digital card details retrieved.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Middlewares;
|
||||
|
||||
use Core\Middleware;
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use Core\Database;
|
||||
use PDO;
|
||||
|
||||
class AdminMiddleware extends Middleware
|
||||
{
|
||||
public function handle(Request $request, callable $next): void
|
||||
{
|
||||
$userId = $request->getHeader('x-user-id');
|
||||
|
||||
try {
|
||||
$pdo = Database::getConnection();
|
||||
$stmt = $pdo->prepare('SELECT role FROM users WHERE id = :user_id LIMIT 1');
|
||||
$stmt->execute([':user_id' => $userId]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$user || !in_array($user['role'], ['ADMIN', 'SUPER_ADMIN'], true)) {
|
||||
Response::forbidden('Administrative privileges required to access this resource.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Response::error('Role verification failure: ' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
$next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Middlewares;
|
||||
|
||||
use Core\Middleware;
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use Core\Database;
|
||||
use PDO;
|
||||
|
||||
class AuthMiddleware extends Middleware
|
||||
{
|
||||
public function handle(Request $request, callable $next): void
|
||||
{
|
||||
$authHeader = $request->getHeader('authorization');
|
||||
if (empty($authHeader) || !str_starts_with($authHeader, 'Bearer ')) {
|
||||
Response::unauthorized('Bearer token required in Authorization header.');
|
||||
}
|
||||
|
||||
$token = substr($authHeader, 7);
|
||||
$userId = $request->getHeader('x-user-id');
|
||||
|
||||
try {
|
||||
$pdo = Database::getConnection();
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT u.id, u.phone, u.full_name, u.role, u.status, us.last_token
|
||||
FROM users u
|
||||
JOIN user_security us ON us.user_id = u.id
|
||||
WHERE u.id = :user_id AND us.last_token = :token
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([
|
||||
':user_id' => $userId,
|
||||
':token' => $token,
|
||||
]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$user) {
|
||||
Response::unauthorized('Invalid or expired authentication session token.');
|
||||
}
|
||||
|
||||
if ($user['status'] !== 'ACTIVE' && $user['status'] !== 'PENDING_VERIFICATION') {
|
||||
Response::forbidden('User account is not currently active.');
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
Response::error('Authentication verification failure: ' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
$next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Middlewares;
|
||||
|
||||
use Core\Middleware;
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use Core\Security;
|
||||
use Core\Database;
|
||||
use PDO;
|
||||
|
||||
class SecurityHeaderMiddleware extends Middleware
|
||||
{
|
||||
public function handle(Request $request, callable $next): void
|
||||
{
|
||||
$userId = $request->getHeader('x-user-id');
|
||||
$fingerprint = $request->getHeader('x-device-fingerprint');
|
||||
$timestamp = (int)$request->getHeader('x-timestamp');
|
||||
$nonce = $request->getHeader('x-nonce');
|
||||
$signature = $request->getHeader('x-signature');
|
||||
|
||||
// 1. Check presence of all required security headers
|
||||
if (empty($userId) || empty($fingerprint) || empty($timestamp) || empty($nonce) || empty($signature)) {
|
||||
Response::unauthorized('Security headers missing: X-User-Id, X-Device-Fingerprint, X-Timestamp, X-Nonce, and X-Signature are mandatory.');
|
||||
}
|
||||
|
||||
// 2. Anti-replay timestamp check (window of 120 seconds)
|
||||
$currentTime = time();
|
||||
if (abs($currentTime - $timestamp) > 120) {
|
||||
Response::unauthorized('Request timestamp expired or skewed. Anti-replay check failed.');
|
||||
}
|
||||
|
||||
// 3. Query user security record from Database
|
||||
try {
|
||||
$pdo = Database::getConnection();
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT us.device_fingerprint, us.device_secret, us.is_locked, u.status
|
||||
FROM user_security us
|
||||
JOIN users u ON u.id = us.user_id
|
||||
WHERE us.user_id = :user_id AND us.device_fingerprint = :fingerprint
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([
|
||||
':user_id' => $userId,
|
||||
':fingerprint' => $fingerprint,
|
||||
]);
|
||||
$secRecord = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$secRecord) {
|
||||
Response::forbidden('Unrecognized device fingerprint or unauthorized device binding.');
|
||||
}
|
||||
|
||||
if ((int)$secRecord['is_locked'] === 1 || in_array($secRecord['status'], ['SUSPENDED', 'BANNED'], true)) {
|
||||
Response::forbidden('User device or account has been locked or suspended.');
|
||||
}
|
||||
|
||||
// 4. Verify HMAC-SHA256 signature
|
||||
$rawBody = $request->getRawBody();
|
||||
$bodyHash = hash('sha256', $rawBody);
|
||||
$signPayload = implode("\n", [
|
||||
$request->getMethod(),
|
||||
$request->getPath(),
|
||||
(string)$timestamp,
|
||||
$nonce,
|
||||
$bodyHash,
|
||||
]);
|
||||
|
||||
if (!Security::verifyHmacSignature($signPayload, $signature, $secRecord['device_secret'])) {
|
||||
Response::unauthorized('Invalid cryptographic signature for this device and payload.');
|
||||
}
|
||||
|
||||
// 5. Update last active timestamp and IP asynchronously / inline
|
||||
$updateStmt = $pdo->prepare('
|
||||
UPDATE user_security
|
||||
SET last_active_at = NOW(), last_ip = :ip
|
||||
WHERE user_id = :user_id AND device_fingerprint = :fingerprint
|
||||
');
|
||||
$updateStmt->execute([
|
||||
':ip' => $request->getIp(),
|
||||
':user_id' => $userId,
|
||||
':fingerprint' => $fingerprint,
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
Response::error('Security verification error: ' . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
$next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class NabihCaptchaService
|
||||
{
|
||||
/**
|
||||
* Generates a unique, distorted Captcha OTP image with noise to bypass Meta template restrictions.
|
||||
* Returns the raw PNG binary or base64 encoded data.
|
||||
*/
|
||||
public static function generateOtpImage(string $code, int $width = 240, int $height = 80): string
|
||||
{
|
||||
$image = imagecreatetruecolor($width, $height);
|
||||
|
||||
// 1. Generate randomized subtle background tint
|
||||
$bgR = random_int(235, 250);
|
||||
$bgG = random_int(245, 255);
|
||||
$bgB = random_int(240, 250);
|
||||
$bgColor = imagecolorallocate($image, $bgR, $bgG, $bgB);
|
||||
imagefilledrectangle($image, 0, 0, $width, $height, $bgColor);
|
||||
|
||||
// 2. Add random noise dots
|
||||
for ($i = 0; $i < 120; $i++) {
|
||||
$dotColor = imagecolorallocate(
|
||||
$image,
|
||||
random_int(120, 200),
|
||||
random_int(180, 230),
|
||||
random_int(150, 210)
|
||||
);
|
||||
imagesetpixel($image, random_int(0, $width), random_int(0, $height), $dotColor);
|
||||
}
|
||||
|
||||
// 3. Add random interference crossing lines
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
$lineColor = imagecolorallocate(
|
||||
$image,
|
||||
random_int(80, 160),
|
||||
random_int(180, 220),
|
||||
random_int(140, 190)
|
||||
);
|
||||
imagesetthickness($image, random_int(1, 2));
|
||||
imageline(
|
||||
$image,
|
||||
random_int(0, $width),
|
||||
random_int(0, $height),
|
||||
random_int(0, $width),
|
||||
random_int(0, $height),
|
||||
$lineColor
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Render Digits with randomized spacing and slight position jitter
|
||||
$textColor = imagecolorallocate($image, 15, 60, 50); // Deep Teal / Charcoal
|
||||
$len = strlen($code);
|
||||
$spacing = (int)($width / ($len + 1));
|
||||
|
||||
for ($idx = 0; $idx < $len; $idx++) {
|
||||
$char = $code[$idx];
|
||||
$x = ($idx + 1) * $spacing - 10 + random_int(-3, 3);
|
||||
$y = (int)($height / 2) - 10 + random_int(-4, 4);
|
||||
|
||||
// Using standard built-in font for zero dependencies
|
||||
imagestring($image, 5, $x, $y, $char, $textColor);
|
||||
}
|
||||
|
||||
ob_start();
|
||||
imagepng($image);
|
||||
$imageData = ob_get_clean();
|
||||
imagedestroy($image);
|
||||
|
||||
return (string)$imageData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class OtpIqService
|
||||
{
|
||||
private string $apiKey;
|
||||
private string $senderId;
|
||||
private string $endpoint;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiKey = getenv('OTPIQ_API_KEY') ?: '';
|
||||
$this->senderId = getenv('OTPIQ_SENDER_ID') ?: 'URUK-PRIZE';
|
||||
$this->endpoint = getenv('OTPIQ_ENDPOINT') ?: 'https://api.otpiq.com/api/sms';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends OTP via OTPIQ with Smart Fallback (WhatsApp first, then SMS).
|
||||
*/
|
||||
public function sendOtp(string $phoneNumber, string $otpCode): array
|
||||
{
|
||||
// If API key is not configured, run in Mock / Dev mode
|
||||
if (empty($this->apiKey)) {
|
||||
error_log("[MOCK OTPIQ] Sent OTP {$otpCode} to {$phoneNumber} via WhatsApp/SMS Smart Fallback.");
|
||||
return [
|
||||
'success' => true,
|
||||
'mode' => 'mock',
|
||||
'phone' => $phoneNumber,
|
||||
'otp' => $otpCode,
|
||||
'provider' => 'mock-smart-fallback',
|
||||
];
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'recipient' => $phoneNumber,
|
||||
'sender_id' => $this->senderId,
|
||||
'type' => 'otp',
|
||||
'code' => $otpCode,
|
||||
'message' => "رمز التحقق الخاص بك لجائزة أوروك الدولية هو: {$otpCode}. لا تشاركه مع أحد.",
|
||||
'channels' => ['whatsapp', 'sms'], // Smart fallback pipeline
|
||||
];
|
||||
|
||||
$ch = curl_init($this->endpoint);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $this->apiKey,
|
||||
],
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error || $httpCode >= 400) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $error ?: "HTTP {$httpCode}: " . substr((string)$response, 0, 100),
|
||||
];
|
||||
}
|
||||
|
||||
$data = json_decode((string)$response, true);
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $data,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class PhoneFormatterService
|
||||
{
|
||||
/**
|
||||
* Normalizes a phone number to standard E.164 digits format (without '+').
|
||||
* Automatically handles leading zeros and national dialing prefixes.
|
||||
*
|
||||
* Example Iraq: 07701234567 -> 9647701234567
|
||||
* Example Iraq: +964 0780 1234 -> 9647801234
|
||||
* Example Jordan: 0791234567 -> 962791234567
|
||||
*/
|
||||
public static function normalize(string $phone, string $defaultCountryCode = '964'): string
|
||||
{
|
||||
// 1. Remove all non-digit characters (+, -, spaces, dots, brackets)
|
||||
$cleaned = preg_replace('/[^\d]/', '', $phone);
|
||||
|
||||
if (empty($cleaned)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 2. Remove international double-zero prefixes (e.g. 00964 -> 964)
|
||||
if (str_starts_with($cleaned, '00')) {
|
||||
$cleaned = substr($cleaned, 2);
|
||||
}
|
||||
|
||||
// 3. Supported Country Code Detection & Cleaning
|
||||
$knownCountryCodes = ['964', '962', '20', '961', '966', '971'];
|
||||
|
||||
foreach ($knownCountryCodes as $cc) {
|
||||
if (str_starts_with($cleaned, $cc)) {
|
||||
$localPart = substr($cleaned, strlen($cc));
|
||||
// Remove any redundant leading zero after country code (e.g. 964 0770... -> 964 770...)
|
||||
$localPart = ltrim($localPart, '0');
|
||||
return $cc . $localPart;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. If no recognized country code is attached, treat as national number with default country code
|
||||
// Remove leading national zero (e.g. 0770... -> 770...)
|
||||
$nationalPart = ltrim($cleaned, '0');
|
||||
|
||||
return $defaultCountryCode . $nationalPart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats normalized number for display in UI.
|
||||
* e.g. 9647701234567 -> +964 770 123 4567
|
||||
*/
|
||||
public static function formatDisplay(string $normalized): string
|
||||
{
|
||||
if (str_starts_with($normalized, '964') && strlen($normalized) === 13) {
|
||||
return '+964 ' . substr($normalized, 3, 3) . ' ' . substr($normalized, 6, 3) . ' ' . substr($normalized, 9);
|
||||
}
|
||||
if (str_starts_with($normalized, '962') && strlen($normalized) === 12) {
|
||||
return '+962 ' . substr($normalized, 3, 2) . ' ' . substr($normalized, 5, 3) . ' ' . substr($normalized, 8);
|
||||
}
|
||||
return '+' . $normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?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): 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),
|
||||
]);
|
||||
|
||||
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,
|
||||
'verified_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Core\Database;
|
||||
use PDO;
|
||||
|
||||
class SuperQiParserService
|
||||
{
|
||||
/**
|
||||
* Ingests and processes a notification or parsed receipt from the Android listener bridge.
|
||||
*/
|
||||
public static function processIngestedNotification(array $data): array
|
||||
{
|
||||
$refNumber = trim((string)($data['reference_number'] ?? ''));
|
||||
$amount = (float)($data['amount'] ?? 0);
|
||||
$sender = trim((string)($data['sender'] ?? ''));
|
||||
$recipient = trim((string)($data['recipient'] ?? ''));
|
||||
$rawText = (string)($data['raw_text'] ?? '');
|
||||
|
||||
if (empty($refNumber) || $amount <= 0) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Invalid transaction reference or amount.',
|
||||
];
|
||||
}
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
// 1. Check if transaction has already been recorded and verified
|
||||
$stmt = $pdo->prepare('SELECT id, status FROM transactions WHERE reference_number = :ref LIMIT 1');
|
||||
$stmt->execute([':ref' => $refNumber]);
|
||||
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($existing && $existing['status'] === 'VERIFIED_AUTO') {
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Transaction already processed and verified previously.',
|
||||
'transaction_id' => $existing['id'],
|
||||
];
|
||||
}
|
||||
|
||||
// 2. Search for a pending subscription matching this reference or pending payment
|
||||
// First, check if any user submitted this reference number
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT t.id AS trans_id, t.subscription_id, s.user_id, s.plan_name, u.phone, u.full_name
|
||||
FROM transactions t
|
||||
JOIN subscriptions s ON s.id = t.subscription_id
|
||||
JOIN users u ON u.id = s.user_id
|
||||
WHERE t.reference_number = :ref AND t.status = "SUBMITTED"
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([':ref' => $refNumber]);
|
||||
$matched = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($matched) {
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
// Update transaction
|
||||
$updTrans = $pdo->prepare('
|
||||
UPDATE transactions
|
||||
SET status = "VERIFIED_AUTO", amount = :amount, verified_at = NOW(),
|
||||
raw_webhook_payload = :raw
|
||||
WHERE id = :id
|
||||
');
|
||||
$updTrans->execute([
|
||||
':amount' => $amount,
|
||||
':raw' => json_encode($data, JSON_UNESCAPED_UNICODE),
|
||||
':id' => $matched['trans_id'],
|
||||
]);
|
||||
|
||||
// Activate subscription
|
||||
$updSub = $pdo->prepare('
|
||||
UPDATE subscriptions
|
||||
SET status = "ACTIVE", starts_at = CURDATE(), expires_at = DATE_ADD(CURDATE(), INTERVAL 2 YEAR)
|
||||
WHERE id = :sub_id
|
||||
');
|
||||
$updSub->execute([':sub_id' => $matched['subscription_id']]);
|
||||
|
||||
// Ensure user status is active
|
||||
$updUser = $pdo->prepare('UPDATE users SET status = "ACTIVE" WHERE id = :user_id');
|
||||
$updUser->execute([':user_id' => $matched['user_id']]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'matched' => true,
|
||||
'message' => 'Subscription activated automatically for ' . $matched['full_name'],
|
||||
'user_id' => $matched['user_id'],
|
||||
'phone' => $matched['phone'],
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
return ['success' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
// If no user has claimed it yet, record it in transactions as unassigned credit
|
||||
$insStmt = $pdo->prepare('
|
||||
INSERT INTO transactions (subscription_id, user_id, method, reference_number, sender_account_or_phone, recipient_account, amount, currency, status, raw_webhook_payload, verified_at)
|
||||
VALUES (0, 0, "SUPER_QI", :ref, :sender, :recipient, :amount, "IQD", "VERIFIED_AUTO", :raw, NOW())
|
||||
ON DUPLICATE KEY UPDATE amount = VALUES(amount), raw_webhook_payload = VALUES(raw_webhook_payload)
|
||||
');
|
||||
$insStmt->execute([
|
||||
':ref' => $refNumber,
|
||||
':sender' => $sender,
|
||||
':recipient' => $recipient,
|
||||
':amount' => $amount,
|
||||
':raw' => json_encode($data, JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'matched' => false,
|
||||
'message' => 'Transaction recorded. Awaiting user claim in app.',
|
||||
'reference_number' => $refNumber,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "uruk/prize-backend",
|
||||
"description": "Uruk International Prize Platform - Pure PHP Core Engine",
|
||||
"type": "project",
|
||||
"license": "proprietary",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Hamza Ayed",
|
||||
"role": "Founding Tech Architect"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"ext-pdo": "*",
|
||||
"ext-pdo_mysql": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-json": "*",
|
||||
"vlucas/phpdotenv": "^5.6",
|
||||
"guzzlehttp/guzzle": "^7.8"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Core\\": "core/",
|
||||
"App\\": "app/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'name' => getenv('APP_NAME') ?: 'Uruk International Prize',
|
||||
'env' => getenv('APP_ENV') ?: 'development',
|
||||
'debug' => (bool)(getenv('APP_DEBUG') ?: true),
|
||||
'url' => getenv('APP_URL') ?: 'http://localhost:8000',
|
||||
'secret' => getenv('APP_SECRET') ?: 'uruk_default_super_secret_signing_key_32_chars_min',
|
||||
'timezone' => 'Asia/Baghdad',
|
||||
'currency' => 'IQD',
|
||||
'membership_price_usd' => 100.00,
|
||||
'membership_price_iqd' => 132000.00,
|
||||
'membership_duration_years' => 2,
|
||||
];
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'host' => getenv('DB_HOST') ?: '127.0.0.1',
|
||||
'port' => (int)(getenv('DB_PORT') ?: 3306),
|
||||
'database' => getenv('DB_DATABASE') ?: 'uruk_prize_db',
|
||||
'username' => getenv('DB_USERNAME') ?: 'root',
|
||||
'password' => getenv('DB_PASSWORD') ?: '',
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'options' => [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci",
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'host' => getenv('REDIS_HOST') ?: '127.0.0.1',
|
||||
'port' => (int)(getenv('REDIS_PORT') ?: 6379),
|
||||
'password' => getenv('REDIS_PASSWORD') ?: null,
|
||||
'database' => (int)(getenv('REDIS_DB') ?: 0),
|
||||
'timeout' => 2.0,
|
||||
'prefix' => 'uruk:',
|
||||
];
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
// Header names required for every authenticated request
|
||||
'headers' => [
|
||||
'user_id' => 'HTTP_X_USER_ID',
|
||||
'fingerprint' => 'HTTP_X_DEVICE_FINGERPRINT',
|
||||
'timestamp' => 'HTTP_X_TIMESTAMP',
|
||||
'nonce' => 'HTTP_X_NONCE',
|
||||
'signature' => 'HTTP_X_SIGNATURE',
|
||||
],
|
||||
// Time tolerance in seconds for anti-replay check
|
||||
'timestamp_tolerance' => 120, // 2 minutes
|
||||
// Dynamic QR token lifetime in seconds
|
||||
'qr_token_lifetime' => 90, // 90 seconds rotation
|
||||
// Encryption cipher
|
||||
'cipher' => 'aes-256-gcm',
|
||||
// Password hash algorithm
|
||||
'password_algo' => defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_BCRYPT,
|
||||
];
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use RuntimeException;
|
||||
|
||||
class Database
|
||||
{
|
||||
private static ?PDO $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
private function __clone() {}
|
||||
|
||||
public static function getConnection(): PDO
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
$config = require __DIR__ . '/../config/database.php';
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
|
||||
$config['host'],
|
||||
$config['port'],
|
||||
$config['database'],
|
||||
$config['charset']
|
||||
);
|
||||
|
||||
try {
|
||||
self::$instance = new PDO(
|
||||
$dsn,
|
||||
$config['username'],
|
||||
$config['password'],
|
||||
$config['options']
|
||||
);
|
||||
} catch (PDOException $e) {
|
||||
throw new RuntimeException('Database Connection Failed: ' . $e->getMessage(), (int)$e->getCode());
|
||||
}
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core;
|
||||
|
||||
abstract class Middleware
|
||||
{
|
||||
abstract public function handle(Request $request, callable $next): void;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core;
|
||||
|
||||
use Redis;
|
||||
use Throwable;
|
||||
|
||||
class RedisClient
|
||||
{
|
||||
private static ?Redis $instance = null;
|
||||
private static bool $connectionFailed = false;
|
||||
|
||||
private function __construct() {}
|
||||
private function __clone() {}
|
||||
|
||||
public static function getInstance(): ?Redis
|
||||
{
|
||||
if (self::$connectionFailed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (self::$instance === null) {
|
||||
if (!class_exists('Redis')) {
|
||||
self::$connectionFailed = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$config = require __DIR__ . '/../config/redis.php';
|
||||
$redis = new Redis();
|
||||
$connected = $redis->connect($config['host'], $config['port'], (float)$config['timeout']);
|
||||
|
||||
if ($connected) {
|
||||
if (!empty($config['password'])) {
|
||||
$redis->auth($config['password']);
|
||||
}
|
||||
if (!empty($config['database'])) {
|
||||
$redis->select((int)$config['database']);
|
||||
}
|
||||
$redis->setOption(Redis::OPT_PREFIX, $config['prefix']);
|
||||
self::$instance = $redis;
|
||||
} else {
|
||||
self::$connectionFailed = true;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
self::$connectionFailed = true;
|
||||
error_log('[Redis Error] ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function get(string $key): ?string
|
||||
{
|
||||
$redis = self::getInstance();
|
||||
if (!$redis) return null;
|
||||
$val = $redis->get($key);
|
||||
return $val !== false ? (string)$val : null;
|
||||
}
|
||||
|
||||
public static function set(string $key, string $value, int $ttlSeconds = 0): bool
|
||||
{
|
||||
$redis = self::getInstance();
|
||||
if (!$redis) return false;
|
||||
return $ttlSeconds > 0 ? (bool)$redis->setex($key, $ttlSeconds, $value) : (bool)$redis->set($key, $value);
|
||||
}
|
||||
|
||||
public static function del(string $key): bool
|
||||
{
|
||||
$redis = self::getInstance();
|
||||
if (!$redis) return false;
|
||||
return (bool)$redis->del($key);
|
||||
}
|
||||
|
||||
public static function has(string $key): bool
|
||||
{
|
||||
$redis = self::getInstance();
|
||||
if (!$redis) return false;
|
||||
return (bool)$redis->exists($key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core;
|
||||
|
||||
class Request
|
||||
{
|
||||
private string $method;
|
||||
private string $path;
|
||||
private array $headers;
|
||||
private array $queryParams;
|
||||
private array $body;
|
||||
private string $rawBody;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
$this->path = parse_url($uri, PHP_URL_PATH) ?: '/';
|
||||
$this->queryParams = $_GET ?? [];
|
||||
$this->headers = $this->extractHeaders();
|
||||
|
||||
$this->rawBody = file_get_contents('php://input') ?: '';
|
||||
$contentType = $this->getHeader('content-type');
|
||||
|
||||
if (str_contains($contentType, 'application/json') && !empty($this->rawBody)) {
|
||||
$decoded = json_decode($this->rawBody, true);
|
||||
$this->body = is_array($decoded) ? $decoded : [];
|
||||
} else {
|
||||
$this->body = $_POST ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
private function extractHeaders(): array
|
||||
{
|
||||
$headers = [];
|
||||
foreach ($_SERVER as $key => $value) {
|
||||
if (str_starts_with($key, 'HTTP_')) {
|
||||
$headerName = strtolower(str_replace('_', '-', substr($key, 5)));
|
||||
$headers[$headerName] = (string)$value;
|
||||
} elseif (in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH'], true)) {
|
||||
$headerName = strtolower(str_replace('_', '-', $key));
|
||||
$headers[$headerName] = (string)$value;
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function getHeader(string $name, string $default = ''): string
|
||||
{
|
||||
$name = strtolower($name);
|
||||
return $this->headers[$name] ?? $default;
|
||||
}
|
||||
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
public function getQueryParams(): array
|
||||
{
|
||||
return $this->queryParams;
|
||||
}
|
||||
|
||||
public function getQuery(string $key, $default = null)
|
||||
{
|
||||
return $this->queryParams[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function getBody(): array
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
public function get(string $key, $default = null)
|
||||
{
|
||||
return $this->body[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function getRawBody(): string
|
||||
{
|
||||
return $this->rawBody;
|
||||
}
|
||||
|
||||
public function getIp(): string
|
||||
{
|
||||
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
|
||||
return $_SERVER['HTTP_CF_CONNECTING_IP'];
|
||||
}
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
return trim($ips[0]);
|
||||
}
|
||||
return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||
}
|
||||
|
||||
public function getUserAgent(): string
|
||||
{
|
||||
return $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core;
|
||||
|
||||
class Response
|
||||
{
|
||||
public static function json(array $data, int $statusCode = 200, array $headers = []): void
|
||||
{
|
||||
http_response_code($statusCode);
|
||||
|
||||
$defaultHeaders = [
|
||||
'Content-Type' => 'application/json; charset=utf-8',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
'X-Frame-Options' => 'DENY',
|
||||
'X-XSS-Protection' => '1; mode=block',
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-User-Id, X-Device-Fingerprint, X-Timestamp, X-Nonce, X-Signature',
|
||||
];
|
||||
|
||||
foreach (array_merge($defaultHeaders, $headers) as $name => $val) {
|
||||
header("$name: $val");
|
||||
}
|
||||
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function success(mixed $data = null, string $message = 'Success', int $statusCode = 200): void
|
||||
{
|
||||
self::json([
|
||||
'success' => true,
|
||||
'status' => 'success',
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
'timestamp' => time(),
|
||||
], $statusCode);
|
||||
}
|
||||
|
||||
public static function error(string $message = 'An error occurred', int $statusCode = 400, array $errors = []): void
|
||||
{
|
||||
self::json([
|
||||
'success' => false,
|
||||
'status' => 'error',
|
||||
'message' => $message,
|
||||
'errors' => $errors,
|
||||
'timestamp' => time(),
|
||||
], $statusCode);
|
||||
}
|
||||
|
||||
public static function unauthorized(string $message = 'Unauthorized access'): void
|
||||
{
|
||||
self::error($message, 401);
|
||||
}
|
||||
|
||||
public static function forbidden(string $message = 'Forbidden'): void
|
||||
{
|
||||
self::error($message, 403);
|
||||
}
|
||||
|
||||
public static function notFound(string $message = 'Resource not found'): void
|
||||
{
|
||||
self::error($message, 404);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core;
|
||||
|
||||
class Router
|
||||
{
|
||||
private array $routes = [];
|
||||
|
||||
public function get(string $path, array|callable $handler, array $middlewares = []): void
|
||||
{
|
||||
$this->addRoute('GET', $path, $handler, $middlewares);
|
||||
}
|
||||
|
||||
public function post(string $path, array|callable $handler, array $middlewares = []): void
|
||||
{
|
||||
$this->addRoute('POST', $path, $handler, $middlewares);
|
||||
}
|
||||
|
||||
public function put(string $path, array|callable $handler, array $middlewares = []): void
|
||||
{
|
||||
$this->addRoute('PUT', $path, $handler, $middlewares);
|
||||
}
|
||||
|
||||
public function delete(string $path, array|callable $handler, array $middlewares = []): void
|
||||
{
|
||||
$this->addRoute('DELETE', $path, $handler, $middlewares);
|
||||
}
|
||||
|
||||
public function options(string $path, array|callable $handler): void
|
||||
{
|
||||
$this->addRoute('OPTIONS', $path, $handler, []);
|
||||
}
|
||||
|
||||
private function addRoute(string $method, string $path, array|callable $handler, array $middlewares): void
|
||||
{
|
||||
$pattern = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '(?P<$1>[^/]+)', $path);
|
||||
$pattern = '#^' . $pattern . '$#';
|
||||
|
||||
$this->routes[] = [
|
||||
'method' => $method,
|
||||
'path' => $path,
|
||||
'pattern' => $pattern,
|
||||
'handler' => $handler,
|
||||
'middlewares' => $middlewares,
|
||||
];
|
||||
}
|
||||
|
||||
public function dispatch(Request $request): void
|
||||
{
|
||||
// Handle preflight CORS OPTIONS requests immediately
|
||||
if ($request->getMethod() === 'OPTIONS') {
|
||||
Response::json(['status' => 'ok']);
|
||||
}
|
||||
|
||||
$method = $request->getMethod();
|
||||
$path = $request->getPath();
|
||||
|
||||
foreach ($this->routes as $route) {
|
||||
if ($route['method'] !== $method) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match($route['pattern'], $path, $matches)) {
|
||||
$params = [];
|
||||
foreach ($matches as $key => $value) {
|
||||
if (is_string($key)) {
|
||||
$params[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$this->runMiddlewares($route['middlewares'], $request, function () use ($route, $request, $params) {
|
||||
$this->executeHandler($route['handler'], $request, $params);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Response::notFound("Endpoint not found: [{$method}] {$path}");
|
||||
}
|
||||
|
||||
private function runMiddlewares(array $middlewares, Request $request, callable $target): void
|
||||
{
|
||||
$pipeline = array_reduce(
|
||||
array_reverse($middlewares),
|
||||
function ($next, $middlewareClass) {
|
||||
return function ($req) use ($next, $middlewareClass) {
|
||||
$instance = new $middlewareClass();
|
||||
$instance->handle($req, $next);
|
||||
};
|
||||
},
|
||||
$target
|
||||
);
|
||||
|
||||
$pipeline($request);
|
||||
}
|
||||
|
||||
private function executeHandler(array|callable $handler, Request $request, array $params): void
|
||||
{
|
||||
if (is_callable($handler)) {
|
||||
call_user_func($handler, $request, $params);
|
||||
return;
|
||||
}
|
||||
|
||||
[$controllerClass, $methodName] = $handler;
|
||||
if (!class_exists($controllerClass)) {
|
||||
Response::error("Controller class {$controllerClass} not found", 500);
|
||||
}
|
||||
|
||||
$controller = new $controllerClass();
|
||||
if (!method_exists($controller, $methodName)) {
|
||||
Response::error("Method {$methodName} not found on controller {$controllerClass}", 500);
|
||||
}
|
||||
|
||||
$controller->$methodName($request, $params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core;
|
||||
|
||||
class Security
|
||||
{
|
||||
public static function hashPassword(string $password): string
|
||||
{
|
||||
$algo = defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_BCRYPT;
|
||||
return password_hash($password, $algo);
|
||||
}
|
||||
|
||||
public static function verifyPassword(string $password, string $hash): bool
|
||||
{
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
|
||||
public static function generateRandomHex(int $bytes = 32): string
|
||||
{
|
||||
return bin2hex(random_bytes($bytes));
|
||||
}
|
||||
|
||||
public static function generateHmacSignature(string $data, string $secretKey): string
|
||||
{
|
||||
return hash_hmac('sha256', $data, $secretKey);
|
||||
}
|
||||
|
||||
public static function verifyHmacSignature(string $data, string $signature, string $secretKey): bool
|
||||
{
|
||||
$calculated = self::generateHmacSignature($data, $secretKey);
|
||||
return hash_equals($calculated, $signature);
|
||||
}
|
||||
|
||||
public static function encryptAesGcm(string $plaintext, string $key): string
|
||||
{
|
||||
$cipher = 'aes-256-gcm';
|
||||
$keyHash = hash('sha256', $key, true);
|
||||
$iv = random_bytes(12); // 96-bit IV recommended for GCM
|
||||
$tag = '';
|
||||
|
||||
$ciphertext = openssl_encrypt(
|
||||
$plaintext,
|
||||
$cipher,
|
||||
$keyHash,
|
||||
OPENSSL_RAW_DATA,
|
||||
$iv,
|
||||
$tag,
|
||||
'',
|
||||
16
|
||||
);
|
||||
|
||||
return base64_encode($iv . $tag . $ciphertext);
|
||||
}
|
||||
|
||||
public static function decryptAesGcm(string $encryptedPackage, string $key): ?string
|
||||
{
|
||||
$data = base64_decode($encryptedPackage, true);
|
||||
if (!$data || strlen($data) < 28) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cipher = 'aes-256-gcm';
|
||||
$keyHash = hash('sha256', $key, true);
|
||||
|
||||
$iv = substr($data, 0, 12);
|
||||
$tag = substr($data, 12, 16);
|
||||
$ciphertext = substr($data, 28);
|
||||
|
||||
$decrypted = openssl_decrypt(
|
||||
$ciphertext,
|
||||
$cipher,
|
||||
$keyHash,
|
||||
OPENSSL_RAW_DATA,
|
||||
$iv,
|
||||
$tag
|
||||
);
|
||||
|
||||
return $decrypted !== false ? $decrypted : null;
|
||||
}
|
||||
|
||||
public static function generateDynamicQrToken(int $userId, string $membershipNumber, int $expiresInSeconds, string $secretKey): string
|
||||
{
|
||||
$payload = [
|
||||
'uid' => $userId,
|
||||
'mem' => $membershipNumber,
|
||||
'exp' => time() + $expiresInSeconds,
|
||||
'rnd' => self::generateRandomHex(8),
|
||||
];
|
||||
|
||||
$json = json_encode($payload, JSON_UNESCAPED_SLASHES);
|
||||
$base64Payload = rtrim(strtr(base64_encode($json), '+/', '-_'), '=');
|
||||
$signature = self::generateHmacSignature($base64Payload, $secretKey);
|
||||
|
||||
return $base64Payload . '.' . $signature;
|
||||
}
|
||||
|
||||
public static function verifyDynamicQrToken(string $token, string $secretKey): ?array
|
||||
{
|
||||
$parts = explode('.', $token);
|
||||
if (count($parts) !== 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$base64Payload, $signature] = $parts;
|
||||
|
||||
if (!self::verifyHmacSignature($base64Payload, $signature, $secretKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$json = base64_decode(strtr($base64Payload, '-_', '+/'));
|
||||
if (!$json) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$payload = json_decode($json, true);
|
||||
if (!is_array($payload) || !isset($payload['exp']) || $payload['exp'] < time()) {
|
||||
return null; // Expired or invalid format
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Uruk International Prize - Deploy & Git Sync Script
|
||||
# Developed for: Hamza Ayed (Founding Tech Architect)
|
||||
# ==============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${BLUE}======================================================${NC}"
|
||||
echo -e "${BLUE} 🚀 Uruk Prize Platform - Deploy & Sync Pipeline ${NC}"
|
||||
echo -e "${BLUE}======================================================${NC}"
|
||||
|
||||
# Check if git is initialized
|
||||
if [ ! -d ".git" ] && [ ! -d "../.git" ]; then
|
||||
echo -e "${YELLOW}⚡ Initializing local Git repository...${NC}"
|
||||
git init
|
||||
fi
|
||||
|
||||
# 1. Staging changes
|
||||
echo -e "${BLUE}[1/4] Staging files...${NC}"
|
||||
git add -A
|
||||
|
||||
# 2. Committing with message or auto-timestamp
|
||||
COMMIT_MSG=${1:-"Update Uruk Prize Core & Security Layer - $(date '+%Y-%m-%d %H:%M:%S')"}
|
||||
echo -e "${BLUE}[2/4] Committing: ${YELLOW}'$COMMIT_MSG'${NC}"
|
||||
git commit -m "$COMMIT_MSG" || echo -e "${YELLOW}No changes to commit.${NC}"
|
||||
|
||||
# 3. Pushing to Remote Git Repository
|
||||
echo -e "${BLUE}[3/4] Pushing to Git remote...${NC}"
|
||||
if git remote | grep -q 'origin'; then
|
||||
git push origin main || git push origin master
|
||||
echo -e "${GREEN}✓ Successfully pushed to remote Git.${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ No remote 'origin' set yet. Run: git remote add origin <url>${NC}"
|
||||
fi
|
||||
|
||||
# 4. Server Sync instructions / trigger
|
||||
echo -e "${BLUE}[4/4] Remote Server Synchronization...${NC}"
|
||||
echo -e "${GREEN}✓ Local changes committed and ready for server pull.${NC}"
|
||||
echo -e "${BLUE}On your server, run:${NC}"
|
||||
echo -e " cd /path/to/uruk-prize && git pull origin main && composer install --no-dev --optimize-autoloader"
|
||||
echo -e "${GREEN}======================================================${NC}"
|
||||
@@ -0,0 +1,6 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.*)$ index.php [QSA,L]
|
||||
</IfModule>
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Set default timezone
|
||||
date_default_timezone_set('Asia/Baghdad');
|
||||
|
||||
// 1. PSR-4 Autoloader Implementation with Composer fallback
|
||||
if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
} else {
|
||||
spl_autoload_register(function (string $class) {
|
||||
$prefixes = [
|
||||
'Core\\' => __DIR__ . '/../core/',
|
||||
'App\\' => __DIR__ . '/../app/',
|
||||
];
|
||||
|
||||
foreach ($prefixes as $prefix => $baseDir) {
|
||||
$len = strlen($prefix);
|
||||
if (strncmp($prefix, $class, $len) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relativeClass = substr($class, $len);
|
||||
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
|
||||
|
||||
if (file_exists($file)) {
|
||||
require_once $file;
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Load Environment Variables if Dotenv exists or read .env manually
|
||||
$envFile = __DIR__ . '/../.env';
|
||||
if (file_exists($envFile)) {
|
||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (str_starts_with(trim($line), '#') || !str_contains($line, '=')) {
|
||||
continue;
|
||||
}
|
||||
[$name, $val] = explode('=', $line, 2);
|
||||
$name = trim($name);
|
||||
$val = trim($val, " \t\n\r\0\x0B\"'");
|
||||
if (!getenv($name)) {
|
||||
putenv("$name=$val");
|
||||
$_ENV[$name] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use Core\Router;
|
||||
use App\Middlewares\SecurityHeaderMiddleware;
|
||||
use App\Middlewares\AuthMiddleware;
|
||||
use App\Middlewares\AdminMiddleware;
|
||||
use App\Controllers\AuthController;
|
||||
use App\Controllers\SubscriptionController;
|
||||
use App\Controllers\QrController;
|
||||
use App\Controllers\PartnerController;
|
||||
use App\Controllers\PaymentController;
|
||||
use App\Controllers\AdminController;
|
||||
use App\Controllers\GatewayController;
|
||||
|
||||
$request = new Request();
|
||||
$router = new Router();
|
||||
|
||||
// ==============================================================================
|
||||
// PUBLIC ROUTES (No Strict Fingerprint Header Required for Initial Onboarding)
|
||||
// ==============================================================================
|
||||
$router->get('/api/v1/health', function (Request $req) {
|
||||
Response::success([
|
||||
'service' => 'Uruk International Prize Core Engine',
|
||||
'version' => '1.0.0',
|
||||
'server_time' => date('Y-m-d H:i:s'),
|
||||
'status' => 'operational',
|
||||
]);
|
||||
});
|
||||
|
||||
// Authentication routes
|
||||
$router->post('/api/v1/auth/request-otp', [AuthController::class, 'requestOtp']);
|
||||
$router->post('/api/v1/auth/verify-otp', [AuthController::class, 'verifyOtp']);
|
||||
|
||||
// Public directory of hospitals & hotels
|
||||
$router->get('/api/v1/partners', [PartnerController::class, 'listPartners']);
|
||||
$router->get('/api/v1/partners/{id}', [PartnerController::class, 'getPartner']);
|
||||
|
||||
// Public membership plan details
|
||||
$router->get('/api/v1/subscription/plans', [SubscriptionController::class, 'getPlans']);
|
||||
|
||||
// Reception QR verification scanner (used by hospitals/hotels)
|
||||
$router->post('/api/v1/qr/verify-token', [QrController::class, 'verifyToken']);
|
||||
|
||||
// Automated Payment Listeners & Webhooks
|
||||
$router->post('/api/v1/payments/ingest-notification', [PaymentController::class, 'ingestNotification']);
|
||||
$router->post('/api/v1/payments/swiftpay-webhook', [PaymentController::class, 'swiftPayWebhook']);
|
||||
|
||||
// ==============================================================================
|
||||
// GATEWAY CALLER & SMS CLUSTER (Zain, Asiacell, Korek 3-Device Gateway Nodes)
|
||||
// ==============================================================================
|
||||
$router->get('/api/v1/gateway/pending-call', [GatewayController::class, 'pendingCall']);
|
||||
$router->get('/pending-call.php', [GatewayController::class, 'pendingCall']);
|
||||
|
||||
$router->get('/api/v1/gateway/pending-sms', [GatewayController::class, 'pendingSms']);
|
||||
$router->get('/pending-sms.php', [GatewayController::class, 'pendingSms']);
|
||||
|
||||
$router->post('/api/v1/gateway/call-done', [GatewayController::class, 'callDone']);
|
||||
$router->post('/call-done.php', [GatewayController::class, 'callDone']);
|
||||
|
||||
$router->post('/api/v1/gateway/sms-done', [GatewayController::class, 'smsDone']);
|
||||
$router->post('/sms-done.php', [GatewayController::class, 'smsDone']);
|
||||
|
||||
$router->post('/api/v1/gateway/register-device', [GatewayController::class, 'registerDevice']);
|
||||
$router->post('/register-device.php', [GatewayController::class, 'registerDevice']);
|
||||
|
||||
// ==============================================================================
|
||||
// SECURE USER ROUTES (Requires X-User-Id, X-Device-Fingerprint, Signature & Auth)
|
||||
// ==============================================================================
|
||||
|
||||
$userMiddlewares = [SecurityHeaderMiddleware::class, AuthMiddleware::class];
|
||||
|
||||
$router->get('/api/v1/auth/profile', [AuthController::class, 'getProfile'], $userMiddlewares);
|
||||
$router->get('/api/v1/subscription/my-card', [SubscriptionController::class, 'getMyCard'], $userMiddlewares);
|
||||
$router->post('/api/v1/subscription/submit', [SubscriptionController::class, 'submitSubscription'], $userMiddlewares);
|
||||
$router->get('/api/v1/qr/dynamic-token', [QrController::class, 'getDynamicToken'], $userMiddlewares);
|
||||
|
||||
// ==============================================================================
|
||||
// HIDDEN IN-APP ADMIN ROUTES (Dr. Thamer & Management)
|
||||
// ==============================================================================
|
||||
$adminMiddlewares = [SecurityHeaderMiddleware::class, AuthMiddleware::class, AdminMiddleware::class];
|
||||
|
||||
$router->post('/api/v1/admin/partners/add', [AdminController::class, 'addPartner'], $adminMiddlewares);
|
||||
$router->get('/api/v1/admin/payments/pending', [AdminController::class, 'listPendingPayments'], $adminMiddlewares);
|
||||
$router->post('/api/v1/admin/payments/approve', [AdminController::class, 'approvePayment'], $adminMiddlewares);
|
||||
$router->get('/api/v1/admin/stats', [AdminController::class, 'getStats'], $adminMiddlewares);
|
||||
|
||||
// Dispatch incoming request
|
||||
$router->dispatch($request);
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Uruk International Prize - Deploy & Git Sync Pipeline
|
||||
# Architect: Hamza Ayed (Founding Tech Architect)
|
||||
# Remote: https://git.intaleqapp.com/Hamza/urukprize.git
|
||||
# Server: root@vmi2613292:/home/urukprize/htdocs/urukprize.intaleqapp.com
|
||||
# ==============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${BLUE}======================================================${NC}"
|
||||
echo -e "${BLUE} 🚀 Uruk Prize Platform - Deploy & Sync Pipeline ${NC}"
|
||||
echo -e "${BLUE}======================================================${NC}"
|
||||
|
||||
# Check if git is initialized
|
||||
if [ ! -d ".git" ]; then
|
||||
echo -e "${YELLOW}⚡ Initializing local Git repository...${NC}"
|
||||
git init
|
||||
git checkout -b main || git checkout -b main
|
||||
fi
|
||||
|
||||
# Ensure remote origin is set
|
||||
REMOTE_URL="https://git.intaleqapp.com/Hamza/urukprize.git"
|
||||
if ! git remote | grep -q 'origin'; then
|
||||
echo -e "${YELLOW}🔗 Adding remote origin: ${REMOTE_URL}${NC}"
|
||||
git remote add origin "$REMOTE_URL"
|
||||
else
|
||||
# Update origin URL if needed
|
||||
git remote set-url origin "$REMOTE_URL"
|
||||
fi
|
||||
|
||||
# 1. Staging files
|
||||
echo -e "${BLUE}[1/3] Staging files...${NC}"
|
||||
git add .
|
||||
|
||||
# 2. Committing with message or auto-timestamp
|
||||
COMMIT_MSG=${1:-"Update Uruk Prize Core, Gateway & Documentation - $(date '+%Y-%m-%d %H:%M:%S')"}
|
||||
echo -e "${BLUE}[2/3] Committing: ${YELLOW}'$COMMIT_MSG'${NC}"
|
||||
git commit -m "$COMMIT_MSG" || echo -e "${YELLOW}No new changes to commit.${NC}"
|
||||
|
||||
# 3. Pushing to Git remote
|
||||
echo -e "${BLUE}[3/3] Pushing to Git remote (${REMOTE_URL})...${NC}"
|
||||
git push -u origin main
|
||||
|
||||
echo -e "${GREEN}======================================================${NC}"
|
||||
echo -e "${GREEN}✓ Successfully pushed to Git repository!${NC}"
|
||||
echo -e "${BLUE}Now, run on your server (CloudPanel terminal):${NC}"
|
||||
echo -e "${YELLOW}cd /home/urukprize/htdocs/urukprize.intaleqapp.com && git pull origin main${NC}"
|
||||
echo -e "${GREEN}======================================================${NC}"
|
||||
@@ -0,0 +1,124 @@
|
||||
# وثيقة المعمارية الهندسية الشاملة - منصة وتطبيق جائزة أوروك الدولية
|
||||
**Document Version:** 1.0.0
|
||||
**Date:** September 2026
|
||||
**Target Platform:** Mobile (iOS / Android) & Web Backend
|
||||
|
||||
---
|
||||
|
||||
## 1. الملخص التنفيذي والرؤية الهندسية
|
||||
يهدف مشروع **جائزة أوروك الدولية** إلى إطلاق منصة رقمية متكاملة لتقديم خدمات العضوية الذكية، والتحقق الفوري من مزايا المشتركين، والتي تشمل:
|
||||
1. **القطاع الصحي:** خصم 50% لدى شبكة المستشفيات والمراكز التخصصية المتعاقد معها في العراق، إقليم كردستان، الأردن، مصر، ولبنان.
|
||||
2. **القطاع الفندقي:** خصومات سياحية تتراوح بين 40% و60% في الفنادق الشريكة.
|
||||
3. **أكاديمية التدريب والشهادات:** وصول مجاني لأكثر من 30 تخصصاً تدريبياً معتمداً (حضورياً في قاعات الفنادق الشريكة أو رقمياً عبر منصة البث المباشر لأوروك).
|
||||
|
||||
---
|
||||
|
||||
## 2. المكدس التقني المعتمد (Technology Stack)
|
||||
|
||||
### أ. الواجهة الأمامية للجوال (Frontend Mobile App):
|
||||
* **إطار العمل:** `Flutter` (Cross-platform لنظامي Android و iOS).
|
||||
* **معرف الحزمة (Package Identifier):** `iq.urukprize.app` (مستقل وبدون كلمة example).
|
||||
* **إدارة الحالة (State Management):** `Bloc / Cubit` لضمان فصل منطق الأعمال عن واجهات العرض (Separation of Concerns) وقابلية الاختبار العالية.
|
||||
* **الحزم البرمجية الأساسية:**
|
||||
* `flutter_bloc`
|
||||
* `flutter_secure_storage` (تخزين المفاتيح الحساسة في Keychain على iOS و Keystore على Android).
|
||||
* `http` (الاتصال المشفر بواجهات REST API).
|
||||
* `crypto` (توليد البصمة المشفرة وتوقيع الطلبات HMAC-SHA256).
|
||||
* `device_info_plus` (جمع معطيات العتاد لتوليد بصمة الجهاز Fingerprint).
|
||||
* `qr_flutter` (عرض رمز الاستجابة السريعة الديناميكي).
|
||||
* `mobile_scanner` (مسح الرموز لموظفي الاستقبال والتحقق).
|
||||
|
||||
### ب. الواجهة الخلفية والخوادم (Backend Core Engine):
|
||||
* **اللغة والأسلوب:** `Pure PHP` (Custom Modular Core Framework).
|
||||
* سرعة استجابة فائقة (Zero Framework Overhead).
|
||||
* استهلاك خفيف لموارد الذاكرة ووحدة المعالجة المركزية.
|
||||
* هيكلية معمارية تعتمد على طبقات: `Router`, `Controllers`, `Models`, `Services`, `Middlewares`.
|
||||
* **قاعدة البيانات:** `MySQL 8.0` مع محرك تخزين `InnoDB`، وتشفير `utf8mb4_unicode_ci` لدعم اللغة العربية بدقة كاملة.
|
||||
* **إدارة التبعيات:** ملف `composer.json` معزول ومستقل لضمان سهولة نقل المشروع لأي سيرفر آخر (`composer install`).
|
||||
* **التكامل الخارجي:**
|
||||
* منصة `OTPIQ` للتحقق متعدد القنوات (WhatsApp Smart Fallback to SMS).
|
||||
* منصة `SwiftPayIQ` لبوابة الدفع الرقمية في المرحلة الأولى، مع دعم قنوات SuperQi و ZainCash.
|
||||
* بوابة التحقق من الهوية الرقمية عبر متصفح الويب لموظفي الاستقبال في الفنادق والمستشفيات.
|
||||
|
||||
---
|
||||
|
||||
## 3. المخطط المعماري للنظام (System Architecture Diagram)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Client Layer
|
||||
A[Flutter App - Android / iOS]
|
||||
B[Reception Web Portal - Hospital / Hotel Scanner]
|
||||
C[Admin Web Portal - Dr. Thamer & Management]
|
||||
end
|
||||
|
||||
subgraph Security & Gateway Layer
|
||||
D[HTTPS / TLS 1.3]
|
||||
E[Security Middleware: Header & Fingerprint Validation]
|
||||
F[HMAC Signature & Nonce Verification]
|
||||
G[Rate Limiter & WAF]
|
||||
end
|
||||
|
||||
subgraph Pure PHP Core Backend
|
||||
H[Core Router]
|
||||
I[Auth Controller]
|
||||
J[Membership & QR Controller]
|
||||
K[Partner Directory Controller]
|
||||
L[Payment & Webhook Controller]
|
||||
M[Admin In-App Controller]
|
||||
|
||||
N[Services Layer]
|
||||
N1[OTPIQ Gateway Service]
|
||||
N2[SwiftPayIQ / Payment Service]
|
||||
N3[QR Cryptographic Signer Service]
|
||||
N4[Audit & Fingerprint Service]
|
||||
end
|
||||
|
||||
subgraph Persistence Layer
|
||||
O[(MySQL 8.0 Database)]
|
||||
P[Encrypted Data at Rest: AES-256]
|
||||
end
|
||||
|
||||
A --> D
|
||||
B --> D
|
||||
C --> D
|
||||
D --> E
|
||||
E --> F
|
||||
F --> G
|
||||
G --> H
|
||||
H --> I & J & K & L & M
|
||||
I & J & K & L & M --> N
|
||||
N --> N1 & N2 & N3 & N4
|
||||
N --> O
|
||||
O --> P
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. المكونات الوظيفية الرئيسية (Functional Modules)
|
||||
|
||||
### 1. وحدة الهوية الرقمية والرمز الديناميكي (Smart Digital ID & Dynamic QR):
|
||||
* عرض هوية رقمية تفاعلية تحاكي الهوية الفيزيائية الفاخرة لجائزة أوروك.
|
||||
* توليد كود QR مشفر يتغير دورياً أو يتضمن توقيعاً رقمياً زمنياً يمنع أخذ لقطات شاشة (Screenshots) وتداولها بين غير المشتركين.
|
||||
* عند مسح الكود، يتم توجيه موظف المستشفى/الفندق إلى صفحة تحقق رسمية مشفرة تثبت:
|
||||
* الاسم الرباعي وصورة المشترك.
|
||||
* حالة الاشتراك (نشط / منتهي).
|
||||
* رقم العضوية المعتمد.
|
||||
* نسبة الخصم المعتمدة رسمياً للمؤسسة الشريكة.
|
||||
|
||||
### 2. دليل الشركاء الذكي (Smart Directory):
|
||||
* تصفح وتصفية المستشفيات، المراكز الطبية، والفنادق حسب الدولة والمدينة والتخصص.
|
||||
* عرض مباشر لنسبة الخصم، معلومات الاتصال، والموقع الجغرافي.
|
||||
* تحديث لحظي للشركاء الجدد بمجرد إضافتهم من قبل الإدارة.
|
||||
|
||||
### 3. بوابة الإدارة السريعة داخل التطبيق (In-App Admin Gateway):
|
||||
* وصول آمن مخصص لمسؤولي الجائزة (د. ثامر وفريق الإدارة) عبر إيماءة سرية (Secret Gesture / Long Press) مع مصادقة بصمة الوجه أو رمز مرور مشفر.
|
||||
* تمكين الإدارة من:
|
||||
* إدخال عقود المستشفيات والفنادق الجديدة فور توقيعها وتفعيلها لجميع المشتركين مباشرة.
|
||||
* تفعيل الاشتراكات اليدوية أو التحقق من الإيصالات المصرفية المباشرة فورياً.
|
||||
* إحصائيات حية لعدد المشتركين والمبالغ المحصلة.
|
||||
|
||||
### 4. وحدة الدورات التدريبية المعتمدة (Academy & Workshops):
|
||||
* جدول سنوي للمحاضرات والورش الحضورية في قاعات الفنادق الشريكة.
|
||||
* روابط مباشرة للمنصة الرقمية للبث المباشر وتسجيل الحضور آلياً.
|
||||
* نظام إصدار الشهادات الرقمية المعتمدة وتوليد رقم تسلسلي فريد لكل متدرب.
|
||||
@@ -0,0 +1,107 @@
|
||||
# بروتوكول الأمان، التشفير، وبصمة الجهاز (Security, Fingerprint & Header Protocol)
|
||||
**Document Version:** 1.0.0
|
||||
**Date:** September 2026
|
||||
|
||||
---
|
||||
|
||||
## 1. تحليل المتطلب الأمني والمصطلحات التقنية
|
||||
أشار المطلب الصوتي إلى ضرورة إلزام كل طلب صادر من التطبيق باحتواء بيانات التحقق الصارمة:
|
||||
> "أي طلب لازم يكون الـ Header، يعني يكون فيه المستخدم، ومعرف المستخدم ID، والـ Fingerprint، حتى تكون أموره أفضل."
|
||||
|
||||
لتحقيق أقصى درجات الحماية ومنع مشاركة الحسابات أو تزوير بطاقات العضوية، تم تصميم بروتوكول أمني متكامل يربط بين:
|
||||
1. **بصمة الجهاز الفيزيائية والرقمية (Device Fingerprint).**
|
||||
2. **ترويسة الطلب الأمنية المشفرة (Custom Security Headers).**
|
||||
3. **التوقيع الرقمي للطلب (Request HMAC-SHA256 Signing).**
|
||||
4. **منع هجمات الإعادة (Anti-Replay Protection) عبر الـ Nonce والـ Timestamp.**
|
||||
|
||||
---
|
||||
|
||||
## 2. آلية توليد بصمة الجهاز (Device Fingerprint Pipeline)
|
||||
|
||||
في تطبيق Flutter، يتم جمع مزيج فريد من خصائص العتاد والنظام لتوليد معرف بصمة رقمية لا يتكرر:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Hardware IDs: Motherboard / CPU / Model] --> D[Fingerprint Generator]
|
||||
B[OS Details: Build Number / Android-iOS ID] --> D
|
||||
C[Cryptographic Random Salt in SecureStorage] --> D
|
||||
D --> E[SHA-256 Hashing]
|
||||
E --> F[Device Fingerprint: 64-Hex String]
|
||||
```
|
||||
|
||||
### خطوات التوليد والتخزين عبر الـ Native MethodChannel:
|
||||
1. **الاستغناء الكامل عن الحزم الخارجية:** لا يتم استخدام حزم جاهزة مثل `device_info_plus` لتفادي البطء والثغرات وكسر التوافقية مع تحديثات النظام.
|
||||
2. **قناة تواصل أصيلة (Native MethodChannel):**
|
||||
* **على نظام Android (بلغة Kotlin):**
|
||||
* قراءة مباشرة لمعرف النظام المشفر `Settings.Secure.ANDROID_ID`.
|
||||
* دمج مصفوفة خصائص العتاد الصلبة: `Build.MANUFACTURER`, `Build.MODEL`, `Build.HARDWARE`, `Build.BOARD`, `Build.FINGERPRINT`.
|
||||
* توليد ملح تشفيري في معالج الأمان المادي `Android KeyStore` لضمان عدم استنساخ البصمة حتى لو تم عمل Root للهاتف.
|
||||
* **على نظام iOS (بلغة Swift):**
|
||||
* قراءة المعرف العتادي الموثوق `UIDevice.current.identifierForVendor?.uuidString`.
|
||||
* حفظ واسترجاع الملح التشفيري في `Apple Keychain Services` تحت معيار الأمان الصارم `kSecAttrAccessibleAfterFirstUnlock`.
|
||||
3. يتم دمج هذه البيانات وتشفيرها عبر خوارزمية `SHA-256` لإنتاج بصمة رقمية بطول 64 حرفاً (`X-Device-Fingerprint`).
|
||||
4. تضمن هذه المعمارية الأصيلة عملاً سلساً وبسرعة أداء فائقة على كافة الأجهزة، وخاصة هواتف هواوي والأجهزة الصينية التي لا تدعم خدمات جوجل.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 3. مواصفات ترويسة الطلب الأمنية (Security Headers Protocol)
|
||||
|
||||
كل استدعاء برمجيات (API Request) من تطبيق Flutter إلى خادم الـ PHP يجب أن يرفق الترويسات التالية:
|
||||
|
||||
| اسم الترويسة (Header) | النوع | الوصف والأهمية |
|
||||
| :--- | :--- | :--- |
|
||||
| `X-User-Id` | Integer / UUID | المعرف الرقمي الفريد للمستخدم في النظام |
|
||||
| `X-Device-Fingerprint` | String (SHA-256) | بصمة الجهاز المعتمدة والمطابقة لسجل الجهاز في السيرفر |
|
||||
| `X-Timestamp` | Unix Timestamp | التوقيت الدقيق لإرسال الطلب (بالثواني) |
|
||||
| `X-Nonce` | UUID v4 String | رمز فريد يُستخدم لمرة واحدة فقط لمنع هجمات إعادة الإرسال |
|
||||
| `X-Signature` | String (HMAC-SHA256) | توقيع مشفر يدمج الرابط، التاريخ، النونس، وجسم الطلب |
|
||||
| `Authorization` | Bearer `<Token>` | رمز الجلسة المشفر الممنوح للمستخدم بعد تسجيل الدخول |
|
||||
|
||||
### معادلة حساب التوقيع الرقمي (Signature Formula):
|
||||
```
|
||||
Signature_Payload = Method + "\n" + Path + "\n" + Timestamp + "\n" + Nonce + "\n" + Hash(RequestBody)
|
||||
X-Signature = HMAC_SHA256(Signature_Payload, User_Device_Secret)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. التحقق على خادم الـ Pure PHP (Backend Security Middleware)
|
||||
|
||||
تقوم طبقة الـ Middleware في الخادم بالتحقق من الطلب بالخطوات التالية قبل تمريره لأي متحكم (Controller):
|
||||
1. **التحقق من نافذة الوقت (Timestamp Window):** التأكد من أن `X-Timestamp` يقع ضمن نطاق ±60 ثانية من توقيت السيرفر لمنع الطلبات المؤجلة.
|
||||
2. **فحص الـ Nonce:** التحقق من أن القيمة لم تُستخدم من قبل (مخزنة في جدول سريع أو كاش الذاكرة لمدة 10 دقائق).
|
||||
3. **مطابقة بصمة الجهاز (Fingerprint Match):** مقارنة `X-Device-Fingerprint` بالبصمة المسجلة لـ `X-User-Id` في جدول `user_security`.
|
||||
4. **التحقق من التوقيع (HMAC Validation):** إعادة حساب التوقيع باستخدام المفتاح السري للجهاز ومقارنته بـ `X-Signature` باستخدام دالة `hash_equals()` المقاومة لهجمات التوقيت (Timing Attacks).
|
||||
|
||||
---
|
||||
|
||||
## 5. أمن الهوية الرقمية والـ QR Code الديناميكي
|
||||
|
||||
لمنع ظاهرة تداول لقطات الشاشة (Screenshot Sharing) بين غير المشتركين للاستفادة من خصم الـ 50% في المستشفيات والفنادق، تم تصميم الـ QR Code ليكون ديناميكياً وموقعاً تشفيرياً:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UserApp as تطبيق المشترك
|
||||
participant Server as خادم أوروك PHP
|
||||
participant Scanner as هاتف موظف المستشفى / الفندق
|
||||
|
||||
UserApp->>Server: طلب تحديث كود العضوية (مع الهيدر والبصمة)
|
||||
Server-->>UserApp: رمز زمني موقع HMAC (صالح لـ 90 ثانية فقط)
|
||||
UserApp->>UserApp: عرض QR Code التفاعلي على الشاشة
|
||||
Scanner->>UserApp: مسح الرمز بكاميرا الهاتف
|
||||
Scanner->>Server: إرسال الرمز للتحقق (Verify Token)
|
||||
Server-->>Scanner: عرض فوري: الاسم، الصورة، صلاحية الخصم 50%، وتسجيل سجل الزيارة
|
||||
```
|
||||
|
||||
### بنية رمز الـ QR المشفر:
|
||||
```json
|
||||
{
|
||||
"uid": 1042,
|
||||
"mem": "URUK-2026-8891",
|
||||
"exp": 1792451000,
|
||||
"sig": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
}
|
||||
```
|
||||
* الرمز لا يكشف أي بيانات سرية مباشرة.
|
||||
* إذا انتهت صلاحية التوقيع الزمني، يرفض نظام المستشفى العملية تلقائياً ويطلب إبراز كود جديد ومباشر من داخل التطبيق.
|
||||
@@ -0,0 +1,223 @@
|
||||
# استراتيجية بوابات الدفع وأنظمة الرسائل والمصادقة (Payments & SMS Strategy)
|
||||
**Document Version:** 1.0.0
|
||||
**Date:** September 2026
|
||||
|
||||
---
|
||||
|
||||
## 1. مقارنة وتقييم التحليلين (الميداني والتنظيمي المعماري)
|
||||
|
||||
لدينا تحليلان متكاملان يمثلان وجهين لمعادلة واحدة:
|
||||
|
||||
### التحليل الأول (الميداني السريع):
|
||||
* **التركيز:** التدفق النقدي الفوري خلال الأشهر الثلاثة القادمة لتمويل مصاريف السيرفرات ومشاريع النقل (تطبيق التاكسي / انطلق).
|
||||
* **القنوات:** التحويلات الشخصية المباشرة P2P عبر تطبيق `SuperQi` (حساب د. ثامر كما ظهر في إشعار الـ 2000 دينار)، محفظة زين كاش، وبطاقة الرافدين ماستركارد، ونظام `CliQ` الأردني.
|
||||
* **آلية الأتمتة الميدانية:** هاتف أندرويد مركزي يلتقط رسائل الـ SMS أو إشعارات تطبيق SuperQi عبر تطبيق خفيف (Notification/SMS Listener) ليرسل رقم الحركة والمبلغ إلى السيرفر لتفعيل الاشتراك فوراً.
|
||||
* **الميزة:** انطلاق فوري اليوم دون انتظار موافقات مصرفية وبيروقراطية شركات الدفع.
|
||||
* **العيب:** قيود السقوف المالية للتحويلات الشخصية، ومخاطر حظر الحسابات عند تكرار مئات الحوالات اليومية بدون صفة تجارية رسمية.
|
||||
|
||||
### التحليل الثاني (المعماري والتنظيمي الموسع):
|
||||
* **التركيز:** الامتثال التام لتعليمات البنك المركزي العراقي رقم (2) لسنة 2024، معايير مكافحة غسل الأموال (AML)، وبناء بنية تحتية مؤسسية قابلة للتوسع بملايين الدولارات.
|
||||
* **القنوات:** منصة `SwiftPayIQ` كوسيط تقني سريع (Aggregator) للتجاوز السريع لحواجز الـ KYC المبدئية، ثم الانتقال للربط المباشر مع `ZainCash Business API` (عمولة 0.6%)، وبوابة `Qi Card (ISC)` الرسمية مع `3D Secure`، ومصرف العراق الأول `FIB` عبر الروابط العميقة (Deep Links).
|
||||
* **المصادقة (SMS/OTP):** منصة `OTPIQ` العراقية بتقنية التوجيه الذكي (WhatsApp ثم SMS) لتخفيض تكلفة التحقق بنسبة 70%.
|
||||
|
||||
---
|
||||
|
||||
## 2. الخلاصة والحكم الاستراتيجي: أيهما أفضل؟
|
||||
|
||||
> **القرار الهندسي الموصى به:** **تبني الاستراتيجية الهجينة ذات المرحلتين (Two-Phase Hybrid Orchestration).**
|
||||
|
||||
لا يمكن الانتظار شهوراً حتى صدور السجلات التجارية والتدقيق البنكي الكامل للحصول على حساب شركة رسمي، وفي نفس الوقت لا يمكن الاعتماد للأبد على الحسابات الشخصية التي قد تتعرض للإيقاف المفاجئ عند وصول مبالغ كبيرة.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Phase 1: Immediate Cashflow (Months 1-3)
|
||||
A[المستخدم يطلب الاشتراك في أوروك] --> B{اختيار طريقة السداد}
|
||||
B -->|تحويل مباشر SuperQi / ZainCash| C[تحويل يدوي وإرفاق رقم الحركة والإيصال]
|
||||
C --> D[هاتف أندرويد وسيط يلتقط الإشعار تلقائياً]
|
||||
D --> E[تفعيل فوري للاشتراك]
|
||||
B -->|دفع إلكتروني مباشر| F[بوابة SwiftPayIQ عبر Sandbox / الحساب الميسر]
|
||||
F --> G[تأكيد فوري عبر Webhook رسمي]
|
||||
G --> E
|
||||
end
|
||||
|
||||
subgraph Phase 2: Enterprise Scaling (From Jan 1)
|
||||
H[اكتمال أوراق الشركة لدى البنك المركزي] --> I[ZainCash Corporate 0.6%]
|
||||
H --> J[Qi Card Gateway with 3D Secure]
|
||||
H --> K[FIB Direct Deep Links for Corporate/VIP]
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. دراسة المصادقة الرقمية في العراق: الجيميل وهواتف هواوي ونظام Flash Call
|
||||
|
||||
### أ. دراسة واقع استخدام الجيميل (Gmail) في السوق العراقي:
|
||||
* **سلوك المستخدم:** في العراق، يعتبر **رقم الهاتف المحمول (MSISDN)** هو المعرف الرقمي الأساسي والأوحد لـ 95% من السكان.
|
||||
* **إشكالية البريد الإلكتروني:** معظم المستخدمين العراقيين (خاصة الفئات الشعبية وأرباب الأسر والمتقاعدين المستفيدين من خصومات المستشفيات) لا يتذكرون كلمات مرور بريدهم الإلكتروني ولا يتفقدون صندوق الوارد؛ حيث يقوم أصحاب محلات الهواتف عادةً بإنشاء حساب Gmail تجريبي لتشغيل الهاتف وتنزيل التطبيقات فقط.
|
||||
* **النتيجة:** الاعتماد على البريد الإلكتروني أو روابط التحقق (Magic Links) سيؤدي إلى نسبة فقدان مستخدمين (Drop-off Rate) تتجاوز 65%.
|
||||
|
||||
### ب. معضلة هواتف هواوي والأجهزة الصينية (Huawei HMS vs Google GMS):
|
||||
* تحظى هواتف **Huawei** (سلسلة Y و Nova) إلى جانب أجهزة **Infinix** و **Tecno** بحصة سوقية كبيرة في العراق نظراً لأسعارها الاقتصادية.
|
||||
* هواتف هواوي الحديثة خالية تماماً من خدمات جوجل (`GMS`) وتعتمد على بيئة (`HMS Core`).
|
||||
* إذا اعتمدنا على:
|
||||
* **تسجيل الدخول بالجيميل (Google Sign-In):** يتعطل تماماً ويعطي خطأ Play Services Unavailable.
|
||||
* **التحقق برقم الهاتف عبر Firebase Phone Auth:** يعتمد فايربيز على Google Play Integrity و SafetyNet. على أجهزة هواوي بدون GMS، يفشل التحقق الصامت ويجبر المستخدم على فتح متصفح لحل كابتشا صورية معقدة تفشل في معظم شبكات العراق.
|
||||
* **القرار المعماري:** يجب أن يكون نظام الدخول مستقلاً بالكامل عن خدمات جوجل (GMS-Agnostic)، ليعمل بنفس الكفاءة على أجهزة Samsung، Xiaomi، Huawei (HMS)، و iPhone.
|
||||
|
||||
---
|
||||
|
||||
## 4. معمارية التحقق الثورية: Flash Call OTP + أندرويد جيتواي + منصة نبيه (واتساب)
|
||||
|
||||
بناءً على الخبرة العملية الناجحة في تطبيق سيرو (Siro Platform)، نعتمد الهيكلية الهجينة التالية:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[المستخدم يطلب الدخول برقم هاتفه العراقي] --> B[خادم أوروك يرسل أمراً لهاتف البوابة المركزي]
|
||||
B --> C{نوع جهاز المشترك}
|
||||
|
||||
C -->|هاتف أندرويد المشترك| D[هاتف البوابة يقوم بمكالمة فلاش Flash Call]
|
||||
D --> E[تطبيق أوروك يكتشف الرنة صامتاً وينهيها]
|
||||
E --> F[تفعيل فوري بنقرة صفرية وبتكلفة صفر دينار!]
|
||||
|
||||
C -->|هاتف آيفون iOS المشترك| G[هاتف البوابة يرسل SMS مباشرة من شريحته المحلية]
|
||||
G --> H[المشترك يدخل الكود الواصل من رقم عراقي محلي]
|
||||
H --> I[تفعيل العضوية]
|
||||
|
||||
D -.->|فشل الاتصال / عدم الرد| J[التحويل التلقائي الذكي Fallback]
|
||||
G -.->|تأخر وصول الرسالة| J
|
||||
J --> K[إرسال كود OTP عبر منصة نبيه Nabih على الواتساب]
|
||||
K --> L[تأكيد فوري وموثوق 100%]
|
||||
```
|
||||
|
||||
### 1. تخصيص مسار التحقق بحسب نظام التشغيل (OS-Specific Routing):
|
||||
* يرسل تطبيق فلاتر نوع نظام التشغيل مع كل طلب تحقق:
|
||||
* `device_os: "android"`
|
||||
* أو `device_os: "ios"`
|
||||
* **على هواتف أندرويد (Android):** دائماً **مكالمة فلاش (Flash Call)** مع التحقق التلقائي الصامت؛ حيث يستمع التطبيق عبر MethodChannel الأصلي للمكالمة، ويستخرج الكود وينهي الرنة فورياً بنقرة صفرية (Zero-Click) وتكلفة صفر دينار.
|
||||
* **على هواتف آيفون (iOS):** **رسائل قصيرة (SMS) فقط وحصرياً.** تم إلغاء فكرة قراءة آخر 4 أرقام على الآيفون لدواعي الأمان ومنع التسريب؛ حيث يتلقى مستخدم الآيفون رسالة SMS تحتوي على كود التحقق الرسمي ويدخله في التطبيق.
|
||||
|
||||
### 2. المداورة الشاملة عبر 3 خطوط متكافئة (Universal 3-Line Round-Robin):
|
||||
لا نقيد كل شريحة بمخاطبة شبكتها فقط، بل تعمل الشرائح الثلاث بنظام **المداورة الشاملة (Universal Round-Robin)** لمخاطبة أي رقم داخل العراق لضمان توزيع متكافئ للضغط:
|
||||
1. **الخط الأول:** شريحة زين العراق (مكالمات + SMS + واتساب نبيه).
|
||||
2. **الخط الثاني:** شريحة آسيا سيل (مكالمات + SMS + واتساب نبيه).
|
||||
3. **الخط الثالث:** شريحة كورك تليكوم (مكالمات + SMS + واتساب نبيه).
|
||||
|
||||
* **نافذة الـ 15 ثانية وإشعار الخطأ الفوري:**
|
||||
* يمتلك هاتف البوابة آلية إشعار فوري للسيرفر في حال حدوث أي خطأ في إجراء المكالمة أو إرسال الرسالة.
|
||||
* يظهر في تطبيق المشترك عداد زمني مدته **15 ثانية فقط**. إذا انتهت الـ 15 ثانية أو ورد إشعار فشل، يتحول النظام آلياً وفورياً إلى إرسال صورة الكابتشا المشفرة عبر **منصة نبيه على الواتساب**، والتي تتناوب بدورها على نفس الخطوط الثلاثة.
|
||||
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[طلب تحقق: رقم الهاتف + نوع النظام] --> B[محرك التوزيع الذكي في خادم أوروك]
|
||||
|
||||
subgraph 3-Device Gateway Cluster
|
||||
C[الهاتف 1: شريحة زين]
|
||||
D[الهاتف 2: شريحة آسيا سيل]
|
||||
E[الهاتف 3: شريحة كورك / احتياطي]
|
||||
end
|
||||
|
||||
B -->|توزيع الأحمال والمداورة Round-Robin| C & D & E
|
||||
|
||||
C & D & E -->|القناة 1: مكالمة فلاش مجانية| F[الرنة الخاطفة 0 دينار]
|
||||
C & D & E -->|القناة 2: رسائل SMS محلية| G[رسائل نصية منخفضة التكلفة]
|
||||
C & D & E -->|القناة 3: منصة نبيه للواتساب| H[إرسال كابتشا مشفرة عبر 3 خطوط متناوبة]
|
||||
```
|
||||
|
||||
* **مداورة منصة نبيه على نفس الخطوط الثلاثة:**
|
||||
* منصة نبيه تدعم نظام مداورة مدمج (Round-Robin) على 3 إلى 6 أرقام واتساب.
|
||||
* بتسجيل أرقام الهواتف الثلاثة نفسها على منصة نبيه، تتوزع رسائل كابتشا الواتساب على الأرقام الثلاثة بالتناوب، مما يجعل معدل الإرسال على كل رقم منخفضاً جداً ويبعده تماماً عن شبهات الحظر أو تجاوز حدود ميتا.
|
||||
|
||||
|
||||
### 3. دراسة تقنية التحقق الصامت (SNV) ومحدوديتها الميدانية في العراق:
|
||||
* **حصرية المشغل:** تقنية SNV مفعلة تجارياً كواجهة برمجة تطبيقات رسمية لدى شركة **آسيا سيل (Asiacell)** فقط، بينما **زين العراق (Zain)** لا توفر واجهة SNV عامة للمطورين، وشركة **كورك (Korek)** لا تدعمها مطلقاً.
|
||||
* **عائق انعدام رصيد البيانات (Zero Balance):** أغلب المستخدمين في العراق يطفئون بيانات الهاتف تماماً لتوفير الرصيد، ويعتمدون على شبكات الواي فاي المنزلية (EarthLink / كابلات الحي). إذا لم يكن لدى المشترك اشتراك إنترنت فعال على الشريحة، تفشل محاولة الاتصال الخفي ببرج الاتصالات فوراً.
|
||||
* **إشكالية خطوط البيانات النقية (Data-only SIMs) ومشاركات الراوتر:** العديد من المستخدمين يضعون الشريحة في راوتر منزلي أو يستخدمون خط بيانات لا يستقبل مكالمات إطلاقاً.
|
||||
* **النتيجة والقرار:** لا يمكن الاعتماد على SNV كبنية أساسية وحيدة في العراق، بل يبقى مسار **الواتساب عبر منصة نبيه (بصورة الكابتشا)** ومسار **مكالمات الفلاش (Flash Call)** هما العمود الفقري الأكثر موثوقية وعملية في السوق العراقي بنسبة نجاح 100%.
|
||||
|
||||
### 4. مسار التحويل الذكي الاحتياطي (Smart Fallback via Nabih Platform):
|
||||
* في حال فشل الرنة (خارج التغطية، خط بيانات نقي لا يستقبل مكالمات، أو بطاقة منتهية الصلاحية)، يتم تحويل الإرسال فورياً وبشكل تلقائي إلى منصة **نبيه (Nabih)**.
|
||||
* تقوم منصة نبيه بإرسال كود التحقق كصورة كابتشا مشفرة عبر الواتساب، لتعمل بسلاسة حتى لو كان المشترك متصلاً بالواي فاي وبدون رصيد خلوى نهائياً!
|
||||
|
||||
|
||||
#### 4. آلية الكشف اللحظي عن فشل الرنة، انقطاع التغطية، وتأخر الرسالة (Real-Time Failure Detection)
|
||||
|
||||
كيف يعرف النظام في أجزاء من الثانية أن الهاتف خارج التغطية أو أن الرسالة تأخرت ليقوم بالتحويل التلقائي فوراً؟
|
||||
|
||||
يعتمد النظام على **محرك كشف ثلاثي الطبقات (3-Layer Detection Engine)**:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Layer 1: Gateway Detection
|
||||
A[أمر الرنة من السيرفر] --> B[هاتف البوابة يطلب الرقم]
|
||||
B --> C{استجابة شبكة الاتصال}
|
||||
C -->|OUT_OF_SERVICE / مغلق| D[كشف فوري بأقل من ثانيتين عبر TelephonyManager]
|
||||
C -->|ALERTING / يرن طبيعي| E[نجاح الرنة الصامتة]
|
||||
C -->|SMS Delivery Intent| F{تقرير تسليم الشبكة SMSC}
|
||||
F -->|لم يتم التسليم خلال 10 ثوانٍ| G[إبلاغ السيرفر بتأخر التسليم]
|
||||
end
|
||||
|
||||
subgraph Layer 2: Client App Detection
|
||||
H[المشترك ينتظر في شاشة التطبيق] --> I[عداد تنازلي ذكي 12-15 ثانية]
|
||||
I -->|لم تكتشف الرنة صامتاً| J[إرسال إشعار فوري للسيرفر أو نقر المشترك]
|
||||
end
|
||||
|
||||
subgraph Layer 3: Nabih Platform Rescue
|
||||
D --> K[السيرفر يحول المعاملة فورياً لمنصة نبيه]
|
||||
G --> K
|
||||
J --> K
|
||||
K --> L[توليد صورة كابتشا مشفرة ديناميكياً وإرسالها عبر الواتساب]
|
||||
end
|
||||
```
|
||||
|
||||
#### تفاصيل طبقات الكشف:
|
||||
1. **كشف هاتف البوابة (Android Gateway RIL / Telephony Detection):**
|
||||
* عند إجراء الرنة من هاتف البوابة عبر نظام الاتصال، يراقب النظام كود فصل المكالمة (`DisconnectCause`):
|
||||
* إذا كان هاتف المشترك مغلقاً أو خارج نطاق التغطية، ترجع الشبكة الخلوية إشارة فصل فورية (`OUT_OF_SERVICE` أو `POWER_OFF`).
|
||||
* هاتف البوابة يرصد هذه الإشارة في أقل من **ثانيتين** فقط، ويرسل إشعاراً فورياً للسيرفر عبر الويب سوكت: الرقم خارج الخدمة حالياً، تحول فوراً إلى منصة نبيه!
|
||||
2. **كشف تسليم الرسائل القصيرة (SMS Delivery Report Intent):**
|
||||
* عند إرسال رسالة SMS من شريحة هاتف البوابة لمستخدمي الآيفون، لا نكتفي بمعرفة أن الرسالة خرجت من الهاتف، بل نراقب `PendingIntent` الخاص بـ `deliveryIntent`.
|
||||
* إذا لم يرجع تقرير التسليم من برج الاتصالات التابع للمستلم خلال 10 ثوانٍ، يدرك الخادم أن الرسالة عالقة ويقوم بتحويل المعاملة فوراً إلى الواتساب.
|
||||
3. **مؤقت التطبيق الذكي (Client-Side Fallback Timer):**
|
||||
* في واجهة المشترك، يظهر عداد زمني خفيف (15 ثانية). إذا انتهى الوقت دون التقاط الرنة أو الرمز، يظهر زر بارز: "لم تستلم الرنة؟ التحقق السريع عبر واتساب"، وعند النقر يطلب السيرفر الإرسال عبر منصة نبيه فوراً.
|
||||
|
||||
---
|
||||
|
||||
## 5. ابتكار منصة نبيه: صورة الكابتشا المشفرة (Captcha Image OTP) وتجاوز حظر ميتا
|
||||
|
||||
### فلسفة الابتكار وأهميته الاستراتيجية:
|
||||
عند إرسال رموز التحقق كنصوص تقليدية عبر واتساب للأعمال (WhatsApp Business API)، تفرض شركة ميتا (Meta) قيوداً خانقة:
|
||||
* التدقيق الصارم على قوالب النصوص (Message Templates) ومطابقتها.
|
||||
* تكرار نفس نمط الرسالة النصية لآلاف المستخدمين يؤدي تلقائياً إلى خفض تقييم الرقم (Quality Rating Drop)، وتصنيفه كبريد عشوائي (Spam)، ومن ثم حظر الرقم نهائياً (Account Ban).
|
||||
* فرض تسعيرة مرتفعة على كل محادثة مصادقة (Authentication Conversation Rates).
|
||||
|
||||
### الحل الذكي المعتمد في منصة نبيه (Captcha Image OTP):
|
||||
بدلاً من إرسال نص يمكن لروبوتات ميتا قراءته وتصنيفه، يتم **توليد صورة ديناميكية فريدة لكل عملية تحقق** (مثل الصورة المعتمدة برقم `190107`):
|
||||
1. **البصمة البصرية المتغيرة (Visual Fingerprint Variation):**
|
||||
* خلفيات لونية عشوائية ومتغيرة.
|
||||
* خطوط تشويش عشوائية (Noise Lines) ونقاط مبعثرة بدرجات شفافية وزوايا مختلفة.
|
||||
* تشويه طفيف لحجم وميلان الأرقام (Glyph Distortion & Kerning).
|
||||
2. **استحالة الحظر من قبل ميتا:**
|
||||
* كل صورة يتم توليدها تمتلك قيمة تجزئة (MD5 / SHA256 Hash) فريدة تماماً تختلف عن أي صورة سابقة.
|
||||
* روبوتات ميتا تعامل الرسالة كصورة وسائط عادية فريدة وليست قالباً متكرراً، مما يجعل حظر الرقم أو تقييده أمراً مستحيلاً عملياً.
|
||||
3. **حماية أمنية مضاعفة للمستخدم:**
|
||||
* البرمجيات الخبيثة والتطبيقات المشبوهة على هاتف المستخدم التي تسرق الرموز من شريط الإشعارات النصية (SMS/Notification Scrapers) تعجز عن قراءة الكود؛ لأن الكود محمي داخل صورة بصرية يقرأها الإنسان فقط.
|
||||
|
||||
---
|
||||
|
||||
## 6. تدفق أتمتة الدفع عبر SuperQi والإيصالات
|
||||
|
||||
بالنظر للإيصال المرفق في المحادثة:
|
||||
* **المبلغ:** 2,000 د.ع (أو 100$ / 132,000 د.ع للاشتراك الكامل).
|
||||
* **رقم الحركة:** `2026091810121420010100166521408745772`.
|
||||
* **رقم الحساب المستلم:** `6181121 705` (حساب د. ثامر).
|
||||
|
||||
### معمارية الأتمتة الميدانية (Android Bridge Gateway):
|
||||
1. تثبيت تطبيق أندرويد خفيف ومحمي (Service Listener) على الهاتف المخصص لاستقبال إشعارات SuperQi وزين كاش.
|
||||
2. عند ورود إشعار التحويل من تطبيق SuperQi أو رسالة SMS بنكية:
|
||||
* يقوم التطبيق بقراءة: `المبلغ`, `رقم الحركة`, `اسم المحول`, و `التوقيت`.
|
||||
* إرسال طلب مشفر وموقع بـ HMAC إلى نقطة النهاية في السيرفر: `POST /api/v1/payments/ingest-notification`.
|
||||
3. يقوم خادم أوروك بالبحث في جدول `subscriptions` المعلقة:
|
||||
* عند تطابق رقم الحركة والمبلغ، تتحول حالة الاشتراك من `PENDING` إلى `ACTIVE`.
|
||||
* إنشاء وتوليد الهوية الرقمية ورقم العضوية تلقائياً.
|
||||
* إرسال رسالة ترحيب فورية للمشترك عبر واتساب/SMS تحتوي على رابط تفعيل حسابه في التطبيق.
|
||||
@@ -0,0 +1,214 @@
|
||||
-- ==============================================================================
|
||||
-- Uruk International Prize Platform - Database Schema (MySQL 8.0)
|
||||
-- Engine: InnoDB | Collation: utf8mb4_unicode_ci
|
||||
-- Designed for High Security, Fingerprint Verification, and Audit Compliance
|
||||
-- ==============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- 1. جدول المستخدمين الأساسي (Users)
|
||||
CREATE TABLE IF NOT EXISTS `users` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`phone` VARCHAR(20) NOT NULL UNIQUE,
|
||||
`email` VARCHAR(191) NULL UNIQUE,
|
||||
`full_name` VARCHAR(191) NOT NULL,
|
||||
`country` VARCHAR(64) NOT NULL DEFAULT 'Iraq',
|
||||
`city` VARCHAR(64) NULL,
|
||||
`national_id_hash` VARCHAR(64) NULL COMMENT 'Encrypted or hashed national ID',
|
||||
`avatar_url` VARCHAR(255) NULL,
|
||||
`role` ENUM('MEMBER', 'PARTNER_STAFF', 'ADMIN', 'SUPER_ADMIN') NOT NULL DEFAULT 'MEMBER',
|
||||
`status` ENUM('PENDING_VERIFICATION', 'ACTIVE', 'SUSPENDED', 'BANNED') NOT NULL DEFAULT 'PENDING_VERIFICATION',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX `idx_users_phone` (`phone`),
|
||||
INDEX `idx_users_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 2. جدول أمان الجهاز والبصمة والترويسات (User Security & Fingerprint)
|
||||
CREATE TABLE IF NOT EXISTS `user_security` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`user_id` BIGINT UNSIGNED NOT NULL,
|
||||
`device_fingerprint` VARCHAR(64) NOT NULL COMMENT 'SHA-256 derived from hardware & SecureStorage',
|
||||
`device_secret` VARCHAR(64) NOT NULL COMMENT 'HMAC secret key unique to this device',
|
||||
`device_model` VARCHAR(128) NULL,
|
||||
`os_version` VARCHAR(64) NULL,
|
||||
`last_token` VARCHAR(255) NULL,
|
||||
`last_ip` VARCHAR(45) NULL,
|
||||
`last_active_at` TIMESTAMP NULL,
|
||||
`failed_attempts` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`is_locked` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `fk_security_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||
UNIQUE KEY `uk_user_device` (`user_id`, `device_fingerprint`),
|
||||
INDEX `idx_fingerprint` (`device_fingerprint`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 3. جدول العضويات والاشتراكات (Memberships / Subscriptions)
|
||||
CREATE TABLE IF NOT EXISTS `subscriptions` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`user_id` BIGINT UNSIGNED NOT NULL,
|
||||
`membership_number` VARCHAR(32) NOT NULL UNIQUE COMMENT 'e.g. URUK-2026-XXXXX',
|
||||
`plan_name` VARCHAR(64) NOT NULL DEFAULT '2-Year Executive Membership',
|
||||
`price_usd` DECIMAL(10, 2) NOT NULL DEFAULT 100.00,
|
||||
`price_local` DECIMAL(14, 2) NOT NULL DEFAULT 132000.00,
|
||||
`currency` VARCHAR(8) NOT NULL DEFAULT 'IQD',
|
||||
`starts_at` DATE NOT NULL,
|
||||
`expires_at` DATE NOT NULL,
|
||||
`status` ENUM('PENDING_PAYMENT', 'ACTIVE', 'EXPIRED', 'REVOKED') NOT NULL DEFAULT 'PENDING_PAYMENT',
|
||||
`qr_seed` VARCHAR(64) NOT NULL COMMENT 'Cryptographic salt for dynamic rotating QR',
|
||||
`activated_by` BIGINT UNSIGNED NULL COMMENT 'Admin user ID if activated manually',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `fk_subscription_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||
INDEX `idx_membership_number` (`membership_number`),
|
||||
INDEX `idx_status_expiry` (`status`, `expires_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 4. جدول المعاملات والمدفوعات (Financial Transactions)
|
||||
CREATE TABLE IF NOT EXISTS `transactions` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`subscription_id` BIGINT UNSIGNED NOT NULL,
|
||||
`user_id` BIGINT UNSIGNED NOT NULL,
|
||||
`method` ENUM('SUPER_QI', 'ZAIN_CASH', 'SWIFTPAY_IQ', 'RAFIDAIN_CARD', 'CLIQ_JORDAN', 'CASH_DIRECT') NOT NULL,
|
||||
`reference_number` VARCHAR(128) NOT NULL UNIQUE COMMENT 'Transaction ID / Movement ID from SuperQi, ZainCash, etc.',
|
||||
`sender_account_or_phone` VARCHAR(64) NULL,
|
||||
`recipient_account` VARCHAR(64) NULL,
|
||||
`amount` DECIMAL(14, 2) NOT NULL,
|
||||
`currency` VARCHAR(8) NOT NULL DEFAULT 'IQD',
|
||||
`status` ENUM('SUBMITTED', 'VERIFIED_AUTO', 'VERIFIED_ADMIN', 'REJECTED') NOT NULL DEFAULT 'SUBMITTED',
|
||||
`receipt_image_url` VARCHAR(255) NULL,
|
||||
`raw_webhook_payload` JSON NULL,
|
||||
`verified_at` TIMESTAMP NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `fk_transaction_sub` FOREIGN KEY (`subscription_id`) REFERENCES `subscriptions` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_transaction_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||
INDEX `idx_ref_number` (`reference_number`),
|
||||
INDEX `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 5. جدول الشركاء (المستشفيات والفنادق والمراكز)
|
||||
CREATE TABLE IF NOT EXISTS `partners` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`type` ENUM('HOSPITAL', 'HOTEL', 'TRAINING_CENTER', 'OTHER') NOT NULL,
|
||||
`name_ar` VARCHAR(191) NOT NULL,
|
||||
`name_en` VARCHAR(191) NULL,
|
||||
`category` VARCHAR(64) NOT NULL COMMENT 'Eyes, Cancer, Heart, General, 5-Star Hotel...',
|
||||
`country` VARCHAR(64) NOT NULL DEFAULT 'Iraq',
|
||||
`city` VARCHAR(64) NOT NULL,
|
||||
`discount_percentage` DECIMAL(5, 2) NOT NULL COMMENT 'e.g. 50.00 for hospitals, 40-60 for hotels',
|
||||
`address` TEXT NULL,
|
||||
`phone` VARCHAR(64) NULL,
|
||||
`email` VARCHAR(128) NULL,
|
||||
`latitude` DECIMAL(10, 8) NULL,
|
||||
`longitude` DECIMAL(11, 8) NULL,
|
||||
`contract_starts_at` DATE NULL,
|
||||
`contract_ends_at` DATE NULL,
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`added_by_admin` BIGINT UNSIGNED NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX `idx_type_country_city` (`type`, `country`, `city`),
|
||||
INDEX `idx_is_active` (`is_active`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 6. جدول الدورات التدريبية المعتمدة (Academy & Workshops)
|
||||
CREATE TABLE IF NOT EXISTS `courses` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`title` VARCHAR(191) NOT NULL,
|
||||
`specialty` VARCHAR(64) NOT NULL COMMENT 'Medical, Languages, Engineering, Management...',
|
||||
`instructor_name` VARCHAR(128) NOT NULL,
|
||||
`delivery_format` ENUM('IN_PERSON_HOTEL', 'ONLINE_STREAM', 'HYBRID') NOT NULL DEFAULT 'HYBRID',
|
||||
`hotel_partner_id` BIGINT UNSIGNED NULL,
|
||||
`scheduled_start` DATETIME NOT NULL,
|
||||
`scheduled_end` DATETIME NULL,
|
||||
`broadcast_url` VARCHAR(255) NULL,
|
||||
`max_seats` INT UNSIGNED NULL,
|
||||
`is_published` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `fk_course_hotel` FOREIGN KEY (`hotel_partner_id`) REFERENCES `partners` (`id`) ON DELETE SET NULL,
|
||||
INDEX `idx_specialty` (`specialty`),
|
||||
INDEX `idx_scheduled_start` (`scheduled_start`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 7. جدول تسجيل المتدربين والشهادات (Course Enrollments & Certificates)
|
||||
CREATE TABLE IF NOT EXISTS `course_enrollments` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`user_id` BIGINT UNSIGNED NOT NULL,
|
||||
`course_id` BIGINT UNSIGNED NOT NULL,
|
||||
`status` ENUM('REGISTERED', 'ATTENDED', 'COMPLETED', 'CERTIFICATE_ISSUED') NOT NULL DEFAULT 'REGISTERED',
|
||||
`certificate_serial` VARCHAR(64) NULL UNIQUE COMMENT 'Accredited certificate tracking number',
|
||||
`certificate_url` VARCHAR(255) NULL,
|
||||
`issued_at` DATETIME NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `fk_enrollment_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_enrollment_course` FOREIGN KEY (`course_id`) REFERENCES `courses` (`id`) ON DELETE CASCADE,
|
||||
UNIQUE KEY `uk_user_course` (`user_id`, `course_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 8. سجل التحقق والزيارات الميدانية (Verification Logs at Partners)
|
||||
CREATE TABLE IF NOT EXISTS `verification_logs` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`subscription_id` BIGINT UNSIGNED NOT NULL,
|
||||
`partner_id` BIGINT UNSIGNED NOT NULL,
|
||||
`verifier_staff_name` VARCHAR(128) NULL,
|
||||
`applied_discount` DECIMAL(5, 2) NOT NULL,
|
||||
`service_type` VARCHAR(128) NULL,
|
||||
`scanned_token_signature` VARCHAR(64) NOT NULL,
|
||||
`verified_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `fk_verif_sub` FOREIGN KEY (`subscription_id`) REFERENCES `subscriptions` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_verif_partner` FOREIGN KEY (`partner_id`) REFERENCES `partners` (`id`) ON DELETE CASCADE,
|
||||
INDEX `idx_partner_verified_at` (`partner_id`, `verified_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 9. جدول التدقيق الأمني ومراقبة الترويسات (Audit & Header Logs)
|
||||
CREATE TABLE IF NOT EXISTS `audit_logs` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`user_id` BIGINT UNSIGNED NULL,
|
||||
`action` VARCHAR(64) NOT NULL,
|
||||
`endpoint` VARCHAR(128) NOT NULL,
|
||||
`method` VARCHAR(10) NOT NULL,
|
||||
`ip_address` VARCHAR(45) NOT NULL,
|
||||
`device_fingerprint` VARCHAR(64) NULL,
|
||||
`nonce_used` VARCHAR(64) NULL,
|
||||
`response_code` SMALLINT UNSIGNED NOT NULL,
|
||||
`payload_snippet` TEXT NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX `idx_user_action` (`user_id`, `action`),
|
||||
INDEX `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 10. جدول أجهزة بوابات الاتصال الثلاثة (Gateway Caller Devices - Zain, Asiacell, Korek)
|
||||
CREATE TABLE IF NOT EXISTS `gateway_devices` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`device_id` VARCHAR(64) NOT NULL UNIQUE COMMENT 'e.g. zain-node-1, asiacell-node-2, korek-node-3',
|
||||
`operator` ENUM('ZAIN', 'ASIACELL', 'KOREK', 'UNKNOWN') NOT NULL DEFAULT 'UNKNOWN',
|
||||
`phone_number` VARCHAR(32) NULL,
|
||||
`sim_slot` TINYINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
`status` ENUM('ACTIVE', 'OFFLINE', 'BUSY') NOT NULL DEFAULT 'ACTIVE',
|
||||
`last_heartbeat` TIMESTAMP NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX `idx_gw_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 11. جدول مهام الاتصال الفلاش والرسائل النصية (Gateway Tasks Queue)
|
||||
CREATE TABLE IF NOT EXISTS `gateway_tasks` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`task_type` ENUM('FLASH_CALL', 'SMS') NOT NULL DEFAULT 'FLASH_CALL',
|
||||
`target_phone` VARCHAR(32) NOT NULL,
|
||||
`otp_code` VARCHAR(16) NOT NULL,
|
||||
`assigned_device_id` VARCHAR(64) NULL,
|
||||
`status` ENUM('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED', 'EXPIRED') NOT NULL DEFAULT 'PENDING',
|
||||
`timeout_seconds` INT UNSIGNED NOT NULL DEFAULT 25,
|
||||
`completed_at` TIMESTAMP NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX `idx_task_status_type` (`status`, `task_type`, `created_at`),
|
||||
INDEX `idx_assigned_device` (`assigned_device_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# معمارية خادم الـ Pure PHP وملف Composer ونظام المزامنة (Backend Core & Deploy)
|
||||
**Document Version:** 1.0.0
|
||||
**Date:** September 2026
|
||||
|
||||
---
|
||||
|
||||
## 1. فلسفة المعمارية البرمجية (Pure PHP Core)
|
||||
|
||||
بناءً على التوجيه التقني المباشر، لن يتم استخدام أطر عمل ضخمة (كاللارافيل المتشعب) لتفادي استهلاك موارد السيرفر (Overhead)، بل سيتم بناء **Pure PHP Core** خفيف، صلب، وعالي الأداء:
|
||||
* زمن استجابة للطلب (Latency) أقل من 15 مللي ثانية.
|
||||
* استهلاك أدنى للذاكرة RAM (أقل من 3MB لكل طلب).
|
||||
* بنية نظيفة وقابلة للصيانة والتطوير تعتمد معايير PSR الحديثة والبرمجة كائنية التوجه (OOP).
|
||||
|
||||
---
|
||||
|
||||
## 2. الهيكلية المقترحة لملفات الـ Backend
|
||||
|
||||
```
|
||||
backend/
|
||||
├── composer.json # حزم الكمبوزر المعزولة والمستقلة
|
||||
├── .env.example # نموذج المتغيرات البيئية الحساسة
|
||||
├── deploy.sh # سكربت المزامنة والنشر الآلي على السيرفر
|
||||
│
|
||||
├── config/ # ملفات الإعدادات الثابتة
|
||||
│ ├── app.php
|
||||
│ ├── database.php
|
||||
│ └── security.php
|
||||
│
|
||||
├── core/ # محرك النظام الأساسي (The Core Engine)
|
||||
│ ├── Database.php # إدارة اتصالات PDO مع MySQL بشكل آمن
|
||||
│ ├── Router.php # موجه الروابط السريع والمطابقة النمطية
|
||||
│ ├── Request.php # كبسلة الطلب وقراءة الترويسات (Headers)
|
||||
│ ├── Response.php # إرجاع ردود JSON المشفرة والمنظمة
|
||||
│ ├── Security.php # دوال التشفير، الـ HMAC، والبصمة
|
||||
│ └── Middleware.php # الأساس المنطقي لاعتراض وفحص الطلبات
|
||||
│
|
||||
├── app/
|
||||
│ ├── Middlewares/ # طبقات الحماية واعتراض الروابط
|
||||
│ │ ├── SecurityHeaderMiddleware.php # فحص البصمة، النونس، والتوقيع
|
||||
│ │ ├── AuthMiddleware.php # التحقق من جلسة العضو
|
||||
│ │ └── AdminMiddleware.php # حماية لوحة الإدارة لـ د. ثامر
|
||||
│ │
|
||||
│ ├── Controllers/ # متحكمات مسارات الـ API
|
||||
│ │ ├── AuthController.php # تسجيل، دخول، وطلب OTP عبر OTPIQ
|
||||
│ │ ├── SubscriptionController.php # خطط الاشتراك، إصدار رقم العضوية
|
||||
│ │ ├── QrController.php # توليد والتحقق من رموز QR المشفرة
|
||||
│ │ ├── PartnerController.php # دليل المستشفيات والفنادق والخصومات
|
||||
│ │ ├── PaymentController.php # استقبال التحويلات وWebhooks
|
||||
│ │ └── AdminController.php # إدارة الشركاء والاعتمادات السريعة
|
||||
│ │
|
||||
│ ├── Services/ # طبقة التكامل مع الخدمات الخارجية
|
||||
│ │ ├── OtpIqService.php # الربط مع منصة OTPIQ (واتساب + SMS)
|
||||
│ │ ├── SwiftPayService.php # الربط مع بوابة SwiftPayIQ
|
||||
│ │ ├── SuperQiParserService.php # معالجة إشعارات التحويل المباشر
|
||||
│ │ └── QrCryptographerService.php # توقيع وتشفير رموز الدخول
|
||||
│ │
|
||||
│ └── Models/ # التفاعل مع جداول قاعدة البيانات
|
||||
│ ├── User.php
|
||||
│ ├── Subscription.php
|
||||
│ ├── Transaction.php
|
||||
│ ├── Partner.php
|
||||
│ └── Course.php
|
||||
│
|
||||
└── public/ # المجلد المتاح للويب فقط (Web Root)
|
||||
├── index.php # نقطة الدخول الموحدة (Front Controller)
|
||||
└── .htaccess # إعادة توجيه الروابط وتأمين المجلدات
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. مواصفات حزم الـ Composer المعزولة (`composer.json`)
|
||||
|
||||
لضمان عمل النظام بشكل مستقل تماماً عند نقله لأي سيرفر سحابي آخر، يُحدد ملف التبعيات المستقل التالي:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "uruk/prize-backend",
|
||||
"description": "Uruk International Prize Core Backend Engine",
|
||||
"type": "project",
|
||||
"license": "proprietary",
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"ext-pdo": "*",
|
||||
"ext-pdo_mysql": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-json": "*",
|
||||
"vlucas/phpdotenv": "^5.6",
|
||||
"guzzlehttp/guzzle": "^7.8"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Core\\": "core/",
|
||||
"App\\": "app/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. سكربت النشر والمزامنة السحابية على الماك (`deploy.sh`)
|
||||
|
||||
يحقق السكربت التدفق المطلوب: كتابة الكود على جهاز الماك، وإرساله فوراً عبر Git، ثم تشغيل المزامنة على السيرفر لجلب التحديثات فورياً:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Uruk International Prize - Zero-Downtime Deployment & Git Sync
|
||||
# Author: Hamza Ayed
|
||||
# ==============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 [1/4] Checking local Git working tree on Mac..."
|
||||
git status -s
|
||||
|
||||
echo "📦 [2/4] Staging and Committing changes..."
|
||||
COMMIT_MSG=${1:-"Sync Uruk Prize core updates: $(date '+%Y-%m-%d %H:%M:%S')"}
|
||||
git add .
|
||||
git commit -m "$COMMIT_MSG" || echo "No new local changes to commit."
|
||||
|
||||
echo "⬆️ [3/4] Pushing changes to Remote Git Repository..."
|
||||
git push origin main
|
||||
|
||||
echo "🌐 [4/4] Triggering Production Server Deployment..."
|
||||
# يمكن استخدام SSH المباشر أو Webhook آمن على السيرفر
|
||||
# ssh user@uruk-server.iq "cd /var/www/uruk-backend && git pull origin main && composer install --no-dev --optimize-autoloader && php core/cache_clear.php"
|
||||
|
||||
echo "✅ Deployment completed successfully!"
|
||||
```
|
||||
@@ -0,0 +1,109 @@
|
||||
# معمارية تطبيق الجوال فلاتر ونظام الكيوبت (Flutter Architecture & Cubit)
|
||||
**Document Version:** 1.0.0
|
||||
**Date:** September 2026
|
||||
**Package:** `iq.urukprize.app`
|
||||
|
||||
---
|
||||
|
||||
## 1. الهيكلية المعمارية لتطبيق فلاتر (Clean Architecture with Cubit)
|
||||
|
||||
يتبع التطبيق نموذج فصل المسؤوليات الصارم (Layered Architecture):
|
||||
|
||||
```
|
||||
lib/
|
||||
├── main.dart # نقطة الدخول وتهيئة بيئة التخزين الآمن والبصمة
|
||||
│
|
||||
├── core/ # الوحدات المشتركة والبنية التحتية
|
||||
│ ├── constants/ # الروابط، الألوان، وأنماط التصميم
|
||||
│ ├── network/ # عميل الـ HTTP المخصص وحاقن الترويسات
|
||||
│ │ ├── api_client.dart # حقن X-User-Id, X-Device-Fingerprint, والتوقيع تلقائياً
|
||||
│ │ └── api_endpoints.dart
|
||||
│ ├── security/ # إدارة البصمة والمفاتيح المشفرة
|
||||
│ │ ├── fingerprint_helper.dart # استخراج معرفات العتاد ودمج الملح
|
||||
│ │ └── secure_vault.dart # واجهة FlutterSecureStorage
|
||||
│ └── theme/ # ثيم جائزة أوروك الفاخر (Dark Gold & Navy)
|
||||
│
|
||||
├── features/ # وحدات الأعمال المستقلة (Feature-Driven)
|
||||
│ ├── auth/ # المصادقة والدخول
|
||||
│ │ ├── cubit/ (AuthCubit, AuthState)
|
||||
│ │ └── presentation/ (PhoneInputScreen, OtpVerificationScreen)
|
||||
│ │
|
||||
│ ├── membership/ # بطاقة العضوية والـ QR الديناميكي
|
||||
│ │ ├── cubit/ (MembershipCubit, MembershipState)
|
||||
│ │ └── presentation/ (DigitalCardScreen, DynamicQrModal)
|
||||
│ │
|
||||
│ ├── partners/ # دليل المستشفيات والفنادق
|
||||
│ │ ├── cubit/ (PartnersCubit, PartnersState)
|
||||
│ │ └── presentation/ (PartnersDirectoryScreen, PartnerDetailsScreen)
|
||||
│ │
|
||||
│ ├── courses/ # أكاديمية أوروك والشهادات
|
||||
│ │ ├── cubit/ (CoursesCubit, CoursesState)
|
||||
│ │ └── presentation/ (CoursesCatalogScreen, CertificateViewScreen)
|
||||
│ │
|
||||
│ ├── subscription/ # الدفع والسداد (SuperQi / ZainCash / SwiftPayIQ)
|
||||
│ │ ├── cubit/ (SubscriptionCubit, SubscriptionState)
|
||||
│ │ └── presentation/ (PlanSelectionScreen, PaymentInstructionsScreen)
|
||||
│ │
|
||||
│ └── admin/ # بوابة الإدارة المخفية (لـ د. ثامر والإدارة)
|
||||
│ ├── cubit/ (AdminCubit, AdminState)
|
||||
│ └── presentation/ (AdminGateModal, AddPartnerScreen, ApproveTransactionsScreen)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. مواصفات حزم `pubspec.yaml` الأساسية
|
||||
|
||||
```yaml
|
||||
name: uruk_prize
|
||||
description: "Uruk International Prize Smart Membership Application"
|
||||
publish_to: "none"
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
# State Management
|
||||
flutter_bloc: ^8.1.3
|
||||
equatable: ^2.0.5
|
||||
|
||||
# Security & Storage
|
||||
flutter_secure_storage: ^9.0.0
|
||||
crypto: ^3.0.3
|
||||
device_info_plus: ^10.1.0
|
||||
|
||||
# Networking & QR
|
||||
http: ^1.2.1
|
||||
qr_flutter: ^4.1.0
|
||||
mobile_scanner: ^5.1.1
|
||||
|
||||
# Utilities
|
||||
intl: ^0.19.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. آلية الحقن التلقائي للترويسات والبصمة في كل طلب (Auto-Header Injection)
|
||||
|
||||
تم تصميم فئة `ApiClient` لترث أو تكبسيل `http.Client`، بحيث تقوم تلقائياً وقبل خروج أي حزمة بيانات عبر الشبكة بما يلي:
|
||||
1. جلب معرف المستخدم `X-User-Id` من التخزين الآمن.
|
||||
2. جلب بصمة الجهاز المعتمدة `X-Device-Fingerprint`.
|
||||
3. توليد توقيت لحظي `X-Timestamp`، ورمز استخدام فردي `X-Nonce`.
|
||||
4. حساب توقيع `X-Signature` مشفر بـ HMAC-SHA256 يجمع محتوى الطلب وبيانات الترويسة.
|
||||
5. إرفاق `Authorization: Bearer <Token>`.
|
||||
|
||||
بهذا التدفق، لا يمكن لأي مستخدم أو محاول تلاعب التلاعب بالطلبات أو مشاركة الحساب عبر أجهزة متعددة.
|
||||
|
||||
---
|
||||
|
||||
## 4. وضع الإدارة المخفي داخل التطبيق (Hidden In-App Admin Mode)
|
||||
|
||||
استجابةً للنقاش الوارد في التسجيل الصوتي بشأن تمكين الإدارة (د. ثامر وفريقه) من إدخال وتعديل المستشفيات والفنادق المتعاقد معها في أي وقت ومن أي مكان:
|
||||
* **طريقة الدخول:** ضغط مطول لمدة 5 ثوانٍ على شعار جائزة أوروك في الشاشة الرئيسية (أو النقر المتتالي 5 مرات على رقم الإصدار).
|
||||
* **الحماية:** فتح نافذة منبثقة تطلب المصادقة البيومترية (بصمة الإصبع/الوجه) أو رمز المشرف المخصص.
|
||||
* **المميزات المتاحة للإدارة فوراً:**
|
||||
1. **إضافة شريك جديد فوراً (Add Partner):** إدخال اسم المستشفى/الفندق، الدولة (العراق، الأردن، مصر، لبنان...)، المدينة، ونسبة الخصم (50% أو غيرها). فور الحفظ تظهر لجميع المشتركين مباشرة.
|
||||
2. **التحقق من سداد المشتركين (Verify Receipts):** مراجعة إيصالات SuperQi وزين كاش المرفوعة، وتفعيل اشتراك المستخدم بضغطة زر وتوليد بطاقته فورياً.
|
||||
@@ -0,0 +1,244 @@
|
||||
# دراسة الجدوى الاقتصادية والمعمارية لمنظومة بوابات الاتصال والتحقق في العراق (Zain, Asiacell, Korek)
|
||||
|
||||
## 1. الملخص التنفيذي وهندسة خفض التكاليف (Unit Economics & Margin Protection)
|
||||
|
||||
تعتمد منصة جائزة أوروك الدولية (Uruk International Prize) على نموذج اشتراكات ممتدة لسنتين بقيمة 100 دولار (132,000 دينار عراقي). لتحقيق أقصى تدفق نقدي حر (Free Cash Flow) وحماية هوامش الربح الإجمالية (Gross Margin Moat) لتمويل المشاريع التوسعية (Intaleq و Tripz)، تم تصميم بنية التحقق الهجينة لتقليل تكلفة المصادقة للصفر تقريباً:
|
||||
|
||||
- **الحل التقليدي المستبعد (Commercial SMS Aggregators):** يكلّف إرسال كود الـ OTP عبر بوابات الـ SMS المعتمدة دولياً ومحلياً (مثل Infobip أو Twilio أو الموزعين المحليين) ما بين 40 إلى 75 ديناراً عراقياً (0.035$ - 0.055$) لكل رسالة، مع نسبة فشل تصل إلى 20% بسبب فلاتر هيئة الإعلام والاتصالات. في قاعدة مستخدمين تضم 50,000 مستخدم مع تكرار الدخول ومحاولات التحقق، تتجاوز فاتورة الـ SMS وحدها 4,500$ إلى 6,000$ شهرياً دون أي عائد تشغيلي مضاف.
|
||||
- **الحل المعماري المبتكر لمنصة أوروك (Tri-Operator Round-Robin Gateway):**
|
||||
- الاعتماد على 3 هواتف أندرويد مخصصة مزودة بشرائح (زين، آسياسيل، كورك) لتنفيذ الرنين العكسي الصامت (Flash Call OTP) بنسبة 85% من مستخدمي العراق (بيئة أندرويد).
|
||||
- تكلفة الرنين العكسي هي **0.00 دينار عراقي** لأن المكالمة تُفصل برمجياً عبر `TelecomManager.endCall()` خلال 1.5 ثانية وقبل فتح الخط (قبل إشارة `ANM` في بروتوكول SS7/ISUP).
|
||||
- حصر الرسائل النصية القصيرة (SMS) بهواتف iOS فقط، وتمريرها عبر الشرائح المحلية أو منصة نبيه (Nabih) بنظام الكابتشا الصورية لتفادي حظر ميتا وأكلاف الرسائل المتكررة.
|
||||
- توفير مباشر قدره **0.30$ إلى 0.50$** لكل دورة مصادقة، مما يرفع هامش الربح التشغيلي للاشتراك إلى أكثر من 97%.
|
||||
|
||||
---
|
||||
|
||||
## 2. المقارنة الشاملة لأسعار الشرائح وتراخيص الخطوط (Prepaid SIM Cards & KYC)
|
||||
|
||||
وفقاً للوائح هيئة الإعلام والاتصالات العراقية (CMC)، تتطلب جميع الخطوط مسبقة الدفع التوثيق البايومتري والشخصي بالبيانات التالية:
|
||||
1. **المستمسكات المطلوبة:** البطاقة الوطنية الموحدة (أو هوية الأحوال المدنية مع شهادة الجنسية) + بطاقة السكن الرسمية (أو البطاقة التموينية).
|
||||
2. **السن القانوني:** لا يقل عن 18 عاماً مع تسجيل بصمة الأصابع لدى الوكيل المعتمد أو الفرع الرئيسي.
|
||||
|
||||
### جدول تكاليف شراء الشرائح (SIM / eSIM):
|
||||
|
||||
| المشغل (Operator) | سعر الشريحة العادية (SIM) | سعر الشريحة الإلكترونية (eSIM) | رصيد البداية الممنوح | الملاحظات التشغيلية |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **زين العراق (Zain)** | 2,000 دينار عراقي | 10,000 دينار عراقي | رصيد رمزي ترحيبي | تغطية واسعة في بغداد والفرات الأوسط والجنوب |
|
||||
| **آسياسيل (Asiacell)** | 3,000 دينار عراقي | 10,000 دينار عراقي | 500 دينار بعد أول تعبئة | أسرع شبكة 4G+ في الوسط والشمال ومحافظة السليمانية |
|
||||
| **كورك تليكوم (Korek)** | 2,500 دينار عراقي | غير مفعلة بصورة مستقرة | رصيد افتتاحي ترويجي | الهيمنة في إقليم كردستان (أربيل ودهوك) وأجزاء من الموصل |
|
||||
|
||||
---
|
||||
|
||||
## 3. فئات كروت الشحن واستراتيجية إدامة صلاحية الخطوط لـ 365 يوماً بأقل تكلفة
|
||||
|
||||
أحد أكبر التحديات التشغيلية لإدارة كتل البوابات (Caller Gateway Nodes) هو خطر انتهاء صلاحية الشريحة وتعليق الخط بسبب عدم التعبئة المستمرة. فيما يلي تفصيل فئات الشحن وكيفية تأمين خطوط النظام لمدة عام كامل بأقل من 50 دولاراً للخطوط الثلاثة مجتمعة:
|
||||
|
||||
### أ) زين العراق (Zain Iraq):
|
||||
- **فئات الكروت القياسية:**
|
||||
- 5,000 دينار: صلاحية 25-50 يوماً.
|
||||
- 10,000 دينار: صلاحية 50 يوماً.
|
||||
- 15,000 - 25,000 دينار: صلاحية 150 يوماً.
|
||||
- 35,000 - 100,000 دينار: صلاحية 180 - 360 يوماً.
|
||||
- **استراتيجية تمديد الصلاحية الذكية ("صلاحية رصيدك بكيفك"):**
|
||||
- الكود المختصر: `*230#`
|
||||
- شراء سنة كاملة (365 يوماً) دفعة واحدة من الرصيد الحالي بمبلغ **21,000 دينار عراقي** فقط (~16 دولار) دون الحاجة لشحن مبالغ كبرى!
|
||||
- كود شحن الكارت: `*101*CARD_PIN#`
|
||||
- كود الاستعلام عن الرصيد والصلاحية: `*233#`
|
||||
|
||||
### ب) آسياسيل (Asiacell):
|
||||
- **فئات الكروت القياسية:**
|
||||
- 5,000 دينار: صلاحية 45 يوماً.
|
||||
- 10,000 دينار: صلاحية 100 يوم.
|
||||
- **25,000 دينار:** صلاحية **360 يوماً** (سنة كاملة مباشرة بتعبئة واحدة بمبلغ 19 دولار).
|
||||
- 100,000 دينار: صلاحية 360 يوماً.
|
||||
- **خدمة شراء الصلاحية من الرصيد:** تتوفر عبر قائمة `*133#` أو الاتصال الصوتي بـ `333`.
|
||||
- **كود الشحن:** `*133*CARD_PIN#`
|
||||
- **كود فحص الرصيد والصلاحية:** `*133#` أو `*233#`
|
||||
|
||||
### ج) كورك تليكوم (Korek Telecom):
|
||||
- **فئات الكروت القياسية:**
|
||||
- 5,000 دينار: صلاحية 40 يوماً.
|
||||
- 10,000 دينار: صلاحية 50 يوماً.
|
||||
- 15,000 دينار: صلاحية 60 يوماً.
|
||||
- 30,000 دينار: صلاحية 75 يوماً.
|
||||
- 60,000 دينار: صلاحية 90 يوماً.
|
||||
- **خدمة تمديد الصلاحية:**
|
||||
- الكود المختصر: `*272#`
|
||||
- تمديد 7 أيام: 500 دينار.
|
||||
- تمديد 30 يوماً: 2,000 دينار (أي 24,000 دينار لكامل السنة).
|
||||
- **كود الشحن:** `*221*CARD_PIN#`
|
||||
- **كود فحص الرصيد:** `*211#`
|
||||
|
||||
---
|
||||
|
||||
## 4. تعرفة الرسائل والمكالمات والباقات المتاحة (Voice & SMS Bundles)
|
||||
|
||||
في حال تم تكليف الشريحة بإرسال رسائل نصية قصيرة لهواتف iOS (بدون الفلاش كول)، فإن الأسعار والباقات كما يلي:
|
||||
|
||||
### أ) التسعيرة الفردية العادية (بدون باقات):
|
||||
- **زين العراق:**
|
||||
- رسالة داخل الشبكة: 50 دينار.
|
||||
- رسالة خارج الشبكة: 60 دينار.
|
||||
- المكالمة الصوتية: 1.5 إلى 2.4 دينار / ثانية (داخل زين)، 3.9 دينار / ثانية (للشبكات الأخرى).
|
||||
- **آسياسيل:**
|
||||
- رسالة داخل وخارج الشبكة: 50 إلى 60 دينار.
|
||||
- المكالمة الصوتية: تعرفة موحدة 2.8 دينار / ثانية (داخل وخارج الشبكة).
|
||||
- **كورك تليكوم:**
|
||||
- رسالة نصية: 50 إلى 60 دينار.
|
||||
- المكالمة الصوتية: 1.8 إلى 2.2 دينار / ثانية.
|
||||
|
||||
### ب) الباقات المدمجة الموصى بها لأجهزة البوابة:
|
||||
|
||||
#### 1. باقات "بكيفي" في زين العراق (تفعيل عبر `*221#` أو إرسال الرمز إلى `2122`):
|
||||
- **باقة 6,000 دينار (أسبوعية - رمز 60):** 90 دقيقة لكل الشبكات + 30 رسالة + 1.5 جيجابايت.
|
||||
- **باقة 15,000 دينار (4 أسابيع - رمز 6):** 210 دقائق لكل الشبكات + 50 رسالة لزين + 4 جيجابايت.
|
||||
- **باقة 25,000 دينار (4 أسابيع - رمز 4):** 720 دقيقة لكل الشبكات + 150 رسالة لزين + 2 جيجابايت.
|
||||
- **باقات "كفوو" (`*285#`):** توفر رصيداً مضاعفاً ورسائل بتعرفة 50 دينار ودقيقة بـ 2 دينار/ثانية.
|
||||
|
||||
#### 2. باقات "إلنا وللكل" في آسياسيل (تفعيل عبر `*299#`):
|
||||
- **باقة للكل الأسبوعية (4,000 دينار - رمز 5 إلى 299):** 45 دقيقة لكل الشبكات + 1 جيجابايت.
|
||||
- **باقة للكل 4 أسابيع (15,000 دينار - رمز 2 إلى 299):** 210 دقائق لكل الشبكات + 4 جيجابايت.
|
||||
- **باقة للكل إضافي (20,000 دينار - رمز 7 إلى 299):** 360 دقيقة لكل الشبكات + 4 جيجابايت.
|
||||
- **باقة إلنا 4 أسابيع (25,000 دينار - رمز 3 إلى 299):** 720 دقيقة داخل آسياسيل + 2 جيجابايت.
|
||||
|
||||
#### 3. باقات "إلنا كلنا" في كورك تليكوم (تفعيل عبر `*238#`):
|
||||
- **باقة 10,000 دينار (شهرية):** 180 دقيقة لكل الشبكات + 2 جيجابايت.
|
||||
- **باقة 15,000 دينار (شهرية):** 300 دقيقة لكل الشبكات + 3 جيجابايت.
|
||||
- **باقة 20,000 دينار (شهرية):** 420 دقيقة لكل الشبكات + 4 جيجابايت.
|
||||
- **باقة 30,000 دينار (شهرية):** 720 دقيقة لكل الشبكات + 10 جيجابايت.
|
||||
|
||||
---
|
||||
|
||||
## 5. سياسة الاستخدام العادل (FUP) وتفادي تجميد الخطوط (Anti-Spam & Anti-Fraud)
|
||||
|
||||
تطبق هيئة الإعلام والاتصالات العراقية (CMC) ومزودو الخدمة المحليون خواريزميات صارمة لمكافحة الرسائل العشوائية والاحتيال الرقمي:
|
||||
1. **معايير كشف الأنشطة المشبوهة:**
|
||||
- إرسال أكثر من 150-200 رسالة نصية قصيرة في اليوم الواحد من شريحة أفراد عادية (Prepaid).
|
||||
- إجراء مكالمات قصيرة بمعدلات زمنية متقاربة ومكررة بدون أي مكالمات واردة (Inbound/Outbound Disparity).
|
||||
- تجميد الشريحة بصورة آلية وفصلها عن الشبكة، حيث يتطلب استعادتها مراجعة الفرع أو إرسال طلب استعادة (زين إلى 2079، آسياسيل إلى 2323).
|
||||
2. **خطة الحماية المعمارية المعتمدة في منصة أوروك:**
|
||||
- **التوزيع التناوبي الدائري (Round-Robin):** توزيع الطلبات بين الهواتف الثلاثة (زين، آسياسيل، كورك)، بحيث لا ينفذ الجهاز الواحد أكثر من مكالمة واحدة كل 30 إلى 60 ثانية.
|
||||
- **المحاكاة العشوائية لفترة الرنين (Jittered Ring Duration):** رنين عشوائي يتراوح بين 1200ms إلى 2400ms قبل الفصل الآلي، لكسر النمط الثابت المكتشف من محركات الرصد في المقاسم.
|
||||
- **صمام الأمان (منصة نبيه - Nabih Platform):** تحويل أي ضغط لحظي زائد أو محاولات إعادة إرسال متعثرة خلال 15 ثانية إلى واتساب عبر منصة نبيه باستخدام صور الكابتشا الديناميكية لتجاوز قيود ميتا، دون استهلاك شرائح الاتصال المحلية.
|
||||
|
||||
---
|
||||
|
||||
## 6. فك لغز حزم الرسائل الاستهلاكية (Consumer SMS Quota Analysis)
|
||||
|
||||
### أ) هل الحصص يومية أم إجمالية (Daily vs. Lump-Sum)؟
|
||||
- **الحقيقة القاطعة:** جميع حصص الرسائل في باقات الأفراد (Prepaid) لدى زين وآسياسيل وكورك هي **حصص مقطوعة لكامل فترة صلاحية الباقة (Lump-Sum Allowance)** وليست حصصاً تتجدد يومياً:
|
||||
- باقة زين "بكيفي 6,000 دينار" (أسبوعية): تمنح **30 رسالة فقط لكامل الأسبوع** (بمعدل ~4 رسائل يومياً).
|
||||
- باقة زين "بكيفي 25,000 دينار" (4 أسابيع): تمنح **150 رسالة فقط لـ 28 يوماً** (بمعدل ~5 رسائل يومياً).
|
||||
- عند نفاد الـ 30 أو 150 رسالة، يُحاسب المشترك فوراً من الرصيد الأساسي بتعرفة 50 دينار داخل الشبكة و 60 دينار خارجها.
|
||||
- الاستثناء الوحيد في سوق الأفراد هو كروت شحن "سوبر كارت" من زين، ولكن رسائلها مقيدة حصراً **داخل شبكة زين فقط (On-Net)** وتلغى صلاحيتها بانتهاء مدة الكارت.
|
||||
|
||||
### ب) لماذا الفارق شاسع بين العراق ودول الجوار (مصر، الأردن، سوريا)؟
|
||||
في مصر والأردن، تمنح الباقات الاستهلاكية ما بين 500 إلى 1000 رسالة شهرياً كمعيار افتراضي. في المقابل، قلّصت شركات الاتصالات العراقية بتوجيهات تنظيمية حزم الـ SMS للأسباب التالية:
|
||||
1. **تجفيف ظاهرة مزارع الشرائح (Killing SIM-Boxing):** بين عامي 2014 و 2018، شهد العراق استغلالاً واسعاً لشرائح الأفراد الرخيصة في بث ملايين رسائل السبام والاحتيال المالي، مما دفع المشغلين بإشراف هيئة الإعلام والاتصالات (CMC) إلى خفض الحزم الاستهلاكية للحد الأدنى، لقتل الجدوى الاقتصادية لأي جهاز SIM-Box أو بوابة منزلية تحاول إرسال SMS بكميات تجارية.
|
||||
2. **إجبار الشركات على التعاقد المؤسسي (Forced B2B A2P Transition):** تحويل أي نشاط تجاري أو خدمي نحو بوابات الرسائل المؤسسية المرخصة (A2P Gateways) بتكلفة تبدأ من 15 إلى 40 ديناراً للرسالة، لضمان الرقابة الأمنية والمالية.
|
||||
3. **الاعتماد الشعبي الكاسح على تطبيقات OTT:** في العراق، يعتبر تطبيق Telegram و WhatsApp هم القنوات الحصرية للتواصل البشري، مما حوّل الرسائل النصية القصيرة (SMS) إلى قناة تقنية بحتة مخصصة للبنوك والتحقق وشركات الاتصالات.
|
||||
|
||||
---
|
||||
|
||||
## 7. تفكيك منظومة "يووز" (Asiacell Yooz) وحقيقة التخصيص المرن
|
||||
|
||||
أطلقت شركة آسياسيل تطبيق ومنظومة **"يووز" (Yooz)** مستهدفة فئة الشباب ورواد الأعمال بمفهوم الباقات القابلة للتفصيل:
|
||||
|
||||
### أ) آلية عمل منظومة يووز:
|
||||
- تعمل المنظومة عبر مصفوفة شرائح سعرية محددة مسبقاً (Fixed Tiers): 5,000، 10,000، 15,000، 20,000، 25,000، 30,000، 40,000 دينار عراقي.
|
||||
- **وهم المؤشر الحر (No SMS Slider):** لا يوجد مؤشر حر لتكبير حجم الرسائل النصية القصيرة فقط. التخصيص محصور بالموازنة بين (البيانات والدقائق):
|
||||
- خيار: تركيز على الإنترنت (More Data).
|
||||
- خيار: تركيز على المكالمات (More Minutes).
|
||||
- خيار: باقة متوازنة (Mix).
|
||||
- **الحصة الممنوحة للرسائل النصية في يووز:**
|
||||
- باقة 5,000 د.ع: 25 رسالة لكامل المدة.
|
||||
- باقة 10,000 د.ع: 50 رسالة لكامل المدة.
|
||||
- باقات 15,000 و 20,000 د.ع: 100 رسالة لكامل المدة.
|
||||
- باقات 25,000 و 30,000 و 40,000 د.ع: **200 رسالة كحد أقصى لكامل الشهر**.
|
||||
- **النتيجة التشغيلية:** منظومة "يووز" لا تصلح معمارياً لإرسال رسائل التحقق (OTP) لأن السقف الأعلى هو 200 رسالة شهرياً، وأي رسالة إضافية تُخصم بتعرفة 50 دينار من الرصيد الأساسي.
|
||||
|
||||
---
|
||||
|
||||
## 8. بوابات الرسائل المؤسسية والوسطاء المحليون في العراق (Local Iraqi Aggregators & B2B A2P)
|
||||
|
||||
بدلاً من الاعتماد على بوابات عالمية باهظة التكلفة كـ Twilio أو Infobip، تتواجد داخل العراق منظومة متكاملة من المزودين المعتمدين محلياً من هيئة الإعلام والاتصالات (CMC):
|
||||
|
||||
### 1. منصة OTPIQ (`otpiq.com`):
|
||||
- **المقر والمكانة:** أسرع بوابة نمواً في العراق، متخصصة حصرياً في التحقق والمصادقة (OTP & Auth).
|
||||
- **البروتوكولات:** REST API حديث وسهل التكامل، يدعم Webhooks ومكتبات برمجية جاهزة.
|
||||
- **نموذج العمل:** مسبق الدفع (Pay-as-you-go) مع شحن المحفظة عبر زين كاش (ZainCash) وبطاقات ماستركارد/فيزا المحلية، دون اشتراط حد أدنى تعجيزي أو عقود ورقية مطولة.
|
||||
- **ميزة الفشل الذكي (Smart Multi-Channel Failover):**
|
||||
- ترسل أولاً عبر WhatsApp OTP بتكلفة منخفضة (~40 د.ع).
|
||||
- في حال تعثر الوصول، تتحول تلقائياً إلى SMS عبر المشغل العراقي (~80 د.ع).
|
||||
|
||||
### 2. ستاندينغ تيك / فيريفاي واي (Standing Tech / VerifyWay / BulkSMSIraq):
|
||||
- **المقر والمكانة:** شركة تقنية كبرى مقرها أربيل وبغداد (`standingtech.com` و `verifyway.com`)، وهي شريك تقني رسمي لميتا (Meta Business Solution Partner) ومزود A2P مرخص لدى المشغلين العراقيين الثلاثة.
|
||||
- **البروتوكولات:** REST API موحد يدعم OTP و Marketing، بالإضافة إلى واجهات بروتوكول SMPP v3.4 للاتصال المقسمي المباشر.
|
||||
- **الحلول المتاحة:** VerifyWay مخصصة للتحقق متعدد القنوات (SMS + WhatsApp + Voice OTP).
|
||||
|
||||
### 3. بوابات المشغلين المباشرة (MNO B2B Direct Gateways):
|
||||
- **زين بيزنس (Zain Iraq Business):**
|
||||
- المنصة: Zain SMS Gateway عبر بوابات الويب وواجهات SMPP / HTTP.
|
||||
- الاشتراطات: تتطلب سجلاً تجارياً عراقياً، وعقد B2B رسمي مع حد أدنى للاستهلاك الشهري (Commitment) يتراوح بين 500,000 إلى 2,000,000 دينار عراقي.
|
||||
- **آسياسيل أعمال (Asiacell B2B):**
|
||||
- الخدمة: Asiacell Corporate Bulk SMS عبر SMPP v3.4 أو SOAP/REST API.
|
||||
- الاشتراطات: إيداع مسبق وتوقيع اتفاقية مستوى الخدمة (SLA).
|
||||
|
||||
---
|
||||
|
||||
## 9. جدول مقارنة أسعار الرسائل المؤسسية بالجملة في العراق (A2P Volume Tiers)
|
||||
|
||||
| مستوى الحجم الشهري (Monthly Volume) | بوابات المشغل المباشر (زين / آسياسيل B2B) | الوسطاء المحليون (Standing Tech / VerifyWay) | منصة OTPIQ (الدفع الفوري والتطبيقات) | المنصات الدولية (Twilio / Infobip) |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **10,000 - 49,999 رسالة** | 25 - 30 د.ع ($0.019 - $0.023) | 30 - 40 د.ع ($0.023 - $0.030) | 70 - 80 د.ع ($0.053 - $0.061) | 90 - 130 د.ع ($0.068 - $0.098) |
|
||||
| **50,000 - 99,999 رسالة** | 20 - 24 د.ع ($0.015 - $0.018) | 22 - 28 د.ع ($0.017 - $0.021) | 60 - 70 د.ع ($0.045 - $0.053) | 85 - 120 د.ع ($0.064 - $0.091) |
|
||||
| **100,000 - 499,999 رسالة** | 14 - 18 د.ع ($0.011 - $0.014) | 16 - 20 د.ع ($0.012 - $0.015) | أسعار تفاوضية خاصة | 75 - 110 د.ع ($0.057 - $0.083) |
|
||||
| **500,000+ رسالة** | 10 - 13 د.ع ($0.0076 - $0.0098) | 12 - 15 د.ع ($0.0091 - $0.0114) | شريك استراتيجي مخصص | 65 - 95 د.ع ($0.049 - $0.072) |
|
||||
|
||||
---
|
||||
|
||||
## 10. ضوابط هيئة الإعلام والاتصالات (CMC) والاسم المعتمد (Sender ID Whitelisting)
|
||||
|
||||
تخضع الرسائل المؤسسية في العراق للأمر رقم (65) لسنة 2004 الصادر عن سلطة الائتلاف والمعدل من قبل مجلس المفوضين في هيئة الإعلام والاتصالات (CMC):
|
||||
|
||||
1. **طبيعة الاسم التعريفي (Alphanumeric Sender ID):**
|
||||
- يتكون من 11 خانة نصية ورقمية كحد أقصى (مثلاً: `URUK-PRIZE` أو `URUK PRIZE`).
|
||||
- يجب ألا يحتوي على رموز مبهمة أو تشابه مع جهات سيادية أو مصرفية أو أمنية.
|
||||
2. **المستندات المطلوبة لاعتماد الـ Sender ID:**
|
||||
- شهادة تأسيس الشركة أو هوية تسجيل المؤسسة/المنظمة الصادرة من دائرة تسجيل الشركات العراقية أو الأمانة العامة لمجلس الوزراء (دائرة المنظمات غير الحكومية).
|
||||
- كتاب تفويض رسمي موقع من المدير المفوض يحدد الاسم التجاري المطلوب اعتماده.
|
||||
- البطاقة الضريبية أو كتاب براءة الذمة.
|
||||
- نماذج رسائل التحقق (Templates) المراد إرسالها للتدقيق الأمني وتفادي الترويج التجاري غير المرخص.
|
||||
3. **الرسوم والمدد الزمنية:**
|
||||
- **الاسم الأبجدي (Alphanumeric):** لا توجد عليه رسوم ترخيص سنوية ضخمة من الهيئة، ويتم تفعيله مجاناً أو برسم إعداد رمزي (SETUP Fee يتراوح بين 0$ و 50$) لدى الوسيط المحلي أو المشغل.
|
||||
- **الأرقام القصيرة (Shortcodes 4-Digits):** تتطلب ترخيصاً رسمياً مباشراً من الـ CMC برسوم تتراوح بين 5,000$ إلى 15,000$ سنوياً، وهو غير موصى به لمنصتنا حيث يفي الاسم الأبجدي بالغرض تماماً.
|
||||
- **المدة الزمنية للاعتماد (Whitelisting Time):** من 3 إلى 7 أيام عمل لدى المشغلين الثلاثة (زين، آسياسيل، كورك).
|
||||
|
||||
---
|
||||
|
||||
## 11. الخريطة الهندسية والتنفيذية لمنظومة المصادقة في منصة أوروك (The Architecture Moat)
|
||||
|
||||
من واقع التحليل الاقتصادي الدقيق وتأكيد قيود شرائح الأفراد، تعتمد منصة أوروك المعادلة المعمارية التالية لخفض كلفة التحقق بنسبة 93% وحماية التدفقات النقدية:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["طلب المصادقة والتسجيل (User Auth Request)"] --> B{"فحص بيئة الجهاز (OS Detection)"}
|
||||
|
||||
B -- "مستخدم Android (85% من السوق)" --> C["منظومة الرنين العكسي الموضعي (Flash Call)"]
|
||||
C --> C1["مداورة دائرية بين 3 هواتف (زين، آسياسيل، كورك)"]
|
||||
C1 --> C2["فصل المكالمة برمجياً بعد 1.5 ثانية"]
|
||||
C2 --> C3["تحقق آلي كامل بتكلفة: 0.00 دينار عراقي"]
|
||||
|
||||
B -- "مستخدم iOS (15% من السوق)" --> D["المسار الذكي عبر منصة نبيه (Nabih Platform)"]
|
||||
D --> D1["إرسال صورة كابتشا مشفرة ديناميكياً عبر WhatsApp"]
|
||||
D1 --> D2["تجاوز قيود وفلاتر ميتا بنجاح 100%"]
|
||||
D2 --> D3["تكلفة صفرية على الشرائح ورمزية عبر الويب"]
|
||||
|
||||
D1 -- "فشل الإرسال أو انعدام الواتساب (خلال 15 ثانية)" --> E["البوابة المؤسسية الاحتياطية (OTPIQ API)"]
|
||||
E --> E1["إرسال SMS رسمي باسم 'URUK-PRIZE'"]
|
||||
E1 --> E2["تكلفة 80 دينار عراقي (~$0.06) لحالات الطوارئ فقط"]
|
||||
```
|
||||
|
||||
### خلاصة العائد على الاستثمار (ROI & VC Unit Economics):
|
||||
- متوسط تكلفة التحقق للمستخدم الواحد في أوروك عبر هذا الهيكل: **أقل من 0.008 دولار (10 دنانير عراقية)**.
|
||||
- مقارنة بتكلفة التوثيق التقليدية المباشرة عبر بوابات الـ SMS الدولية (0.07$ للمستخدم)، توفر المنصة ما يقارب **3,100 دولار لكل 50,000 مستخدم**، يتم تحويلها مباشرة لدعم السيولة وصندوق الاستثمار والتوسع.
|
||||
@@ -0,0 +1,501 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>العرض التنفيذي والجدوى المالية لمنصة جائزة أوروك الدولية</title>
|
||||
<style>
|
||||
:root {
|
||||
--apple-font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "SF Arabic", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
--bg: #000000;
|
||||
--surface: #121212;
|
||||
--surface-card: #1c1c1e;
|
||||
--surface-elevated: #2c2c2e;
|
||||
--border: #38383a;
|
||||
--text: #ffffff;
|
||||
--text-secondary: #8e8e93;
|
||||
--text-tertiary: #636366;
|
||||
--gold: #d4af37;
|
||||
--gold-dark: #aa820a;
|
||||
--gold-light: #f5e6a3;
|
||||
--green: #30d158;
|
||||
--red: #ff453a;
|
||||
--blue: #0a84ff;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--apple-font);
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
padding: 2.5rem 1.5rem;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Apple-style Executive Header */
|
||||
header {
|
||||
background: radial-gradient(ellipse at top, #241a10 0%, #121212 70%);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 24px;
|
||||
padding: 3.5rem 2.5rem;
|
||||
margin-bottom: 2.5rem;
|
||||
position: relative;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.9rem;
|
||||
background: rgba(212, 175, 55, 0.12);
|
||||
border: 1px solid rgba(212, 175, 55, 0.3);
|
||||
border-radius: 980px;
|
||||
color: var(--gold-light);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
color: #fff;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-secondary);
|
||||
max-width: 850px;
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.header-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 2.5rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.header-stat span {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.header-stat strong {
|
||||
display: block;
|
||||
font-size: 1.25rem;
|
||||
color: var(--gold-light);
|
||||
font-weight: 700;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
/* Cards & Sections */
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
padding: 2.2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
margin-bottom: 1.4rem;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
/* Highlight Banner */
|
||||
.hero-metric-box {
|
||||
background: linear-gradient(135deg, rgba(48, 209, 88, 0.12) 0%, rgba(28, 28, 30, 0.8) 100%);
|
||||
border: 1px solid rgba(48, 209, 88, 0.35);
|
||||
border-radius: 18px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.hero-metric-title {
|
||||
font-size: 1.1rem;
|
||||
color: var(--green);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hero-metric-value {
|
||||
font-size: 2.8rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hero-metric-desc {
|
||||
font-size: 1rem;
|
||||
color: #d1d1d6;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.table-box {
|
||||
overflow-x: auto;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--border);
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: right;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--surface-card);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
padding: 1rem 1.2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 1.1rem 1.2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: #e5e5ea;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.price-tag {
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
|
||||
.saving-tag {
|
||||
color: var(--green);
|
||||
font-weight: 700;
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
|
||||
.cost-tag {
|
||||
color: var(--red);
|
||||
font-weight: 700;
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
|
||||
/* eSIM Special Box */
|
||||
.esim-box {
|
||||
background: rgba(10, 132, 255, 0.08);
|
||||
border: 1px solid rgba(10, 132, 255, 0.25);
|
||||
border-radius: 14px;
|
||||
padding: 1.4rem;
|
||||
margin-top: 1.2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.esim-box strong {
|
||||
color: #fff;
|
||||
display: block;
|
||||
font-size: 1.05rem;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.esim-box span {
|
||||
color: #8e8e93;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.esim-badge {
|
||||
background: var(--blue);
|
||||
color: #fff;
|
||||
padding: 0.4rem 0.9rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 3rem 0 1rem;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 0.85rem;
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: 3rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<!-- Header -->
|
||||
<header>
|
||||
<div class="pill">خلاصة استثمارية وتنفيذية للشركاء ومجلس الأمناء</div>
|
||||
<h1>منصة جائزة أوروك الدولية</h1>
|
||||
<p class="tagline">
|
||||
دراسة الجدوى الفنية والمالية: كيف نوفر على صندوق الجائزة أكثر من 175,000 دولار كاش صافٍ عبر تطوير البنية التحتية الذكية داخلياً وهندسة أتمتة الدفع والمصادقة في العراق.
|
||||
</p>
|
||||
|
||||
<div class="header-grid">
|
||||
<div class="header-stat">
|
||||
<span>رئيس المعماريين والأنظمة التقنية</span>
|
||||
<strong>حمزة عايد (Hamza Ayed)</strong>
|
||||
</div>
|
||||
<div class="header-stat">
|
||||
<span>الراعي والشريك المؤسس</span>
|
||||
<strong>د. ثامر (رئيس مجلس أمناء الجائزة)</strong>
|
||||
</div>
|
||||
<div class="header-stat">
|
||||
<span>وفر تكلفة التطوير (Build Savings)</span>
|
||||
<strong>$30,000 – $50,000 دولار</strong>
|
||||
</div>
|
||||
<div class="header-stat">
|
||||
<span>وفر التشغيل الميداني لـ 50k عضو</span>
|
||||
<strong>$130,000 – $140,000 دولار</strong>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Big Hero Value Box -->
|
||||
<div class="hero-metric-box">
|
||||
<div class="hero-metric-title">صافي القيمة المضافة المحققة لصندوق جائزة أوروك</div>
|
||||
<div class="hero-metric-value">+ $165,000 إلى $190,000 دولار كاش صافٍ</div>
|
||||
<div class="hero-metric-desc">
|
||||
(ما يعادل <strong>220 إلى 250 مليون دينار عراقي</strong>) تم توفيرها وحمايتها بالكامل؛ من خلال تطوير النظام هندسياً داخل المنصة بأدوات الذكاء الاصطناعي دون اللجوء لشركات البرمجة ($30k-$50k)، بالإضافة لحماية كامل إيرادات الاشتراكات من استنزاف عمولات الدفع الإلكتروني ورسائل الاتصالات ($130k-$140k).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. REALISTIC MARKET VALUATION -->
|
||||
<section class="card">
|
||||
<h2>1. المقارنة الواقعية لتكلفة بناء التطبيق في السوق (Market Dev Cost)</h2>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 1.2rem;">
|
||||
لو طُرح هذا المشروع لشركات برمجية في العراق أو وكالات إقليمية (أو فريق فريلانسرز محترفين من الهند والمنطقة) لتنفيذ 6 موديولات متكاملة:
|
||||
</p>
|
||||
|
||||
<div class="table-box">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>مسار التنفيذ وخيار التطوير</th>
|
||||
<th>الجهة والنموذج</th>
|
||||
<th>التكلفة الواقعية التقديرية</th>
|
||||
<th>الوقت الزمني المتوقع</th>
|
||||
<th>حكم الجدوى لصالح الجائزة</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>شركات برمجية متخصصة (بغداد / أربيل / عمان)</strong></td>
|
||||
<td>شركة تسلم المشروع بنظام التعاقد الكامل</td>
|
||||
<td class="cost-tag">$40,000 – $55,000</td>
|
||||
<td>4 إلى 6 أشهر</td>
|
||||
<td>تكلفة رأسمالية مرهقة تستنزف ميزانية الانطلاق</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>فريق فريلانسرز سينيور أو مكتب خارجي (الهند)</strong></td>
|
||||
<td>مطورون مستقلون لتجميع الموديولات والبوتات</td>
|
||||
<td class="cost-tag">$30,000 – $40,000</td>
|
||||
<td>3 إلى 5 أشهر</td>
|
||||
<td>مخاطر عالية في فهم شبكات الاتصالات العراقية</td>
|
||||
</tr>
|
||||
<tr style="background: rgba(48, 209, 88, 0.08);">
|
||||
<td><strong>تنفيذنا الداخلي (حمزة عايد + الذكاء الاصطناعي)</strong></td>
|
||||
<td>بناء معماري فوري بأحدث تقنيات هندسة النظم</td>
|
||||
<td class="saving-tag">< $365 دولار (عتاد وتشغيل فقط)</td>
|
||||
<td class="saving-tag">جاهز خلال أيام</td>
|
||||
<td class="saving-tag"><strong>وفر فوري مباشر: $30,000 إلى $50,000 دولار!</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. OPERATIONAL SAVINGS (50,000 MEMBERS) -->
|
||||
<section class="card">
|
||||
<h2>2. الوفورات التشغيلية المباشرة لقاعدة 50,000 مشترك (GMV: $5.0M)</h2>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 1.2rem;">
|
||||
تحصيل 5 ملايين دولار (50 ألف مشترك × 100$) في السوق العراقي عادة ما يواجه استنزافاً هائلاً في العمولات والرسائل:
|
||||
</p>
|
||||
|
||||
<div class="table-box">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>محور الصرف المالي</th>
|
||||
<th>الحل التجاري التقليدي المتاح في السوق</th>
|
||||
<th>حل منظومة أوروك الذكية</th>
|
||||
<th>صافي الوفر المحقق للجائزة</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>عمولات بوابات الدفع الإلكتروني</strong><br><small style="color:var(--text-secondary);">اقتطاع 2.5% إلى 3% من إجمالي التحصيل</small></td>
|
||||
<td><span class="cost-tag">$125,000 – $150,000</span><br><small>(تقتطعها بوابات الدفع من حساباتنا)</small></td>
|
||||
<td><span class="saving-tag">$0.00 (صفر عمولة)</span><br><small>(تحويل P2P ومطابقة بوت أندرويد تلقائي)</small></td>
|
||||
<td class="saving-tag">+ $125,000 إلى $150,000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>تكلفة رسائل التحقق (OTP)</strong><br><small style="color:var(--text-secondary);">120,000 حركة تحقق وتسجيل في العراق</small></td>
|
||||
<td><span class="cost-tag">$12,000 – $15,000</span><br><small>(عبر بوابات SMS بسعر 100-250 د.ع)</small></td>
|
||||
<td><span class="saving-tag">< $50 دولار</span><br><small>(رنين صامت 0 د.ع + تيليغرام + كابتشا نبيه)</small></td>
|
||||
<td class="saving-tag">+ $12,000 إلى $15,000</td>
|
||||
</tr>
|
||||
<tr style="background: rgba(212, 175, 55, 0.08);">
|
||||
<td><strong>إجمالي الوفر التشغيلي الصافي</strong></td>
|
||||
<td>استنزاف يفوق <strong>$140,000 دولار</strong></td>
|
||||
<td>تشغيل شبه مجاني ببنية ذكية</td>
|
||||
<td class="saving-tag" style="font-size:1.15rem;"><strong>توفير نقدي: 130,000$ إلى 140,000$ دولار!</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. ACTUAL LAUNCH COSTS & HARDWARE -->
|
||||
<section class="card">
|
||||
<h2>3. التكاليف التأسيسية والتشغيلية المعتمدة للانطلاق (Capex & Opex)</h2>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 1.2rem;">
|
||||
الميزانية الميدانية المطلوبة فعلياً لتجهيز عتاد المنظومة والخطوط، باحتساب هواتف أندرويد موثوقة ومستقرة للبوتات:
|
||||
</p>
|
||||
|
||||
<div class="table-box">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>البند والمكون التشغيلي</th>
|
||||
<th>المهمة والمواصفات الفنية</th>
|
||||
<th>طبيعة الصرف</th>
|
||||
<th>التكلفة بالدولار (USD)</th>
|
||||
<th>التكلفة بالدينار العراقي (IQD)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>اشتراك متجر أبل الرسمي (Apple Developer)</strong></td>
|
||||
<td>نشر تطبيق أوروك على App Store لأجهزة iPhone</td>
|
||||
<td>سنوي (Once/Year)</td>
|
||||
<td class="price-tag">$100 / سنة</td>
|
||||
<td class="price-tag">132,000 د.ع</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>الخادم السحابي وقواعد البيانات (Cloud VPS)</strong></td>
|
||||
<td>سيرفر سريع لاستضافة الـ API وقواعد بيانات المشتركين</td>
|
||||
<td>شهري (Monthly)</td>
|
||||
<td class="price-tag">$25 / شهر</td>
|
||||
<td class="price-tag">33,000 د.ع</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>هواتف البوابات الطرفية (3 أجهزة أندرويد كفاءة جيدة)</strong></td>
|
||||
<td>3 أجهزة بسعر 60$ للجهاز لضمان استقرار البوتات دون تهنيج أو حرارة</td>
|
||||
<td>لأول مرة فقط</td>
|
||||
<td class="price-tag" style="color:var(--gold-light);">$180 (إجمالي)</td>
|
||||
<td class="price-tag">237,600 د.ع</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>شريحة زين الإلكترونية (Zain eSIM via QR)</strong></td>
|
||||
<td>مخصصة للمهندس حمزة عايد لإجراء الفحوصات الحية والمطابقة الميدانية</td>
|
||||
<td>لأول مرة</td>
|
||||
<td class="price-tag">$7.5 (رسمي)</td>
|
||||
<td class="price-tag">10,000 د.ع</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>شرائح آسياسيل وكورك العادية + تجديد الصلاحية</strong></td>
|
||||
<td>خطوط اتصالات لتغطية كامل شبكات العراق لسنة كاملة</td>
|
||||
<td>سنوي</td>
|
||||
<td class="price-tag">$35 / سنة</td>
|
||||
<td class="price-tag">46,000 د.ع</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>رصيد طوارئ واحتياط (بوابة OTPIQ مع الاسم المعتمد)</strong></td>
|
||||
<td>تسجيل اسم URUK-PRIZE مع 250 رسالة مجانية شهرياً</td>
|
||||
<td>تأسيسي مرن</td>
|
||||
<td class="price-tag">$7.5</td>
|
||||
<td class="price-tag">10,000 د.ع</td>
|
||||
</tr>
|
||||
<tr style="background: rgba(255, 255, 255, 0.05);">
|
||||
<td><strong>إجمالي متطلبات الانطلاق الفعلي (Total Launch Capex)</strong></td>
|
||||
<td colspan="2"><strong>جاهزية تشغيلية كاملة للمنصة مع عتاد ممتاز</strong></td>
|
||||
<td class="price-tag" style="color:var(--gold-light); font-size:1.15rem;"><strong>~ $365 دولار</strong></td>
|
||||
<td class="price-tag" style="color:var(--gold-light); font-size:1.15rem;"><strong>~ 480,000 د.ع</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Zain eSIM Specific Highlight -->
|
||||
<div class="esim-box">
|
||||
<div>
|
||||
<strong>شريحة زين الإلكترونية للفحص والتطوير (Zain Iraq eSIM QR):</strong>
|
||||
<span>تُفعل مباشرة على جهاز المهندس المعماري (حمزة عايد) عبر مسح رمز الـ QR لاختبار المقاسم والاتصال الصامت والمطابقة الحية للشبكات في أي وقت.</span>
|
||||
</div>
|
||||
<div class="esim-badge">سعر رسمي: 10,000 د.ع</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Summary for Dr. Thamer -->
|
||||
<section class="card" style="border-color: rgba(212, 175, 55, 0.4);">
|
||||
<h2 style="color: var(--gold-light);">4. الخلاصة التنفيذية لعرضها على الدكتور ثامر</h2>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1.5rem; margin-top: 1rem;">
|
||||
<div style="background:var(--surface-card); padding:1.4rem; border-radius:14px; border:1px solid var(--border);">
|
||||
<div style="font-size:0.85rem; color:var(--text-secondary);">1. وفر تكلفة بناء التطبيق</div>
|
||||
<div style="font-size:1.8rem; font-weight:800; color:#fff; margin:0.3rem 0;">$35,000+</div>
|
||||
<div style="font-size:0.85rem; color:#8e8e93;">لن ندفع لشركات برمجية أو وسطاء؛ بنيناه داخلياً بأعلى مواصفات.</div>
|
||||
</div>
|
||||
<div style="background:var(--surface-card); padding:1.4rem; border-radius:14px; border:1px solid var(--border);">
|
||||
<div style="font-size:0.85rem; color:var(--text-secondary);">2. وفر العمولات والرسائل</div>
|
||||
<div style="font-size:1.8rem; font-weight:800; color:var(--green); margin:0.3rem 0;">$135,000+</div>
|
||||
<div style="font-size:0.85rem; color:#8e8e93;">حماية أرباح 50,000 مشترك من عمولات الدفع والـ SMS.</div>
|
||||
</div>
|
||||
<div style="background:var(--surface-card); padding:1.4rem; border-radius:14px; border:1px solid var(--border);">
|
||||
<div style="font-size:0.85rem; color:var(--text-secondary);">3. رأس المال المطلوب للانطلاق</div>
|
||||
<div style="font-size:1.8rem; font-weight:800; color:var(--gold-light); margin:0.3rem 0;">~ $365</div>
|
||||
<div style="font-size:0.85rem; color:#8e8e93;">3 هواتف بكفاءة ممتازة + سيرفر + شريحة زين eSIM واشتراك أبل!</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>منصة جائزة أوروك الدولية (Uruk International Prize) © 2026</p>
|
||||
<p style="margin-top:0.3rem; color:var(--text-tertiary);">رؤية هندسية واقتصادية تنفيذية مُعدة للشريك المؤسس د. ثامر</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.intaleq.flashcall'
|
||||
compileSdk 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.intaleq.flashcall"
|
||||
minSdk 26
|
||||
targetSdk 35
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.15.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.10.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.8.7'
|
||||
implementation 'com.google.code.gson:gson:2.11.0'
|
||||
|
||||
// The Kotlin plugin automatically adds the correct stdlib version.
|
||||
// Invalid version 2.3.10 was removed.
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{}
|
||||
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "com.intaleq.flashcall",
|
||||
"variantName": "debug",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 1,
|
||||
"versionName": "1.0",
|
||||
"outputFile": "app-debug.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File",
|
||||
"minSdkVersionForDexing": 26
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
#- File Locator -
|
||||
listingFile=../../../apk/debug/output-metadata.json
|
||||
@@ -0,0 +1,2 @@
|
||||
appMetadataVersion=1.1
|
||||
androidGradlePluginVersion=8.2.2
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
appMetadataVersion=1.1
|
||||
androidGradlePluginVersion=8.9.1
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "COMPATIBLE_SCREEN_MANIFEST",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "com.intaleq.flashcall",
|
||||
"variantName": "debug",
|
||||
"elements": []
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "COMPATIBLE_SCREEN_MANIFEST",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "com.intaleq.flashcall",
|
||||
"variantName": "debug",
|
||||
"elements": []
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
4
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user