Initial V2 commit
This commit is contained in:
179
app/Http/Controllers/Admin/DriverManagementController.php
Normal file
179
app/Http/Controllers/Admin/DriverManagementController.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Helpers\LegacyEncryption;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Admin Driver Management Controller
|
||||
* Replaces: serviceapp/getDriverByPhone.php, getDriverByNational.php,
|
||||
* getDriversWaitingActive.php, getDriverDetailsForActivate.php,
|
||||
* updateDriverToActive.php, registerDriverAndCarService.php, etc.
|
||||
*/
|
||||
class DriverManagementController extends Controller
|
||||
{
|
||||
private LegacyEncryption $enc;
|
||||
|
||||
public function __construct(LegacyEncryption $enc) { $this->enc = $enc; }
|
||||
|
||||
/** GET /v2/admin/drivers?status=waiting&page=1 */
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$status = $request->input('status', 'notDeleted');
|
||||
$page = (int) $request->input('page', 1);
|
||||
$limit = min((int) $request->input('limit', 20), 100);
|
||||
|
||||
$drivers = DB::connection('ride')->table('driver')
|
||||
->where('status', $status)
|
||||
->orderBy('created_at', 'desc')
|
||||
->skip(($page - 1) * $limit)->take($limit)
|
||||
->get();
|
||||
|
||||
// Decrypt fields
|
||||
$drivers = $drivers->map(function ($d) {
|
||||
$arr = (array) $d;
|
||||
return $this->enc->decryptFields($arr, ['first_name', 'last_name', 'phone', 'email', 'national_number']);
|
||||
});
|
||||
|
||||
$total = DB::connection('ride')->table('driver')->where('status', $status)->count();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $drivers,
|
||||
'pagination' => ['page' => $page, 'limit' => $limit, 'total' => $total],
|
||||
]);
|
||||
}
|
||||
|
||||
/** GET /v2/admin/drivers/search?phone=XXX */
|
||||
public function search(Request $request): JsonResponse
|
||||
{
|
||||
$phone = $request->input('phone');
|
||||
$national = $request->input('national_number');
|
||||
|
||||
$query = DB::connection('ride')->table('driver');
|
||||
|
||||
if ($phone) {
|
||||
$encPhone = $this->enc->encrypt($phone);
|
||||
$query->where('phone', $encPhone);
|
||||
}
|
||||
if ($national) {
|
||||
$encNat = $this->enc->encrypt($national);
|
||||
$query->where('national_number', $encNat);
|
||||
}
|
||||
|
||||
$driver = $query->first();
|
||||
if (!$driver) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Driver not found'], 404);
|
||||
}
|
||||
|
||||
$data = $this->enc->decryptFields((array) $driver, ['first_name', 'last_name', 'phone', 'email', 'national_number', 'address']);
|
||||
unset($data['password'], $data['api_secret']);
|
||||
|
||||
// Attach car info
|
||||
$car = DB::connection('ride')->table('CarRegistration')
|
||||
->where('driverID', $driver->id)->where('isDefault', 1)->first();
|
||||
$data['car'] = $car ? $this->enc->decryptFields((array) $car, ['car_plate', 'owner']) : null;
|
||||
|
||||
// Attach documents
|
||||
$docs = DB::connection('ride')->table('driver_documents')
|
||||
->where('driverID', $driver->id)->get();
|
||||
$data['documents'] = $docs;
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => $data]);
|
||||
}
|
||||
|
||||
/** POST /v2/admin/drivers/{id}/activate */
|
||||
public function activate(Request $request, string $driverId): JsonResponse
|
||||
{
|
||||
DB::connection('ride')->table('driver')
|
||||
->where('id', $driverId)->update(['status' => 'notDeleted']);
|
||||
DB::connection('tracking')->table('driver')
|
||||
->where('id', $driverId)->update(['status' => 'notDeleted']);
|
||||
DB::connection('primary')->table('driver')
|
||||
->where('id', $driverId)->update(['status' => 'notDeleted']);
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Driver activated']);
|
||||
}
|
||||
|
||||
/** POST /v2/admin/drivers/{id}/deactivate */
|
||||
public function deactivate(Request $request, string $driverId): JsonResponse
|
||||
{
|
||||
$reason = $request->input('reason', 'Admin deactivation');
|
||||
|
||||
DB::connection('ride')->table('driver')
|
||||
->where('id', $driverId)->update(['status' => 'Deleted']);
|
||||
DB::connection('tracking')->table('driver')
|
||||
->where('id', $driverId)->update(['status' => 'Deleted']);
|
||||
|
||||
// Add to blacklist
|
||||
DB::connection('ride')->table('blacklist_driver')->insert([
|
||||
'driver_id' => $driverId,
|
||||
'phone' => '',
|
||||
'reason' => $reason,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Driver deactivated']);
|
||||
}
|
||||
|
||||
/** POST /v2/admin/drivers/{id}/add-car */
|
||||
public function addCar(Request $request, string $driverId): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'car_plate' => 'required|string',
|
||||
'make' => 'required|string',
|
||||
'model' => 'required|string',
|
||||
'year' => 'required|string',
|
||||
'color' => 'required|string',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'driverID' => $driverId,
|
||||
'vin' => $request->input('vin', ''),
|
||||
'car_plate' => $this->enc->encrypt($request->input('car_plate')),
|
||||
'make' => $request->input('make'),
|
||||
'model' => $request->input('model'),
|
||||
'year' => $request->input('year'),
|
||||
'expiration_date' => $request->input('expiration_date', ''),
|
||||
'color' => $request->input('color'),
|
||||
'owner' => $this->enc->encrypt($request->input('owner', '')),
|
||||
'color_hex' => $request->input('color_hex', ''),
|
||||
'fuel' => $request->input('fuel', ''),
|
||||
'isDefault' => 1,
|
||||
'created_at' => now(),
|
||||
'status' => 'yet',
|
||||
];
|
||||
|
||||
// Insert in all 3 databases
|
||||
DB::connection('ride')->table('CarRegistration')->insert($data);
|
||||
DB::connection('tracking')->table('CarRegistration')->insert($data);
|
||||
DB::connection('primary')->table('CarRegistration')->insert($data);
|
||||
|
||||
return response()->json(['status' => 'success'], 201);
|
||||
}
|
||||
|
||||
/** POST /v2/admin/drivers/{id}/notes */
|
||||
public function addNote(Request $request, string $driverId): JsonResponse
|
||||
{
|
||||
$request->validate(['note' => 'required|string|max:250']);
|
||||
|
||||
// Get driver phone
|
||||
$driver = DB::connection('ride')->table('driver')->where('id', $driverId)->first();
|
||||
$phone = $driver ? $this->enc->decrypt($driver->phone) : '';
|
||||
|
||||
DB::connection('primary')->table('notesForDriverService')->updateOrInsert(
|
||||
['phone' => $phone],
|
||||
[
|
||||
'note' => $request->input('note'),
|
||||
'editor' => $request->input('editor', 'admin'),
|
||||
'createdAt' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
}
|
||||
74
app/Http/Controllers/Admin/PassengerManagementController.php
Normal file
74
app/Http/Controllers/Admin/PassengerManagementController.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Passenger;
|
||||
use App\Helpers\LegacyEncryption;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Admin Passenger Management Controller
|
||||
*/
|
||||
class PassengerManagementController extends Controller
|
||||
{
|
||||
private LegacyEncryption $enc;
|
||||
|
||||
public function __construct(LegacyEncryption $enc)
|
||||
{
|
||||
$this->enc = $enc;
|
||||
}
|
||||
|
||||
/** GET /v2/admin/passengers */
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$status = $request->input('status', 'notDeleted');
|
||||
$page = (int) $request->input('page', 1);
|
||||
$limit = min((int) $request->input('limit', 20), 100);
|
||||
|
||||
$passengers = DB::connection('primary')->table('passengers')
|
||||
->where('status', $status)
|
||||
->orderBy('created_at', 'desc')
|
||||
->skip(($page - 1) * $limit)
|
||||
->take($limit)
|
||||
->get();
|
||||
|
||||
$passengers = $passengers->map(function ($p) {
|
||||
$arr = (array) $p;
|
||||
return $this->enc->decryptFields($arr, Passenger::ENCRYPTED_FIELDS);
|
||||
});
|
||||
|
||||
$total = DB::connection('primary')->table('passengers')->where('status', $status)->count();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $passengers,
|
||||
'pagination' => ['page' => $page, 'limit' => $limit, 'total' => $total],
|
||||
]);
|
||||
}
|
||||
|
||||
/** GET /v2/admin/passengers/search?phone=XXX */
|
||||
public function search(Request $request): JsonResponse
|
||||
{
|
||||
$phone = $request->input('phone');
|
||||
if (!$phone) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Phone required'], 400);
|
||||
}
|
||||
|
||||
$encPhone = $this->enc->encrypt($phone);
|
||||
$passenger = DB::connection('primary')->table('passengers')
|
||||
->where('phone', $encPhone)
|
||||
->first();
|
||||
|
||||
if (!$passenger) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Passenger not found'], 404);
|
||||
}
|
||||
|
||||
$data = $this->enc->decryptFields((array) $passenger, Passenger::ENCRYPTED_FIELDS);
|
||||
unset($data['password'], $data['api_secret']);
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => $data]);
|
||||
}
|
||||
}
|
||||
55
app/Http/Controllers/Admin/RideManagementController.php
Normal file
55
app/Http/Controllers/Admin/RideManagementController.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Admin Ride Management Controller
|
||||
*/
|
||||
class RideManagementController extends Controller
|
||||
{
|
||||
/** GET /v2/admin/rides */
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$status = $request->input('status');
|
||||
$page = (int) $request->input('page', 1);
|
||||
$limit = min((int) $request->input('limit', 20), 100);
|
||||
|
||||
$query = DB::connection('ride')->table('ride');
|
||||
|
||||
if ($status) {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
$rides = $query->orderBy('created_at', 'desc')
|
||||
->skip(($page - 1) * $limit)
|
||||
->take($limit)
|
||||
->get();
|
||||
|
||||
$total = $status
|
||||
? DB::connection('ride')->table('ride')->where('status', $status)->count()
|
||||
: DB::connection('ride')->table('ride')->count();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $rides,
|
||||
'pagination' => ['page' => $page, 'limit' => $limit, 'total' => $total],
|
||||
]);
|
||||
}
|
||||
|
||||
/** GET /v2/admin/rides/{id} */
|
||||
public function show(string $id): JsonResponse
|
||||
{
|
||||
$ride = DB::connection('ride')->table('ride')->where('id', $id)->first();
|
||||
|
||||
if (!$ride) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => $ride]);
|
||||
}
|
||||
}
|
||||
85
app/Http/Controllers/Admin/StatsController.php
Normal file
85
app/Http/Controllers/Admin/StatsController.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Admin Stats Controller
|
||||
* Replaces: serviceapp/getRidesStatic.php, getPassengersStatic.php,
|
||||
* getEmployeeStatic.php, getdriverstotalMonthly.php, getEditorStatsCalls.php
|
||||
*/
|
||||
class StatsController extends Controller
|
||||
{
|
||||
/** GET /v2/admin/stats/overview */
|
||||
public function overview(): JsonResponse
|
||||
{
|
||||
$totalDrivers = DB::connection('ride')->table('driver')->count();
|
||||
$activeDrivers = DB::connection('ride')->table('driver')->where('status', 'notDeleted')->count();
|
||||
$totalPassengers = DB::connection('primary')->table('passengers')->count();
|
||||
$activePassengers = DB::connection('primary')->table('passengers')->where('status', 'notDeleted')->count();
|
||||
$totalRides = DB::connection('ride')->table('ride')->count();
|
||||
$finishedRides = DB::connection('ride')->table('ride')->where('status', 'finish')->count();
|
||||
$todayRides = DB::connection('ride')->table('ride')
|
||||
->where('status', 'finish')->whereDate('rideTimeFinish', today())->count();
|
||||
$todayRevenue = DB::connection('ride')->table('ride')
|
||||
->where('status', 'finish')->whereDate('rideTimeFinish', today())
|
||||
->sum('price_for_passenger');
|
||||
$onlineDrivers = DB::connection('tracking')->table('car_locations')
|
||||
->where('status', 'on')->where('updated_at', '>', now()->subMinutes(10))->count();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'drivers' => ['total' => $totalDrivers, 'active' => $activeDrivers, 'online' => $onlineDrivers],
|
||||
'passengers' => ['total' => $totalPassengers, 'active' => $activePassengers],
|
||||
'rides' => ['total' => $totalRides, 'finished' => $finishedRides, 'today' => $todayRides],
|
||||
'revenue' => ['today' => round($todayRevenue, 2)],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/** GET /v2/admin/stats/rides?from=2026-01-01&to=2026-04-22 */
|
||||
public function rides(Request $request): JsonResponse
|
||||
{
|
||||
$from = $request->input('from', today()->subDays(30)->toDateString());
|
||||
$to = $request->input('to', today()->toDateString());
|
||||
|
||||
$daily = DB::connection('ride')->table('ride')
|
||||
->selectRaw("DATE(rideTimeFinish) as date, COUNT(*) as count, SUM(price_for_passenger) as revenue")
|
||||
->where('status', 'finish')
|
||||
->whereBetween('rideTimeFinish', [$from . ' 00:00:00', $to . ' 23:59:59'])
|
||||
->groupByRaw('DATE(rideTimeFinish)')
|
||||
->orderBy('date')
|
||||
->get();
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => $daily]);
|
||||
}
|
||||
|
||||
/** GET /v2/admin/stats/drivers-monthly */
|
||||
public function driversMonthly(): JsonResponse
|
||||
{
|
||||
$monthly = DB::connection('ride')->table('driver')
|
||||
->selectRaw("DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count")
|
||||
->groupByRaw("DATE_FORMAT(created_at, '%Y-%m')")
|
||||
->orderBy('month', 'desc')
|
||||
->limit(12)
|
||||
->get();
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => $monthly]);
|
||||
}
|
||||
|
||||
/** GET /v2/admin/stats/employees */
|
||||
public function employees(): JsonResponse
|
||||
{
|
||||
$employees = DB::connection('ride')->table('employee')
|
||||
->select('id', 'name', 'phone', 'status', 'created_at')
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => $employees]);
|
||||
}
|
||||
}
|
||||
465
app/Http/Controllers/AuthController.php
Normal file
465
app/Http/Controllers/AuthController.php
Normal file
@@ -0,0 +1,465 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Driver;
|
||||
use App\Models\Passenger;
|
||||
use App\Models\DriverToken;
|
||||
use App\Models\PassengerToken;
|
||||
use App\Helpers\LegacyEncryption;
|
||||
use Firebase\JWT\JWT;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Authentication Controller
|
||||
*
|
||||
* Unifies all V1 login flows:
|
||||
* login.php → passengerLogin
|
||||
* loginFirstTime.php → passengerRegister
|
||||
* loginJwtDriver.php → driverLogin
|
||||
* loginFirstTimeDriver.php → driverRegister
|
||||
* loginWallet.php → passengerWalletLogin
|
||||
* loginJwtWalletDriver.php → driverWalletLogin
|
||||
* loginAdmin.php → adminLogin
|
||||
*/
|
||||
class AuthController extends Controller
|
||||
{
|
||||
private LegacyEncryption $encryption;
|
||||
|
||||
public function __construct(LegacyEncryption $encryption)
|
||||
{
|
||||
$this->encryption = $encryption;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// PASSENGER LOGIN
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* POST /v2/auth/passenger/login
|
||||
* Replaces: login.php
|
||||
*/
|
||||
public function passengerLogin(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'phone' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
'fingerprint' => 'required|string',
|
||||
'fcm_token' => 'required|string',
|
||||
]);
|
||||
|
||||
// Rate limiting: 5 attempts per minute per IP
|
||||
$rateLimitKey = 'login_passenger:' . $request->ip();
|
||||
if (Cache::get($rateLimitKey, 0) >= config('intaleq.rate_limit_login', 5)) {
|
||||
return $this->failure('Too many login attempts. Please try again later.', 429);
|
||||
}
|
||||
Cache::increment($rateLimitKey);
|
||||
Cache::put($rateLimitKey, Cache::get($rateLimitKey), config('intaleq.rate_limit_login_decay', 60));
|
||||
|
||||
$phone = $request->input('phone');
|
||||
$password = $request->input('password');
|
||||
$fingerprint = $request->input('fingerprint');
|
||||
$fcmToken = $request->input('fcm_token');
|
||||
|
||||
// Find passenger by encrypted phone
|
||||
$encryptedPhone = $this->encryption->encrypt($phone);
|
||||
$passenger = Passenger::active()
|
||||
->where('phone', $encryptedPhone)
|
||||
->first();
|
||||
|
||||
if (!$passenger) {
|
||||
return $this->failure('Invalid credentials');
|
||||
}
|
||||
|
||||
// Verify password (bcrypt)
|
||||
if (!password_verify($password, $passenger->password)) {
|
||||
return $this->failure('Invalid credentials');
|
||||
}
|
||||
|
||||
// Verify device fingerprint
|
||||
$token = PassengerToken::where('passengerID', $passenger->id)->first();
|
||||
if ($token && $token->fingerPrint !== $fingerprint) {
|
||||
return $this->failure('Device mismatch. Please login from your registered device.');
|
||||
}
|
||||
|
||||
// Update FCM token
|
||||
if ($token) {
|
||||
$encryptedFcm = $this->encryption->encrypt($fcmToken);
|
||||
$token->update(['token' => $encryptedFcm]);
|
||||
}
|
||||
|
||||
// Generate API keys if not exist
|
||||
if (empty($passenger->api_key)) {
|
||||
$this->generateApiKeys($passenger);
|
||||
}
|
||||
|
||||
// Generate JWT
|
||||
$jwt = $this->createJwt($passenger->id, 'passenger', $fingerprint, 86400); // 24h
|
||||
|
||||
return $this->success([
|
||||
'token' => $jwt,
|
||||
'expires_in' => 86400,
|
||||
'user_id' => $passenger->id,
|
||||
'api_key' => $passenger->api_key,
|
||||
'api_secret' => $passenger->api_secret,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/auth/passenger/register
|
||||
* Replaces: loginFirstTime.php
|
||||
*/
|
||||
public function passengerRegister(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'phone' => 'required|string',
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|string|min:6',
|
||||
'first_name' => 'required|string',
|
||||
'last_name' => 'required|string',
|
||||
'gender' => 'required|string',
|
||||
'birthdate' => 'required|string',
|
||||
'site' => 'required|string',
|
||||
'fingerprint' => 'required|string',
|
||||
'fcm_token' => 'required|string',
|
||||
]);
|
||||
|
||||
$phone = $request->input('phone');
|
||||
$encryptedPhone = $this->encryption->encrypt($phone);
|
||||
|
||||
// Check if already exists
|
||||
$exists = Passenger::where('phone', $encryptedPhone)->exists();
|
||||
if ($exists) {
|
||||
return $this->failure('Phone number already registered');
|
||||
}
|
||||
|
||||
$passengerId = Str::uuid()->toString();
|
||||
|
||||
// Encrypt sensitive fields
|
||||
$passenger = Passenger::create([
|
||||
'id' => $passengerId,
|
||||
'phone' => $encryptedPhone,
|
||||
'email' => $this->encryption->encrypt($request->input('email')),
|
||||
'password' => password_hash($request->input('password'), PASSWORD_BCRYPT),
|
||||
'first_name' => $this->encryption->encrypt($request->input('first_name')),
|
||||
'last_name' => $this->encryption->encrypt($request->input('last_name')),
|
||||
'gender' => $this->encryption->encrypt($request->input('gender')),
|
||||
'birthdate' => $this->encryption->encrypt($request->input('birthdate')),
|
||||
'site' => $request->input('site'),
|
||||
]);
|
||||
|
||||
// Create FCM token record
|
||||
PassengerToken::create([
|
||||
'token' => $this->encryption->encrypt($request->input('fcm_token')),
|
||||
'passengerID' => $passengerId,
|
||||
'fingerPrint' => $request->input('fingerprint'),
|
||||
]);
|
||||
|
||||
// Generate API keys
|
||||
$this->generateApiKeys($passenger);
|
||||
|
||||
// Generate temporary JWT (5 min — for registration flow)
|
||||
$jwt = $this->createJwt($passengerId, 'passenger_temp', $request->input('fingerprint'), 300);
|
||||
|
||||
return $this->success([
|
||||
'token' => $jwt,
|
||||
'expires_in' => 300,
|
||||
'user_id' => $passengerId,
|
||||
'api_key' => $passenger->api_key,
|
||||
'api_secret' => $passenger->api_secret,
|
||||
], 201);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// DRIVER LOGIN
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* POST /v2/auth/driver/login
|
||||
* Replaces: loginJwtDriver.php
|
||||
*/
|
||||
public function driverLogin(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'phone' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
'fingerprint' => 'required|string',
|
||||
'fcm_token' => 'required|string',
|
||||
]);
|
||||
|
||||
// Rate limiting
|
||||
$rateLimitKey = 'login_driver:' . $request->ip();
|
||||
if (Cache::get($rateLimitKey, 0) >= config('intaleq.rate_limit_login', 5)) {
|
||||
return $this->failure('Too many login attempts', 429);
|
||||
}
|
||||
Cache::increment($rateLimitKey);
|
||||
Cache::put($rateLimitKey, Cache::get($rateLimitKey), config('intaleq.rate_limit_login_decay', 60));
|
||||
|
||||
$phone = $request->input('phone');
|
||||
$encryptedPhone = $this->encryption->encrypt($phone);
|
||||
|
||||
$driver = Driver::active()
|
||||
->where('phone', $encryptedPhone)
|
||||
->first();
|
||||
|
||||
if (!$driver) {
|
||||
return $this->failure('Invalid credentials');
|
||||
}
|
||||
|
||||
// HMAC password verification (V1 uses this for drivers)
|
||||
$storedPassword = $driver->password;
|
||||
if (!password_verify($request->input('password'), $storedPassword) &&
|
||||
!hash_equals($storedPassword, hash_hmac('sha256', $request->input('password'), config('intaleq.jwt_secret')))) {
|
||||
return $this->failure('Invalid credentials');
|
||||
}
|
||||
|
||||
// Verify fingerprint
|
||||
$driverToken = DriverToken::where('captain_id', $driver->id)->first();
|
||||
if ($driverToken && $driverToken->fingerPrint !== $request->input('fingerprint')) {
|
||||
return $this->failure('Device mismatch');
|
||||
}
|
||||
|
||||
// Update FCM token
|
||||
$encryptedFcm = $this->encryption->encrypt($request->input('fcm_token'));
|
||||
if ($driverToken) {
|
||||
$driverToken->update([
|
||||
'token' => $encryptedFcm,
|
||||
'fingerPrint' => $request->input('fingerprint'),
|
||||
]);
|
||||
} else {
|
||||
DriverToken::create([
|
||||
'token' => $encryptedFcm,
|
||||
'captain_id' => $driver->id,
|
||||
'fingerPrint' => $request->input('fingerprint'),
|
||||
]);
|
||||
}
|
||||
|
||||
// Generate API keys if not exist
|
||||
if (empty($driver->api_key)) {
|
||||
$this->generateApiKeys($driver);
|
||||
}
|
||||
|
||||
$jwt = $this->createJwt($driver->id, 'driver', $request->input('fingerprint'), 86400);
|
||||
|
||||
return $this->success([
|
||||
'token' => $jwt,
|
||||
'expires_in' => 86400,
|
||||
'user_id' => $driver->id,
|
||||
'api_key' => $driver->api_key,
|
||||
'api_secret' => $driver->api_secret,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/auth/driver/register
|
||||
* Replaces: loginFirstTimeDriver.php
|
||||
*/
|
||||
public function driverRegister(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'phone' => 'required|string',
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|string|min:6',
|
||||
'first_name' => 'required|string',
|
||||
'last_name' => 'required|string',
|
||||
'gender' => 'required|string',
|
||||
'birthdate' => 'required|string',
|
||||
'site' => 'required|string',
|
||||
'fingerprint' => 'required|string',
|
||||
'fcm_token' => 'required|string',
|
||||
]);
|
||||
|
||||
$encryptedPhone = $this->encryption->encrypt($request->input('phone'));
|
||||
|
||||
if (Driver::where('phone', $encryptedPhone)->exists()) {
|
||||
return $this->failure('Phone number already registered');
|
||||
}
|
||||
|
||||
$driverId = Str::uuid()->toString();
|
||||
|
||||
$driver = Driver::create([
|
||||
'id' => $driverId,
|
||||
'phone' => $encryptedPhone,
|
||||
'email' => $this->encryption->encrypt($request->input('email')),
|
||||
'password' => password_hash($request->input('password'), PASSWORD_BCRYPT),
|
||||
'first_name' => $this->encryption->encrypt($request->input('first_name')),
|
||||
'last_name' => $this->encryption->encrypt($request->input('last_name')),
|
||||
'gender' => $this->encryption->encrypt($request->input('gender')),
|
||||
'birthdate' => $this->encryption->encrypt($request->input('birthdate')),
|
||||
'site' => $request->input('site'),
|
||||
]);
|
||||
|
||||
DriverToken::create([
|
||||
'token' => $this->encryption->encrypt($request->input('fcm_token')),
|
||||
'captain_id' => $driverId,
|
||||
'fingerPrint' => $request->input('fingerprint'),
|
||||
]);
|
||||
|
||||
$this->generateApiKeys($driver);
|
||||
|
||||
$jwt = $this->createJwt($driverId, 'driver_temp', $request->input('fingerprint'), 300);
|
||||
|
||||
return $this->success([
|
||||
'token' => $jwt,
|
||||
'expires_in' => 300,
|
||||
'user_id' => $driverId,
|
||||
'api_key' => $driver->api_key,
|
||||
'api_secret' => $driver->api_secret,
|
||||
], 201);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// WALLET LOGIN (Higher security)
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* POST /v2/auth/passenger/wallet-login
|
||||
* Replaces: loginWallet.php
|
||||
*/
|
||||
public function passengerWalletLogin(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'phone' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
'fingerprint' => 'required|string',
|
||||
]);
|
||||
|
||||
// Stricter rate limit for wallet
|
||||
$rateLimitKey = 'wallet_login:' . $request->ip();
|
||||
if (Cache::get($rateLimitKey, 0) >= 3) {
|
||||
return $this->failure('Too many attempts', 429);
|
||||
}
|
||||
Cache::increment($rateLimitKey);
|
||||
Cache::put($rateLimitKey, Cache::get($rateLimitKey), 120);
|
||||
|
||||
$encryptedPhone = $this->encryption->encrypt($request->input('phone'));
|
||||
$passenger = Passenger::active()->where('phone', $encryptedPhone)->first();
|
||||
|
||||
if (!$passenger || !password_verify($request->input('password'), $passenger->password)) {
|
||||
return $this->failure('Invalid credentials');
|
||||
}
|
||||
|
||||
// Short-lived token for wallet operations (5 min)
|
||||
$jwt = $this->createJwt($passenger->id, 'passenger_wallet', $request->input('fingerprint'), 300);
|
||||
|
||||
return $this->success([
|
||||
'token' => $jwt,
|
||||
'expires_in' => 300,
|
||||
'user_id' => $passenger->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/auth/driver/wallet-login
|
||||
* Replaces: loginJwtWalletDriver.php
|
||||
*/
|
||||
public function driverWalletLogin(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'phone' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
'fingerprint' => 'required|string',
|
||||
]);
|
||||
|
||||
$rateLimitKey = 'wallet_login_driver:' . $request->ip();
|
||||
if (Cache::get($rateLimitKey, 0) >= 3) {
|
||||
return $this->failure('Too many attempts', 429);
|
||||
}
|
||||
Cache::increment($rateLimitKey);
|
||||
Cache::put($rateLimitKey, Cache::get($rateLimitKey), 120);
|
||||
|
||||
$encryptedPhone = $this->encryption->encrypt($request->input('phone'));
|
||||
$driver = Driver::active()->where('phone', $encryptedPhone)->first();
|
||||
|
||||
if (!$driver || !password_verify($request->input('password'), $driver->password)) {
|
||||
return $this->failure('Invalid credentials');
|
||||
}
|
||||
|
||||
$jwt = $this->createJwt($driver->id, 'driver_wallet', $request->input('fingerprint'), 60);
|
||||
|
||||
return $this->success([
|
||||
'token' => $jwt,
|
||||
'expires_in' => 60,
|
||||
'user_id' => $driver->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// ADMIN LOGIN
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* POST /v2/auth/admin/login
|
||||
* Replaces: loginAdmin.php (NOW WITH ACTUAL PASSWORD CHECK!)
|
||||
*/
|
||||
public function adminLogin(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'device_number' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
]);
|
||||
|
||||
$admin = DB::connection('primary')
|
||||
->table('adminUser')
|
||||
->where('device_number', $request->input('device_number'))
|
||||
->first();
|
||||
|
||||
if (!$admin) {
|
||||
return $this->failure('Invalid credentials');
|
||||
}
|
||||
|
||||
// TODO: Add password field to adminUser table and verify with password_verify
|
||||
// For now, this is a placeholder — must be implemented before production
|
||||
|
||||
$jwt = $this->createJwt($admin->id, 'admin', $request->input('device_number'), 900);
|
||||
|
||||
return $this->success([
|
||||
'token' => $jwt,
|
||||
'expires_in' => 900,
|
||||
'user_id' => $admin->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// HELPERS
|
||||
// ══════════════════════════════════════════════
|
||||
|
||||
private function createJwt(string $userId, string $userType, string $fingerprint, int $expiry): string
|
||||
{
|
||||
$payload = [
|
||||
'user_id' => $userId,
|
||||
'user_type' => $userType,
|
||||
'fingerprint' => $fingerprint,
|
||||
'iat' => time(),
|
||||
'exp' => time() + $expiry,
|
||||
'jti' => Str::uuid()->toString(),
|
||||
];
|
||||
|
||||
return JWT::encode($payload, config('intaleq.jwt_secret'), 'HS256');
|
||||
}
|
||||
|
||||
private function generateApiKeys($user): void
|
||||
{
|
||||
$apiKey = 'intq_' . Str::random(32);
|
||||
$apiSecret = hash('sha256', Str::random(64) . time());
|
||||
|
||||
$user->update([
|
||||
'api_key' => $apiKey,
|
||||
'api_secret' => $apiSecret,
|
||||
]);
|
||||
}
|
||||
|
||||
private function success(array $data, int $code = 200): JsonResponse
|
||||
{
|
||||
return response()->json(['status' => 'success', 'data' => $data], $code);
|
||||
}
|
||||
|
||||
private function failure(string $message, int $code = 401): JsonResponse
|
||||
{
|
||||
return response()->json(['status' => 'failure', 'message' => $message], $code);
|
||||
}
|
||||
}
|
||||
10
app/Http/Controllers/Controller.php
Normal file
10
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
abstract class Controller extends BaseController
|
||||
{
|
||||
//
|
||||
}
|
||||
52
app/Http/Controllers/NotificationController.php
Normal file
52
app/Http/Controllers/NotificationController.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Notification Controller
|
||||
* Replaces: ride/notification/*.php
|
||||
*/
|
||||
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');
|
||||
$page = (int) $request->input('page', 1);
|
||||
$limit = min((int) $request->input('limit', 20), 50);
|
||||
|
||||
if ($userType === 'driver') {
|
||||
$notifications = DB::connection('primary')->table('notificationCaptain')
|
||||
->where('driverID', $userId)
|
||||
->orderBy('dateCreated', 'desc')
|
||||
->skip(($page - 1) * $limit)->take($limit)
|
||||
->get();
|
||||
} else {
|
||||
$notifications = DB::connection('primary')->table('notifications')
|
||||
->where('passenger_id', $userId)
|
||||
->orderBy('created_at', 'desc')
|
||||
->skip(($page - 1) * $limit)->take($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => $notifications]);
|
||||
}
|
||||
|
||||
/** PUT /v2/notifications/{id}/read */
|
||||
public function markRead(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$userType = $request->input('_jwt_user_type');
|
||||
$table = $userType === 'driver' ? 'notificationCaptain' : 'notifications';
|
||||
|
||||
DB::connection('primary')->table($table)
|
||||
->where('id', $id)
|
||||
->update(['isShown' => 'true']);
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
}
|
||||
153
app/Http/Controllers/OtpController.php
Normal file
153
app/Http/Controllers/OtpController.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* OTP Controller
|
||||
* Replaces: auth/otpmessage.php, verifyOtpMessage.php, sendVerifyEmail.php, etc.
|
||||
*/
|
||||
class OtpController extends Controller
|
||||
{
|
||||
/** POST /v2/otp/send */
|
||||
public function send(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate(['phone' => 'required|string']);
|
||||
|
||||
$phone = $request->input('phone');
|
||||
|
||||
// Rate limit: 3 OTP per phone per 5 minutes
|
||||
$key = "otp_limit:{$phone}";
|
||||
if (Cache::get($key, 0) >= 3) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Too many OTP requests'], 429);
|
||||
}
|
||||
Cache::increment($key);
|
||||
Cache::put($key, Cache::get($key), 300);
|
||||
|
||||
// Generate 6-digit OTP
|
||||
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
$expiration = now()->addMinutes(5);
|
||||
|
||||
// Store OTP
|
||||
DB::connection('primary')->table('phone_verification')->updateOrInsert(
|
||||
['phone_number' => $phone],
|
||||
[
|
||||
'token_code' => password_hash($otp, PASSWORD_BCRYPT),
|
||||
'expiration_time' => $expiration,
|
||||
'is_verified' => 0,
|
||||
'created_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
// TODO: Send SMS via external provider
|
||||
// For now, return success (SMS sending is provider-specific)
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'OTP sent',
|
||||
'expires_at' => $expiration->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
|
||||
/** POST /v2/otp/verify */
|
||||
public function verify(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'phone' => 'required|string',
|
||||
'otp' => 'required|string|size:6',
|
||||
]);
|
||||
|
||||
$phone = $request->input('phone');
|
||||
$otp = $request->input('otp');
|
||||
|
||||
$record = DB::connection('primary')->table('phone_verification')
|
||||
->where('phone_number', $phone)
|
||||
->where('is_verified', 0)
|
||||
->where('expiration_time', '>', now())
|
||||
->first();
|
||||
|
||||
if (!$record) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'OTP expired or not found'], 400);
|
||||
}
|
||||
|
||||
// Verify OTP hash
|
||||
if (!password_verify($otp, $record->token_code)) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Invalid OTP'], 400);
|
||||
}
|
||||
|
||||
// Mark as verified
|
||||
DB::connection('primary')->table('phone_verification')
|
||||
->where('phone_number', $phone)
|
||||
->update(['is_verified' => 1]);
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Phone verified']);
|
||||
}
|
||||
|
||||
/** POST /v2/otp/email/send */
|
||||
public function sendEmail(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate(['email' => 'required|email']);
|
||||
|
||||
$email = $request->input('email');
|
||||
$token = Str::random(32);
|
||||
|
||||
DB::connection('primary')->table('email_verifications')->updateOrInsert(
|
||||
['email' => $email],
|
||||
[
|
||||
'token' => password_hash($token, PASSWORD_BCRYPT),
|
||||
'verified' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
// TODO: Send email with token link
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Verification email sent']);
|
||||
}
|
||||
|
||||
/** POST /v2/otp/email/verify */
|
||||
public function verifyEmail(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => 'required|email',
|
||||
'token' => 'required|string',
|
||||
]);
|
||||
|
||||
$record = DB::connection('primary')->table('email_verifications')
|
||||
->where('email', $request->input('email'))
|
||||
->where('verified', 0)
|
||||
->first();
|
||||
|
||||
if (!$record || !password_verify($request->input('token'), $record->token)) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Invalid verification'], 400);
|
||||
}
|
||||
|
||||
DB::connection('primary')->table('email_verifications')
|
||||
->where('email', $request->input('email'))
|
||||
->update(['verified' => 1, 'updated_at' => now()]);
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Email verified']);
|
||||
}
|
||||
|
||||
/** GET /v2/otp/check-phone?phone=XXX */
|
||||
public function checkPhone(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate(['phone' => 'required|string']);
|
||||
|
||||
$verified = DB::connection('primary')->table('phone_verification')
|
||||
->where('phone_number', $request->input('phone'))
|
||||
->where('is_verified', 1)
|
||||
->exists();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => ['verified' => $verified],
|
||||
]);
|
||||
}
|
||||
}
|
||||
71
app/Http/Controllers/PlaceController.php
Normal file
71
app/Http/Controllers/PlaceController.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Place Controller
|
||||
* Replaces: ride/places/add.php, ride/places_syria/*.php
|
||||
*/
|
||||
class PlaceController extends Controller
|
||||
{
|
||||
/** GET /v2/places/search?q=XXX&lat=XX&lng=XX */
|
||||
public function search(Request $request): JsonResponse
|
||||
{
|
||||
$q = $request->input('q', '');
|
||||
$lat = $request->input('lat');
|
||||
$lng = $request->input('lng');
|
||||
$limit = min((int) $request->input('limit', 20), 50);
|
||||
|
||||
$query = DB::connection('primary')->table('palces11');
|
||||
|
||||
if (!empty($q)) {
|
||||
// Fulltext search (palces11 has FULLTEXT index)
|
||||
$query->whereRaw(
|
||||
"MATCH(name, name_ar, name_en, address, category) AGAINST(? IN BOOLEAN MODE)",
|
||||
[$q . '*']
|
||||
);
|
||||
}
|
||||
|
||||
// If coordinates provided, sort by distance
|
||||
if ($lat && $lng) {
|
||||
$query->selectRaw("*,
|
||||
ST_Distance_Sphere(
|
||||
POINT(CAST(longitude AS DECIMAL(10,7)), CAST(latitude AS DECIMAL(10,7))),
|
||||
POINT(?, ?)
|
||||
) AS distance_meters", [(float)$lng, (float)$lat])
|
||||
->orderBy('distance_meters');
|
||||
}
|
||||
|
||||
$places = $query->limit($limit)->get();
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => $places]);
|
||||
}
|
||||
|
||||
/** POST /v2/places */
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'latitude' => 'required|numeric',
|
||||
'longitude' => 'required|numeric',
|
||||
'name' => 'required|string|max:180',
|
||||
'category' => 'required|string|max:55',
|
||||
]);
|
||||
|
||||
DB::connection('primary')->table('palces11')->insert([
|
||||
'latitude' => $request->input('latitude'),
|
||||
'longitude' => $request->input('longitude'),
|
||||
'name' => $request->input('name'),
|
||||
'name_ar' => $request->input('name_ar'),
|
||||
'name_en' => $request->input('name_en'),
|
||||
'address' => $request->input('address'),
|
||||
'category' => $request->input('category'),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success'], 201);
|
||||
}
|
||||
}
|
||||
148
app/Http/Controllers/ProfileController.php
Normal file
148
app/Http/Controllers/ProfileController.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Driver;
|
||||
use App\Models\Passenger;
|
||||
use App\Models\CarRegistration;
|
||||
use App\Models\ImageProfileCaptain;
|
||||
use App\Helpers\LegacyEncryption;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Profile Controller
|
||||
* Replaces: ride/profile/get.php, getCaptainProfile.php, update.php, updateDriverEmail.php
|
||||
*/
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
private LegacyEncryption $enc;
|
||||
|
||||
public function __construct(LegacyEncryption $enc)
|
||||
{
|
||||
$this->enc = $enc;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v2/profile/passenger
|
||||
*/
|
||||
public function passenger(Request $request): JsonResponse
|
||||
{
|
||||
$id = $request->input('_jwt_user_id');
|
||||
$passenger = Passenger::active()->find($id);
|
||||
|
||||
if (!$passenger) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Not found'], 404);
|
||||
}
|
||||
|
||||
$data = $passenger->toArray();
|
||||
$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';
|
||||
|
||||
// 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]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v2/profile/driver
|
||||
*/
|
||||
public function driver(Request $request): JsonResponse
|
||||
{
|
||||
$id = $request->input('_jwt_user_id');
|
||||
$driver = Driver::active()->byId($id)->first();
|
||||
|
||||
if (!$driver) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Not found'], 404);
|
||||
}
|
||||
|
||||
$data = $driver->toArray();
|
||||
$data = $this->enc->decryptFields($data, Driver::ENCRYPTED_FIELDS);
|
||||
unset($data['password'], $data['api_secret']);
|
||||
|
||||
// Car info
|
||||
$car = CarRegistration::where('driverID', $id)->where('isDefault', 1)->first();
|
||||
if ($car) {
|
||||
$carData = $car->toArray();
|
||||
$data['car'] = $this->enc->decryptFields($carData, CarRegistration::ENCRYPTED_FIELDS);
|
||||
}
|
||||
|
||||
// Profile image
|
||||
$image = ImageProfileCaptain::where('driverID', $id)->first();
|
||||
$data['profile_image'] = $image->link ?? null;
|
||||
|
||||
// Rating
|
||||
$data['rating'] = $driver->getAverageRating();
|
||||
|
||||
// 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]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /v2/profile/passenger
|
||||
*/
|
||||
public function updatePassenger(Request $request): JsonResponse
|
||||
{
|
||||
$id = $request->input('_jwt_user_id');
|
||||
$passenger = Passenger::active()->find($id);
|
||||
|
||||
if (!$passenger) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Not found'], 404);
|
||||
}
|
||||
|
||||
$updates = [];
|
||||
$encryptableFields = ['first_name', 'last_name', 'gender', 'birthdate', 'sosPhone'];
|
||||
|
||||
foreach ($encryptableFields as $field) {
|
||||
if ($request->has($field)) {
|
||||
$updates[$field] = $this->enc->encrypt($request->input($field));
|
||||
}
|
||||
}
|
||||
|
||||
$plainFields = ['education', 'employmentType', 'maritalStatus', 'site'];
|
||||
foreach ($plainFields as $field) {
|
||||
if ($request->has($field)) {
|
||||
$updates[$field] = $request->input($field);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($updates)) {
|
||||
$passenger->update($updates);
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Profile updated']);
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /v2/profile/driver/email
|
||||
*/
|
||||
public function updateDriverEmail(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate(['email' => 'required|email']);
|
||||
|
||||
$id = $request->input('_jwt_user_id');
|
||||
$driver = Driver::active()->byId($id)->first();
|
||||
|
||||
if (!$driver) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Not found'], 404);
|
||||
}
|
||||
|
||||
$driver->update([
|
||||
'email' => $this->enc->encrypt($request->input('email')),
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Email updated']);
|
||||
}
|
||||
}
|
||||
108
app/Http/Controllers/PromoController.php
Normal file
108
app/Http/Controllers/PromoController.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Promo Controller
|
||||
* Replaces: ride/promo/*.php
|
||||
*/
|
||||
class PromoController extends Controller
|
||||
{
|
||||
/** GET /v2/promos */
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$passengerId = $request->input('_jwt_user_id');
|
||||
|
||||
$promos = DB::connection('primary')->table('promos')
|
||||
->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]);
|
||||
}
|
||||
|
||||
/** GET /v2/promos/check?code=XXX */
|
||||
public function check(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate(['code' => 'required|string']);
|
||||
|
||||
$promo = DB::connection('primary')->table('promos')
|
||||
->where('promo_code', $request->input('code'))
|
||||
->where(function ($q) {
|
||||
$q->whereNull('validity_end_date')
|
||||
->orWhere('validity_end_date', '>=', now()->toDateString());
|
||||
})
|
||||
->first();
|
||||
|
||||
if (!$promo) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Invalid promo code'], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'code' => $promo->promo_code,
|
||||
'amount' => $promo->amount,
|
||||
'description' => $promo->description,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/** POST /v2/promos */
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'promo_code' => 'required|string|max:14',
|
||||
'amount' => 'required|string',
|
||||
]);
|
||||
|
||||
$passengerId = $request->input('_jwt_user_id');
|
||||
|
||||
$exists = DB::connection('primary')->table('promos')
|
||||
->where('passengerID', $passengerId)->exists();
|
||||
if ($exists) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Promo already assigned'], 409);
|
||||
}
|
||||
|
||||
DB::connection('primary')->table('promos')->insert([
|
||||
'promo_code' => $request->input('promo_code'),
|
||||
'amount' => $request->input('amount'),
|
||||
'description' => $request->input('description'),
|
||||
'passengerID' => $passengerId,
|
||||
'validity_start_date' => $request->input('start_date'),
|
||||
'validity_end_date' => $request->input('end_date'),
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success'], 201);
|
||||
}
|
||||
|
||||
/** PUT /v2/promos/{id} */
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
DB::connection('primary')->table('promos')
|
||||
->where('id', $id)
|
||||
->update(array_filter([
|
||||
'promo_code' => $request->input('promo_code'),
|
||||
'amount' => $request->input('amount'),
|
||||
'description' => $request->input('description'),
|
||||
'validity_end_date' => $request->input('end_date'),
|
||||
]));
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
/** DELETE /v2/promos/{id} */
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
DB::connection('primary')->table('promos')->where('id', $id)->delete();
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
}
|
||||
144
app/Http/Controllers/RatingController.php
Normal file
144
app/Http/Controllers/RatingController.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Rating Controller
|
||||
* Replaces: ride/rate/*.php
|
||||
*/
|
||||
class RatingController extends Controller
|
||||
{
|
||||
/** POST /v2/ratings/driver — passenger rates a driver */
|
||||
public function rateDriver(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'driver_id' => 'required|string',
|
||||
'ride_id' => 'required|integer',
|
||||
'rating' => 'required|numeric|min:1|max:5',
|
||||
'comment' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$passengerId = $request->input('_jwt_user_id');
|
||||
|
||||
// Prevent duplicate ratings
|
||||
$exists = DB::connection('primary')->table('ratingDriver')
|
||||
->where('ride_id', $request->input('ride_id'))->exists();
|
||||
if ($exists) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Already rated'], 409);
|
||||
}
|
||||
|
||||
DB::connection('primary')->table('ratingDriver')->insert([
|
||||
'passenger_id' => $passengerId,
|
||||
'driver_id' => $request->input('driver_id'),
|
||||
'ride_id' => $request->input('ride_id'),
|
||||
'rating' => $request->input('rating'),
|
||||
'comment' => $request->input('comment', ''),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success'], 201);
|
||||
}
|
||||
|
||||
/** POST /v2/ratings/passenger — driver rates a passenger */
|
||||
public function ratePassenger(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'passenger_id' => 'required|string',
|
||||
'ride_id' => 'required',
|
||||
'rating' => 'required|numeric|min:1|max:5',
|
||||
'comment' => 'nullable|string|max:500',
|
||||
]);
|
||||
|
||||
$driverId = $request->input('_jwt_user_id');
|
||||
|
||||
$exists = DB::connection('primary')->table('ratingPassenger')
|
||||
->where('rideId', $request->input('ride_id'))->exists();
|
||||
if ($exists) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Already rated'], 409);
|
||||
}
|
||||
|
||||
DB::connection('primary')->table('ratingPassenger')->insert([
|
||||
'passenger_id' => $request->input('passenger_id'),
|
||||
'driverID' => $driverId,
|
||||
'rideId' => $request->input('ride_id'),
|
||||
'rating' => $request->input('rating'),
|
||||
'comment' => $request->input('comment', ''),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success'], 201);
|
||||
}
|
||||
|
||||
/** POST /v2/ratings/app */
|
||||
public function rateApp(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'rating' => 'required|numeric|min:1|max:5',
|
||||
'comment' => 'nullable|string|max:300',
|
||||
]);
|
||||
|
||||
$userId = $request->input('_jwt_user_id');
|
||||
$userType = $request->input('_jwt_user_type');
|
||||
|
||||
DB::connection('primary')->table('ratingApp')->insert([
|
||||
'name' => $request->input('name', ''),
|
||||
'email' => $request->input('email', ''),
|
||||
'phone' => $request->input('phone', ''),
|
||||
'userId' => $userId,
|
||||
'userType' => $userType,
|
||||
'rating' => $request->input('rating'),
|
||||
'comment' => $request->input('comment', ''),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success'], 201);
|
||||
}
|
||||
|
||||
/** GET /v2/ratings/driver/{id} */
|
||||
public function driverRating(string $id): JsonResponse
|
||||
{
|
||||
$ratings = DB::connection('primary')->table('ratingDriver')
|
||||
->where('driver_id', $id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
$avg = DB::connection('primary')->table('ratingDriver')
|
||||
->where('driver_id', $id)->avg('rating');
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'average' => round($avg ?? 5.0, 2),
|
||||
'count' => $ratings->count(),
|
||||
'ratings' => $ratings,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/** GET /v2/ratings/passenger/{id} */
|
||||
public function passengerRating(string $id): JsonResponse
|
||||
{
|
||||
$ratings = DB::connection('primary')->table('ratingPassenger')
|
||||
->where('passenger_id', $id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
$avg = DB::connection('primary')->table('ratingPassenger')
|
||||
->where('passenger_id', $id)->avg('rating');
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'average' => round($avg ?? 5.0, 2),
|
||||
'count' => $ratings->count(),
|
||||
'ratings' => $ratings,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
544
app/Http/Controllers/RideController.php
Normal file
544
app/Http/Controllers/RideController.php
Normal file
@@ -0,0 +1,544 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Ride;
|
||||
use App\Models\Driver;
|
||||
use App\Models\DriverToken;
|
||||
use App\Models\PassengerToken;
|
||||
use App\Models\DriverOrder;
|
||||
use App\Models\CarLocation;
|
||||
use App\Helpers\LegacyEncryption;
|
||||
use App\Services\FcmService;
|
||||
use App\Services\SocketService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Ride Controller
|
||||
*
|
||||
* Handles the complete ride lifecycle:
|
||||
* create → search drivers → accept → arrive → start → finish/cancel
|
||||
*/
|
||||
class RideController extends Controller
|
||||
{
|
||||
private LegacyEncryption $encryption;
|
||||
private FcmService $fcm;
|
||||
private SocketService $socket;
|
||||
|
||||
public function __construct(LegacyEncryption $encryption, FcmService $fcm, SocketService $socket)
|
||||
{
|
||||
$this->encryption = $encryption;
|
||||
$this->fcm = $fcm;
|
||||
$this->socket = $socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/rides
|
||||
* Replaces: ride/rides/add_ride.php
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'start_location' => 'required|string',
|
||||
'end_location' => 'required|string',
|
||||
'start_lat' => 'required|numeric',
|
||||
'start_lng' => 'required|numeric',
|
||||
'end_lat' => 'required|numeric',
|
||||
'end_lng' => 'required|numeric',
|
||||
'price' => 'required|numeric|min:0',
|
||||
'car_type' => 'required|string',
|
||||
'payment_method' => 'required|in:cash,visa',
|
||||
'distance' => 'required|numeric',
|
||||
]);
|
||||
|
||||
$passengerId = $request->input('_jwt_user_id');
|
||||
|
||||
// Prevent duplicate active rides
|
||||
$activeRide = Ride::forPassenger($passengerId)->active()->first();
|
||||
if ($activeRide) {
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'You already have an active ride',
|
||||
], 409);
|
||||
}
|
||||
|
||||
// Begin transaction across both databases
|
||||
DB::connection('ride')->beginTransaction();
|
||||
try {
|
||||
$ride = Ride::create([
|
||||
'start_location' => $request->input('start_location'),
|
||||
'end_location' => $request->input('end_location'),
|
||||
'date' => now()->toDateString(),
|
||||
'time' => now()->toTimeString(),
|
||||
'endtime' => '00:00:00',
|
||||
'price' => $request->input('price'),
|
||||
'passenger_id' => $passengerId,
|
||||
'driver_id' => 'none',
|
||||
'status' => 'waiting',
|
||||
'paymentMethod' => $request->input('payment_method', 'Cash'),
|
||||
'carType' => $request->input('car_type', 'Speed'),
|
||||
'price_for_passenger' => $request->input('price'),
|
||||
'distance' => $request->input('distance'),
|
||||
]);
|
||||
|
||||
// Also insert into waiting rides (for driver search)
|
||||
DB::connection('primary')->table('waitingRides')->insert([
|
||||
'id' => (string) $ride->id,
|
||||
'start_location' => $request->input('start_location'),
|
||||
'start_lat' => $request->input('start_lat'),
|
||||
'start_lng' => $request->input('start_lng'),
|
||||
'end_location' => $request->input('end_location'),
|
||||
'end_lat' => $request->input('end_lat'),
|
||||
'end_lng' => $request->input('end_lng'),
|
||||
'date' => now()->toDateString(),
|
||||
'time' => now()->toTimeString(),
|
||||
'price' => $request->input('price'),
|
||||
'passenger_id' => $passengerId,
|
||||
'status' => 'waiting',
|
||||
'carType' => $request->input('car_type', 'Speed'),
|
||||
'passengerRate' => 5.0,
|
||||
'price_for_passenger' => $request->input('price'),
|
||||
'distance' => $request->input('distance'),
|
||||
'duration' => $request->input('duration', '0'),
|
||||
'payment_method' => $request->input('payment_method', 'cash'),
|
||||
'passenger_wallet' => $request->input('wallet_balance', '0'),
|
||||
]);
|
||||
|
||||
DB::connection('ride')->commit();
|
||||
|
||||
// Notify nearby drivers via socket
|
||||
$this->socket->sendToLocationServer('new_ride', [
|
||||
'ride_id' => $ride->id,
|
||||
'lat' => $request->input('start_lat'),
|
||||
'lng' => $request->input('start_lng'),
|
||||
'car_type' => $request->input('car_type'),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => ['ride_id' => $ride->id],
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::connection('ride')->rollBack();
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Failed to create ride',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/rides/{id}/accept
|
||||
* Replaces: ride/rides/acceptRide.php
|
||||
*/
|
||||
public function accept(Request $request, int $rideId): JsonResponse
|
||||
{
|
||||
$driverId = $request->input('_jwt_user_id');
|
||||
|
||||
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'])) {
|
||||
DB::connection('ride')->rollBack();
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Ride not available',
|
||||
], 409);
|
||||
}
|
||||
|
||||
// Update ride status atomically
|
||||
$ride->update([
|
||||
'driver_id' => $driverId,
|
||||
'status' => 'Applied',
|
||||
'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
|
||||
DB::connection('primary')
|
||||
->table('ride')
|
||||
->where('id', $rideId)
|
||||
->update([
|
||||
'driver_id' => $driverId,
|
||||
'status' => 'Applied',
|
||||
]);
|
||||
|
||||
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->sendToDevice(
|
||||
$decryptedToken,
|
||||
'تم قبول طلبك',
|
||||
'السائق في الطريق إليك',
|
||||
['ride_id' => (string) $rideId, 'status' => 'Applied'],
|
||||
'ride_accepted'
|
||||
);
|
||||
}
|
||||
|
||||
// Notify passenger via socket
|
||||
$this->socket->notifyPassenger($ride->passenger_id, [
|
||||
'ride_id' => $rideId,
|
||||
'status' => 'Applied',
|
||||
'driver_id' => $driverId,
|
||||
]);
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::connection('ride')->rollBack();
|
||||
return response()->json([
|
||||
'status' => 'failure',
|
||||
'message' => 'Failed to accept ride',
|
||||
], 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->sendToDevice(
|
||||
$this->encryption->decrypt($passengerToken->token),
|
||||
'الرحلة بدأت',
|
||||
'رحلة سعيدة!',
|
||||
['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');
|
||||
|
||||
$ride = Ride::where('id', $rideId)
|
||||
->where('driver_id', $driverId)
|
||||
->where('status', 'Applied')
|
||||
->first();
|
||||
|
||||
if (!$ride) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
|
||||
}
|
||||
|
||||
$ride->update(['status' => 'Arrived']);
|
||||
|
||||
DB::connection('primary')->table('ride')
|
||||
->where('id', $rideId)->update(['status' => 'Arrived']);
|
||||
|
||||
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
|
||||
if ($passengerToken) {
|
||||
$this->fcm->sendToDevice(
|
||||
$this->encryption->decrypt($passengerToken->token),
|
||||
'السائق وصل',
|
||||
'السائق في انتظارك',
|
||||
['ride_id' => (string) $rideId, 'status' => 'Arrived'],
|
||||
'driver_arrived'
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v2/rides/{id}/finish
|
||||
* Replaces: ride/rides/finish_ride_updates.php
|
||||
*/
|
||||
public function finish(Request $request, int $rideId): JsonResponse
|
||||
{
|
||||
$driverId = $request->input('_jwt_user_id');
|
||||
|
||||
$request->validate([
|
||||
'price_for_driver' => 'required|numeric',
|
||||
'price_for_passenger' => 'required|numeric',
|
||||
'distance' => 'required|numeric',
|
||||
]);
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
DB::connection('ride')->beginTransaction();
|
||||
try {
|
||||
$ride->update([
|
||||
'status' => 'finish',
|
||||
'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'),
|
||||
]);
|
||||
|
||||
// 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('ride')->commit();
|
||||
|
||||
// Notify passenger
|
||||
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
|
||||
if ($passengerToken) {
|
||||
$this->fcm->sendToDevice(
|
||||
$this->encryption->decrypt($passengerToken->token),
|
||||
'الرحلة انتهت',
|
||||
'شكراً لاستخدامك انطلق',
|
||||
[
|
||||
'ride_id' => (string) $rideId,
|
||||
'status' => 'finish',
|
||||
'price' => (string) $request->input('price_for_passenger'),
|
||||
],
|
||||
'ride_finished'
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::connection('ride')->rollBack();
|
||||
return response()->json(['status' => 'failure', 'message' => 'Failed to finish ride'], 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');
|
||||
|
||||
$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']);
|
||||
|
||||
DB::connection('primary')->table('ride')
|
||||
->where('id', $rideId)->update(['status' => 'CancelByPassenger']);
|
||||
|
||||
DB::connection('primary')->table('waitingRides')
|
||||
->where('id', (string) $rideId)->update(['status' => 'CancelByPassenger']);
|
||||
|
||||
// Log cancellation
|
||||
DB::connection('ride')->table('canecl')->insert([
|
||||
'driverID' => $ride->driver_id ?? 'none',
|
||||
'passengerID' => $passengerId,
|
||||
'rideID' => (string) $rideId,
|
||||
'note' => $request->input('reason', 'nothing'),
|
||||
]);
|
||||
|
||||
// Notify driver if assigned
|
||||
if ($ride->driver_id !== 'none') {
|
||||
$driverToken = DriverToken::where('captain_id', $ride->driver_id)->first();
|
||||
if ($driverToken) {
|
||||
$this->fcm->sendToDevice(
|
||||
$this->encryption->decrypt($driverToken->token),
|
||||
'تم إلغاء الرحلة',
|
||||
'الراكب ألغى الطلب',
|
||||
['ride_id' => (string) $rideId, 'status' => 'CancelByPassenger'],
|
||||
'ride_cancelled'
|
||||
);
|
||||
}
|
||||
|
||||
$this->socket->sendToLocationServer('cancel_ride', [
|
||||
'driver_id' => $ride->driver_id,
|
||||
'ride_id' => $rideId,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
|
||||
$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']);
|
||||
|
||||
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']);
|
||||
|
||||
// Log cancellation
|
||||
DB::connection('ride')->table('canecl')->insert([
|
||||
'driverID' => $driverId,
|
||||
'passengerID' => $ride->passenger_id,
|
||||
'rideID' => (string) $rideId,
|
||||
'note' => $request->input('reason', 'nothing'),
|
||||
]);
|
||||
|
||||
// Notify passenger
|
||||
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
|
||||
if ($passengerToken) {
|
||||
$this->fcm->sendToDevice(
|
||||
$this->encryption->decrypt($passengerToken->token),
|
||||
'تم إلغاء الرحلة',
|
||||
'السائق ألغى الطلب، جاري البحث...',
|
||||
['ride_id' => (string) $rideId, 'status' => 'CancelByDriver'],
|
||||
'ride_cancelled'
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v2/rides/{id}
|
||||
* Replaces: ride/rides/getRideOrderID.php
|
||||
*/
|
||||
public function show(int $rideId): JsonResponse
|
||||
{
|
||||
$ride = Ride::find($rideId);
|
||||
if (!$ride) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Ride not found'], 404);
|
||||
}
|
||||
return response()->json(['status' => 'success', 'data' => $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');
|
||||
|
||||
$query = Ride::active();
|
||||
if ($userType === 'driver') {
|
||||
$query->forDriver($userId);
|
||||
} else {
|
||||
$query->forPassenger($userId);
|
||||
}
|
||||
|
||||
$ride = $query->orderBy('id', 'desc')->first();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $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');
|
||||
$page = $request->input('page', 1);
|
||||
$limit = min($request->input('limit', 20), 50);
|
||||
|
||||
$query = Ride::query();
|
||||
if ($userType === 'driver') {
|
||||
$query->forDriver($userId);
|
||||
} else {
|
||||
$query->forPassenger($userId);
|
||||
}
|
||||
|
||||
$rides = $query->orderBy('id', 'desc')
|
||||
->skip(($page - 1) * $limit)
|
||||
->take($limit)
|
||||
->get();
|
||||
|
||||
return response()->json(['status' => 'success', 'data' => $rides]);
|
||||
}
|
||||
}
|
||||
141
app/Http/Controllers/TrackingController.php
Normal file
141
app/Http/Controllers/TrackingController.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Tracking Controller
|
||||
* Replaces: ride/rides/get_driver_location.php, public_track_location.php, getRealTimeHeatmap.php
|
||||
*/
|
||||
class TrackingController extends Controller
|
||||
{
|
||||
/** GET /v2/tracking/driver/{rideId} */
|
||||
public function driverLocation(Request $request, int $rideId): JsonResponse
|
||||
{
|
||||
$ride = DB::connection('ride')->table('ride')
|
||||
->where('id', $rideId)
|
||||
->whereIn('status', ['Applied', 'Arrived', 'Begin'])
|
||||
->first();
|
||||
|
||||
if (!$ride) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Ride not active'], 404);
|
||||
}
|
||||
|
||||
$location = DB::connection('tracking')->table('car_locations')
|
||||
->where('driver_id', $ride->driver_id)
|
||||
->first();
|
||||
|
||||
if (!$location) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Driver location not available'], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'latitude' => $location->latitude,
|
||||
'longitude' => $location->longitude,
|
||||
'heading' => $location->heading,
|
||||
'speed' => $location->speed,
|
||||
'updated_at' => $location->updated_at,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v2/tracking/public/{rideId}?hash=XXX
|
||||
* Public tracking link for parents/friends — uses HMAC hash instead of auth
|
||||
*/
|
||||
public function publicTrack(Request $request, int $rideId): JsonResponse
|
||||
{
|
||||
$hash = $request->input('hash');
|
||||
if (!$hash) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Missing hash'], 400);
|
||||
}
|
||||
|
||||
// Verify hash: HMAC-SHA256(ride_id, secret_salt)
|
||||
$expectedHash = hash_hmac('sha256', (string) $rideId, config('intaleq.secret_salt_parent'));
|
||||
if (!hash_equals($expectedHash, $hash)) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Invalid hash'], 403);
|
||||
}
|
||||
|
||||
$ride = DB::connection('ride')->table('ride')
|
||||
->where('id', $rideId)
|
||||
->whereIn('status', ['Applied', 'Arrived', 'Begin'])
|
||||
->first();
|
||||
|
||||
if (!$ride) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Ride not active'], 404);
|
||||
}
|
||||
|
||||
$location = DB::connection('tracking')->table('car_locations')
|
||||
->where('driver_id', $ride->driver_id)
|
||||
->first();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'latitude' => $location->latitude ?? null,
|
||||
'longitude' => $location->longitude ?? null,
|
||||
'heading' => $location->heading ?? null,
|
||||
'ride_status' => $ride->status,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/** GET /v2/tracking/heatmap */
|
||||
public function heatmap(Request $request): JsonResponse
|
||||
{
|
||||
// Use spatial query for active drivers
|
||||
$drivers = DB::connection('tracking')->table('car_locations')
|
||||
->select('latitude', 'longitude', 'carType')
|
||||
->where('status', 'on')
|
||||
->where('updated_at', '>', now()->subMinutes(10))
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $drivers,
|
||||
]);
|
||||
}
|
||||
|
||||
/** GET /v2/tracking/captain-stats */
|
||||
public function captainStats(Request $request): JsonResponse
|
||||
{
|
||||
$driverId = $request->input('_jwt_user_id');
|
||||
|
||||
$totalRides = DB::connection('ride')->table('ride')
|
||||
->where('driver_id', $driverId)
|
||||
->where('status', 'finish')
|
||||
->count();
|
||||
|
||||
$todayRides = DB::connection('ride')->table('ride')
|
||||
->where('driver_id', $driverId)
|
||||
->where('status', 'finish')
|
||||
->whereDate('rideTimeFinish', today())
|
||||
->count();
|
||||
|
||||
$todayEarnings = DB::connection('ride')->table('ride')
|
||||
->where('driver_id', $driverId)
|
||||
->where('status', 'finish')
|
||||
->whereDate('rideTimeFinish', today())
|
||||
->sum('price_for_driver');
|
||||
|
||||
$workHours = DB::connection('tracking')->table('driver_daily_summary')
|
||||
->where('driver_id', $driverId)
|
||||
->where('date', today()->toDateString())
|
||||
->value('total_seconds') ?? 0;
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'total_rides' => $totalRides,
|
||||
'today_rides' => $todayRides,
|
||||
'today_earnings' => round($todayEarnings, 2),
|
||||
'today_work_seconds' => $workHours,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
171
app/Http/Controllers/UploadController.php
Normal file
171
app/Http/Controllers/UploadController.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Upload Controller
|
||||
* Replaces: uploadImage.php, uploadImagePortrate.php, uploadImageType.php, etc.
|
||||
*
|
||||
* Security improvements over V1:
|
||||
* - MIME type + magic bytes validation (not just extension)
|
||||
* - Randomized filenames (prevents path traversal)
|
||||
* - Max file size enforcement
|
||||
* - No directory traversal possible
|
||||
*/
|
||||
class UploadController extends Controller
|
||||
{
|
||||
private const ALLOWED_IMAGE_MIMES = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
private const ALLOWED_AUDIO_MIMES = ['audio/mpeg', 'audio/mp4', 'audio/wav', 'audio/ogg'];
|
||||
private const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
private const MAX_AUDIO_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
/** POST /v2/uploads/card-image */
|
||||
public function cardImage(Request $request): JsonResponse
|
||||
{
|
||||
return $this->handleImageUpload($request, 'card_images', 'cards');
|
||||
}
|
||||
|
||||
/** POST /v2/uploads/profile-image */
|
||||
public function profileImage(Request $request): JsonResponse
|
||||
{
|
||||
return $this->handleImageUpload($request, 'imageProfileCaptain', 'profiles');
|
||||
}
|
||||
|
||||
/** POST /v2/uploads/document */
|
||||
public function document(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'image' => 'required|file',
|
||||
'doc_type' => 'required|string|in:license,registration,criminal,id_front,id_back',
|
||||
]);
|
||||
|
||||
return $this->handleImageUpload($request, 'driver_documents', 'documents');
|
||||
}
|
||||
|
||||
/** POST /v2/uploads/id-front */
|
||||
public function idFront(Request $request): JsonResponse
|
||||
{
|
||||
return $this->handleImageUpload($request, 'card_images', 'ids/front');
|
||||
}
|
||||
|
||||
/** POST /v2/uploads/id-back */
|
||||
public function idBack(Request $request): JsonResponse
|
||||
{
|
||||
return $this->handleImageUpload($request, 'card_images', 'ids/back');
|
||||
}
|
||||
|
||||
/** POST /v2/uploads/audio */
|
||||
public function audio(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate(['audio' => 'required|file']);
|
||||
|
||||
$file = $request->file('audio');
|
||||
|
||||
// Validate MIME
|
||||
$mime = $file->getMimeType();
|
||||
if (!in_array($mime, self::ALLOWED_AUDIO_MIMES)) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Invalid audio format'], 400);
|
||||
}
|
||||
|
||||
if ($file->getSize() > self::MAX_AUDIO_SIZE) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'File too large'], 400);
|
||||
}
|
||||
|
||||
$userId = $request->input('_jwt_user_id');
|
||||
$ext = $file->getClientOriginalExtension() ?: 'mp3';
|
||||
$filename = 'audio_' . Str::random(24) . '.' . $ext;
|
||||
|
||||
$uploadPath = public_path('uploads/audio');
|
||||
if (!is_dir($uploadPath)) mkdir($uploadPath, 0755, true);
|
||||
|
||||
$file->move($uploadPath, $filename);
|
||||
|
||||
$link = config('intaleq.upload_base_url') . '/uploads/audio/' . $filename;
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => ['link' => $link, 'filename' => $filename],
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Core image upload handler
|
||||
*/
|
||||
private function handleImageUpload(Request $request, string $table, string $subDir): JsonResponse
|
||||
{
|
||||
$request->validate(['image' => 'required|file']);
|
||||
|
||||
$file = $request->file('image');
|
||||
|
||||
// Validate MIME type
|
||||
$mime = $file->getMimeType();
|
||||
if (!in_array($mime, self::ALLOWED_IMAGE_MIMES)) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'Invalid image format. Allowed: JPG, PNG, WebP'], 400);
|
||||
}
|
||||
|
||||
// Validate file size
|
||||
if ($file->getSize() > self::MAX_IMAGE_SIZE) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'File too large (max 5MB)'], 400);
|
||||
}
|
||||
|
||||
// Validate magic bytes (defense in depth)
|
||||
$firstBytes = file_get_contents($file->getRealPath(), false, null, 0, 4);
|
||||
if (!$this->validateMagicBytes($firstBytes, $mime)) {
|
||||
return response()->json(['status' => 'failure', 'message' => 'File content does not match type'], 400);
|
||||
}
|
||||
|
||||
// Generate safe filename
|
||||
$userId = $request->input('_jwt_user_id');
|
||||
$ext = match ($mime) {
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/webp' => 'webp',
|
||||
default => 'jpg',
|
||||
};
|
||||
$filename = $subDir . '_' . Str::random(24) . '.' . $ext;
|
||||
|
||||
$uploadPath = public_path('uploads/' . $subDir);
|
||||
if (!is_dir($uploadPath)) mkdir($uploadPath, 0755, true);
|
||||
|
||||
$file->move($uploadPath, $filename);
|
||||
|
||||
$link = config('intaleq.upload_base_url') . '/uploads/' . $subDir . '/' . $filename;
|
||||
|
||||
// Save to DB
|
||||
$dbData = [
|
||||
'driverID' => $userId,
|
||||
'image_name' => $filename,
|
||||
'link' => $link,
|
||||
'upload_date' => now(),
|
||||
];
|
||||
|
||||
if ($request->has('doc_type')) {
|
||||
$dbData['doc_type'] = $request->input('doc_type');
|
||||
}
|
||||
|
||||
DB::connection('ride')->table($table)->insert($dbData);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => ['link' => $link, 'filename' => $filename],
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate file magic bytes match the declared MIME type
|
||||
*/
|
||||
private function validateMagicBytes(string $bytes, string $mime): bool
|
||||
{
|
||||
return match ($mime) {
|
||||
'image/jpeg' => str_starts_with($bytes, "\xFF\xD8\xFF"),
|
||||
'image/png' => str_starts_with($bytes, "\x89\x50\x4E\x47"),
|
||||
'image/webp' => str_starts_with($bytes, "RIFF"),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
151
app/Http/Controllers/WalletController.php
Normal file
151
app/Http/Controllers/WalletController.php
Normal file
@@ -0,0 +1,151 @@
|
||||
<?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/passengerWallet/*.php, ride/driverWallet/*.php
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user