567 lines
20 KiB
PHP
567 lines
20 KiB
PHP
<?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)
|
|
*
|
|
* الغرض من الملف:
|
|
* إدارة دورة حياة الرحلة بالكامل؛ بدءاً من طلب الراكب للرحلة حتى وصوله ودفعه للأجرة.
|
|
*
|
|
* كيفية العمل:
|
|
* 1. يستقبل طلبات الرحلات الجديدة ويحفظها في جدول (waitingRides).
|
|
* 2. يسمح للسائقين بقبول الرحلات المتاحة وتحديث حالتهم.
|
|
* 3. يدير حالات الرحلة المختلفة: (انتظار، قبول، وصول السائق، بدء الرحلة، انتهاء الرحلة).
|
|
* 4. يرسل إشعارات فورية للركاب والسائقين عند أي تغيير في حالة الرحلة.
|
|
*/
|
|
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',
|
|
'price' => 'required|numeric|min:0',
|
|
'car_type' => 'required|string',
|
|
'distance' => 'required|numeric',
|
|
'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',
|
|
'start_name' => 'nullable|string',
|
|
'end_name' => 'nullable|string',
|
|
'duration_text' => 'nullable|string',
|
|
'passenger_rating' => 'nullable|numeric',
|
|
]);
|
|
|
|
$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'])
|
|
->first();
|
|
|
|
if ($activeRide) {
|
|
return response()->json([
|
|
'status' => 'failure',
|
|
'message' => 'You already have an active ride',
|
|
], 409);
|
|
}
|
|
|
|
// Data array as expected by V1 Database
|
|
$rideData = [
|
|
'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' => '0', // 0 in V1 instead of 'none'
|
|
'status' => $request->input('status', 'waiting'),
|
|
'carType' => $request->input('car_type', 'Speed'),
|
|
'price_for_driver' => $request->input('price_for_driver', $request->input('price')),
|
|
'price_for_passenger' => $request->input('price_for_passenger', $request->input('price')),
|
|
'distance' => $request->input('distance'),
|
|
];
|
|
|
|
DB::connection('primary')->beginTransaction();
|
|
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
|
|
DB::connection('ride')->table('ride')->insert($rideData);
|
|
|
|
DB::connection('primary')->commit();
|
|
DB::connection('ride')->commit();
|
|
|
|
// 3. Broadcast to Marketplace (Location Socket)
|
|
$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'),
|
|
]);
|
|
|
|
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
|
|
|
|
} 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',
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* POST /v2/rides/{id}/accept
|
|
* Replaces: ride/rides/acceptRide.php
|
|
*/
|
|
public function accept(Request $request, int $rideId): JsonResponse
|
|
{
|
|
$driverId = $request->attributes->get('_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->sendLocalizedToDevice(
|
|
$decryptedToken,
|
|
'ride_accepted_title',
|
|
'ride_accepted_body',
|
|
['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->attributes->get('_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->attributes->get('_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->sendLocalizedToDevice(
|
|
$this->encryption->decrypt($passengerToken->token),
|
|
'driver_arrived_title',
|
|
'driver_arrived_body',
|
|
['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->attributes->get('_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->sendLocalizedToDevice(
|
|
$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'
|
|
);
|
|
}
|
|
|
|
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->attributes->get('_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', 'No reason specified'),
|
|
]);
|
|
|
|
// Notify driver if assigned
|
|
if ($ride->driver_id !== 'none') {
|
|
$driverToken = DriverToken::where('captain_id', $ride->driver_id)->first();
|
|
if ($driverToken) {
|
|
$this->fcm->sendLocalizedToDevice(
|
|
$this->encryption->decrypt($driverToken->token),
|
|
'ride_cancelled_title',
|
|
'ride_cancelled_body_passenger',
|
|
['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->attributes->get('_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', 'No reason specified'),
|
|
]);
|
|
|
|
// Notify passenger via FCM
|
|
$passengerToken = PassengerToken::where('passengerID', $ride->passenger_id)->first();
|
|
if ($passengerToken) {
|
|
$this->fcm->sendLocalizedToDevice(
|
|
$this->encryption->decrypt($passengerToken->token),
|
|
'ride_cancelled_title',
|
|
'ride_cancelled_body_driver',
|
|
['ride_id' => (string) $rideId, 'status' => 'CancelByDriver'],
|
|
'ride_cancelled'
|
|
);
|
|
}
|
|
|
|
// Notify passenger via Socket (Faster than FCM)
|
|
$this->socket->notifyPassenger($ride->passenger_id, [
|
|
'ride_id' => $rideId,
|
|
'status' => 'CancelByDriver',
|
|
]);
|
|
|
|
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->attributes->get('_jwt_user_id');
|
|
$userType = $request->attributes->get('_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->attributes->get('_jwt_user_id');
|
|
$userType = $request->attributes->get('_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]);
|
|
}
|
|
}
|