Compare commits
66 Commits
c4bf4ea679
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c6e110cd0 | ||
|
|
27e9d89af3 | ||
|
|
d0211ecb86 | ||
|
|
cc088decfd | ||
|
|
6d65f4d09f | ||
|
|
d20e041009 | ||
|
|
fadb373d42 | ||
|
|
fca292f2a4 | ||
|
|
671b90a954 | ||
|
|
f535f7db1d | ||
|
|
61212b60af | ||
|
|
da590e7fc0 | ||
|
|
a724680755 | ||
|
|
ee9c0f3a04 | ||
|
|
e306217806 | ||
|
|
b9e66772a4 | ||
|
|
8e692d1b55 | ||
|
|
67c5043426 | ||
|
|
b85e49f4b8 | ||
|
|
8545c09b76 | ||
|
|
b4dd178075 | ||
|
|
761254ab3c | ||
|
|
d78da5de88 | ||
|
|
fe5fa1feff | ||
|
|
fccd758e93 | ||
|
|
540c5cc7ab | ||
|
|
cc85fe1815 | ||
|
|
2b9176e229 | ||
|
|
5622b57da9 | ||
|
|
238cf844c8 | ||
|
|
18933c8480 | ||
|
|
c438bd5da0 | ||
|
|
ee36011f35 | ||
|
|
8ac07c4b3f | ||
|
|
bcc6639a3a | ||
|
|
7805f02cd6 | ||
|
|
8145e459fd | ||
|
|
e8f9c8bd05 | ||
|
|
ff5a7bdc0e | ||
|
|
c536500c15 | ||
|
|
5b5d97b1f3 | ||
|
|
2540bef154 | ||
|
|
392e37c198 | ||
|
|
756980b6d7 | ||
|
|
4534e8769b | ||
|
|
2745b307a9 | ||
|
|
d9039aaf14 | ||
|
|
2ecc1536e2 | ||
|
|
c7f3f09f0e | ||
|
|
733f1b98f5 | ||
|
|
f6ad0773f3 | ||
|
|
555b6a261f | ||
|
|
93b57d2ece | ||
|
|
69993ff775 | ||
|
|
e78bfc6b5c | ||
|
|
3f4b4ef659 | ||
|
|
425f28a715 | ||
|
|
75e928650b | ||
|
|
ac49dd92bd | ||
|
|
b52eb3bed2 | ||
|
|
650cce2e86 | ||
|
|
8c9836a20c | ||
|
|
3c0b0a7dcd | ||
|
|
ded925620d | ||
|
|
a81ab40127 | ||
|
|
0e916da9a6 |
@@ -28,7 +28,10 @@ class LegacyEncryption
|
||||
}
|
||||
|
||||
$this->key = trim(file_get_contents($keyPath));
|
||||
$this->iv = env('LEGACY_IV', '');
|
||||
$this->iv = config('intaleq.legacy_iv', env('initializationVector', ''));
|
||||
if (strlen($this->iv) !== 16) {
|
||||
$this->iv = str_pad($this->iv, 16, "\0");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
79
app/Http/Controllers/Api/PaymentTokenController.php
Normal file
79
app/Http/Controllers/Api/PaymentTokenController.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Firebase\JWT\JWT;
|
||||
|
||||
/**
|
||||
* PaymentTokenController — توليد رموز الدفع المتوافقة مع سيرفر المحفظة V1
|
||||
*
|
||||
* ⚠️ مهم جداً:
|
||||
* سيرفر المحفظة (walletintaleq.intaleq.xyz) يعمل بكود V1 ويتحقق من التوكن باستخدام:
|
||||
* - المفتاح: /home/intaleq-api/.secret_key (وليس PAYMENT_INTERNAL_KEY_PATH)
|
||||
* - الـ Issuer: 'Tripz' أو 'Tripz-Wallet'
|
||||
* - الـ Claim: user_id (وليس sub)
|
||||
*
|
||||
* لذلك يجب أن يكون التوكن المُولّد هنا متوافقاً تماماً مع V1.
|
||||
*/
|
||||
class PaymentTokenController extends Controller
|
||||
{
|
||||
// 1. مسار الراكب
|
||||
public function generatePassengerToken(Request $request)
|
||||
{
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
return $this->buildToken($userId, 'android/ios_passenger', $request->input('fingerPrint'));
|
||||
}
|
||||
|
||||
// 2. مسار السائق
|
||||
public function generateDriverToken(Request $request)
|
||||
{
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
return $this->buildToken($userId, 'android/ios_driver', $request->input('fingerPrint'));
|
||||
}
|
||||
|
||||
// 3. مسار المدير
|
||||
public function generateAdminToken(Request $request)
|
||||
{
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
return $this->buildToken($userId, 'web_admin', 'admin_secure_context');
|
||||
}
|
||||
|
||||
// 4. دالة البناء المركزية — متوافقة مع V1 authenticateJWT()
|
||||
private function buildToken($userId, $audience, $fingerprint)
|
||||
{
|
||||
// ⚠️ مهم جداً لموافاة V1:
|
||||
// سيرفر المحفظة (walletintaleq.intaleq.xyz) يستخدم V1authenticateJWT()
|
||||
// التي تقرأ المفتاح من: /home/intaleq-api/.secret_key
|
||||
// لذلك يجب استخدام jwt_secret هنا وليس wallet_jwt_secret.
|
||||
$secret = config('intaleq.jwt_secret');
|
||||
|
||||
if (empty($secret)) {
|
||||
return response()->json(['status' => 'error', 'message' => 'Security Key Missing (JWT)'], 500);
|
||||
}
|
||||
|
||||
// بناء التوكن بنفس الهيكل المطلوب من V1
|
||||
$payload = [
|
||||
'user_id' => $userId, // V1 يستخرج: $decoded->user_id
|
||||
'iss' => 'Tripz-Wallet', // V1 يتحقق: $decoded->iss === 'Tripz-Wallet'
|
||||
'aud' => $audience,
|
||||
'iat' => time(),
|
||||
'exp' => time() + 120, // زيادة الوقت قليلاً لـ 120 ثانية
|
||||
'fingerPrint' => hash('sha256', ($fingerprint ?? '') . config('intaleq.fp_pepper', '')),
|
||||
'jti' => bin2hex(random_bytes(16)),
|
||||
];
|
||||
|
||||
$token = JWT::encode($payload, $secret, 'HS256');
|
||||
|
||||
// HMAC: يستخدم SECRET_KEY_HMAC (متوفر في .env)
|
||||
$hmacSecret = config('intaleq.wallet_hmac_secret');
|
||||
$hmac = hash_hmac('sha256', $userId, $hmacSecret);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'token' => $token,
|
||||
'hmac' => $hmac
|
||||
]);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,5 +16,5 @@ use Illuminate\Routing\Controller as BaseController;
|
||||
*/
|
||||
abstract class Controller extends BaseController
|
||||
{
|
||||
//
|
||||
use \App\Traits\ApiResponses;
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ class DriverDocController extends Controller
|
||||
/** POST /v2/driver/scams */
|
||||
public function reportScam(Request $request): JsonResponse
|
||||
{
|
||||
$userId = $request->input('_jwt_user_id');
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
/** GET /v2/driver/registration-car */
|
||||
public function getCarReg(Request $request): JsonResponse
|
||||
{
|
||||
$userId = $request->input('_jwt_user_id');
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
$data = DB::connection('primary')->table('RegisrationCar')
|
||||
->where('driverID', $userId)->get();
|
||||
return response()->json(['status' => 'success', 'data' => $data]);
|
||||
@@ -27,7 +27,7 @@ class DriverDocController extends Controller
|
||||
/** POST /v2/driver/registration-car */
|
||||
public function storeCarReg(Request $request): JsonResponse
|
||||
{
|
||||
$userId = $request->input('_jwt_user_id');
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
// Logic to store...
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
@@ -20,16 +20,18 @@ class InviteController extends Controller
|
||||
/** POST /v2/invites/driver */
|
||||
public function inviteDriver(Request $request): JsonResponse
|
||||
{
|
||||
if (!$request->has(['driverId', 'inviterDriverPhone'])) {
|
||||
$driverId = $request->input('driverId') ?? $request->attributes->get('_jwt_user_id');
|
||||
$inviterPhone = $request->input('inviterDriverPhone');
|
||||
|
||||
if (!$driverId || !$inviterPhone) {
|
||||
\Log::warning('Invite driver parameters missing: ' . json_encode($request->all()) . ' JWT ID: ' . $request->attributes->get('_jwt_user_id'));
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Missing required parameters'
|
||||
]);
|
||||
'message' => 'Missing required parameters: driverId or inviterDriverPhone'
|
||||
], 400);
|
||||
}
|
||||
|
||||
$driverId = $request->input('driverId');
|
||||
$phone = $request->input('inviterDriverPhone');
|
||||
$phoneEnc = $this->enc->encrypt($phone);
|
||||
$phoneEnc = $this->enc->encrypt($inviterPhone);
|
||||
|
||||
// التحقق من وجود دعوة مسبقة
|
||||
$existing = DB::connection('primary')->table('invites')
|
||||
@@ -88,24 +90,111 @@ class InviteController extends Controller
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Database error: ' . $e->getMessage()
|
||||
'message' => 'An error occurred'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/** GET /v2/invites/driver */
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$driverId = $request->attributes->get('_jwt_user_id') ?? $request->input('driverId');
|
||||
|
||||
$invites = DB::connection('primary')->table('invites')
|
||||
->where('driverId', $driverId)
|
||||
->get()
|
||||
->map(function ($invite) {
|
||||
// V1 logic expects certain field names
|
||||
$invite->invitorName = "Driver " . substr($invite->inviterDriverPhone, -4);
|
||||
$invite->countOfInvitDriver = $invite->isInstall == 1 ? "100" : "0";
|
||||
$invite->isGiftToken = $invite->isGiftToken ?? 0;
|
||||
return $invite;
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => $invites
|
||||
]);
|
||||
}
|
||||
|
||||
/** POST /v2/invites/passenger */
|
||||
public function invitePassenger(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Not implemented yet'
|
||||
]);
|
||||
if (!$request->has(['driverId', 'inviterPassengerPhone'])) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Missing required parameters'
|
||||
]);
|
||||
}
|
||||
|
||||
$driverId = $request->input('driverId');
|
||||
$phone = $request->input('inviterPassengerPhone');
|
||||
$phoneEnc = $this->enc->encrypt($phone);
|
||||
|
||||
$existing = DB::connection('primary')->table('invitesToPassengers')
|
||||
->where('inviterPassengerPhone', $phoneEnc)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
if ($existing->isInstall == 1) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => $existing->inviteCode
|
||||
]);
|
||||
}
|
||||
|
||||
$expirationTime = now()->addHour();
|
||||
DB::connection('primary')->table('invitesToPassengers')
|
||||
->where('id', $existing->id)
|
||||
->update([
|
||||
'driverId' => $driverId,
|
||||
'expirationTime' => $expirationTime,
|
||||
'createdAt' => now()
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => [
|
||||
'inviteId' => $existing->id,
|
||||
'inviteCode' => $existing->inviteCode,
|
||||
'expirationTime' => $expirationTime->toDateTimeString()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$inviteCode = $this->generateUniqueCodePassenger();
|
||||
$expirationTime = now()->addHour();
|
||||
|
||||
try {
|
||||
$id = DB::connection('primary')->table('invitesToPassengers')->insertGetId([
|
||||
'driverId' => $driverId,
|
||||
'inviterPassengerPhone' => $phoneEnc,
|
||||
'inviteCode' => $inviteCode,
|
||||
'expirationTime' => $expirationTime,
|
||||
'createdAt' => now(),
|
||||
'isInstall' => 0
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => [
|
||||
'inviteId' => $id,
|
||||
'inviteCode' => $inviteCode,
|
||||
'expirationTime' => $expirationTime->toDateTimeString()
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'An error occurred'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/** GET /v2/invites/gift */
|
||||
public function checkGift(Request $request): JsonResponse
|
||||
{
|
||||
$userId = $request->input('_jwt_user_id');
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => ['gift_available' => true]
|
||||
@@ -128,4 +217,21 @@ class InviteController extends Controller
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function generateUniqueCodePassenger(): string
|
||||
{
|
||||
while (true) {
|
||||
$letters = strtoupper(Str::random(4));
|
||||
$numbers = rand(100, 999);
|
||||
$code = $letters . $numbers;
|
||||
|
||||
$exists = DB::connection('primary')->table('invitesToPassengers')
|
||||
->where('inviteCode', $code)
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +34,24 @@ class MiscController extends Controller
|
||||
}
|
||||
|
||||
/** GET /v2/misc/package-info */
|
||||
public function packageInfo(): JsonResponse
|
||||
public function packageInfo(Request $request): JsonResponse
|
||||
{
|
||||
$info = DB::connection('primary')->table('packageInfo')->orderBy('id', 'desc')->first();
|
||||
$platform = $request->input('platform', 'android');
|
||||
$appName = $request->input('appName');
|
||||
|
||||
$query = DB::connection('primary')->table('packageInfo')
|
||||
->where('platform', $platform);
|
||||
|
||||
if ($appName) {
|
||||
$query->where('appName', $appName);
|
||||
}
|
||||
|
||||
$info = $query->orderBy('id', 'desc')->first();
|
||||
|
||||
if (!$info) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'No package info found'
|
||||
'message' => 'No package info found for platform: ' . $platform
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -83,14 +93,7 @@ class MiscController extends Controller
|
||||
/** GET /v2/misc/help-center */
|
||||
public function getHelpCenter(Request $request): JsonResponse
|
||||
{
|
||||
$driverId = $request->input('driverID');
|
||||
|
||||
if (!$driverId) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'driverID is required'
|
||||
]);
|
||||
}
|
||||
$driverId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
$data = DB::connection('primary')->table('helpCenter')
|
||||
->where('driverID', $driverId)
|
||||
@@ -113,23 +116,15 @@ class MiscController extends Controller
|
||||
/** GET /v2/misc/tips */
|
||||
public function getTips(Request $request): JsonResponse
|
||||
{
|
||||
$driverId = $request->input('driverID');
|
||||
$passengerId = $request->input('passendgerID');
|
||||
|
||||
if (!$driverId && !$passengerId) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'driverID or passendgerID is required'
|
||||
]);
|
||||
}
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
$userType = $request->attributes->get('_jwt_user_type');
|
||||
|
||||
$query = DB::connection('primary')->table('tips');
|
||||
|
||||
if ($driverId) {
|
||||
$query->where('driverID', $driverId);
|
||||
}
|
||||
if ($passengerId) {
|
||||
$query->orWhere('passendgerID', $passengerId);
|
||||
if ($userType === 'driver') {
|
||||
$query->where('driverID', $userId);
|
||||
} else {
|
||||
$query->where('passengerID', $userId);
|
||||
}
|
||||
|
||||
$data = $query->get();
|
||||
@@ -168,36 +163,36 @@ class MiscController extends Controller
|
||||
/** POST /v2/misc/help-center */
|
||||
public function storeHelpCenter(Request $request): JsonResponse
|
||||
{
|
||||
$driverId = $request->input('driverID');
|
||||
$passengerId = $request->input('passengerID');
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
$helpQuestion = $request->input('helpQuestion');
|
||||
|
||||
if ((!$driverId && !$passengerId) || !$helpQuestion) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Missing parameters']);
|
||||
if (!$helpQuestion) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Missing help question']);
|
||||
}
|
||||
|
||||
try {
|
||||
DB::connection('primary')->table('helpCenter')->insert([
|
||||
'driverID' => $driverId ?? $passengerId,
|
||||
'driverID' => $userId,
|
||||
'helpQuestion' => $helpQuestion,
|
||||
'datecreated' => now()
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Help question saved successfully']);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Database error: ' . $e->getMessage()]);
|
||||
\Log::error('MiscController HelpCenter Error: ' . $e->getMessage());
|
||||
return response()->json(['status' => 'failure', 'message' => 'An error occurred']);
|
||||
}
|
||||
}
|
||||
|
||||
/** POST /v2/misc/tips */
|
||||
public function storeTips(Request $request): JsonResponse
|
||||
{
|
||||
$passengerId = $request->input('passengerID');
|
||||
$passengerId = $request->attributes->get('_jwt_user_id'); // From JWT
|
||||
$driverId = $request->input('driverID');
|
||||
$rideId = $request->input('rideID');
|
||||
$tipAmount = $request->input('tipAmount');
|
||||
|
||||
if (!$passengerId || !$driverId || !$rideId || !$tipAmount) {
|
||||
if (!$driverId || !$rideId || !$tipAmount) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Missing parameters']);
|
||||
}
|
||||
|
||||
@@ -212,7 +207,8 @@ class MiscController extends Controller
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Tip inserted successfully']);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Database error: ' . $e->getMessage()]);
|
||||
\Log::error('MiscController Tips Error: ' . $e->getMessage());
|
||||
return response()->json(['status' => 'failure', 'message' => 'An error occurred']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,10 +258,32 @@ class MiscController extends Controller
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('MiscController EgyptPhones Error: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Database error: ' . $e->getMessage()
|
||||
'message' => 'An error occurred'
|
||||
]);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* POST /v2/admin/errors
|
||||
* Accepts client-side error reports from Flutter apps.
|
||||
* Public endpoint (no auth required) — just logs the error.
|
||||
*/
|
||||
public function logClientError(Request $request): JsonResponse
|
||||
{
|
||||
$body = $request->getContent();
|
||||
|
||||
\Illuminate\Support\Facades\Log::channel('single')->warning('Client Error Report', [
|
||||
'ip' => $request->ip(),
|
||||
'body' => substr($body, 0, 2000), // Limit to 2KB
|
||||
'user_agent' => $request->userAgent(),
|
||||
]);
|
||||
|
||||
$error = $request->input('error') ?? 'Error logged';
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => $error,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ class NotificationController extends Controller
|
||||
/** GET /v2/notifications */
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$userId = $request->input('_jwt_user_id');
|
||||
$userType = $request->input('_jwt_user_type');
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
$userType = $request->attributes->get('_jwt_user_type');
|
||||
$page = (int) $request->input('page', 1);
|
||||
$limit = min((int) $request->input('limit', 20), 50);
|
||||
|
||||
@@ -40,21 +40,59 @@ class NotificationController extends Controller
|
||||
->get();
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => $notifications]);
|
||||
return response()->json(['status' => 'success', 'message' => $notifications]);
|
||||
}
|
||||
|
||||
/** PUT /v2/notifications/{id}/read */
|
||||
public function markRead(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$userType = $request->input('_jwt_user_type');
|
||||
$userType = $request->attributes->get('_jwt_user_type');
|
||||
$table = $userType === 'driver' ? 'notificationCaptain' : 'notifications';
|
||||
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
$userField = $userType === 'driver' ? 'driverID' : 'passenger_id';
|
||||
|
||||
DB::connection('primary')->table($table)
|
||||
->where('id', $id)
|
||||
->where($userField, $userId)
|
||||
->update(['isShown' => 'true']);
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
/** POST /v2/notifications/update (For V1 Compatibility) */
|
||||
public function updateNotification(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$id = $request->input('id');
|
||||
if (!$id) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Missing notification ID']);
|
||||
}
|
||||
|
||||
$isShown = $request->input('isShown', 'true');
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
$userType = $request->attributes->get('_jwt_user_type');
|
||||
|
||||
$table = $userType === 'driver' ? 'notificationCaptain' : 'notifications';
|
||||
$userField = $userType === 'driver' ? 'driverID' : 'passenger_id';
|
||||
|
||||
$affected = DB::connection('primary')->table($table)
|
||||
->where('id', $id)
|
||||
->where($userField, $userId)
|
||||
->update(['isShown' => $isShown]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'affected' => $affected
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error('NotificationController Update Error: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Internal server error occurred'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
/** POST /v2/notifications/token */
|
||||
public function updateToken(Request $request): JsonResponse
|
||||
{
|
||||
@@ -63,21 +101,20 @@ class NotificationController extends Controller
|
||||
'fingerPrint' => 'required|string',
|
||||
]);
|
||||
|
||||
$userId = $request->input('_jwt_user_id') ?? $request->input('passengerID');
|
||||
$userType = $request->input('_jwt_user_type') ?? 'passenger';
|
||||
$userId = $request->attributes->get('_jwt_user_id') ?? $request->input('passengerID');
|
||||
$userType = $request->attributes->get('_jwt_user_type') ?? 'passenger';
|
||||
|
||||
if (!$userId) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'User ID missing'], 400);
|
||||
}
|
||||
|
||||
if ($userType === 'driver') {
|
||||
DB::connection('primary')->table('captainToken')
|
||||
DB::connection('primary')->table('driverToken')
|
||||
->updateOrInsert(
|
||||
['captain_id' => $userId],
|
||||
[
|
||||
'token' => $request->input('token'),
|
||||
'fingerPrint' => $request->input('fingerPrint'),
|
||||
'status' => 'active'
|
||||
]
|
||||
);
|
||||
} else {
|
||||
@@ -87,7 +124,6 @@ class NotificationController extends Controller
|
||||
[
|
||||
'token' => $request->input('token'),
|
||||
'fingerPrint' => $request->input('fingerPrint'),
|
||||
'status' => 'active'
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -98,15 +134,15 @@ class NotificationController extends Controller
|
||||
/** GET /v2/notifications/token */
|
||||
public function getToken(Request $request): JsonResponse
|
||||
{
|
||||
$userId = $request->input('_jwt_user_id') ?? $request->input('passengerID');
|
||||
$userType = $request->input('_jwt_user_type') ?? 'passenger';
|
||||
$userId = $request->attributes->get('_jwt_user_id') ?? $request->input('passengerID');
|
||||
$userType = $request->attributes->get('_jwt_user_type') ?? 'passenger';
|
||||
|
||||
if (!$userId) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'User ID missing'], 400);
|
||||
}
|
||||
|
||||
if ($userType === 'driver') {
|
||||
$data = DB::connection('primary')->table('captainToken')
|
||||
$data = DB::connection('primary')->table('driverToken')
|
||||
->where('captain_id', $userId)
|
||||
->first();
|
||||
} else {
|
||||
@@ -121,7 +157,7 @@ class NotificationController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'message' => [
|
||||
'token' => $data->token,
|
||||
'fingerPrint' => $data->fingerPrint ?? null,
|
||||
]
|
||||
|
||||
@@ -6,8 +6,9 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Helpers\LegacyEncryption;
|
||||
use App\Services\LegacyEncryption;
|
||||
|
||||
/**
|
||||
* متحكم رموز التحقق (OTP Controller)
|
||||
@@ -24,6 +25,20 @@ class OtpController extends Controller
|
||||
$this->encryption = $encryption;
|
||||
}
|
||||
|
||||
/** POST /v2/otp/driver/send */
|
||||
public function sendDriver(Request $request): JsonResponse
|
||||
{
|
||||
$request->merge(['user_type' => 'driver']);
|
||||
return $this->send($request);
|
||||
}
|
||||
|
||||
/** POST /v2/otp/driver/verify */
|
||||
public function verifyDriver(Request $request): JsonResponse
|
||||
{
|
||||
$request->merge(['user_type' => 'driver']);
|
||||
return $this->verify($request);
|
||||
}
|
||||
|
||||
/** POST /v2/otp/send */
|
||||
public function send(Request $request): JsonResponse
|
||||
{
|
||||
@@ -32,16 +47,19 @@ class OtpController extends Controller
|
||||
'user_type' => 'nullable|in:passenger,driver,admin',
|
||||
]);
|
||||
|
||||
$phone = $request->input('phone');
|
||||
$phone = $request->input('phone') ?? $request->input('phone_number');
|
||||
$userType = $request->input('user_type', 'passenger');
|
||||
|
||||
if (!$phone) {
|
||||
return $this->failure('The phone field is required', 400);
|
||||
}
|
||||
|
||||
// Rate limit: 3 OTP per phone per 5 minutes
|
||||
$key = "otp_limit_{$userType}:{$phone}";
|
||||
if (Cache::get($key, 0) >= 3) {
|
||||
return $this->failure('Too many OTP requests', 429);
|
||||
if (RateLimiter::tooManyAttempts($key, 3)) {
|
||||
return $this->failure('Too many OTP requests. Please try again later.', 429);
|
||||
}
|
||||
Cache::increment($key);
|
||||
Cache::put($key, Cache::get($key), 300);
|
||||
RateLimiter::hit($key, 300);
|
||||
|
||||
// Generate 5-digit OTP
|
||||
$otp = (string) random_int(10000, 99999);
|
||||
@@ -82,21 +100,44 @@ class OtpController extends Controller
|
||||
$encPhone = $this->encryption->encrypt($phone);
|
||||
$encOtp = $this->encryption->encrypt($otp);
|
||||
|
||||
DB::connection('primary')->table($table)->where('phone_number', $encPhone)->delete();
|
||||
DB::connection('primary')->table($table)->insert([
|
||||
'phone_number' => $encPhone,
|
||||
'token' => $encOtp,
|
||||
'expiration_time' => $expiration,
|
||||
'verified' => 0,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
try {
|
||||
DB::connection('primary')->table($table)->where('phone_number', $encPhone)->delete();
|
||||
DB::connection('primary')->table($table)->insert([
|
||||
'phone_number' => $encPhone,
|
||||
'token' => $encOtp,
|
||||
'expiration_time' => $expiration,
|
||||
'verified' => 0,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error("OTP Send Error ($table): " . $e->getMessage());
|
||||
// Procedural success even if DB fails for now, to allow dev flow
|
||||
return $this->success([
|
||||
'message' => 'OTP procedural success (DB log error)',
|
||||
'expires_at' => $expiration->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO: Send SMS/WhatsApp via external provider
|
||||
// Send WhatsApp message (especially for drivers changing devices)
|
||||
// $message = "Your Intaleq App verification code is: $otp";
|
||||
// $this->sendWhatsAppFromServer($phone, $message);
|
||||
|
||||
// Check if passenger exists to allow immediate login (V1 style)
|
||||
// We check both encrypted and raw phone with multiple formats (963... and 0...)
|
||||
$rawPhone = $phone;
|
||||
$localPhone = '0' . substr($phone, 3); // Convert 9639... to 09...
|
||||
|
||||
$encRawPhone = $this->encryption->encrypt($rawPhone);
|
||||
$encLocalPhone = $this->encryption->encrypt($localPhone);
|
||||
|
||||
$passenger = DB::connection('primary')->table('passengers')
|
||||
->whereIn('phone', [$rawPhone, $localPhone, $encRawPhone, $encLocalPhone])
|
||||
->first();
|
||||
|
||||
return $this->success([
|
||||
'message' => 'OTP sent successfully',
|
||||
'message' => 'OTP process initiated',
|
||||
'isRegistered' => !is_null($passenger),
|
||||
'expires_at' => $expiration->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
@@ -108,7 +149,7 @@ class OtpController extends Controller
|
||||
'phone' => 'required|string',
|
||||
'otp' => 'required|string',
|
||||
'user_type' => 'nullable|in:passenger,driver,admin',
|
||||
'device_number' => 'nullable|string', // Used for admin
|
||||
'device_number' => 'nullable|string|max:64|regex:/^[a-zA-Z0-9_\-\.]+$/', // Used for admin
|
||||
]);
|
||||
|
||||
$phone = $request->input('phone');
|
||||
@@ -247,10 +288,15 @@ class OtpController extends Controller
|
||||
/** GET /v2/otp/check-phone?phone=XXX */
|
||||
public function checkPhone(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate(['phone' => 'required|string']);
|
||||
$phone = $request->input('phone') ?? $request->input('phone_number') ?? $request->query('phone') ?? $request->query('phone_number');
|
||||
|
||||
if (!$phone) {
|
||||
return $this->failure('Phone parameter is missing', 400);
|
||||
}
|
||||
|
||||
// Match V1 exact column name: is_verified
|
||||
$verified = DB::connection('primary')->table('phone_verification')
|
||||
->where('phone_number', $request->input('phone'))
|
||||
->where('phone_number', $phone)
|
||||
->where('is_verified', 1)
|
||||
->exists();
|
||||
|
||||
@@ -259,4 +305,45 @@ class OtpController extends Controller
|
||||
'data' => ['verified' => $verified],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send WhatsApp message using the available bot servers (Ported from V1 functions.php)
|
||||
*/
|
||||
private function sendWhatsAppFromServer($to, $message)
|
||||
{
|
||||
$servers = [
|
||||
"https://bot5.intaleq.xyz/send", // ramat bus
|
||||
"https://bot3.intaleq.xyz/send", // shahd
|
||||
];
|
||||
|
||||
$url = $servers[array_rand($servers)];
|
||||
|
||||
$payload = [
|
||||
"to" => $to,
|
||||
"message" => $message
|
||||
];
|
||||
|
||||
$curl = curl_init();
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CUSTOMREQUEST => "POST",
|
||||
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
"Content-Type: application/json"
|
||||
],
|
||||
CURLOPT_TIMEOUT => 5 // Don't block API for too long
|
||||
]);
|
||||
|
||||
$response = curl_exec($curl);
|
||||
$err = curl_error($curl);
|
||||
curl_close($curl);
|
||||
|
||||
if ($err) {
|
||||
\Log::error("[sendWhatsAppFromServer] cURL Error on $url: $err");
|
||||
return false;
|
||||
}
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
}
|
||||
|
||||
127
app/Http/Controllers/OverlayController.php
Normal file
127
app/Http/Controllers/OverlayController.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* OverlayController — إدارة بيانات الرحلة المقبولة من الخلفية
|
||||
*
|
||||
* الغرض:
|
||||
* عندما يقبل السائق رحلة وهو في الخلفية (Background)، يتم تخزين بيانات الرحلة
|
||||
* في جدول write_argument_after_applied_from_background حتى يتمكن التطبيق من
|
||||
* استرجاعها عند العودة للواجهة (Foreground).
|
||||
*
|
||||
* يعادل: intaleq_v1/ride/overLay/getArgumentAfterAppliedFromBackground.php
|
||||
* intaleq_v1/ride/overLay/add.php
|
||||
*/
|
||||
class OverlayController extends Controller
|
||||
{
|
||||
/**
|
||||
* GET /v2/overlay/background-args
|
||||
* جلب بيانات الرحلة المقبولة من الخلفية (آخر دقيقتين فقط)
|
||||
*/
|
||||
public function getBackgroundArgs(Request $request): JsonResponse
|
||||
{
|
||||
$driverId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
if (empty($driverId)) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Missing driver ID']);
|
||||
}
|
||||
|
||||
$row = DB::connection('primary')
|
||||
->table('write_argument_after_applied_from_background')
|
||||
->where('driver_id', $driverId)
|
||||
->whereRaw('TIMESTAMPDIFF(MINUTE, time_of_order, NOW()) <= 2')
|
||||
->orderBy('time_of_order', 'desc')
|
||||
->first();
|
||||
|
||||
if ($row) {
|
||||
return response()->json(['status' => 'success', 'message' => (array) $row]);
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'failure', 'message' => 'No data found']);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/overlay/background-args
|
||||
* تخزين بيانات الرحلة المقبولة من الخلفية
|
||||
*/
|
||||
public function storeBackgroundArgs(Request $request): JsonResponse
|
||||
{
|
||||
$driverId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
$rideId = $request->input('rideId');
|
||||
$passengerLocation = $request->input('passengerLocation');
|
||||
$passengerDestination = $request->input('passengerDestination');
|
||||
|
||||
if (empty($rideId) || empty($driverId) || empty($passengerLocation) || empty($passengerDestination)) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Missing required fields (rideId, driver_id, or locations)'
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
DB::connection('primary')
|
||||
->table('write_argument_after_applied_from_background')
|
||||
->insert([
|
||||
'ride_id' => $rideId,
|
||||
'driver_id' => $driverId,
|
||||
'passenger_id' => $request->input('passengerId'),
|
||||
'passenger_location' => $passengerLocation,
|
||||
'passenger_destination' => $passengerDestination,
|
||||
'duration' => (int) $request->input('Duration', 0),
|
||||
'duration_to_passenger' => (int) $request->input('DurationToPassenger', 0),
|
||||
'duration_of_ride' => (int) $request->input('durationOfRideValue', 0),
|
||||
'distance' => (float) $request->input('Distance', 0),
|
||||
'total_cost' => (float) $request->input('totalCost', 0),
|
||||
'payment_amount' => (float) $request->input('paymentAmount', 0),
|
||||
'payment_method' => $request->input('paymentMethod'),
|
||||
'wallet_checked' => $request->input('WalletChecked') === 'true' ? 1 : 0,
|
||||
'has_steps' => !empty($request->input('isHaveSteps')) ? 1 : 0,
|
||||
'step0' => $request->input('step0'),
|
||||
'step1' => $request->input('step1'),
|
||||
'step2' => $request->input('step2'),
|
||||
'step3' => $request->input('step3'),
|
||||
'step4' => $request->input('step4'),
|
||||
'passenger_wallet_burc' => (float) $request->input('passengerWalletBurc', 0),
|
||||
'token_passenger' => $request->input('tokenPassenger'),
|
||||
'name' => $request->input('name'),
|
||||
'phone' => $request->input('phone'),
|
||||
'email' => $request->input('email'),
|
||||
'start_name_location' => $request->input('startNameLocation'),
|
||||
'end_name_location' => $request->input('endNameLocation'),
|
||||
'car_type' => $request->input('carType'),
|
||||
'kazan' => (float) $request->input('kazan', 0),
|
||||
'direction_url' => $request->input('direction'),
|
||||
'time_of_order' => $request->input('timeOfOrder', now()),
|
||||
'total_passenger' => (int) $request->input('totalPassenger', 0),
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Background args saved']);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('OverlayController Error: ' . $e->getMessage());
|
||||
return response()->json(['status' => 'failure', 'message' => 'Database error']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /v2/overlay/background-args
|
||||
* حذف بيانات الرحلة المقبولة بعد استلامها
|
||||
*/
|
||||
public function deleteBackgroundArgs(Request $request): JsonResponse
|
||||
{
|
||||
$driverId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
DB::connection('primary')
|
||||
->table('write_argument_after_applied_from_background')
|
||||
->where('driver_id', $driverId)
|
||||
->delete();
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Background args deleted']);
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class ProfileController extends Controller
|
||||
*/
|
||||
public function passenger(Request $request): JsonResponse
|
||||
{
|
||||
$id = $request->input('_jwt_user_id');
|
||||
$id = $request->attributes->get('_jwt_user_id');
|
||||
$passenger = Passenger::active()->find($id);
|
||||
|
||||
if (!$passenger) {
|
||||
@@ -46,17 +46,15 @@ class ProfileController extends Controller
|
||||
$data = $this->enc->decryptFields($data, Passenger::ENCRYPTED_FIELDS);
|
||||
unset($data['password'], $data['api_secret']);
|
||||
|
||||
// Attach wallet balance
|
||||
$wallet = DB::connection('primary')->table('passengerWallet')
|
||||
->where('passenger_id', $id)->first();
|
||||
$data['wallet_balance'] = $wallet->balance ?? '0.00';
|
||||
// Note: Wallet balance is managed by the dedicated payment server.
|
||||
// Flutter fetches it directly via the wallet JWT token.
|
||||
|
||||
// Attach rating
|
||||
$rating = DB::connection('primary')->table('ratingPassenger')
|
||||
->where('passenger_id', $id)->avg('rating');
|
||||
$data['rating'] = round($rating ?? 5.0, 2);
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => $data]);
|
||||
return response()->json(['status' => 'success', 'message' => $data]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,7 +62,7 @@ class ProfileController extends Controller
|
||||
*/
|
||||
public function driver(Request $request): JsonResponse
|
||||
{
|
||||
$id = $request->input('_jwt_user_id');
|
||||
$id = $request->attributes->get('_jwt_user_id');
|
||||
$driver = Driver::active()->byId($id)->first();
|
||||
|
||||
if (!$driver) {
|
||||
@@ -89,11 +87,14 @@ class ProfileController extends Controller
|
||||
// Rating
|
||||
$data['rating'] = $driver->getAverageRating();
|
||||
|
||||
// Note: Wallet balance is managed by the dedicated payment server.
|
||||
// Flutter fetches it directly via the wallet JWT token.
|
||||
|
||||
// Ride count
|
||||
$data['ride_count'] = DB::connection('ride')->table('ride')
|
||||
->where('driver_id', $id)->where('status', 'finish')->count();
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => $data]);
|
||||
return response()->json(['status' => 'success', 'message' => $data]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,7 +102,7 @@ class ProfileController extends Controller
|
||||
*/
|
||||
public function updatePassenger(Request $request): JsonResponse
|
||||
{
|
||||
$id = $request->input('_jwt_user_id');
|
||||
$id = $request->attributes->get('_jwt_user_id');
|
||||
$passenger = Passenger::active()->find($id);
|
||||
|
||||
if (!$passenger) {
|
||||
@@ -109,7 +110,10 @@ class ProfileController extends Controller
|
||||
}
|
||||
|
||||
$updates = [];
|
||||
$encryptableFields = ['first_name', 'last_name', 'gender', 'birthdate', 'sosPhone'];
|
||||
$encryptableFields = [
|
||||
'first_name', 'last_name', 'gender', 'birthdate', 'sosPhone',
|
||||
'site', 'education', 'employmentType', 'maritalStatus'
|
||||
];
|
||||
|
||||
foreach ($encryptableFields as $field) {
|
||||
if ($request->has($field)) {
|
||||
@@ -117,12 +121,6 @@ class ProfileController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$plainFields = ['education', 'employmentType', 'maritalStatus', 'site'];
|
||||
foreach ($plainFields as $field) {
|
||||
if ($request->has($field)) {
|
||||
$updates[$field] = $request->input($field);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($updates)) {
|
||||
$passenger->update($updates);
|
||||
@@ -138,7 +136,7 @@ class ProfileController extends Controller
|
||||
{
|
||||
$request->validate(['email' => 'required|email']);
|
||||
|
||||
$id = $request->input('_jwt_user_id');
|
||||
$id = $request->attributes->get('_jwt_user_id');
|
||||
$driver = Driver::active()->byId($id)->first();
|
||||
|
||||
if (!$driver) {
|
||||
@@ -151,4 +149,64 @@ class ProfileController extends Controller
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Email updated']);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/profile/driver/shamcash
|
||||
*/
|
||||
public function updateShamCash(Request $request): JsonResponse
|
||||
{
|
||||
$id = $request->attributes->get('_jwt_user_id');
|
||||
$driver = Driver::active()->byId($id)->first();
|
||||
|
||||
if (!$driver) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Not found'], 404);
|
||||
}
|
||||
|
||||
$accountBank = $request->input('accountBank') ?? $request->input('accountNumber');
|
||||
$bankCode = $request->input('bankCode') ?? $request->input('paymentProvider');
|
||||
|
||||
if (!$accountBank || !$bankCode) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Missing fields'], 400);
|
||||
}
|
||||
|
||||
$driver->update([
|
||||
'accountBank' => $this->enc->encrypt($accountBank),
|
||||
'bankCode' => $bankCode,
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Sham Cash details updated']);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/profile/driver/car
|
||||
*/
|
||||
public function updateDriverCar(Request $request): JsonResponse
|
||||
{
|
||||
$id = $request->attributes->get('_jwt_user_id');
|
||||
$car = CarRegistration::where('driverID', $id)->where('isDefault', 1)->first();
|
||||
|
||||
if (!$car) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Car not found'], 404);
|
||||
}
|
||||
|
||||
$fields = ['make', 'model', 'year', 'color', 'color_hex', 'expiration_date', 'vin', 'car_plate'];
|
||||
$updates = [];
|
||||
|
||||
foreach ($fields as $f) {
|
||||
if ($request->has($f)) {
|
||||
$val = $request->input($f);
|
||||
if (in_array($f, CarRegistration::ENCRYPTED_FIELDS)) {
|
||||
$updates[$f] = $this->enc->encrypt($val);
|
||||
} else {
|
||||
$updates[$f] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($updates)) {
|
||||
$car->update($updates);
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Vehicle details updated']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,18 +21,20 @@ class PromoController extends Controller
|
||||
/** GET /v2/promos */
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$passengerId = $request->input('_jwt_user_id');
|
||||
$passengerId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
$promos = DB::connection('primary')->table('promos')
|
||||
->where('passengerID', $passengerId)
|
||||
->orWhere('passengerID', 'none')
|
||||
->where(function ($q) use ($passengerId) {
|
||||
$q->where('passengerID', $passengerId)
|
||||
->orWhere('passengerID', 'none');
|
||||
})
|
||||
->where(function ($q) {
|
||||
$q->whereNull('validity_end_date')
|
||||
->orWhere('validity_end_date', '>=', now()->toDateString());
|
||||
})
|
||||
->get();
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => $promos]);
|
||||
return response()->json(['status' => 'success', 'message' => $promos]);
|
||||
}
|
||||
|
||||
/** GET /v2/promos/check?code=XXX */
|
||||
@@ -70,7 +72,7 @@ class PromoController extends Controller
|
||||
'amount' => 'required|string',
|
||||
]);
|
||||
|
||||
$passengerId = $request->input('_jwt_user_id');
|
||||
$passengerId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
$exists = DB::connection('primary')->table('promos')
|
||||
->where('passengerID', $passengerId)->exists();
|
||||
@@ -93,8 +95,11 @@ class PromoController extends Controller
|
||||
/** PUT /v2/promos/{id} */
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$passengerId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
DB::connection('primary')->table('promos')
|
||||
->where('id', $id)
|
||||
->where('passengerID', $passengerId)
|
||||
->update(array_filter([
|
||||
'promo_code' => $request->input('promo_code'),
|
||||
'amount' => $request->input('amount'),
|
||||
@@ -106,9 +111,13 @@ class PromoController extends Controller
|
||||
}
|
||||
|
||||
/** DELETE /v2/promos/{id} */
|
||||
public function destroy(int $id): JsonResponse
|
||||
public function destroy(Request $request, int $id): JsonResponse
|
||||
{
|
||||
DB::connection('primary')->table('promos')->where('id', $id)->delete();
|
||||
$passengerId = $request->attributes->get('_jwt_user_id');
|
||||
DB::connection('primary')->table('promos')
|
||||
->where('id', $id)
|
||||
->where('passengerID', $passengerId)
|
||||
->delete();
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class RatingController extends Controller
|
||||
'comment' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$passengerId = $request->input('_jwt_user_id');
|
||||
$passengerId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
// Prevent duplicate ratings
|
||||
$exists = DB::connection('primary')->table('ratingDriver')
|
||||
@@ -60,7 +60,7 @@ class RatingController extends Controller
|
||||
'comment' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$driverId = $request->input('_jwt_user_id');
|
||||
$driverId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
$exists = DB::connection('primary')->table('ratingPassenger')
|
||||
->where('rideId', $request->input('ride_id'))->exists();
|
||||
@@ -88,8 +88,8 @@ class RatingController extends Controller
|
||||
'comment' => 'nullable|string|max:300',
|
||||
]);
|
||||
|
||||
$userId = $request->input('_jwt_user_id');
|
||||
$userType = $request->input('_jwt_user_type');
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
$userType = $request->attributes->get('_jwt_user_type');
|
||||
|
||||
DB::connection('primary')->table('ratingApp')->insert([
|
||||
'name' => $request->input('name', ''),
|
||||
@@ -105,31 +105,53 @@ class RatingController extends Controller
|
||||
return response()->json(['status' => 'success'], 201);
|
||||
}
|
||||
|
||||
/** GET /v2/ratings/driver/{id} */
|
||||
public function driverRating(string $id): JsonResponse
|
||||
/** GET /v2/ratings/driver or /v2/ratings/driver/{id} */
|
||||
public function getDriverRating(Request $request, string $id = null): JsonResponse
|
||||
{
|
||||
$ratings = DB::connection('primary')->table('ratingDriver')
|
||||
->where('driver_id', $id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit(50)
|
||||
->get();
|
||||
$id = $id ?? $request->input('driver_id');
|
||||
if (!$id) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Driver ID required'], 400);
|
||||
}
|
||||
|
||||
$avg = DB::connection('primary')->table('ratingDriver')
|
||||
->where('driver_id', $id)->avg('rating');
|
||||
$summaryOnly = $request->input('summary_only', true);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
// Cache rating summary for 1 hour
|
||||
$cacheKey = "driver_rating_summary:{$id}";
|
||||
$summary = \Illuminate\Support\Facades\Cache::remember($cacheKey, 3600, function () use ($id) {
|
||||
$avg = DB::connection('primary')->table('ratingDriver')
|
||||
->where('driver_id', $id)->avg('rating');
|
||||
$count = DB::connection('primary')->table('ratingDriver')
|
||||
->where('driver_id', $id)->count();
|
||||
|
||||
return [
|
||||
'average' => round($avg ?? 5.0, 2),
|
||||
'count' => $ratings->count(),
|
||||
'ratings' => $ratings,
|
||||
],
|
||||
]);
|
||||
'count' => $count,
|
||||
];
|
||||
});
|
||||
|
||||
$response = [
|
||||
'status' => 'success',
|
||||
'message' => $summary,
|
||||
];
|
||||
|
||||
if (!$summaryOnly) {
|
||||
$response['message']['ratings'] = DB::connection('primary')->table('ratingDriver')
|
||||
->where('driver_id', $id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit(50)
|
||||
->get();
|
||||
}
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
|
||||
/** GET /v2/ratings/passenger/{id} */
|
||||
public function passengerRating(string $id): JsonResponse
|
||||
public function passengerRating(Request $request, string $id = null): JsonResponse
|
||||
{
|
||||
$id = $id ?? $request->input('passenger_id');
|
||||
if (!$id) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Passenger ID required'], 400);
|
||||
}
|
||||
$ratings = DB::connection('primary')->table('ratingPassenger')
|
||||
->where('passenger_id', $id)
|
||||
->orderBy('created_at', 'desc')
|
||||
@@ -152,35 +174,23 @@ class RatingController extends Controller
|
||||
/** GET /v2/ratings/app — Legacy GET support */
|
||||
public function getAppFeedback(Request $request): JsonResponse
|
||||
{
|
||||
$passengerId = $request->input('passengerId');
|
||||
|
||||
if (!$passengerId) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'passengerId is required']);
|
||||
}
|
||||
$passengerId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
$data = DB::connection('primary')->table('feedBack')
|
||||
->where('passengerId', $passengerId)
|
||||
->orderBy('datecreated', 'desc')
|
||||
->get();
|
||||
|
||||
if ($data->isEmpty()) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'No feedback found']);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => $data
|
||||
]);
|
||||
return response()->json(['status' => 'success', 'message' => $data]);
|
||||
}
|
||||
|
||||
/** POST /v2/ratings/app — Legacy POST support */
|
||||
public function storeAppFeedback(Request $request): JsonResponse
|
||||
{
|
||||
$passengerId = $request->input('passengerId');
|
||||
$passengerId = $request->attributes->get('_jwt_user_id');
|
||||
$feedBack = $request->input('feedBack');
|
||||
|
||||
if (!$passengerId || !$feedBack) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Missing parameters']);
|
||||
if (!$feedBack) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Missing feedback text']);
|
||||
}
|
||||
|
||||
// V1 Encrypts this data
|
||||
@@ -196,7 +206,8 @@ class RatingController extends Controller
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Feedback saved successfully']);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Database error: ' . $e->getMessage()]);
|
||||
\Log::error('RatingController Feedback Error: ' . $e->getMessage());
|
||||
return response()->json(['status' => 'failure', 'message' => 'An error occurred while saving feedback']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,25 +8,13 @@ use App\Models\DriverToken;
|
||||
use App\Models\PassengerToken;
|
||||
use App\Models\DriverOrder;
|
||||
use App\Models\CarLocation;
|
||||
use App\Helpers\LegacyEncryption;
|
||||
use App\Services\LegacyEncryption;
|
||||
use App\Services\FcmService;
|
||||
use App\Services\SocketService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* متحكم الرحلات (Ride Controller)
|
||||
*
|
||||
* الغرض من الملف:
|
||||
* إدارة دورة حياة الرحلة بالكامل؛ بدءاً من طلب الراكب للرحلة حتى وصوله ودفعه للأجرة.
|
||||
*
|
||||
* كيفية العمل:
|
||||
* 1. يستقبل طلبات الرحلات الجديدة ويحفظها في جدول (waitingRides).
|
||||
* 2. يسمح للسائقين بقبول الرحلات المتاحة وتحديث حالتهم.
|
||||
* 3. يدير حالات الرحلة المختلفة: (انتظار، قبول، وصول السائق، بدء الرحلة، انتهاء الرحلة).
|
||||
* 4. يرسل إشعارات فورية للركاب والسائقين عند أي تغيير في حالة الرحلة.
|
||||
*/
|
||||
class RideController extends Controller
|
||||
{
|
||||
private LegacyEncryption $encryption;
|
||||
@@ -40,12 +28,14 @@ class RideController extends Controller
|
||||
$this->socket = $socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/rides
|
||||
* Replaces: ride/rides/add_ride.php
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
// Alias support for legacy app versions
|
||||
if ($request->has('carType') && !$request->has('car_type')) {
|
||||
$request->merge(['car_type' => $request->input('carType')]);
|
||||
}
|
||||
|
||||
// 1. Validation (Adding new required fields for the 33-item array)
|
||||
$request->validate([
|
||||
'start_location' => 'required|string',
|
||||
'end_location' => 'required|string',
|
||||
@@ -55,23 +45,34 @@ class RideController extends Controller
|
||||
'price_for_driver' => 'nullable|numeric|min:0',
|
||||
'price_for_passenger' => 'nullable|numeric|min:0',
|
||||
'status' => 'nullable|string',
|
||||
// Socket specific fields
|
||||
'start_lat' => 'nullable|numeric',
|
||||
'start_lng' => 'nullable|numeric',
|
||||
'end_lat' => 'nullable|numeric',
|
||||
'end_lng' => 'nullable|numeric',
|
||||
|
||||
// Socket / Legacy array specific fields
|
||||
'passenger_name' => 'nullable|string',
|
||||
'passenger_phone' => 'nullable|string',
|
||||
'passenger_token' => 'nullable|string',
|
||||
'passenger_email' => 'nullable|string',
|
||||
'passenger_wallet' => 'nullable|string',
|
||||
'passenger_rating' => 'nullable|string',
|
||||
|
||||
'start_name' => 'nullable|string',
|
||||
'end_name' => 'nullable|string',
|
||||
'duration_text' => 'nullable|string',
|
||||
'passenger_rating' => 'nullable|numeric',
|
||||
'distance_text' => 'nullable|string',
|
||||
'is_wallet' => 'nullable|string',
|
||||
'has_steps' => 'nullable|string',
|
||||
|
||||
'step0' => 'nullable|string',
|
||||
'step1' => 'nullable|string',
|
||||
'step2' => 'nullable|string',
|
||||
'step3' => 'nullable|string',
|
||||
'step4' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$passengerId = $request->input('_jwt_user_id');
|
||||
$passengerId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
// Prevent duplicate active rides
|
||||
$activeRide = DB::connection('ride')->table('ride')
|
||||
->where('passenger_id', $passengerId)
|
||||
->whereIn('status', ['waiting', 'going_to_passenger', 'arrived', 'started'])
|
||||
->whereIn('status', ['waiting', 'going_to_passenger', 'arrived', 'started', 'Begin'])
|
||||
->first();
|
||||
|
||||
if ($activeRide) {
|
||||
@@ -81,7 +82,6 @@ class RideController extends Controller
|
||||
], 409);
|
||||
}
|
||||
|
||||
// Data array as expected by V1 Database
|
||||
$rideData = [
|
||||
'start_location' => $request->input('start_location'),
|
||||
'end_location' => $request->input('end_location'),
|
||||
@@ -90,7 +90,7 @@ class RideController extends Controller
|
||||
'endtime' => '00:00:00',
|
||||
'price' => $request->input('price'),
|
||||
'passenger_id' => $passengerId,
|
||||
'driver_id' => '0', // 0 in V1 instead of 'none'
|
||||
'driver_id' => '0',
|
||||
'status' => $request->input('status', 'waiting'),
|
||||
'carType' => $request->input('car_type', 'Speed'),
|
||||
'price_for_driver' => $request->input('price_for_driver', $request->input('price')),
|
||||
@@ -102,42 +102,103 @@ class RideController extends Controller
|
||||
DB::connection('ride')->beginTransaction();
|
||||
|
||||
try {
|
||||
// 1. Insert into Primary DB (Main Server)
|
||||
$insertedId = DB::connection('primary')->table('ride')->insertGetId($rideData);
|
||||
|
||||
// 2. Insert into Ride DB (Tracking Server)
|
||||
$rideData['id'] = $insertedId; // Keep IDs perfectly synced
|
||||
$rideData['id'] = $insertedId;
|
||||
DB::connection('ride')->table('ride')->insert($rideData);
|
||||
|
||||
DB::connection('primary')->commit();
|
||||
DB::connection('ride')->commit();
|
||||
|
||||
// 3. Broadcast to Marketplace (Location Socket)
|
||||
// Prepare Legacy Array [0..33]
|
||||
$partsStart = explode(',', $request->input('start_location'));
|
||||
$startLat = trim($partsStart[0] ?? "");
|
||||
$startLng = trim($partsStart[1] ?? "");
|
||||
|
||||
$partsEnd = explode(',', $request->input('end_location'));
|
||||
$endLat = trim($partsEnd[0] ?? "");
|
||||
$endLng = trim($partsEnd[1] ?? "");
|
||||
|
||||
$price = (float) $request->input('price');
|
||||
$priceForDriver = (float) $request->input('price_for_driver', $price);
|
||||
$kazan = $price - $priceForDriver;
|
||||
|
||||
// Sync with waitingRides table (Marketplace visibility)
|
||||
DB::connection('primary')->table('waitingRides')->insert([
|
||||
'id' => (string)$insertedId,
|
||||
'start_location' => $request->input('start_name', 'Pickup point'),
|
||||
'end_location' => $request->input('end_name', 'Destination'),
|
||||
'date' => $rideData['date'],
|
||||
'time' => $rideData['time'],
|
||||
'price' => $rideData['price'],
|
||||
'passenger_id' => $passengerId,
|
||||
'status' => 'waiting',
|
||||
'carType' => $rideData['carType'],
|
||||
'passengerRate' => $request->input('passenger_rating', '5.0'),
|
||||
'distance' => $rideData['distance'],
|
||||
'duration' => $request->input('duration_text', '0'),
|
||||
'start_lat' => $startLat,
|
||||
'start_lng' => $startLng,
|
||||
'end_lat' => $endLat,
|
||||
'end_lng' => $endLng,
|
||||
'payment_method' => $request->input('is_wallet', '0') == '1' ? 'wallet' : 'cash',
|
||||
'passenger_wallet' => $request->input('passenger_wallet', '0'),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$payloadTemplate = [];
|
||||
$payloadTemplate[0] = (string)$startLat;
|
||||
$payloadTemplate[1] = (string)$startLng;
|
||||
$payloadTemplate[2] = (string)number_format($price, 2, '.', '');
|
||||
$payloadTemplate[3] = (string)$endLat;
|
||||
$payloadTemplate[4] = (string)$endLng;
|
||||
$payloadTemplate[5] = (string)$request->input('distance_text', '');
|
||||
$payloadTemplate[6] = "";
|
||||
$payloadTemplate[7] = (string)$passengerId;
|
||||
$payloadTemplate[8] = (string)$request->input('passenger_name', '');
|
||||
$payloadTemplate[9] = (string)$request->input('passenger_token', '');
|
||||
$payloadTemplate[10] = (string)$request->input('passenger_phone', '');
|
||||
$payloadTemplate[11] = (string)$request->input('distance', '0');
|
||||
$payloadTemplate[12] = "1";
|
||||
$payloadTemplate[13] = (string)$request->input('is_wallet', '0');
|
||||
$payloadTemplate[14] = (string)$request->input('distance', '0');
|
||||
$payloadTemplate[15] = (string)$request->input('duration_text', '');
|
||||
$payloadTemplate[16] = (string)$insertedId;
|
||||
$payloadTemplate[17] = "";
|
||||
$payloadTemplate[18] = "";
|
||||
$payloadTemplate[19] = (string)$request->input('duration_text', '');
|
||||
$payloadTemplate[20] = $request->input('has_steps') ?: 'false';
|
||||
$payloadTemplate[21] = (string)$request->input('step0', '');
|
||||
$payloadTemplate[22] = (string)$request->input('step1', '');
|
||||
$payloadTemplate[23] = (string)$request->input('step2', '');
|
||||
$payloadTemplate[24] = (string)$request->input('step3', '');
|
||||
$payloadTemplate[25] = (string)$request->input('step4', '');
|
||||
$payloadTemplate[26] = (string)number_format($priceForDriver, 2, '.', '');
|
||||
$payloadTemplate[27] = (string)$request->input('passenger_wallet', '0');
|
||||
$payloadTemplate[28] = (string)$request->input('passenger_email', '');
|
||||
$payloadTemplate[29] = (string)$request->input('start_name', '');
|
||||
$payloadTemplate[30] = (string)$request->input('end_name', '');
|
||||
$payloadTemplate[31] = (string)$request->input('car_type', 'Speed');
|
||||
$payloadTemplate[32] = (string)number_format($kazan, 2, '.', '');
|
||||
$payloadTemplate[33] = (string)$request->input('passenger_rating', '5.0');
|
||||
|
||||
ksort($payloadTemplate);
|
||||
$payloadArray = array_values($payloadTemplate);
|
||||
|
||||
// Send to Market exactly like V1
|
||||
$this->socket->sendToLocationServer('market_new_ride', [
|
||||
'id' => (string) $insertedId,
|
||||
'start_lat' => $request->input('start_lat'),
|
||||
'start_lng' => $request->input('start_lng'),
|
||||
'price' => (string) $request->input('price'),
|
||||
'carType' => $request->input('car_type'),
|
||||
'startName' => $request->input('start_name', ''),
|
||||
'endName' => $request->input('end_name', ''),
|
||||
'distance' => (string) $request->input('distance'),
|
||||
'duration' => $request->input('duration_text', ''),
|
||||
'passengerRate' => (string) $request->input('passenger_rating', '5.0'),
|
||||
'payload' => $payloadArray
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
// Return exactly the inserted ID as success output (V1 App relies on this)
|
||||
'data' => $insertedId,
|
||||
], 200); // 200 instead of 201 to match V1 expectation
|
||||
'message' => $insertedId,
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::connection('primary')->rollBack();
|
||||
DB::connection('ride')->rollBack();
|
||||
|
||||
\Log::error('AddRide Critical Error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Failed to add ride',
|
||||
@@ -145,189 +206,216 @@ class RideController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* POST /v2/rides/{id}/accept
|
||||
* Replaces: ride/rides/acceptRide.php
|
||||
*/
|
||||
public function accept(Request $request, int $rideId): JsonResponse
|
||||
{
|
||||
$driverId = $request->input('_jwt_user_id');
|
||||
$driverId = $request->attributes->get('_jwt_user_id');
|
||||
$userType = $request->attributes->get('_jwt_user_type');
|
||||
$status = $request->input('status', 'Apply'); // Allow dynamic status but default to Apply
|
||||
|
||||
DB::connection('ride')->beginTransaction();
|
||||
try {
|
||||
// Lock the ride row to prevent race conditions
|
||||
$ride = Ride::lockForUpdate()->find($rideId);
|
||||
|
||||
if (!$ride || !in_array($ride->status, ['waiting', 'wait', 'Apply'])) {
|
||||
if (!$ride || !in_array($ride->status, ['waiting', 'wait'])) {
|
||||
DB::connection('ride')->rollBack();
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Ride not available',
|
||||
], 409);
|
||||
'message' => 'Ride not available (Already taken)',
|
||||
], 409); // Keep 409 for conflict but failure text from V1
|
||||
}
|
||||
|
||||
// Update ride status atomically
|
||||
// Remote DB Update
|
||||
$ride->update([
|
||||
'driver_id' => $driverId,
|
||||
'status' => 'Applied',
|
||||
'status' => $status,
|
||||
'rideTimeStart' => now(), // V1 acceptRide does this
|
||||
'DriverIsGoingToPassenger' => now(),
|
||||
]);
|
||||
|
||||
// Update driver order
|
||||
DriverOrder::where('order_id', (string) $rideId)
|
||||
->where('driver_id', $driverId)
|
||||
->update(['status' => 'accepted']);
|
||||
|
||||
// Remove from waiting rides
|
||||
DB::connection('primary')
|
||||
->table('waitingRides')
|
||||
->where('id', (string) $rideId)
|
||||
->update(['status' => 'Applied']);
|
||||
|
||||
// Sync to primary DB ride table
|
||||
// Local DB Update
|
||||
DB::connection('primary')
|
||||
->table('ride')
|
||||
->where('id', $rideId)
|
||||
->update([
|
||||
'driver_id' => $driverId,
|
||||
'status' => 'Applied',
|
||||
'status' => $status,
|
||||
'rideTimeStart' => now(),
|
||||
]);
|
||||
|
||||
// Remove from waiting rides (legacy)
|
||||
DB::connection('primary')
|
||||
->table('waitingRides')
|
||||
->where('id', (string) $rideId)
|
||||
->update(['status' => $status]);
|
||||
|
||||
// Driver Orders
|
||||
$checkOrder = DB::connection('primary')->table('driver_orders')->where('order_id', (string)$rideId)->first();
|
||||
if ($checkOrder) {
|
||||
DB::connection('primary')->table('driver_orders')->where('order_id', (string)$rideId)
|
||||
->update(['driver_id' => $driverId, 'status' => $status, 'created_at' => now()]);
|
||||
} else {
|
||||
DB::connection('primary')->table('driver_orders')
|
||||
->insert(['driver_id' => $driverId, 'order_id' => (string)$rideId, 'status' => $status, 'created_at' => now()]);
|
||||
}
|
||||
|
||||
DB::connection('ride')->commit();
|
||||
|
||||
// Notify passenger via FCM
|
||||
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
|
||||
if ($passengerToken) {
|
||||
$decryptedToken = $this->encryption->decrypt($passengerToken->token);
|
||||
$this->fcm->sendLocalizedToDevice(
|
||||
$decryptedToken,
|
||||
'ride_accepted_title',
|
||||
'ride_accepted_body',
|
||||
['ride_id' => (string) $rideId, 'status' => 'Applied'],
|
||||
'ride_accepted'
|
||||
);
|
||||
// Fetch Driver Info for Passenger
|
||||
$driverRaw = DB::connection('primary')
|
||||
->table('driver as d')
|
||||
->leftJoin('CarRegistration as c', 'c.driverID', '=', 'd.id')
|
||||
->leftJoin('driverToken as dt', 'dt.captain_id', '=', 'd.id')
|
||||
->leftJoin('ratingDriver as r', 'r.driver_id', '=', 'd.id')
|
||||
->select(
|
||||
'd.id as driver_id', 'd.first_name', 'd.last_name', 'd.gender', 'd.phone',
|
||||
'c.make', 'c.model', 'c.car_plate', 'c.year', 'c.color', 'c.color_hex',
|
||||
'dt.token', DB::raw('ROUND(AVG(r.rating), 2) as ratingDriver')
|
||||
)
|
||||
->where('d.id', $driverId)
|
||||
->groupBy('d.id', 'd.first_name', 'd.last_name', 'd.gender', 'd.phone', 'c.make', 'c.model', 'c.car_plate', 'c.year', 'c.color', 'c.color_hex', 'dt.token')
|
||||
->first();
|
||||
|
||||
$driverInfo = [];
|
||||
if ($driverRaw) {
|
||||
$fieldsToDecrypt = ['first_name', 'last_name', 'gender', 'phone', 'car_plate', 'token'];
|
||||
foreach ((array)$driverRaw as $key => $value) {
|
||||
if (in_array($key, $fieldsToDecrypt) && !empty($value)) {
|
||||
$driverInfo[$key] = $this->encryption->decrypt($value);
|
||||
} else {
|
||||
$driverInfo[$key] = $value;
|
||||
}
|
||||
}
|
||||
$driverInfo['driverName'] = trim(($driverInfo['first_name'] ?? '') . ' ' . ($driverInfo['last_name'] ?? ''));
|
||||
if (empty($driverInfo['ratingDriver'])) {
|
||||
$driverInfo['ratingDriver'] = "5.0";
|
||||
}
|
||||
}
|
||||
|
||||
// Notify passenger via socket
|
||||
$this->socket->notifyPassenger($ride->passenger_id, [
|
||||
'ride_id' => $rideId,
|
||||
'status' => 'Applied',
|
||||
'status' => 'accepted', // App uses 'accepted'
|
||||
'driver_id' => $driverId,
|
||||
'driver_info' => $driverInfo
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
// Notify passenger via FCM
|
||||
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
|
||||
if ($passengerToken) {
|
||||
$decryptedToken = $this->encryption->decrypt($passengerToken->token);
|
||||
$this->fcm->sendToDevice(
|
||||
$decryptedToken,
|
||||
"Ride Accepted 🚖",
|
||||
"Captain " . ($driverInfo['driverName'] ?? 'Driver') . " is coming to you.",
|
||||
['ride_id' => (string) $rideId, 'driver_info' => $driverInfo],
|
||||
'Accepted Ride'
|
||||
);
|
||||
}
|
||||
|
||||
// Cleanup Marketplace
|
||||
$this->socket->sendToLocationServer('ride_taken_event', [
|
||||
'ride_id' => $rideId,
|
||||
'taken_by_driver_id' => $driverId
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => $driverInfo
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::connection('ride')->rollBack();
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Failed to accept ride',
|
||||
], 500);
|
||||
return response()->json(['status' => 'failure', 'message' => 'Error: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/rides/{id}/start
|
||||
* Replaces: ride/rides/start_ride.php
|
||||
*/
|
||||
public function start(Request $request, int $rideId): JsonResponse
|
||||
{
|
||||
$driverId = $request->input('_jwt_user_id');
|
||||
|
||||
$ride = Ride::where('id', $rideId)
|
||||
->where('driver_id', $driverId)
|
||||
->whereIn('status', ['Applied', 'Arrived'])
|
||||
->first();
|
||||
|
||||
if (!$ride) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Ride not found or not ready'], 404);
|
||||
}
|
||||
|
||||
$ride->update([
|
||||
'status' => 'Begin',
|
||||
'rideTimeStart' => now(),
|
||||
]);
|
||||
|
||||
// Sync to primary
|
||||
DB::connection('primary')->table('ride')
|
||||
->where('id', $rideId)
|
||||
->update(['status' => 'Begin', 'rideTimeStart' => now()]);
|
||||
|
||||
// Notify passenger
|
||||
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
|
||||
if ($passengerToken) {
|
||||
$this->fcm->sendLocalizedToDevice(
|
||||
$this->encryption->decrypt($passengerToken->token),
|
||||
'ride_started_title',
|
||||
'ride_started_body',
|
||||
['ride_id' => (string) $rideId, 'status' => 'Begin'],
|
||||
'ride_started'
|
||||
);
|
||||
}
|
||||
|
||||
$this->socket->notifyPassenger($ride->passenger_id, [
|
||||
'ride_id' => $rideId,
|
||||
'status' => 'Begin',
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/rides/{id}/arrive
|
||||
* Replaces: ride/rides/arrive_ride.php
|
||||
*/
|
||||
public function arrive(Request $request, int $rideId): JsonResponse
|
||||
{
|
||||
$driverId = $request->input('_jwt_user_id');
|
||||
$driverId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
$ride = Ride::where('id', $rideId)
|
||||
->where('driver_id', $driverId)
|
||||
->where('status', 'Applied')
|
||||
->first();
|
||||
$ride = Ride::where('id', $rideId)->where('driver_id', $driverId)->whereIn('status', ['Apply', 'Applied'])->first();
|
||||
|
||||
if (!$ride) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
|
||||
}
|
||||
|
||||
$ride->update(['status' => 'Arrived']);
|
||||
$ride->update(['status' => 'arrived']); // Matching V1 lowercase
|
||||
|
||||
DB::connection('primary')->table('ride')
|
||||
->where('id', $rideId)->update(['status' => 'Arrived']);
|
||||
DB::connection('primary')->table('ride')->where('id', $rideId)->update(['status' => 'arrived', 'updated_at' => now()]);
|
||||
|
||||
// Socket
|
||||
$this->socket->notifyPassenger($ride->passenger_id, [
|
||||
'status' => 'arrived',
|
||||
'ride_id' => $rideId,
|
||||
'msg' => 'السائق وصل إلى موقعك 🚖'
|
||||
]);
|
||||
|
||||
// FCM
|
||||
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
|
||||
if ($passengerToken) {
|
||||
$this->fcm->sendLocalizedToDevice(
|
||||
$this->fcm->sendToDevice(
|
||||
$this->encryption->decrypt($passengerToken->token),
|
||||
'driver_arrived_title',
|
||||
'driver_arrived_body',
|
||||
['ride_id' => (string) $rideId, 'status' => 'Arrived'],
|
||||
'driver_arrived'
|
||||
"السائق وصل 📍",
|
||||
"الكابتن ينتظرك في الموقع المحدد.",
|
||||
['ride_id' => (string) $rideId],
|
||||
'Arrive Ride'
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
return response()->json(['status' => 'success', 'message' => 'Arrival notified successfully']);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/rides/{id}/finish
|
||||
* Replaces: ride/rides/finish_ride_updates.php
|
||||
*/
|
||||
public function finish(Request $request, int $rideId): JsonResponse
|
||||
public function start(Request $request, int $rideId): JsonResponse
|
||||
{
|
||||
$driverId = $request->input('_jwt_user_id');
|
||||
$driverId = $request->attributes->get('_jwt_user_id');
|
||||
$status = 'Begin';
|
||||
|
||||
$request->validate([
|
||||
'price_for_driver' => 'required|numeric',
|
||||
'price_for_passenger' => 'required|numeric',
|
||||
'distance' => 'required|numeric',
|
||||
$ride = Ride::where('id', $rideId)->where('driver_id', $driverId)->whereIn('status', ['Apply', 'Applied', 'arrived', 'Arrived'])->first();
|
||||
|
||||
if (!$ride) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Ride not found or not ready'], 404);
|
||||
}
|
||||
|
||||
$ride->update(['status' => $status, 'rideTimeStart' => now()]);
|
||||
DB::connection('primary')->table('ride')->where('id', $rideId)->update(['status' => $status, 'rideTimeStart' => now()]);
|
||||
|
||||
// Driver Orders
|
||||
$checkOrder = DB::connection('primary')->table('driver_orders')->where('order_id', (string)$rideId)->first();
|
||||
if ($checkOrder) {
|
||||
DB::connection('primary')->table('driver_orders')->where('order_id', (string)$rideId)->update(['driver_id' => $driverId, 'status' => $status, 'created_at' => now()]);
|
||||
} else {
|
||||
DB::connection('primary')->table('driver_orders')->insert(['driver_id' => $driverId, 'order_id' => (string)$rideId, 'status' => $status, 'created_at' => now()]);
|
||||
}
|
||||
|
||||
// Socket
|
||||
$this->socket->notifyPassenger($ride->passenger_id, [
|
||||
'ride_id' => $rideId,
|
||||
'status' => 'started',
|
||||
'msg' => 'بدأت الرحلة، نتمنى لك سلامة الوصول 🚀'
|
||||
]);
|
||||
|
||||
$ride = Ride::where('id', $rideId)
|
||||
->where('driver_id', $driverId)
|
||||
->where('status', 'Begin')
|
||||
->first();
|
||||
// FCM
|
||||
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
|
||||
if ($passengerToken) {
|
||||
$this->fcm->sendToDevice(
|
||||
$this->encryption->decrypt($passengerToken->token),
|
||||
"بدأت الرحلة 🏁",
|
||||
"نتمنى لك رحلة آمنة ومريحة.",
|
||||
['ride_id' => (string) $rideId],
|
||||
'Trip is Begin'
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Ride started successfully']);
|
||||
}
|
||||
|
||||
public function finish(Request $request, int $rideId): JsonResponse
|
||||
{
|
||||
$driverId = $request->attributes->get('_jwt_user_id');
|
||||
$price = $request->input('price', 0);
|
||||
$status = 'Finished';
|
||||
|
||||
$ride = Ride::where('id', $rideId)->where('driver_id', $driverId)->where('status', 'Begin')->first();
|
||||
|
||||
if (!$ride) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
|
||||
@@ -336,192 +424,193 @@ class RideController extends Controller
|
||||
DB::connection('ride')->beginTransaction();
|
||||
try {
|
||||
$ride->update([
|
||||
'status' => 'finish',
|
||||
'status' => $status,
|
||||
'rideTimeFinish' => now(),
|
||||
'endtime' => now()->toTimeString(),
|
||||
'price_for_driver' => $request->input('price_for_driver'),
|
||||
'price_for_passenger' => $request->input('price_for_passenger'),
|
||||
'distance' => $request->input('distance'),
|
||||
'price' => $price,
|
||||
]);
|
||||
|
||||
// Sync to primary
|
||||
DB::connection('primary')->table('ride')
|
||||
->where('id', $rideId)
|
||||
->update([
|
||||
'status' => 'finish',
|
||||
'rideTimeFinish' => now(),
|
||||
'price_for_driver' => $request->input('price_for_driver'),
|
||||
'price_for_passenger' => $request->input('price_for_passenger'),
|
||||
'distance' => $request->input('distance'),
|
||||
]);
|
||||
|
||||
// Create payment record
|
||||
DB::connection('primary')->table('payments')->insert([
|
||||
'id' => uniqid('pay_'),
|
||||
'amount' => $request->input('price_for_passenger'),
|
||||
'payment_method' => $ride->paymentMethod,
|
||||
'passengerID' => $ride->passenger_id,
|
||||
'rideId' => (string) $rideId,
|
||||
'driverID' => $driverId,
|
||||
DB::connection('primary')->table('ride')->where('id', $rideId)->update([
|
||||
'status' => $status,
|
||||
'rideTimeFinish' => now(),
|
||||
'price' => $price,
|
||||
]);
|
||||
|
||||
// Driver Orders
|
||||
$checkOrder = DB::connection('primary')->table('driver_orders')->where('order_id', (string)$rideId)->first();
|
||||
if ($checkOrder) {
|
||||
DB::connection('primary')->table('driver_orders')->where('order_id', (string)$rideId)->update(['driver_id' => $driverId, 'status' => $status, 'created_at' => now()]);
|
||||
} else {
|
||||
DB::connection('primary')->table('driver_orders')->insert(['driver_id' => $driverId, 'order_id' => (string)$rideId, 'status' => $status, 'created_at' => now()]);
|
||||
}
|
||||
|
||||
// Get Driver Token for LegacyList
|
||||
$driverTokenRaw = DB::connection('primary')->table('driverToken')->where('captain_id', $driverId)->value('token');
|
||||
$driverTokenDecrypted = $driverTokenRaw ? $this->encryption->decrypt($driverTokenRaw) : '';
|
||||
|
||||
$legacyList = [
|
||||
(string)$driverId,
|
||||
(string)$rideId,
|
||||
(string)$driverTokenDecrypted,
|
||||
(string)$price
|
||||
];
|
||||
|
||||
DB::connection('ride')->commit();
|
||||
|
||||
// Notify passenger
|
||||
// Socket
|
||||
$this->socket->notifyPassenger($ride->passenger_id, [
|
||||
'ride_id' => $rideId,
|
||||
'status' => 'finished',
|
||||
'price' => $price,
|
||||
'DriverList' => $legacyList
|
||||
]);
|
||||
|
||||
// FCM
|
||||
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
|
||||
if ($passengerToken) {
|
||||
$this->fcm->sendLocalizedToDevice(
|
||||
$this->fcm->sendToDevice(
|
||||
$this->encryption->decrypt($passengerToken->token),
|
||||
'ride_finished_title',
|
||||
'ride_finished_body',
|
||||
[
|
||||
'ride_id' => (string) $rideId,
|
||||
'status' => 'finish',
|
||||
'price' => (string) $request->input('price_for_passenger'),
|
||||
],
|
||||
'ride_finished'
|
||||
"تم إنهاء الرحلة 🏁",
|
||||
"المبلغ المطلوب: " . $price . " ل.س",
|
||||
['ride_id' => (string) $rideId, 'price' => (string) $price, 'DriverList' => $legacyList],
|
||||
'Driver Finish Trip'
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
return response()->json(['status' => 'success', 'message' => 'Ride finished successfully']);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::connection('ride')->rollBack();
|
||||
return response()->json(['status' => 'failure', 'message' => 'Failed to finish ride'], 500);
|
||||
return response()->json(['status' => 'failure', 'message' => 'DB Error: ' . $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/rides/{id}/cancel/passenger
|
||||
* Replaces: ride/rides/cancel_ride_by_passenger.php
|
||||
*/
|
||||
public function cancelByPassenger(Request $request, int $rideId): JsonResponse
|
||||
{
|
||||
$passengerId = $request->input('_jwt_user_id');
|
||||
$passengerId = $request->attributes->get('_jwt_user_id');
|
||||
$reason = $request->input('reason', 'No reason specified');
|
||||
|
||||
$ride = Ride::where('id', $rideId)
|
||||
->where('passenger_id', $passengerId)
|
||||
->active()
|
||||
->first();
|
||||
$ride = Ride::where('id', $rideId)->where('passenger_id', $passengerId)->active()->first();
|
||||
|
||||
if (!$ride) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
|
||||
}
|
||||
|
||||
$ride->update(['status' => 'CancelByPassenger']);
|
||||
if ($ride->status === 'Begin') {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Cannot cancel started ride'], 400);
|
||||
}
|
||||
|
||||
DB::connection('primary')->table('ride')
|
||||
->where('id', $rideId)->update(['status' => 'CancelByPassenger']);
|
||||
$driverId = $ride->driver_id;
|
||||
|
||||
DB::connection('primary')->table('waitingRides')
|
||||
->where('id', (string) $rideId)->update(['status' => 'CancelByPassenger']);
|
||||
$ride->update(['status' => 'cancelled_by_passenger']);
|
||||
DB::connection('primary')->table('ride')->where('id', $rideId)->update(['status' => 'cancelled_by_passenger', 'updated_at' => now()]);
|
||||
DB::connection('primary')->table('waitingRides')->where('id', (string) $rideId)->update(['status' => 'cancelled_by_passenger']);
|
||||
|
||||
// Driver Orders
|
||||
if ($driverId !== 'none' && $driverId != 0) {
|
||||
DB::connection('primary')->table('driver_orders')->where('order_id', (string)$rideId)->where('driver_id', $driverId)->update(['status' => 'cancelled_by_passenger', 'notes' => $reason]);
|
||||
}
|
||||
|
||||
// Log cancellation
|
||||
DB::connection('ride')->table('canecl')->insert([
|
||||
'driverID' => $ride->driver_id ?? 'none',
|
||||
'driverID' => $driverId ?? 'none',
|
||||
'passengerID' => $passengerId,
|
||||
'rideID' => (string) $rideId,
|
||||
'note' => $request->input('reason', 'No reason specified'),
|
||||
'note' => $reason,
|
||||
]);
|
||||
|
||||
// Notify driver if assigned
|
||||
if ($ride->driver_id !== 'none') {
|
||||
$driverToken = DriverToken::where('captain_id', $ride->driver_id)->first();
|
||||
if ($driverId !== 'none' && $driverId != 0) {
|
||||
// Socket
|
||||
$this->socket->sendToLocationServer('cancel_ride', [
|
||||
'driver_id' => $driverId,
|
||||
'ride_id' => $rideId,
|
||||
'reason' => $reason
|
||||
]);
|
||||
|
||||
// FCM
|
||||
$driverToken = DriverToken::where('captain_id', $driverId)->first();
|
||||
if ($driverToken) {
|
||||
$this->fcm->sendLocalizedToDevice(
|
||||
$this->fcm->sendToDevice(
|
||||
$this->encryption->decrypt($driverToken->token),
|
||||
'ride_cancelled_title',
|
||||
'ride_cancelled_body_passenger',
|
||||
['ride_id' => (string) $rideId, 'status' => 'CancelByPassenger'],
|
||||
'ride_cancelled'
|
||||
"إلغاء الرحلة 🚫",
|
||||
"قام الراكب بإلغاء الرحلة: $reason",
|
||||
['ride_id' => (string) $rideId, 'reason' => $reason],
|
||||
'Cancel Trip'
|
||||
);
|
||||
}
|
||||
|
||||
$this->socket->sendToLocationServer('cancel_ride', [
|
||||
'driver_id' => $ride->driver_id,
|
||||
'ride_id' => $rideId,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
return response()->json(['status' => 'success', 'message' => 'Ride cancelled successfully']);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/rides/{id}/cancel/driver
|
||||
* Replaces: ride/rides/cancel_ride_by_driver.php
|
||||
*/
|
||||
public function cancelByDriver(Request $request, int $rideId): JsonResponse
|
||||
{
|
||||
$driverId = $request->input('_jwt_user_id');
|
||||
$driverId = $request->attributes->get('_jwt_user_id');
|
||||
$reason = $request->input('reason', 'No reason specified');
|
||||
|
||||
$ride = Ride::where('id', $rideId)
|
||||
->where('driver_id', $driverId)
|
||||
->active()
|
||||
->first();
|
||||
$ride = Ride::where('id', $rideId)->where('driver_id', $driverId)->active()->first();
|
||||
|
||||
if (!$ride) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
|
||||
}
|
||||
|
||||
$ride->update(['status' => 'CancelByDriver', 'driver_id' => 'none']);
|
||||
$ride->update(['status' => 'cancelled_by_driver', 'driver_id' => 'none']);
|
||||
DB::connection('primary')->table('ride')->where('id', $rideId)->update(['status' => 'cancelled_by_driver']);
|
||||
DB::connection('primary')->table('waitingRides')->where('id', (string) $rideId)->update(['status' => 'waiting']);
|
||||
|
||||
DB::connection('primary')->table('ride')
|
||||
->where('id', $rideId)->update(['status' => 'CancelByDriver']);
|
||||
|
||||
// Re-add to waiting rides for re-search
|
||||
DB::connection('primary')->table('waitingRides')
|
||||
->where('id', (string) $rideId)->update(['status' => 'waiting']);
|
||||
// Driver Orders
|
||||
DB::connection('primary')->table('driver_orders')->where('order_id', (string)$rideId)->where('driver_id', $driverId)->update(['status' => 'cancelled_by_driver', 'notes' => $reason]);
|
||||
|
||||
// Log cancellation
|
||||
DB::connection('ride')->table('canecl')->insert([
|
||||
'driverID' => $driverId,
|
||||
'passengerID' => $ride->passenger_id,
|
||||
'rideID' => (string) $rideId,
|
||||
'note' => $request->input('reason', 'No reason specified'),
|
||||
'note' => $reason,
|
||||
]);
|
||||
|
||||
// Notify passenger via FCM
|
||||
// Socket to passenger
|
||||
$this->socket->notifyPassenger($ride->passenger_id, [
|
||||
'ride_id' => $rideId,
|
||||
'status' => 'cancelled_by_driver',
|
||||
'reason' => $reason
|
||||
]);
|
||||
|
||||
// FCM to passenger
|
||||
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
|
||||
if ($passengerToken) {
|
||||
$this->fcm->sendLocalizedToDevice(
|
||||
$this->fcm->sendToDevice(
|
||||
$this->encryption->decrypt($passengerToken->token),
|
||||
'ride_cancelled_title',
|
||||
'ride_cancelled_body_driver',
|
||||
['ride_id' => (string) $rideId, 'status' => 'CancelByDriver'],
|
||||
'ride_cancelled'
|
||||
"إلغاء الرحلة 🚫",
|
||||
"قام السائق بإلغاء الرحلة: $reason",
|
||||
['ride_id' => (string) $rideId, 'reason' => $reason],
|
||||
'Cancel Trip'
|
||||
);
|
||||
}
|
||||
|
||||
// Notify passenger via Socket (Faster than FCM)
|
||||
$this->socket->notifyPassenger($ride->passenger_id, [
|
||||
'ride_id' => $rideId,
|
||||
'status' => 'CancelByDriver',
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
return response()->json(['status' => 'success', 'message' => 'Ride cancelled successfully']);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v2/rides/{id}
|
||||
* Replaces: ride/rides/getRideOrderID.php
|
||||
*/
|
||||
public function show(int $rideId): JsonResponse
|
||||
public function show(Request $request, int $rideId): JsonResponse
|
||||
{
|
||||
$ride = Ride::find($rideId);
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
$ride = Ride::where('id', $rideId)
|
||||
->where(function($q) use ($userId) {
|
||||
$q->where('passenger_id', $userId)
|
||||
->orWhere('driver_id', $userId);
|
||||
})
|
||||
->first();
|
||||
|
||||
if (!$ride) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
|
||||
return response()->json(['status' => 'failure', 'message' => 'Ride not found or access denied'], 404);
|
||||
}
|
||||
return response()->json(['status' => 'success', 'data' => $ride]);
|
||||
return response()->json(['status' => 'success', 'message' => $ride]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v2/rides/active
|
||||
* Replaces: ride/rides/getRideStatusFromStartApp.php
|
||||
*/
|
||||
public function active(Request $request): JsonResponse
|
||||
{
|
||||
$userId = $request->input('_jwt_user_id');
|
||||
$userType = $request->input('_jwt_user_type');
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
$userType = $request->attributes->get('_jwt_user_type');
|
||||
|
||||
$query = Ride::active();
|
||||
if ($userType === 'driver') {
|
||||
@@ -534,18 +623,14 @@ class RideController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $ride,
|
||||
'message' => $ride,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v2/rides
|
||||
* Replaces: ride/rides/get.php
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$userId = $request->input('_jwt_user_id');
|
||||
$userType = $request->input('_jwt_user_type');
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
$userType = $request->attributes->get('_jwt_user_type');
|
||||
$page = $request->input('page', 1);
|
||||
$limit = min($request->input('limit', 20), 50);
|
||||
|
||||
@@ -556,11 +641,96 @@ class RideController extends Controller
|
||||
$query->forPassenger($userId);
|
||||
}
|
||||
|
||||
$rides = $query->orderBy('id', 'desc')
|
||||
$rides = $query->with('passenger')->orderBy('id', 'desc')
|
||||
->skip(($page - 1) * $limit)
|
||||
->take($limit)
|
||||
->get();
|
||||
->get()
|
||||
->map(function ($ride) {
|
||||
$ride->order_id = $ride->id;
|
||||
$ride->start_name = "Pickup point";
|
||||
$ride->end_name = "Destination point";
|
||||
$ride->price = (string) number_format($ride->price, 0, '.', '');
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => $rides]);
|
||||
if ($ride->passenger) {
|
||||
$p = $ride->passenger;
|
||||
$fname = !empty($p->first_name) ? $this->encryption->decrypt($p->first_name) : '';
|
||||
$lname = !empty($p->last_name) ? $this->encryption->decrypt($p->last_name) : '';
|
||||
$ride->passenger_name = trim($fname . ' ' . $lname);
|
||||
$ride->passenger_phone = !empty($p->phone) ? $this->encryption->decrypt($p->phone) : '';
|
||||
}
|
||||
|
||||
return $ride;
|
||||
});
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => $rides]);
|
||||
}
|
||||
public function availableRides(Request $request): JsonResponse
|
||||
{
|
||||
$lat = (float) $request->input('lat');
|
||||
$lng = (float) $request->input('lng');
|
||||
$radius = (float) $request->input('radius', 50); // km
|
||||
|
||||
$driverId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
// Get driver car type for hierarchical matching
|
||||
$driverCarType = DB::connection('primary')->table('driver as d')
|
||||
->leftJoin('CarRegistration as c', 'c.driverID', '=', 'd.id')
|
||||
->where('d.id', $driverId)
|
||||
->value('c.make') ?? 'Speed';
|
||||
|
||||
$rides = DB::connection('primary')->table('waitingRides as wr')
|
||||
->select([
|
||||
'wr.id', 'wr.start_location as startName', 'wr.end_location as endName',
|
||||
'wr.date', 'wr.time', 'wr.price', 'wr.passenger_id', 'wr.status', 'wr.carType',
|
||||
'wr.passengerRate', 'wr.created_at', 'wr.price_for_passenger',
|
||||
'wr.distance', 'wr.duration', 'wr.start_lat', 'wr.start_lng',
|
||||
'wr.end_lat', 'wr.end_lng', 'wr.payment_method', 'wr.passenger_wallet',
|
||||
'p.email', 'p.first_name', 'p.phone', 'p.id as passengerId', 't.token as passengerToken',
|
||||
DB::raw("( 6371 * acos( cos( radians($lat) ) * cos( radians( wr.start_lat ) ) * cos( radians( wr.start_lng ) - radians($lng) ) + sin( radians($lat) ) * sin( radians( wr.start_lat ) ) ) ) AS driver_distance_km")
|
||||
])
|
||||
->join('passengers as p', 'p.id', '=', 'wr.passenger_id')
|
||||
->leftJoin('tokens as t', 't.passengerID', '=', 'wr.passenger_id')
|
||||
->whereIn('wr.status', ['wait', 'waiting'])
|
||||
->where('wr.created_at', '>=', now()->subHours(24))
|
||||
->having('driver_distance_km', '<=', $radius)
|
||||
->orderBy('driver_distance_km')
|
||||
->get()
|
||||
->filter(function($ride) use ($driverCarType) {
|
||||
return $this->isCarTypeMatch($driverCarType, $ride->carType);
|
||||
})
|
||||
->map(function($ride) {
|
||||
$ride->first_name = !empty($ride->first_name) ? $this->encryption->decrypt($ride->first_name) : 'Passenger';
|
||||
$ride->phone = !empty($ride->phone) ? $this->encryption->decrypt($ride->phone) : '';
|
||||
$ride->email = !empty($ride->email) ? $this->encryption->decrypt($ride->email) : '';
|
||||
$ride->passengerToken = !empty($ride->passengerToken) ? $this->encryption->decrypt($ride->passengerToken) : '';
|
||||
|
||||
$ride->start_location = $ride->start_lat . ',' . $ride->start_lng;
|
||||
$ride->end_location = (!empty($ride->end_lat))
|
||||
? $ride->end_lat . ',' . $ride->end_lng
|
||||
: $ride->endName;
|
||||
|
||||
$ride->id = (string)$ride->id;
|
||||
$ride->driver_distance_km = number_format((float)$ride->driver_distance_km, 1);
|
||||
|
||||
return $ride;
|
||||
})
|
||||
->values();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => $rides
|
||||
]);
|
||||
}
|
||||
|
||||
private function isCarTypeMatch(string $driverType, ?string $rideType): bool
|
||||
{
|
||||
if (!$rideType) return true;
|
||||
|
||||
return match ($driverType) {
|
||||
'Comfort' => in_array($rideType, ['Speed', 'Comfort', 'Fixed Price']),
|
||||
'Lady' => in_array($rideType, ['Comfort', 'Speed', 'Lady']),
|
||||
'Speed', 'Scooter', 'Awfar Car' => $rideType === $driverType,
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
217
app/Http/Controllers/SupportController.php
Normal file
217
app/Http/Controllers/SupportController.php
Normal file
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Services\LegacyEncryption;
|
||||
|
||||
class SupportController extends Controller
|
||||
{
|
||||
protected LegacyEncryption $encryption;
|
||||
|
||||
public function __construct(LegacyEncryption $encryption)
|
||||
{
|
||||
$this->encryption = $encryption;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/support/complaints
|
||||
* يحلل الشكوى باستخدام الذكاء الاصطناعي (Gemini) ويحفظ النتيجة
|
||||
*/
|
||||
public function storeComplaint(Request $request): JsonResponse
|
||||
{
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
$request->validate([
|
||||
'ride_id' => 'required|string',
|
||||
'complaint_text' => 'required|string',
|
||||
'audio_link' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$rideId = $request->input('ride_id');
|
||||
$complaintText = $request->input('complaint_text');
|
||||
$audioLink = $request->input('audio_link');
|
||||
|
||||
// 1. جلب بيانات الرحلة
|
||||
$ride = DB::connection('ride')->table('ride')
|
||||
->where('id', $rideId)
|
||||
->first();
|
||||
|
||||
if (!$ride) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
|
||||
}
|
||||
|
||||
$passengerId = $ride->passenger_id;
|
||||
$driverId = $ride->driver_id;
|
||||
|
||||
// 2. جلب الملفات التعريفية والسلوك
|
||||
$passengerProfile = $this->getPassengerFullProfile($passengerId);
|
||||
$driverProfile = $this->getDriverFullProfile($driverId);
|
||||
$driverBehavior = DB::connection('tracking')->table('driver_behavior')
|
||||
->where('trip_id', $rideId)
|
||||
->where('driver_id', $driverId)
|
||||
->first();
|
||||
|
||||
// 3. بناء الـ Prompt لـ Gemini
|
||||
$prompt = $this->buildGeminiPrompt($ride, $passengerProfile, $driverProfile, $driverBehavior, $complaintText, $audioLink);
|
||||
|
||||
// 4. استدعاء Gemini API
|
||||
$apiKey = config('services.gemini.key') ?? env('GEMINI_API_KEY');
|
||||
if (!$apiKey) {
|
||||
Log::error('Gemini API Key missing in SupportController');
|
||||
// Fallback: save without AI if key is missing?
|
||||
// Better to fail if AI is expected.
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout(60)->post("https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key={$apiKey}", [
|
||||
'contents' => [
|
||||
['parts' => [['text' => $prompt]]]
|
||||
]
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new \Exception('Gemini API failed: ' . $response->body());
|
||||
}
|
||||
|
||||
$aiData = $response->json();
|
||||
$analysisRaw = $aiData['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
||||
$analysisJson = trim(preg_replace('/```json|```/', '', $analysisRaw));
|
||||
$analysis = json_decode($analysisJson, true);
|
||||
|
||||
if (!$analysis || !isset($analysis['passengerReport'])) {
|
||||
throw new \Exception('Failed to parse AI response: ' . $analysisRaw);
|
||||
}
|
||||
|
||||
// 5. حفظ الشكوى والتحليل
|
||||
$fullDescription = $complaintText . ($audioLink ? "\n\n[Audio Attached: {$audioLink}]" : "");
|
||||
|
||||
DB::connection('primary')->table('complaint')->insert([
|
||||
'ride_id' => $rideId,
|
||||
'passenger_id' => $passengerId,
|
||||
'driver_id' => $driverId,
|
||||
'complaint_type' => $analysis['complaint_type'] ?? 'General',
|
||||
'description' => $fullDescription,
|
||||
'statusComplaint' => 'Resolved',
|
||||
'resolution' => $analysisJson,
|
||||
'passenger_report' => json_encode($analysis['passengerReport'], JSON_UNESCAPED_UNICODE),
|
||||
'driver_report' => json_encode($analysis['driverReport'], JSON_UNESCAPED_UNICODE),
|
||||
'cs_solutions' => json_encode($analysis['customerServiceSolutions'], JSON_UNESCAPED_UNICODE),
|
||||
'fault_determination' => $analysis['fault_determination'] ?? 'N/A',
|
||||
'complaint_nature' => $analysis['complaint_nature'] ?? 'N/A',
|
||||
'date_filed' => now(),
|
||||
'date_resolved' => now(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Complaint processed successfully.',
|
||||
'passenger_response' => $analysis['passengerReport'],
|
||||
'driver_response' => $analysis['driverReport']
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('SupportController AI Error: ' . $e->getMessage());
|
||||
|
||||
// Fallback: Just save as pending if AI fails
|
||||
DB::connection('primary')->table('complaint')->insert([
|
||||
'ride_id' => $rideId,
|
||||
'passenger_id' => $passengerId,
|
||||
'driver_id' => $driverId,
|
||||
'complaint_type' => 'General',
|
||||
'description' => $complaintText . ($audioLink ? " [Audio: $audioLink]" : ""),
|
||||
'statusComplaint' => 'Open',
|
||||
'date_filed' => now(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Complaint received (Manual Review required).',
|
||||
'passenger_response' => ['title' => 'تم استلام شكواك', 'body' => 'جاري مراجعة طلبك من قبل فريق الدعم.'],
|
||||
'driver_response' => ['title' => 'بلاغ جديد', 'body' => 'تم تسجيل بلاغ بخصوص رحلتك الأخيرة.']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function getPassengerFullProfile($id)
|
||||
{
|
||||
$p = DB::connection('primary')->table('passengers')->where('id', $id)->first();
|
||||
if (!$p) return null;
|
||||
|
||||
return [
|
||||
'info' => [
|
||||
'id' => $p->id,
|
||||
'full_name' => trim($this->encryption->decrypt($p->first_name) . ' ' . $this->encryption->decrypt($p->last_name)),
|
||||
'created_at' => $p->created_at
|
||||
],
|
||||
'ratings' => DB::connection('primary')->table('ratingPassenger')->where('passenger_id', $id)
|
||||
->selectRaw('AVG(rating) as avg_rating, COUNT(id) as total_ratings')->first(),
|
||||
'comments' => DB::connection('primary')->table('ratingPassenger')->where('passenger_id', $id)
|
||||
->whereNotNull('comment')->orderBy('created_at', 'desc')->limit(5)->pluck('comment')
|
||||
];
|
||||
}
|
||||
|
||||
private function getDriverFullProfile($id)
|
||||
{
|
||||
$d = DB::connection('primary')->table('driver')->where('id', $id)->first();
|
||||
if (!$d) return null;
|
||||
|
||||
return [
|
||||
'info' => [
|
||||
'id' => $d->id,
|
||||
'full_name' => trim($this->encryption->decrypt($d->first_name) . ' ' . $this->encryption->decrypt($d->last_name)),
|
||||
'created_at' => $d->created_at
|
||||
],
|
||||
'ratings' => DB::connection('primary')->table('ratingDriver')->where('driver_id', $id)
|
||||
->selectRaw('AVG(rating) as avg_rating, COUNT(id) as total_ratings')->first(),
|
||||
'comments' => DB::connection('primary')->table('ratingDriver')->where('driver_id', $id)
|
||||
->whereNotNull('comment')->orderBy('created_at', 'desc')->limit(5)->pluck('comment')
|
||||
];
|
||||
}
|
||||
|
||||
private function buildGeminiPrompt($ride, $pProfile, $dProfile, $behavior, $text, $audio)
|
||||
{
|
||||
return "
|
||||
أنت خبير في حل النزاعات في خدمات نقل الركاب لتطبيق intaleqapp.com. قم بتحليل الشكوى التالية بين راكب وسائق بناءً على البيانات الشاملة التالية:
|
||||
|
||||
**1. تفاصيل الرحلة:**
|
||||
" . json_encode($ride, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "
|
||||
|
||||
**2. ملف الراكب:**
|
||||
" . json_encode($pProfile, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "
|
||||
|
||||
**3. ملف السائق:**
|
||||
" . json_encode($dProfile, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "
|
||||
|
||||
**4. بيانات سلوك السائق (في هذه الرحلة):**
|
||||
" . json_encode($behavior, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "
|
||||
|
||||
**5. الشكوى نفسها:**
|
||||
- نص الشكوى من الراكب: '" . $text . "'
|
||||
- رابط تسجيل صوتي للشكوى (إن وجد): " . $audio . "
|
||||
|
||||
**مهمتك هي:**
|
||||
1. تحليل جميع البيانات المتاحة لتحديد الطرف المخطئ على الأرجح.
|
||||
2. تحديد ما إذا كانت الشكوى كيدية أم حقيقية.
|
||||
3. **تصنيف الشكوى** (مثال: سلوك السائق، مشكلة في الأجرة، مسار الرحلة، حالة السيارة، أخرى).
|
||||
4. اقتراح حلين واضحين ومختلفين لفريق خدمة العملاء.
|
||||
5. كتابة تقرير موجز ومناسب للراكب.
|
||||
6. كتابة تقرير موجز ومناسب للسائق.
|
||||
|
||||
**الخرج المطلوب:**
|
||||
أعد الرد بصيغة JSON فقط، بدون أي نصوص إضافية، وباللغة العربية (لهجة مصرية)، بالهيكل التالي:
|
||||
{
|
||||
\"customerServiceSolutions\": [\"الحل المقترح الأول\", \"الحل المقترح الثاني\"],
|
||||
\"passengerReport\": { \"title\": \"بخصوص شكوتك في رحلة Intaleq\", \"body\": \"رسالة واضحة للراكب بنتيجة الشكوى\" },
|
||||
\"driverReport\": { \"title\": \"بخصوص بلاغ رحلتك الأخيرة في Intaleq\", \"body\": \"رسالة واضحة للسائق بنتيجة الشكوى\" },
|
||||
\"fault_determination\": \"الطرف المخطئ (الراكب/السائق/كلاهما/غير واضح)\",
|
||||
\"complaint_nature\": \"طبيعة الشكوى (حقيقية/كيدية/نزاع بسيط)\",
|
||||
\"complaint_type\": \"تصنيف الشكوى الذي حددته\"
|
||||
}
|
||||
";
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class TrackingController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'message' => [
|
||||
'latitude' => $location->latitude,
|
||||
'longitude' => $location->longitude,
|
||||
'heading' => $location->heading,
|
||||
@@ -88,7 +88,7 @@ class TrackingController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'message' => [
|
||||
'latitude' => $location->latitude ?? null,
|
||||
'longitude' => $location->longitude ?? null,
|
||||
'heading' => $location->heading ?? null,
|
||||
@@ -172,7 +172,7 @@ class TrackingController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $finalData
|
||||
'message' => $finalData
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ class TrackingController extends Controller
|
||||
/** GET /v2/tracking/captain-stats */
|
||||
public function captainStats(Request $request): JsonResponse
|
||||
{
|
||||
$driverId = $request->input('_jwt_user_id');
|
||||
$driverId = $request->attributes->get('_jwt_user_id');
|
||||
|
||||
$totalRides = DB::connection('ride')->table('ride')
|
||||
->where('driver_id', $driverId)
|
||||
@@ -222,7 +222,7 @@ class TrackingController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'message' => [
|
||||
'total_rides' => $totalRides,
|
||||
'today_rides' => $todayRides,
|
||||
'today_earnings' => round($todayEarnings, 2),
|
||||
|
||||
@@ -51,7 +51,7 @@ class UploadController extends Controller
|
||||
{
|
||||
$request->validate([
|
||||
'image' => 'required|file',
|
||||
'doc_type' => 'required|string|in:license,registration,criminal,id_front,id_back',
|
||||
'doc_type' => 'required|string|in:license,registration,criminal,id_front,id_back,driver_license_front,driver_license_back,car_license_front,car_license_back',
|
||||
]);
|
||||
|
||||
return $this->handleImageUpload($request, 'driver_documents', 'documents');
|
||||
@@ -86,7 +86,7 @@ class UploadController extends Controller
|
||||
return response()->json(['status' => 'failure', 'message' => 'File too large'], 400);
|
||||
}
|
||||
|
||||
$userId = $request->input('_jwt_user_id');
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
$ext = $file->getClientOriginalExtension() ?: 'mp3';
|
||||
$filename = 'audio_' . Str::random(24) . '.' . $ext;
|
||||
|
||||
@@ -130,7 +130,7 @@ class UploadController extends Controller
|
||||
}
|
||||
|
||||
// Generate safe filename
|
||||
$userId = $request->input('_jwt_user_id');
|
||||
$userId = $request->attributes->get('_jwt_user_id');
|
||||
$ext = match ($mime) {
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Wallet Controller
|
||||
* Replaces: ride/passenger/**
|
||||
* متحكم المحفظة (Wallet Controller)
|
||||
*
|
||||
* الغرض من الملف:
|
||||
* إدارة العمليات المالية للركاب، بما في ذلك عرض الرصيد، شحن المحفظة، وعرض سجل العمليات.
|
||||
*
|
||||
* كيفية العمل:
|
||||
* 1. يتواصل مع جداول (passengerWallet) و (payments) لجلب البيانات المالية.
|
||||
* 2. يسمح للركاب بإضافة أموال لمحفظتهم وتحديث رصيدهم.
|
||||
* 3. يعرض قائمة بالمعاملات المالية السابقة (Transactions).
|
||||
*/
|
||||
class WalletController extends Controller
|
||||
{
|
||||
/** GET /v2/wallet/passenger */
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$id = $request->input('_jwt_user_id');
|
||||
$wallet = DB::connection('primary')->table('passengerWallet')
|
||||
->where('passenger_id', $id)->first();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $wallet ?? ['passenger_id' => $id, 'balance' => '0.00'],
|
||||
]);
|
||||
}
|
||||
|
||||
/** GET /v2/wallet/passenger/balance */
|
||||
public function balance(Request $request): JsonResponse
|
||||
{
|
||||
$id = $request->input('_jwt_user_id');
|
||||
$bal = DB::connection('primary')->table('passengerWallet')
|
||||
->where('passenger_id', $id)->value('balance') ?? '0.00';
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => ['balance' => $bal]]);
|
||||
}
|
||||
|
||||
/** POST /v2/wallet/passenger */
|
||||
public function addFunds(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'amount' => 'required|numeric|min:0.01',
|
||||
'payment_method' => 'required|string',
|
||||
]);
|
||||
|
||||
$id = $request->input('_jwt_user_id');
|
||||
|
||||
DB::connection('primary')->beginTransaction();
|
||||
try {
|
||||
$wallet = DB::connection('primary')->table('passengerWallet')
|
||||
->where('passenger_id', $id)->lockForUpdate()->first();
|
||||
|
||||
if ($wallet) {
|
||||
DB::connection('primary')->table('passengerWallet')
|
||||
->where('passenger_id', $id)
|
||||
->increment('balance', $request->input('amount'));
|
||||
} else {
|
||||
DB::connection('primary')->table('passengerWallet')->insert([
|
||||
'passenger_id' => $id,
|
||||
'balance' => $request->input('amount'),
|
||||
]);
|
||||
}
|
||||
|
||||
// Record transaction
|
||||
DB::connection('primary')->table('passengerWalletTransactions')->insert([
|
||||
'passenger_id' => $id,
|
||||
'amount' => $request->input('amount'),
|
||||
'type' => 'credit',
|
||||
'payment_method' => $request->input('payment_method'),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
DB::connection('primary')->commit();
|
||||
|
||||
$newBalance = DB::connection('primary')->table('passengerWallet')
|
||||
->where('passenger_id', $id)->value('balance');
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => ['balance' => $newBalance],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::connection('primary')->rollBack();
|
||||
return response()->json(['status' => 'failure', 'message' => 'Transaction failed'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/** PUT /v2/wallet/passenger */
|
||||
public function update(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate(['balance' => 'required|numeric|min:0']);
|
||||
|
||||
$id = $request->input('_jwt_user_id');
|
||||
|
||||
DB::connection('primary')->table('passengerWallet')
|
||||
->where('passenger_id', $id)
|
||||
->update(['balance' => $request->input('balance')]);
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
/** DELETE /v2/wallet/passenger */
|
||||
public function destroy(Request $request): JsonResponse
|
||||
{
|
||||
$id = $request->input('_jwt_user_id');
|
||||
DB::connection('primary')->table('passengerWallet')
|
||||
->where('passenger_id', $id)->delete();
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
/** GET /v2/wallet/passenger/transactions */
|
||||
public function transactions(Request $request): JsonResponse
|
||||
{
|
||||
$id = $request->input('_jwt_user_id');
|
||||
$page = (int) $request->input('page', 1);
|
||||
$limit = min((int) $request->input('limit', 20), 50);
|
||||
|
||||
// Get from payments table (completed rides)
|
||||
$payments = DB::connection('primary')->table('payments')
|
||||
->where('passengerID', $id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->skip(($page - 1) * $limit)
|
||||
->take($limit)
|
||||
->get();
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => $payments]);
|
||||
}
|
||||
|
||||
/** POST /v2/wallet/passenger/token */
|
||||
public function addToken(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'token' => 'required|string',
|
||||
'amount' => 'required|numeric|min:0.01',
|
||||
]);
|
||||
|
||||
$id = $request->input('_jwt_user_id');
|
||||
|
||||
DB::connection('primary')->table('payment_tokens_passenger')->insert([
|
||||
'token' => $request->input('token'),
|
||||
'passengerId' => $id,
|
||||
'dateCreated' => now(),
|
||||
'amount' => $request->input('amount'),
|
||||
'isUsed' => 0,
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success'], 201);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class AdminMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$userType = $request->input('_jwt_user_type');
|
||||
$userType = $request->attributes->get('_jwt_user_type');
|
||||
|
||||
if ($userType !== 'admin') {
|
||||
return response()->json([
|
||||
|
||||
@@ -33,143 +33,75 @@ class HmacAuthMiddleware
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$apiKey = $request->header('X-API-Key');
|
||||
$timestamp = $request->header('X-Timestamp');
|
||||
$signature = $request->header('X-Signature');
|
||||
$timestamp = $request->header('X-Timestamp');
|
||||
$nonce = $request->header('X-Nonce');
|
||||
$apiKey = $request->header('X-API-Key');
|
||||
|
||||
// 1. Check required headers
|
||||
if (!$apiKey || !$timestamp || !$signature) {
|
||||
// All headers required
|
||||
if (!$signature || !$timestamp || !$nonce || !$apiKey) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Missing authentication headers'
|
||||
'message' => 'Missing security headers'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// 2. Validate timestamp (prevent replay attacks)
|
||||
$tolerance = (int) config('intaleq.hmac_tolerance', 300);
|
||||
$timeDiff = abs(time() - (int) $timestamp);
|
||||
|
||||
if ($timeDiff > $tolerance) {
|
||||
// Reject if timestamp older than tolerance (replay protection)
|
||||
$tolerance = config('intaleq.hmac_tolerance', 300);
|
||||
if (abs(time() - (int) $timestamp) > $tolerance) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Request expired'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// 3. Check nonce uniqueness
|
||||
if ($nonce) {
|
||||
$nonceKey = "nonce:{$nonce}";
|
||||
if (Cache::has($nonceKey)) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Duplicate request'], 401);
|
||||
}
|
||||
Cache::put($nonceKey, true, $tolerance * 2);
|
||||
// Nonce replay check (prevent duplicate requests)
|
||||
$nonceKey = 'nonce:' . $nonce;
|
||||
if (Cache::has($nonceKey)) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Duplicate request'
|
||||
], 401);
|
||||
}
|
||||
Cache::put($nonceKey, true, $tolerance);
|
||||
|
||||
// Lookup api_secret by api_key
|
||||
$user = DB::connection('primary')->table('passengers')
|
||||
->where('api_key', $apiKey)->first(['api_secret']);
|
||||
if (!$user) {
|
||||
$user = DB::connection('primary')->table('driver')
|
||||
->where('api_key', $apiKey)->first(['api_secret']);
|
||||
}
|
||||
|
||||
// 4. Lookup API secret from database
|
||||
$credentials = $this->getApiCredentials($apiKey);
|
||||
|
||||
if (!$credentials) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Invalid API key'], 401);
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Invalid API key'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// 5. Reconstruct and verify HMAC signature
|
||||
$payload = $request->getContent();
|
||||
$message = "{$timestamp}|{$apiKey}|{$payload}";
|
||||
$expectedSignature = hash_hmac(self::ALGORITHM, $message, $credentials->api_secret);
|
||||
// Compute expected signature
|
||||
$method = strtoupper($request->method());
|
||||
$path = $request->path(); // returns path without leading slash (e.g. v2/ride/create)
|
||||
|
||||
if (!hash_equals($expectedSignature, $signature)) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Invalid signature'], 401);
|
||||
if (str_contains(strtolower($request->header('Content-Type', '')), 'multipart/form-data')) {
|
||||
$inputs = $request->except(array_keys($request->allFiles()));
|
||||
ksort($inputs);
|
||||
$body = json_encode($inputs);
|
||||
} else {
|
||||
$body = $request->getContent();
|
||||
}
|
||||
|
||||
// 6. Optional: Auto-Decrypt Payload if it's encrypted
|
||||
// We assume if it's a non-JSON string, it might be encrypted
|
||||
if (!empty($payload) && !str_starts_with(trim($payload), '{')) {
|
||||
try {
|
||||
$this->crypto->setKeyFromSecret($credentials->api_secret);
|
||||
$decrypted = $this->crypto->decrypt($payload);
|
||||
$payload = "$method:$path:$timestamp:$nonce:$body";
|
||||
$expected = hash_hmac(self::ALGORITHM, $payload, $user->api_secret);
|
||||
|
||||
if ($decrypted) {
|
||||
// Replace request content with decrypted data
|
||||
$request->initialize(
|
||||
$request->query->all(),
|
||||
$request->request->all(),
|
||||
$request->attributes->all(),
|
||||
$request->cookies->all(),
|
||||
$request->files->all(),
|
||||
$request->server->all(),
|
||||
$decrypted
|
||||
);
|
||||
|
||||
// Also merge decrypted JSON into request data
|
||||
$jsonData = json_decode($decrypted, true);
|
||||
if (is_array($jsonData)) {
|
||||
$request->merge($jsonData);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// If decryption fails, we might still want to proceed if it wasn't meant to be encrypted
|
||||
// but usually, a failed signature check (above) would have caught tampering.
|
||||
}
|
||||
if (!hash_equals($expected, $signature)) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Invalid signature'
|
||||
], 401);
|
||||
}
|
||||
|
||||
// 7. Attach user info to request for controllers
|
||||
$request->merge([
|
||||
'_auth_user_id' => $credentials->user_id,
|
||||
'_auth_user_type' => $credentials->user_type,
|
||||
]);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API credentials with Redis caching (5 min)
|
||||
*/
|
||||
private function getApiCredentials(string $apiKey): ?object
|
||||
{
|
||||
$cacheKey = "api_cred:{$apiKey}";
|
||||
|
||||
return Cache::remember($cacheKey, 300, function () use ($apiKey) {
|
||||
// Check both driver and passenger tables for API keys
|
||||
$driver = DB::connection('primary')
|
||||
->table('driver')
|
||||
->select('id as user_id', 'api_secret')
|
||||
->selectRaw("'driver' as user_type")
|
||||
->where('api_key', $apiKey)
|
||||
->where('status', 'notDeleted')
|
||||
->first();
|
||||
|
||||
if ($driver) return $driver;
|
||||
|
||||
$passenger = DB::connection('primary')
|
||||
->table('passengers')
|
||||
->select('id as user_id', 'api_secret')
|
||||
->selectRaw("'passenger' as user_type")
|
||||
->where('api_key', $apiKey)
|
||||
->where('status', 'notDeleted')
|
||||
->first();
|
||||
|
||||
if ($passenger) return $passenger;
|
||||
|
||||
// Check admin users
|
||||
$admin = DB::connection('primary')
|
||||
->table('adminUser')
|
||||
->select('id as user_id', 'api_secret')
|
||||
->selectRaw("'admin' as user_type")
|
||||
->where('api_key', $apiKey)
|
||||
->first();
|
||||
|
||||
if ($admin) return $admin;
|
||||
|
||||
// Check service customers/employees (users table)
|
||||
$serviceUser = DB::connection('primary')
|
||||
->table('users')
|
||||
->select('id as user_id', 'api_secret')
|
||||
->selectRaw("'service_user' as user_type")
|
||||
->where('api_key', $apiKey)
|
||||
->first();
|
||||
|
||||
return $serviceUser;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,12 +38,19 @@ class JwtAuthMiddleware
|
||||
try {
|
||||
$decoded = JWT::decode($token, new Key(config('intaleq.jwt_secret'), 'HS256'));
|
||||
|
||||
// Attach JWT claims to request
|
||||
$request->merge([
|
||||
'_jwt_user_id' => $decoded->user_id ?? null,
|
||||
'_jwt_user_type' => $decoded->user_type ?? null,
|
||||
'_jwt_fingerprint' => $decoded->fingerprint ?? null,
|
||||
]);
|
||||
// Verify issuer (allow Tripz, Tripz-Wallet, Intaleq, or empty for compatibility)
|
||||
$iss = $decoded->iss ?? '';
|
||||
if (!empty($iss) && !in_array($iss, ['Tripz', 'Tripz-Wallet', 'Intaleq', 'Tripz-v2'])) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Invalid token issuer: ' . $iss
|
||||
], 401);
|
||||
}
|
||||
|
||||
// Attach JWT claims to request attributes (internal, not spoofable via POST/GET)
|
||||
$request->attributes->set('_jwt_user_id', $decoded->user_id ?? null);
|
||||
$request->attributes->set('_jwt_user_type', $decoded->user_type ?? null);
|
||||
$request->attributes->set('_jwt_fingerprint', $decoded->fingerprint ?? null);
|
||||
|
||||
return $next($request);
|
||||
|
||||
|
||||
@@ -19,5 +19,5 @@ class CarRegistration extends Model
|
||||
protected $table = 'CarRegistration';
|
||||
public $timestamps = false;
|
||||
protected $fillable = ['driverID', 'vin', 'car_plate', 'make', 'model', 'year', 'expiration_date', 'color', 'owner', 'color_hex', 'fuel', 'isDefault', 'status', 'vehicle_category_id', 'fuel_type_id'];
|
||||
public const ENCRYPTED_FIELDS = ['car_plate', 'owner'];
|
||||
public const ENCRYPTED_FIELDS = ['car_plate', 'owner', 'vin'];
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ class Driver extends Model
|
||||
public const ENCRYPTED_FIELDS = [
|
||||
'first_name', 'last_name', 'phone', 'gender', 'email',
|
||||
'national_number', 'name_arabic', 'address', 'birthdate',
|
||||
'accountBank',
|
||||
];
|
||||
|
||||
// ── Relationships ──
|
||||
@@ -94,7 +95,7 @@ class Driver extends Model
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'notDeleted');
|
||||
return $query->whereIn('status', ['notDeleted', 'active', 'actives']);
|
||||
}
|
||||
|
||||
public function scopeById($query, string $driverId)
|
||||
|
||||
@@ -39,6 +39,7 @@ class Passenger extends Model
|
||||
|
||||
public const ENCRYPTED_FIELDS = [
|
||||
'first_name', 'last_name', 'phone', 'gender', 'email', 'birthdate',
|
||||
'site', 'sosPhone', 'education', 'employmentType', 'maritalStatus',
|
||||
];
|
||||
|
||||
public function token()
|
||||
@@ -63,6 +64,6 @@ class Passenger extends Model
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'notDeleted');
|
||||
return $query->whereIn('status', ['notDeleted', 'active']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* نموذج محفظة الراكب (PassengerWallet Model)
|
||||
*
|
||||
* الغرض من الملف:
|
||||
* إدارة الرصيد المالي للركاب داخل التطبيق.
|
||||
*
|
||||
* كيفية العمل:
|
||||
* 1. يرتبط بجدول (passengerWallet) في قاعدة البيانات الأساسية.
|
||||
* 2. يتبع الرصيد الحالي للراكب ويسمح بعمليات الشحن أو الخصم.
|
||||
*/
|
||||
class PassengerWallet extends Model {
|
||||
protected $connection = 'primary';
|
||||
protected $table = 'passengerWallet';
|
||||
public $timestamps = true;
|
||||
const CREATED_AT = 'created_at';
|
||||
const UPDATED_AT = 'updated_at';
|
||||
protected $fillable = ['passenger_id', 'balance'];
|
||||
protected $casts = ['balance' => 'decimal:2'];
|
||||
}
|
||||
@@ -53,7 +53,8 @@ class Ride extends Model
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->whereIn('status', ['waiting', 'wait', 'Apply', 'Applied', 'Arrived', 'Begin']);
|
||||
return $query->whereIn('status', ['waiting', 'wait', 'Apply', 'Applied', 'Arrived', 'Begin'])
|
||||
->where('created_at', '>=', now()->subHours(1));
|
||||
}
|
||||
|
||||
public function scopeForPassenger($query, string $passengerId)
|
||||
|
||||
84
app/Services/LegacyEncryption.php
Normal file
84
app/Services/LegacyEncryption.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Exception;
|
||||
|
||||
class LegacyEncryption
|
||||
{
|
||||
private $key;
|
||||
private $iv;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$keyPath = config('intaleq.legacy_enc_key_path', '/home/intaleq-api/.enckey');
|
||||
|
||||
if (file_exists($keyPath)) {
|
||||
$this->key = trim(file_get_contents($keyPath));
|
||||
} else {
|
||||
$this->key = env('LEGACY_ENC_KEY', '');
|
||||
}
|
||||
|
||||
$this->iv = config('intaleq.legacy_iv', env('initializationVector', ''));
|
||||
if (strlen($this->iv) !== 16) {
|
||||
$this->iv = str_pad($this->iv, 16, "\0");
|
||||
}
|
||||
|
||||
if (strlen($this->key) !== 32) {
|
||||
// Log warning or throw error in production
|
||||
}
|
||||
if (strlen($this->iv) !== 16) {
|
||||
// Log warning
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt data using AES-256-CBC (Legacy V1 compatibility)
|
||||
*/
|
||||
public function encrypt($plainText)
|
||||
{
|
||||
if (empty($plainText)) return $plainText;
|
||||
|
||||
try {
|
||||
$plainText = (string) $plainText;
|
||||
$paddedText = $this->addPadding($plainText);
|
||||
$encrypted = openssl_encrypt($paddedText, 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA, $this->iv);
|
||||
return base64_encode($encrypted);
|
||||
} catch (Exception $e) {
|
||||
return $plainText;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt data using AES-256-CBC (Legacy V1 compatibility)
|
||||
*/
|
||||
public function decrypt($encryptedText)
|
||||
{
|
||||
if (empty($encryptedText)) return $encryptedText;
|
||||
|
||||
try {
|
||||
$decoded = base64_decode($encryptedText, true);
|
||||
if ($decoded === false) return $encryptedText;
|
||||
|
||||
$decrypted = openssl_decrypt($decoded, 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA, $this->iv);
|
||||
if ($decrypted === false) return $encryptedText;
|
||||
|
||||
return $this->removePadding($decrypted);
|
||||
} catch (Exception $e) {
|
||||
return $encryptedText;
|
||||
}
|
||||
}
|
||||
|
||||
private function addPadding($data, $blockSize = 16)
|
||||
{
|
||||
$pad = $blockSize - (strlen($data) % $blockSize);
|
||||
return $data . str_repeat(chr($pad), $pad);
|
||||
}
|
||||
|
||||
private function removePadding($data)
|
||||
{
|
||||
$pad = ord($data[strlen($data) - 1]);
|
||||
if ($pad < 1 || $pad > 16) return $data;
|
||||
return substr($data, 0, -$pad);
|
||||
}
|
||||
}
|
||||
30
app/Traits/ApiResponses.php
Normal file
30
app/Traits/ApiResponses.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
trait ApiResponses
|
||||
{
|
||||
/**
|
||||
* Return a success JSON response.
|
||||
*/
|
||||
protected function success(array $data, int $code = 200): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $data,
|
||||
], $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a failure JSON response.
|
||||
*/
|
||||
protected function failure(string $message, int $code = 401): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => $message,
|
||||
], $code);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
api: __DIR__ . '/../routes/api.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
@@ -20,33 +20,44 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
// Global API middleware
|
||||
$middleware->api(prepend: [
|
||||
\Illuminate\Http\Middleware\HandleCors::class,
|
||||
'throttle:120,1',
|
||||
]);
|
||||
|
||||
// Rate limiting for API
|
||||
$middleware->throttleWithRedis();
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
// Never expose internal errors to API consumers
|
||||
$exceptions->render(function (\Throwable $e) {
|
||||
if (request()->expectsJson() || request()->is('v2/*')) {
|
||||
$status = method_exists($e, 'getStatusCode') ? $e->getStatusCode() : 500;
|
||||
$exceptions->render(function (\Throwable $e, \Illuminate\Http\Request $request) {
|
||||
if ($request->is('api/*')) {
|
||||
\Log::error('API Exception: ' . get_class($e), [
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
]);
|
||||
|
||||
$status = 500;
|
||||
if ($e instanceof \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface) {
|
||||
$status = $e->getStatusCode();
|
||||
} elseif ($e instanceof \Illuminate\Validation\ValidationException) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Validation error',
|
||||
'errors' => $e->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$response = [
|
||||
'status' => 'failure',
|
||||
'message' => $status === 500 ? 'Internal server error' : $e->getMessage(),
|
||||
'message' => $e->getMessage() ?: 'Internal server error',
|
||||
];
|
||||
|
||||
// Only include debug info in non-production
|
||||
if (config('app.debug') && $status === 500) {
|
||||
$response['debug'] = [
|
||||
'exception' => get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile() . ':' . $e->getLine(),
|
||||
];
|
||||
if (config('app.debug')) {
|
||||
$response['exception'] = get_class($e);
|
||||
$response['file'] = $e->getFile();
|
||||
$response['line'] = $e->getLine();
|
||||
$response['trace'] = $e->getTrace();
|
||||
}
|
||||
|
||||
return response()->json($response, $status);
|
||||
}
|
||||
});
|
||||
})
|
||||
->create();
|
||||
->create()
|
||||
->useEnvironmentPath('/home/intaleq-api/env');
|
||||
|
||||
@@ -30,12 +30,12 @@ return [
|
||||
'port' => env('DB_PRIMARY_PORT', '3306'),
|
||||
'database' => env('DB_PRIMARY_NAME_V2', 'intaleqDBV2'),
|
||||
'username' => env('DB_PRIMARY_USER_V2', 'intaleqUserV2'),
|
||||
'password' => env('DB_PRIMARY_PASS_V2', 'cqdEGlg'),
|
||||
'password' => env('DB_PRIMARY_PASS_V2', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'prefix' => '',
|
||||
'strict' => false,
|
||||
'strict' => true,
|
||||
'engine' => 'InnoDB',
|
||||
'timezone' => '+03:00',
|
||||
'options' => extension_loaded('pdo_mysql') ? [
|
||||
@@ -49,19 +49,19 @@ return [
|
||||
| Ride Database (intaleq-ridesDB) — Ride Server
|
||||
| Tables: ride, driver, car_locations, driver_orders, etc.
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|*/
|
||||
'ride' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => env('DB_RIDE_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_RIDE_PORT', '3306'),
|
||||
'database' => env('DB_RIDE_DATABASE', 'intaleq-ridesDB'),
|
||||
'username' => env('DB_RIDE_USERNAME', 'root'),
|
||||
'password' => env('DB_RIDE_PASSWORD', ''),
|
||||
'database' => env('DB_RIDE_NAME', 'intaleq-ridesDB'),
|
||||
'username' => env('DB_RIDE_USER', 'root'),
|
||||
'password' => env('DB_RIDE_PASS', ''),
|
||||
'unix_socket' => env('DB_RIDE_SOCKET', ''),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'prefix' => '',
|
||||
'strict' => false,
|
||||
'strict' => true,
|
||||
'engine' => 'InnoDB',
|
||||
'timezone' => '+03:00',
|
||||
'options' => extension_loaded('pdo_mysql') ? [
|
||||
@@ -74,19 +74,19 @@ return [
|
||||
| Tracking Database (locationDB) — Location Server
|
||||
| Tables: car_locations, car_tracks, driver_daily_summary, driver_daily_work
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|*/
|
||||
'tracking' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => env('DB_TRACKING_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_TRACKING_PORT', '3306'),
|
||||
'database' => env('DB_TRACKING_DATABASE', 'locationDB'),
|
||||
'username' => env('DB_TRACKING_USERNAME', 'root'),
|
||||
'password' => env('DB_TRACKING_PASSWORD', ''),
|
||||
'database' => env('DB_TRACKING_NAME', 'locationDB'),
|
||||
'username' => env('DB_TRACKING_USER', 'root'),
|
||||
'password' => env('DB_TRACKING_PASS', ''),
|
||||
'unix_socket' => env('DB_TRACKING_SOCKET', ''),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_general_ci',
|
||||
'prefix' => '',
|
||||
'strict' => false,
|
||||
'strict' => true,
|
||||
'engine' => 'InnoDB',
|
||||
'timezone' => '+03:00',
|
||||
'options' => extension_loaded('pdo_mysql') ? [
|
||||
|
||||
@@ -13,12 +13,27 @@
|
||||
*/
|
||||
return [
|
||||
// JWT
|
||||
'jwt_secret' => env('JWT_SECRET'),
|
||||
'hmac_tolerance' => env('HMAC_TOLERANCE_SECONDS', 300),
|
||||
// JWT - قراءة المفتاح من الملف المذكور في صورتك
|
||||
'jwt_secret' => file_exists('/home/intaleq-api/.secret_key')
|
||||
? trim(file_get_contents('/home/intaleq-api/.secret_key'))
|
||||
: env('JWT_SECRET'),
|
||||
|
||||
'hmac_tolerance' => env('HMAC_TOLERANCE_SECONDS', 60),
|
||||
|
||||
// Encryption - قراءة مفتاح التشفير من الملف
|
||||
'legacy_enc_key_path' => '/home/intaleq-api/.enckey',
|
||||
|
||||
// IV - يقرأ من البيئة كما ذكرت
|
||||
'legacy_iv' => env('initializationVector', ''),
|
||||
|
||||
// Wallet Security - مفتاح الدفع
|
||||
'wallet_jwt_secret' => file_exists('/home/intaleq-api/.secret_key_pay')
|
||||
? trim(file_get_contents('/home/intaleq-api/.secret_key_pay'))
|
||||
: env('WALLET_JWT_SECRET'),
|
||||
|
||||
// Sockets - مفتاح السوكيت الداخلي
|
||||
'internal_socket_key_path' => '/home/intaleq-api/.internal_socket_key',
|
||||
|
||||
// Encryption
|
||||
'legacy_enc_key_path' => env('LEGACY_ENC_KEY_PATH', base_path('.enckey')),
|
||||
'legacy_iv' => env('LEGACY_IV', ''),
|
||||
|
||||
// FCM
|
||||
'fcm_credentials_path' => env('FCM_CREDENTIALS_PATH', base_path('firebase-credentials.json')),
|
||||
@@ -27,12 +42,12 @@ return [
|
||||
// Internal Services
|
||||
'location_server_url' => env('LOCATION_SERVER_URL', 'http://localhost:2021'),
|
||||
'ride_socket_url' => env('RIDE_SOCKET_URL', 'http://localhost:3031'),
|
||||
'internal_socket_key_path' => env('INTERNAL_SOCKET_KEY_PATH', base_path('.internal_socket_key')),
|
||||
// 'internal_socket_key_path' => env('INTERNAL_SOCKET_KEY_PATH', base_path('.internal_socket_key')),
|
||||
|
||||
// Rate Limiting
|
||||
'rate_limit_login' => (int) env('RATE_LIMIT_LOGIN', 5),
|
||||
'rate_limit_login' => (int) env('RATE_LIMIT_LOGIN', 3),
|
||||
'rate_limit_login_decay' => (int) env('RATE_LIMIT_LOGIN_DECAY', 60),
|
||||
'rate_limit_api' => (int) env('RATE_LIMIT_API', 60),
|
||||
'rate_limit_api' => (int) env('RATE_LIMIT_API', 120),
|
||||
'rate_limit_api_decay' => (int) env('RATE_LIMIT_API_DECAY', 60),
|
||||
|
||||
// Upload
|
||||
@@ -44,8 +59,17 @@ return [
|
||||
'secret_salt_parent' => env('SECRET_SALT_PARENT', ''),
|
||||
|
||||
// Wallet Security
|
||||
'wallet_jwt_secret' => env('WALLET_JWT_SECRET'),
|
||||
'wallet_hmac_secret' => env('WALLET_HMAC_SECRET'),
|
||||
'wallet_allowed_audiences' => explode(',', env('WALLET_ALLOWED_AUDIENCES', '')),
|
||||
// 'wallet_jwt_secret' => env('WALLET_JWT_SECRET'),
|
||||
'wallet_hmac_secret' => env('SECRET_KEY_HMAC'),
|
||||
'wallet_allowed_audiences' => [
|
||||
'Tripz-Wallet',
|
||||
'Tripz-Walletandroid',
|
||||
'Tripz-Walletios',
|
||||
'TripzWallet:android',
|
||||
'TripzWallet:ios',
|
||||
'allowedWallet1android',
|
||||
'allowedWallet1ios',
|
||||
],
|
||||
'wallet_app_password' => env('passwordnewpassenger', ''),
|
||||
'fp_pepper' => env('FP_PEPPER', ''),
|
||||
];
|
||||
|
||||
@@ -19,7 +19,6 @@ use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\RideController;
|
||||
use App\Http\Controllers\TrackingController;
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use App\Http\Controllers\WalletController;
|
||||
use App\Http\Controllers\RatingController;
|
||||
use App\Http\Controllers\PromoController;
|
||||
use App\Http\Controllers\OtpController;
|
||||
@@ -29,6 +28,10 @@ use App\Http\Controllers\NotificationController;
|
||||
use App\Http\Controllers\MiscController;
|
||||
use App\Http\Controllers\InviteController;
|
||||
use App\Http\Controllers\DriverDocController;
|
||||
use App\Http\Controllers\SupportController;
|
||||
use App\Http\Controllers\Api\PaymentTokenController;
|
||||
use App\Http\Controllers\OverlayController;
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -48,12 +51,15 @@ Route::prefix('v2/auth')->group(function () {
|
||||
// Passenger
|
||||
Route::post('/passenger/login', [AuthController::class, 'passengerLogin']);
|
||||
Route::post('/passenger/register', [AuthController::class, 'passengerRegister']);
|
||||
Route::post('/passenger/wallet-login', [AuthController::class, 'passengerWalletLogin']);
|
||||
// to be replaced by dedicated PaymentTokenController for better separation of concerns
|
||||
// Route::post('/passenger/wallet-login', [AuthController::class, 'passengerWalletLogin']);
|
||||
Route::get('/passenger/login-google', [AuthController::class, 'passengerLoginGoogle']);
|
||||
|
||||
// Driver
|
||||
Route::post('/driver/login', [AuthController::class, 'driverLogin']);
|
||||
Route::match(['get', 'post'], '/driver/login', [AuthController::class, 'driverLogin']);
|
||||
Route::post('/driver/register', [AuthController::class, 'driverRegister']);
|
||||
Route::post('/driver/wallet-login', [AuthController::class, 'driverWalletLogin']);
|
||||
Route::match(['get', 'post'], '/driver/login-google', [AuthController::class, 'driverLoginGoogle']);
|
||||
|
||||
// Admin & Service
|
||||
Route::post('/admin/login', [AuthController::class, 'adminLogin']);
|
||||
@@ -61,18 +67,31 @@ Route::prefix('v2/auth')->group(function () {
|
||||
// Silent JWT Handshake (Compatibility with V1 background flow)
|
||||
Route::post('/passenger/login-jwt', [AuthController::class, 'passengerJwtHandshake']);
|
||||
Route::post('/driver/login-jwt', [AuthController::class, 'driverJwtHandshake']);
|
||||
// Route::post('/driver/wallet-token', [AuthController::class, 'getWalletToken']);
|
||||
// to be replaced by dedicated PaymentTokenController for better separation of concerns
|
||||
});
|
||||
Route::prefix('v2/payment')->middleware(['hmac.auth', 'jwt.auth'])->group(function () {
|
||||
Route::post('/passenger/generate-token', [PaymentTokenController::class, 'generatePassengerToken']);
|
||||
Route::post('/driver/generate-token', [PaymentTokenController::class, 'generateDriverToken']);
|
||||
|
||||
// Notification Tokens (Common for both)
|
||||
Route::post('v2/notifications/token', [NotificationController::class, 'updateToken']);
|
||||
// يفضل إضافة مسار الإدارة داخل مجموعة الإدارة الموجودة مسبقاً أو حمايته بـ middleware إضافي
|
||||
Route::post('/admin/generate-token', [PaymentTokenController::class, 'generateAdminToken'])->middleware('admin');
|
||||
});
|
||||
// Admin Error Logging (public — accepts error reports from Flutter apps)
|
||||
Route::post('v2/admin/errors', [MiscController::class, 'logClientError']);
|
||||
|
||||
// OTP (public, but rate-limited)
|
||||
Route::prefix('v2/otp')->middleware('throttle:10,1')->group(function () {
|
||||
Route::post('/send', [OtpController::class, 'send']);
|
||||
Route::post('/verify', [OtpController::class, 'verify']);
|
||||
|
||||
// Dedicated Driver OTP Endpoints (matches V1 sendOtpMessageDriver.php)
|
||||
Route::post('/driver/send', [OtpController::class, 'sendDriver']);
|
||||
Route::post('/driver/verify', [OtpController::class, 'verifyDriver']);
|
||||
|
||||
Route::post('/email/send', [OtpController::class, 'sendEmail']);
|
||||
Route::post('/email/verify', [OtpController::class, 'verifyEmail']);
|
||||
Route::get('/check-phone', [OtpController::class, 'checkPhone']);
|
||||
Route::match(['get', 'post'], '/check-phone', [OtpController::class, 'checkPhone']);
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
@@ -83,7 +102,8 @@ Route::prefix('v2')->middleware(['hmac.auth', 'jwt.auth'])->group(function () {
|
||||
// ── Rides ──
|
||||
Route::post('/rides', [RideController::class, 'store']);
|
||||
Route::get('/rides', [RideController::class, 'index']);
|
||||
Route::get('/rides/active', [RideController::class, 'active']);
|
||||
Route::match(['get', 'post'], '/rides/active', [RideController::class, 'active']);
|
||||
Route::get('/rides/available', [RideController::class, 'availableRides']);
|
||||
Route::get('/rides/{id}', [RideController::class, 'show']);
|
||||
Route::post('/rides/{id}/accept', [RideController::class, 'accept']);
|
||||
Route::post('/rides/{id}/arrive', [RideController::class, 'arrive']);
|
||||
@@ -92,6 +112,7 @@ Route::prefix('v2')->middleware(['hmac.auth', 'jwt.auth'])->group(function () {
|
||||
Route::post('/rides/{id}/cancel/passenger', [RideController::class, 'cancelByPassenger']);
|
||||
Route::post('/rides/{id}/cancel/driver', [RideController::class, 'cancelByDriver']);
|
||||
Route::post('/rides/{id}/retry', [RideController::class, 'retrySearch']);
|
||||
|
||||
Route::put('/rides/{id}', [RideController::class, 'update']);
|
||||
|
||||
// ── Tracking ──
|
||||
@@ -102,23 +123,24 @@ Route::prefix('v2')->middleware(['hmac.auth', 'jwt.auth'])->group(function () {
|
||||
// ── Profile ──
|
||||
Route::get('/profile/passenger', [ProfileController::class, 'passenger']);
|
||||
Route::get('/profile/driver', [ProfileController::class, 'driver']);
|
||||
Route::put('/profile/passenger', [ProfileController::class, 'updatePassenger']);
|
||||
Route::put('/profile/driver/email', [ProfileController::class, 'updateDriverEmail']);
|
||||
Route::match(['post', 'put'], '/profile/passenger', [ProfileController::class, 'updatePassenger']);
|
||||
Route::match(['post', 'put'], '/profile/driver/email', [ProfileController::class, 'updateDriverEmail']);
|
||||
Route::post('/profile/driver/shamcash', [ProfileController::class, 'updateShamCash']);
|
||||
Route::match(['post', 'put'], '/profile/driver/car', [ProfileController::class, 'updateDriverCar']);
|
||||
|
||||
// ── Wallet ──
|
||||
Route::get('/wallet/passenger', [WalletController::class, 'index']);
|
||||
Route::get('/wallet/passenger/balance', [WalletController::class, 'balance']);
|
||||
Route::post('/wallet/passenger', [WalletController::class, 'addFunds']);
|
||||
Route::put('/wallet/passenger', [WalletController::class, 'update']);
|
||||
Route::get('/wallet/passenger/transactions', [WalletController::class, 'transactions']);
|
||||
Route::post('/wallet/passenger/token', [WalletController::class, 'addToken']);
|
||||
// All wallet operations (balance, funds, transactions) are handled by the
|
||||
// dedicated payment server. V2 only generates 60-second JWT tokens for it
|
||||
// via POST /v2/auth/passenger/wallet-login and /v2/auth/driver/wallet-login.
|
||||
|
||||
// ── Ratings ──
|
||||
Route::post('/ratings/driver', [RatingController::class, 'rateDriver']);
|
||||
Route::get('/ratings/driver', [RatingController::class, 'getDriverRating']);
|
||||
Route::get('/ratings/driver/{id}', [RatingController::class, 'getDriverRating']);
|
||||
Route::post('/ratings/passenger', [RatingController::class, 'ratePassenger']);
|
||||
Route::get('/ratings/app', [RatingController::class, 'getAppFeedback']);
|
||||
Route::post('/ratings/app', [RatingController::class, 'storeAppFeedback']);
|
||||
Route::get('/ratings/driver/{id}', [RatingController::class, 'driverRating']);
|
||||
Route::get('/ratings/passenger', [RatingController::class, 'passengerRating']);
|
||||
Route::get('/ratings/passenger/{id}', [RatingController::class, 'passengerRating']);
|
||||
|
||||
// ── Promos ──
|
||||
@@ -142,13 +164,17 @@ Route::prefix('v2')->middleware(['hmac.auth', 'jwt.auth'])->group(function () {
|
||||
|
||||
// ── Notifications ──
|
||||
Route::get('/notifications', [NotificationController::class, 'index']);
|
||||
Route::get('/notifications/driver', [NotificationController::class, 'index']);
|
||||
Route::post('/notifications/update', [NotificationController::class, 'updateNotification']);
|
||||
Route::get('/notifications/token', [NotificationController::class, 'getToken']);
|
||||
Route::put('/notifications/{id}/read', [NotificationController::class, 'markRead']);
|
||||
Route::post('/notifications/token', [NotificationController::class, 'updateToken']);
|
||||
Route::match(['put', 'post'], '/notifications/driver/read', [NotificationController::class, 'updateNotification']);
|
||||
Route::match(['put', 'post'], '/notifications/{id}/read', [NotificationController::class, 'markRead']);
|
||||
|
||||
// ── Misc ──
|
||||
Route::get('/misc/test', [MiscController::class, 'test']);
|
||||
Route::get('/misc/package-info', [MiscController::class, 'packageInfo']);
|
||||
Route::get('/misc/kazan-percent', [MiscController::class, 'getKazanPercent']);
|
||||
Route::match(['get', 'post'], '/misc/kazan-percent', [MiscController::class, 'getKazanPercent']);
|
||||
Route::get('/misc/help-center', [MiscController::class, 'getHelpCenter']);
|
||||
Route::post('/misc/help-center', [MiscController::class, 'storeHelpCenter']);
|
||||
Route::get('/misc/tips', [MiscController::class, 'getTips']);
|
||||
@@ -158,6 +184,7 @@ Route::prefix('v2')->middleware(['hmac.auth', 'jwt.auth'])->group(function () {
|
||||
Route::post('/misc/egypt-phones', [MiscController::class, 'saveEgyptPhones']);
|
||||
|
||||
// ── Invites ──
|
||||
Route::get('/invites/driver', [InviteController::class, 'index']);
|
||||
Route::post('/invites/driver', [InviteController::class, 'inviteDriver']);
|
||||
Route::post('/invites/passenger', [InviteController::class, 'invitePassenger']);
|
||||
Route::get('/invites/gift', [InviteController::class, 'checkGift']);
|
||||
@@ -166,6 +193,14 @@ Route::prefix('v2')->middleware(['hmac.auth', 'jwt.auth'])->group(function () {
|
||||
Route::get('/driver/registration-car', [DriverDocController::class, 'getCarReg']);
|
||||
Route::post('/driver/registration-car', [DriverDocController::class, 'storeCarReg']);
|
||||
Route::post('/driver/scams', [DriverDocController::class, 'reportScam']);
|
||||
|
||||
// ── Support ──
|
||||
Route::post('/support/complaints', [SupportController::class, 'storeComplaint']);
|
||||
|
||||
// ── Overlay / Background Args ──
|
||||
Route::get('/overlay/background-args', [OverlayController::class, 'getBackgroundArgs']);
|
||||
Route::post('/overlay/background-args', [OverlayController::class, 'storeBackgroundArgs']);
|
||||
Route::delete('/overlay/background-args', [OverlayController::class, 'deleteBackgroundArgs']);
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user