├── .env.example
├── .gitignore
├── LICENSE
├── README.md
├── README.persian.md
├── Snap Clone.png
├── app
├── Enums
│ └── PaymentType.php
├── Events
│ ├── RideRequestBroadcast.php
│ ├── RideRequestConfirmed.php
│ ├── RideRequestConfirmedByOthers.php
│ └── RideRequestCreated.php
├── Http
│ ├── Controllers
│ │ ├── Auth
│ │ │ ├── Driver
│ │ │ │ └── AuthController.php
│ │ │ └── User
│ │ │ │ └── AuthController.php
│ │ ├── Controller.php
│ │ ├── DriverController.php
│ │ ├── PaymentController.php
│ │ ├── RideController.php
│ │ ├── RideRequestController.php
│ │ └── UserController.php
│ ├── Requests
│ │ ├── AcceptRideRequestRequest.php
│ │ ├── CompleteRideRequest.php
│ │ ├── NewRideRequestRequest.php
│ │ └── UpdateLocationDriverRequest.php
│ └── Resources
│ │ └── DriverLocationResource.php
├── Interfaces
│ ├── Controllers
│ │ ├── DriverInterface.php
│ │ ├── RideRequestInterface.php
│ │ ├── TripInterface.php
│ │ └── UserControllerInterface.php
│ ├── Repositories
│ │ └── DriverRepositoryInterface.php
│ └── Services
│ │ ├── DistanceCalculatorServiceInterface.php
│ │ ├── PaymentGatewayInterface.php
│ │ ├── RideAcceptationServiceInterface.php
│ │ ├── RideCompletionServiceInterface.php
│ │ └── TripServiceInterface.php
├── Jobs
│ ├── SendRideRequestToDrivers.php
│ └── SyncDriverLocationsToDatabase.php
├── Listeners
│ ├── ProcessRiderAcceptRequest.php
│ └── ProcessUserRideRequest.php
├── Models
│ ├── CompleteRide.php
│ ├── CurrentRide.php
│ ├── Driver.php
│ ├── Invoice.php
│ ├── RequestDriver.php
│ ├── RideRequest.php
│ └── User.php
├── Providers
│ └── AppServiceProvider.php
├── Repositories
│ └── DriverRepository.php
└── Services
│ ├── HaversineDistanceCalculatorService.php
│ ├── Payments
│ └── ZarinpalGateway.php
│ ├── RideAcceptationService.php
│ ├── RideCompletionService.php
│ └── RidePreparationService.php
├── artisan
├── bootstrap
├── app.php
├── cache
│ └── .gitignore
└── providers.php
├── composer.json
├── composer.lock
├── config
├── app.php
├── auth.php
├── broadcasting.php
├── cache.php
├── database.php
├── filesystems.php
├── l5-swagger.php
├── logging.php
├── mail.php
├── queue.php
├── reverb.php
├── sanctum.php
├── services.php
└── session.php
├── database
├── .gitignore
├── factories
│ ├── CurrentRideFactory.php
│ ├── DriverFactory.php
│ ├── InvoiceFactory.php
│ ├── RideRequestFactory.php
│ └── UserFactory.php
├── migrations
│ ├── 0001_01_01_000000_create_users_table.php
│ ├── 0001_01_01_000001_create_cache_table.php
│ ├── 0001_01_01_000002_create_jobs_table.php
│ ├── 2025_03_22_113439_create_ride_requests_table.php
│ ├── 2025_03_22_113521_create_drivers_table.php
│ ├── 2025_03_22_113525_create_req_dri_table.php
│ ├── 2025_03_22_113540_create_current_rides_table.php
│ ├── 2025_03_22_113553_create_complete_rides_table.php
│ ├── 2025_03_22_151923_create_personal_access_tokens_table.php
│ └── 2025_04_18_092450_create_invoices_table.php
└── seeders
│ └── DatabaseSeeder.php
├── drawSQL.png
├── eer Diagram.png
├── package-lock.json
├── package.json
├── phpunit.xml
├── public
├── .htaccess
├── favicon.ico
├── index.php
└── robots.txt
├── resources
├── css
│ └── app.css
├── js
│ ├── app.js
│ ├── bootstrap.js
│ └── echo.js
└── views
│ ├── reverb.blade.php
│ ├── reverb2.blade.php
│ └── vendor
│ └── l5-swagger
│ └── index.blade.php
├── routes
├── api.php
├── channels.php
├── console.php
└── web.php
├── storage
├── app
│ ├── .gitignore
│ ├── private
│ │ └── .gitignore
│ └── public
│ │ └── .gitignore
├── framework
│ ├── .gitignore
│ ├── cache
│ │ ├── .gitignore
│ │ └── data
│ │ │ └── .gitignore
│ ├── sessions
│ │ └── .gitignore
│ ├── testing
│ │ └── .gitignore
│ └── views
│ │ └── .gitignore
└── logs
│ └── .gitignore
├── tests
├── Feature
│ ├── CompleteRideControllerTest.php
│ ├── DriverControllerTest.php
│ ├── PaymentTest.php
│ ├── StatusDriverControllerTest.php
│ ├── StatusUserContorllerTest.php
│ └── StoreRideRequestControllerTest.php
└── TestCase.php
└── vite.config.js
/.env.example:
--------------------------------------------------------------------------------
1 | APP_NAME=SnappAPI
2 | APP_ENV=local
3 | APP_KEY=base64:YOUR_APP_KEY_HERE
4 | APP_DEBUG=true
5 | APP_URL=http://localhost
6 |
7 | APP_LOCALE=en
8 | APP_FALLBACK_LOCALE=en
9 | APP_FAKER_LOCALE=en_US
10 |
11 | APP_MAINTENANCE_DRIVER=file
12 | # APP_MAINTENANCE_STORE=database
13 |
14 | PHP_CLI_SERVER_WORKERS=4
15 |
16 | BCRYPT_ROUNDS=12
17 |
18 | LOG_CHANNEL=stack
19 | LOG_STACK=single
20 | LOG_DEPRECATIONS_CHANNEL=null
21 | LOG_LEVEL=debug
22 |
23 | DB_CONNECTION=sqlite
24 | # DB_HOST=127.0.0.1
25 | # DB_PORT=3306
26 | # DB_DATABASE=laravel
27 | # DB_USERNAME=root
28 | # DB_PASSWORD=
29 |
30 | SESSION_DRIVER=database
31 | SESSION_LIFETIME=120
32 | SESSION_ENCRYPT=false
33 | SESSION_PATH=/
34 | SESSION_DOMAIN=null
35 |
36 | BROADCAST_CONNECTION=reverb
37 | FILESYSTEM_DISK=local
38 | QUEUE_CONNECTION=database
39 |
40 | CACHE_STORE=database
41 | # CACHE_PREFIX=
42 |
43 | MEMCACHED_HOST=127.0.0.1
44 |
45 | REDIS_CLIENT=phpredis
46 | REDIS_HOST=127.0.0.1
47 | REDIS_PASSWORD=null
48 | REDIS_PORT=6379
49 |
50 | MAIL_MAILER=log
51 | MAIL_SCHEME=null
52 | MAIL_HOST=127.0.0.1
53 | MAIL_PORT=2525
54 | MAIL_USERNAME=null
55 | MAIL_PASSWORD=null
56 | MAIL_FROM_ADDRESS="hello@example.com"
57 | MAIL_FROM_NAME="${APP_NAME}"
58 |
59 | AWS_ACCESS_KEY_ID=
60 | AWS_SECRET_ACCESS_KEY=
61 | AWS_DEFAULT_REGION=us-east-1
62 | AWS_BUCKET=
63 | AWS_USE_PATH_STYLE_ENDPOINT=false
64 |
65 | VITE_APP_NAME="${APP_NAME}"
66 |
67 | REVERB_APP_ID=230787
68 | REVERB_APP_KEY=k75ey9jnkcfszvcmwebi
69 | REVERB_APP_SECRET=yxvp4mhth4hazlg5gvei
70 | REVERB_HOST="localhost"
71 | REVERB_PORT=8080
72 | REVERB_SCHEME=http
73 |
74 |
75 | VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
76 | VITE_REVERB_HOST="${REVERB_HOST}"
77 | VITE_REVERB_PORT="${REVERB_PORT}"
78 | VITE_REVERB_SCHEME="${REVERB_SCHEME}"
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.phpunit.cache
2 | /node_modules
3 | /public/build
4 | /public/hot
5 | /public/storage
6 | /storage/*.key
7 | /storage/pail
8 | /storage/api-docs
9 | /vendor
10 | .env
11 | .env.backup
12 | .env.production
13 | .phpactor.json
14 | .phpunit.result.cache
15 | Homestead.json
16 | Homestead.yaml
17 | npm-debug.log
18 | yarn-error.log
19 | /auth.json
20 | /.fleet
21 | /.idea
22 | /.nova
23 | /.vscode
24 | /.zed
25 | .editorconfig
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **SnappAPI – Online Ride Request System 🚖**
2 |
3 | #### Overall System Diagram:
4 |
5 |
6 |
7 | #### Project EER Diagram:
8 |
9 |
10 | #### Project SQL Tables:
11 |
12 |
13 |
14 | SnappAPI is an online taxi request system developed using Laravel 12. This project enables users to request rides, manage trip statuses, and facilitates interaction between passengers and drivers.
15 |
16 | ---
17 |
18 | ### 📌 **Project Workflow**
19 |
20 | 1. **Request a Ride**: Each user can submit a ride request using the `store` method.
21 |
22 | 2. **Request Processing**: The ride request is processed by a **Listener**, which calculates the distance between the origin and destination. Based on this distance, the **trip cost** is calculated.
23 |
24 | 3. **Broadcast to Drivers**: Ride details (origin, destination, distance, and price) are broadcasted via the `driver` **channel** to the nearest available drivers.
25 |
26 | 4. **Driver Accepts Request**: If a driver accepts the request using the `accept` method, the driver's and request information is broadcasted on the `users` **channel**.
27 |
28 | 5. **Track Ride Status**: Users can check their current trip status with the `status` method (e.g., whether the driver has arrived).
29 | Drivers can also monitor their current status using the `status` method.
30 |
31 | 6. **Complete Ride**: Once the ride finishes, the driver signals completion with the `complete` method, and their status changes back to "available."
32 |
33 |
34 | ---
35 |
36 | ### 🔥 **Technologies Used**
37 |
38 | - **Laravel 12** 🚀 (Primary backend framework)
39 |
40 | - **Laravel Sanctum** 🔐 (User and driver authentication)
41 |
42 | - **Redis** ⚡ (Caching and performance optimization)
43 |
44 | - **Laravel Reverb** 📡 (Event broadcasting and real-time communication)
45 |
46 | - **SQLite** 🛢️ (Primary database)
47 |
48 | - **l5-swagger** 📜 (API documentation)
49 |
50 | - **PhpUnit** 🧪 (Code quality testing)
51 |
52 |
53 | ---
54 |
55 | ### 🔧 Installation & Setup
56 |
57 | 1. Clone the project:
58 |
59 |
60 | ```bash
61 | git clone git@github.com:pouria-azad/SnappAPI.git
62 | cd SnappAPI
63 | ```
64 |
65 | 2. Install dependencies:
66 |
67 |
68 | ```bash
69 | composer install
70 | npm install
71 | ```
72 |
73 | 3. Configure `.env` file with database details:
74 |
75 |
76 | ```bash
77 | cp .env.example .env
78 | php artisan key:generate
79 | ```
80 |
81 | 4. Run migrations and seed database:
82 |
83 |
84 | ```bash
85 | php artisan migrate --seed
86 | ```
87 |
88 | 5. Generate API documentation:
89 |
90 |
91 | ```bash
92 | php artisan l5-swagger:generate
93 | ```
94 |
95 | 6. Start Redis for optimized performance:
96 |
97 |
98 | ```bash
99 | redis-server
100 | ```
101 |
102 | 7. Run the project:
103 |
104 |
105 | ```bash
106 | php artisan serve
107 | php artisan reverb:start
108 | php artisan queue:work
109 | ```
110 |
111 | ---
112 |
113 | ### 🛠️ **Key Features**
114 |
115 | ###### **Concurrent Request Limitations:**
116 |
117 | ⏳ A user cannot submit a new request until their current request is accepted or ongoing trip is completed.
118 | 🚗 Similarly, drivers cannot accept new requests while on a trip.
119 |
120 | ###### **Improved Request Processing:**
121 |
122 | 🔄 Ride requests are currently processed via a **Listener**.
123 | 📊 The database keeps track of drivers who have received the request. Future enhancements could use **Jobs** to manage request processing, gradually expanding the search radius for nearby drivers every few seconds—similar to Snapp.
124 |
125 | ###### **Token-based Authentication:**
126 |
127 | 🔑 All users (drivers and passengers) authenticate using **Sanctum tokens**.
128 |
129 | ###### **Complete Documentation:**
130 |
131 | 📜 All methods are fully documented, and **PHPUnit tests** cover functionality.
132 |
133 | ---
134 |
135 | ### 👨💻 **Developer**
136 |
137 | [**Pouria Azad**](https://www.linkedin.com/in/pouria-azad)
138 |
139 | ---
140 |
141 | ### Contributors
142 |
143 | ### 👤 Amir Hossein Taghizadeh
144 |
145 | - **Role:** Developer
146 |
147 | - **GitHub:** [Amyrosein](https://github.com/Amyrosein)
148 |
149 |
150 | ---
151 |
152 | 📌 This project is designed as a clone of **Snapp / Uber**, with extensibility and customization in mind.
153 |
154 | 🚀 **Feel free to contribute or suggest improvements on GitHub!** 😎
155 |
156 | ---
157 |
158 | ## License
159 |
160 | This project is licensed under the **GNU General Public License v3.0** — see the [LICENSE](https://chatgpt.com/LICENSE) file for details.
161 |
--------------------------------------------------------------------------------
/README.persian.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | **SnappAPI – سیستم درخواست سفر آنلاین 🚖**
4 |
5 | #### دیاگرام عملکرد کلی برنامه:
6 |
7 |
8 | #### EER diagram پروژه:
9 |
10 |
11 | #### SQL tables پروژه:
12 |
13 |
14 | SnappAPI یک سیستم درخواست تاکسی آنلاین است که با استفاده از Laravel 12 توسعه داده شده است. این پروژه امکان درخواست سفر، مدیریت وضعیت سفر و تعامل میان کاربران و رانندگان را فراهم میکند.
15 |
16 | ---
17 |
18 | ### 📌 **عملکرد کلی پروژه**
19 | 1. **ثبت درخواست سفر**: هر کاربر میتواند با استفاده از متد `store` درخواست سفر ثبت کند.
20 |
21 | 2. **پردازش درخواست**: درخواست سفر در یک **Listener** پردازش شده و فاصلهی مبدأ و مقصد محاسبه میشود. بر اساس این فاصله، **هزینه سفر** نیز محاسبه خواهد شد.
22 |
23 | 3. **برادکست به رانندگان**: اطلاعات سفر (مبدا، مقصد، فاصله و قیمت) برای نزدیکترین رانندگان در دسترس، روی **کانال** `**driver**` برادکست میشود.
24 |
25 | 4. **پذیرش درخواست توسط راننده**: در صورتی که یک راننده درخواست را با متد `accept` بپذیرد، اطلاعات راننده و درخواست، روی **کانال** `**users**` برادکست میشود.
26 |
27 | 5. **پیگیری وضعیت**: کاربر میتواند با متد `status` وضعیت فعلی سفر خود را بررسی کند (مثلاً مشاهده کند که آیا راننده رسیده است یا خیر).
28 | 6. راننده میتواند با متد `status` وضعیت فعلی خود را در سیستم مشاهده کند.
29 |
30 | 7. **پایان سفر**: پس از اتمام سفر، راننده با متد `complete` پایان سفر را اعلام میکند و وضعیت او دوباره به "در دسترس" تغییر مییابد.
31 |
32 | ---
33 |
34 | ### 🔥 **تکنولوژیهای استفادهشده**
35 | - **Laravel 12** 🚀 (بکاند اصلی)
36 | - **Laravel Sanctum** 🔐 (احراز هویت کاربران و رانندگان)
37 | - **Redis** ⚡ (کشینگ و بهینهسازی عملکرد)
38 | - **Laravel Reverb** 📡 (برادکست رویدادها و ارتباط زنده)
39 | - **SQLite** 🛢 (پایگاه داده اصلی)
40 | - **l5-swagger** 📜 (مستندسازی API)
41 | - **PhpUnit** 🧪 (تست کیفیت کد)
42 |
43 | ---
44 |
45 | ### 🔧 نصب و راهاندازی
46 | ۱. پروژه رو کلون کن:
47 | ```bash
48 | git clone git@github.com:pouria-azad/SnappAPI.git
49 | cd SnappAPI
50 | ```
51 | ۲. وابستگیها رو نصب کن:
52 | ```bash
53 | composer install
54 | npm install
55 | ```
56 | ۳. فایل **.env** رو تنظیم کن و اطلاعات دیتابیس رو وارد کن:
57 | ```bash
58 | cp .env.example .env
59 | php artisan key:generate
60 | ```
61 | ۴. دیتابیس رو migrate کن:
62 | ```bash
63 | php artisan migrate --seed
64 | ```
65 |
66 | ۵. داکیومنت هارو بساز کن:
67 | ```bash
68 | php artisan l5-swagger:generate
69 | ```
70 |
71 | ۶. اجرای Redis برای بهینهسازی سرعت:
72 |
73 | ```bash
74 | redis-server
75 | ```
76 |
77 | ۷. پروژه رو اجرا کن:
78 | ```bash
79 | php artisan serve
80 | php artisan reverb:start
81 | php artisan queue:work
82 | ```
83 | ---
84 | ### 🛠 **ویژگیهای کلیدی**
85 |
86 | ###### **محدودیت درخواستهای همزمان**:
87 | ⏳ تا زمانی که درخواست فعلی کاربر پذیرفته نشده یا سفر فعلی او به پایان نرسیده، نمیتواند درخواست جدیدی ثبت کند.
88 | 🚗 راننده نیز تا زمانی که در یک سفر باشد، نمیتواند درخواست جدیدی را بپذیرد.
89 |
90 | ###### **بهبود فرآیند پردازش درخواستها**:
91 | 🔄 در حال حاضر، پردازش درخواست سفر از طریق **Listener** انجام میشود.
92 | 📊 اطلاعات رانندگانی که درخواست را دریافت کردهاند در دیتابیس ذخیره میشود. این امکان در آینده وجود دارد که با استفاده از **Job** پردازش درخواستها را مدیریت کنیم و مانند اسنپ، هر چند ثانیه یکبار دامنهی جستجوی رانندگان نزدیک را افزایش دهیم.
93 |
94 | ###### **احراز هویت با توکن**:
95 | 🔑 تمامی کاربران (رانندگان و مسافران) نیاز به احراز هویت با **Sanctum Token** دارند.
96 |
97 | ###### **مستندسازی کامل**:
98 | 📜 تمامی متدها مستندسازی شده و **تستهای PHPUnit** برای آنها نوشته شده است.
99 |
100 | ---
101 |
102 | ### 👨💻 **توسعهدهنده**
103 | [**Pouria Azad**](https://www.linkedin.com/in/pouria-azad)
104 |
105 | # مشارکتکنندگان
106 |
107 |
108 | ### 👤 Amir Hossein Taghizadeh
109 | - **Role:** Developer
110 | - **GitHub:** [Amyrosein](https://github.com/Amyrosein)
111 |
112 | ---
113 |
114 |
115 | 📌 این پروژه بهعنوان یک کلون از **Snapp / Uber** طراحی شده و قابلیت گسترش و شخصیسازی دارد.
116 |
117 | 🚀 **اگر سوالی داشتی یا میخوای پروژه رو بهبود بدی، مشارکت در گیتهاب آزاد است!** 😎
118 |
119 |
120 | ## License
121 |
122 | This project is licensed under the **GNU General Public License v3.0** - see the [LICENSE](LICENSE) file for details.
123 |
124 |
--------------------------------------------------------------------------------
/Snap Clone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pourya-azad/SnappAPI/340ae7241c579e0f970955eb618892a21192c49c/Snap Clone.png
--------------------------------------------------------------------------------
/app/Enums/PaymentType.php:
--------------------------------------------------------------------------------
1 | requestId = $requestId;
28 | $this->pickup_latitude = $pickup_latitude;
29 | $this->pickup_longitude = $pickup_longitude;
30 | $this->driverIds = $driverIds;
31 | $this->tripCost = $tripCost;
32 | }
33 |
34 | /**
35 | * Get the channels the event should broadcast on.
36 | *
37 | *
38 | */
39 | public function broadcastOn()
40 | {
41 | return collect($this->driverIds)
42 | ->map(fn($driverId) => new PrivateChannel("drivers.{$driverId}"))
43 | ->all();
44 | }
45 |
46 | public function broadcastWith()
47 | {
48 | return [
49 | 'requestId' => $this->requestId,
50 | // 'driverIds' => $this->driverIds,
51 | 'pickup_latitude' => $this->pickup_latitude,
52 | 'pickup_longitude'=> $this->pickup_longitude,
53 | 'trip_cost' => $this->tripCost
54 | ];
55 | }
56 |
57 | public function broadcastAs(): string
58 | {
59 | return 'ride.request';
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/Events/RideRequestConfirmed.php:
--------------------------------------------------------------------------------
1 | driverId = $driverId;
27 |
28 | }
29 |
30 | /**
31 | * Get the channels the event should broadcast on.
32 | *
33 | * @return PrivateChannel
34 | */
35 | public function broadcastOn(): PrivateChannel
36 | {
37 | return new PrivateChannel("drivers.{$this->driverId}");
38 | }
39 |
40 | public function broadcastAs(): string
41 | {
42 | return 'rideRequest.confirmed';
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/Events/RideRequestConfirmedByOthers.php:
--------------------------------------------------------------------------------
1 | driverIds = $driverIds;
25 | }
26 |
27 | /**
28 | * Get the channels the event should broadcast on.
29 | *
30 | * @return array
31 | */
32 | public function broadcastOn(): array
33 | {
34 | return collect($this->driverIds)
35 | ->map(fn($driverId) => new PrivateChannel("drivers.{$driverId}"))
36 | ->all();
37 | }
38 |
39 | public function broadcastAs(): string
40 | {
41 | return 'rideRequest.confirmedByOthers';
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/Events/RideRequestCreated.php:
--------------------------------------------------------------------------------
1 | rideRequest = $rideRequest;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/Driver/AuthController.php:
--------------------------------------------------------------------------------
1 | validate([
45 | 'email' => 'required|email',
46 | 'password' => 'required',
47 | ]);
48 |
49 | $driver = Driver::where('email', $credentials['email'])->first();
50 |
51 | if ($driver && Hash::check($credentials['password'], $driver->password)) {
52 | $token = $driver->createToken('driver-token')->plainTextToken;
53 | return response()->json([
54 | 'token' => $token,
55 | ]);
56 | }
57 |
58 | return response()->json(['message' => 'Invalid username or password.'], 401);
59 | }
60 |
61 | /**
62 | * @OA\Post(
63 | * path="/api/drivers/logout",
64 | * summary="Driver logout",
65 | * tags={"Driver Authentication"},
66 | * security={{"Bearer": {}}},
67 | * @OA\Response(
68 | * response=200,
69 | * description="Successful logout",
70 | * @OA\JsonContent(
71 | * @OA\Property(property="message", type="string", example="Successfully logged out.")
72 | * )
73 | * ),
74 | * @OA\Response(
75 | * response=400,
76 | * description="Token not provided",
77 | * @OA\JsonContent(
78 | * @OA\Property(property="message", type="string", example="Valid token was not provided.")
79 | * )
80 | * ),
81 | * @OA\Response(
82 | * response=401,
83 | * description="User not logged in",
84 | * @OA\JsonContent(
85 | * @OA\Property(property="message", type="string", example="Please log in.")
86 | * )
87 | * )
88 | * )
89 | */
90 | public function logout(Request $request)
91 | {
92 | if (!$request->bearerToken()) {
93 | return response()->json(['message' => 'Valid token was not provided.'], 400);
94 | }
95 |
96 | $user = $request->user('driver');
97 | if (!$user) {
98 | return response()->json(['message' => 'Please log in.'], 401);
99 | }
100 |
101 | $user->currentAccessToken()->delete();
102 |
103 | return response()->json(['message' => 'Successfully logged out.']);
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/User/AuthController.php:
--------------------------------------------------------------------------------
1 | validate([
44 | 'email' => 'required|email',
45 | 'password' => 'required',
46 | ]);
47 |
48 | $user = User::where('email', $credentials['email'])->first();
49 |
50 | if ($user && Hash::check($credentials['password'], $user->password)) {
51 | $token = $user->createToken('user-token')->plainTextToken;
52 | return response()->json([
53 | 'token' => $token,
54 | ]);
55 | }
56 |
57 | return response()->json(['message' => 'Invalid username or password.'], 401);
58 | }
59 |
60 | /**
61 | * @OA\Post(
62 | * path="/api/users/logout",
63 | * summary="User logout",
64 | * tags={"User Authentication"},
65 | * security={{"Bearer": {}}},
66 | * @OA\Response(
67 | * response=200,
68 | * description="Successful logout",
69 | * @OA\JsonContent(
70 | * @OA\Property(property="message", type="string", example="Successfully logged out.")
71 | * )
72 | * ),
73 | * @OA\Response(
74 | * response=400,
75 | * description="Token not provided",
76 | * @OA\JsonContent(
77 | * @OA\Property(property="message", type="string", example="Valid token was not provided.")
78 | * )
79 | * ),
80 | * @OA\Response(
81 | * response=401,
82 | * description="User not logged in",
83 | * @OA\JsonContent(
84 | * @OA\Property(property="message", type="string", example="Please log in.")
85 | * )
86 | * )
87 | * )
88 | */
89 | public function logout(Request $request)
90 | {
91 | if (!$request->bearerToken()) {
92 | return response()->json(['message' => 'Valid token was not provided.'], 400);
93 | }
94 |
95 | $user = $request->user('user');
96 | if (!$user) {
97 | return response()->json(['message' => 'Please log in.'], 401);
98 | }
99 |
100 | $user->currentAccessToken()->delete();
101 |
102 | return response()->json(['message' => 'Successfully logged out.']);
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Controller.php:
--------------------------------------------------------------------------------
1 | validated())
72 | ->put('driver_id', $request->user('driver')->id)
73 | ->all();
74 |
75 | $locationData = $request->validated();
76 |
77 | $redisKey = "driver:location:{$validatedData['driver_id']}";
78 |
79 | Redis::setex($redisKey, 3600, json_encode($validatedData));
80 |
81 | Log::info('Driver location updated successfully: ', [$validatedData['driver_id']]);
82 |
83 | $responseData = [
84 | 'driver_id' => $validatedData['driver_id'],
85 | 'location' => $locationData,
86 | 'updated_at' => now()->toIso8601String(),
87 | ];
88 |
89 | return (new DriverLocationResource($responseData))
90 | ->additional(['message' => 'Driver location updated successfully in Redis'])
91 | ->response()
92 | ->setStatusCode(200);
93 | }
94 | catch (\Exception $e) {
95 | Log::error("Failed to update driver location: ", [$e->getMessage()]);
96 | return response()->json([
97 | 'message' => 'An error occurred while updating driver location',
98 | 'error' => config('app.debug') ? $e->getMessage() : null,
99 | ], 500);
100 | }
101 |
102 | }
103 |
104 |
105 | /**
106 | * @OA\Get(
107 | * path="/api/drivers/status",
108 | * summary="Get driver status",
109 | * description="Returns the current status of the driver based on their ongoing ride.
110 | * If a ride is accepted but not yet started, it returns a message indicating the driver is heading to the user.
111 | * If a ride has started, it informs the driver they are currently on a ride.
112 | * If there is no active ride, it states the driver is idle.",
113 | * tags={"Drivers"},
114 | * security={{"Bearer": {}}},
115 | * @OA\Response(
116 | * response=200,
117 | * description="Driver status retrieved successfully",
118 | * @OA\JsonContent(
119 | * oneOf={
120 | * @OA\Schema(
121 | * @OA\Property(property="message", type="string", example="You have accepted a request and are heading to the user!")
122 | * ),
123 | * @OA\Schema(
124 | * @OA\Property(property="message", type="string", example="You are currently on a ride, please end it!")
125 | * ),
126 | * @OA\Schema(
127 | * @OA\Property(property="message", type="string", example="You are currently idle.")
128 | * )
129 | * }
130 | * )
131 | * ),
132 | * @OA\Response(
133 | * response=401,
134 | * description="Driver not authenticated",
135 | * @OA\JsonContent(
136 | * @OA\Property(property="message", type="string", example="Unauthorized. Please log in.")
137 | * )
138 | * )
139 | * )
140 | */
141 | public function status(Request $request): JsonResponse
142 | {
143 | if (CurrentRide::where('driver_id', $request->user('driver')->id)->where('isArrived', false)->exists()) {
144 | return response()->json([
145 | 'message' => 'You have accepted a request and are heading to the user!',
146 | ], 200);
147 | }
148 | if (CurrentRide::where('driver_id', $request->user('driver')->id)->where('isArrived', true)->exists()) {
149 | return response()->json([
150 | 'message' => 'You are currently on a ride, please end it!',
151 | ], 200);
152 | }
153 | return response()->json([
154 | 'message' => 'You are currently idle.'
155 | ], 200);
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/app/Http/Controllers/PaymentController.php:
--------------------------------------------------------------------------------
1 | validate([
67 | 'ride_request_id' => ['required', 'exists:ride_requests,id'],
68 | ]);
69 | $user = $request->user('user');
70 | $invoice = Invoice::where('ride_request_id', $validated['ride_request_id'])
71 | ->where('user_id', $user->id)
72 | ->where('isPaid', false)
73 | ->first();
74 |
75 | if ( ! $invoice) {
76 | return response()->json([
77 | "message" => "No unpaid ride request!",
78 | ], 404);
79 | }
80 |
81 | $paymentResponse = $this->paymentService->request($invoice, ['email' => $user->email]);
82 | if ($paymentResponse['status']) {
83 | return response()->json([
84 | "payment_url" => $paymentResponse['payment_url'],
85 | ]);
86 | }
87 |
88 | return response()->json([
89 | "message" => $paymentResponse['message'],
90 | ], 400);
91 | }
92 |
93 | /**
94 | * Verify a payment for a ride request.
95 | *
96 | * @OA\Get(
97 | * path="/api/payment-verify/{invoice}",
98 | * summary="Verify Payment",
99 | * description="Verifies a payment for a ride request using Authority and Status parameters returned from the payment gateway.
100 | * The invoice is identified by its ID in the path.",
101 | * tags={"Payment"},
102 | * security={{"Bearer": {}}},
103 | * @OA\Parameter(
104 | * name="invoice",
105 | * in="path",
106 | * required=true,
107 | * @OA\Schema(type="integer"),
108 | * description="The ID of the invoice to verify",
109 | * example=42
110 | * ),
111 | * @OA\Parameter(
112 | * name="Authority",
113 | * in="query",
114 | * required=true,
115 | * @OA\Schema(type="string"),
116 | * description="Authority code returned by the payment gateway",
117 | * example="S00000000000000000000000000000026mxp"
118 | * ),
119 | * @OA\Parameter(
120 | * name="Status",
121 | * in="query",
122 | * required=true,
123 | * @OA\Schema(type="string"),
124 | * description="Payment status returned by the payment gateway",
125 | * example="OK"
126 | * ),
127 | * @OA\Response(
128 | * response=200,
129 | * description="Payment verification successful",
130 | * @OA\JsonContent(
131 | * @OA\Property(property="message", type="string", example="Transaction Completed")
132 | * )
133 | * ),
134 | * @OA\Response(
135 | * response=400,
136 | * description="Payment verification failed",
137 | * @OA\JsonContent(
138 | * @OA\Property(property="message", type="string", example="Transaction not verified")
139 | * )
140 | * ),
141 | * @OA\Response(
142 | * response=401,
143 | * description="User not authenticated",
144 | * @OA\JsonContent(
145 | * @OA\Property(property="message", type="string", example="Unauthorized. Please log in.")
146 | * )
147 | * )
148 | * )
149 | */
150 | public function verify(Request $request, Invoice $invoice)
151 | {
152 | $verifyData = [
153 | 'status' => $request->input('Status', 'NOK'),
154 | 'authority' => $request->input('Authority'),
155 | ];
156 |
157 | $isVerified = $this->paymentService->verify($invoice, $verifyData);
158 | if ($isVerified) {
159 | return response()->json([
160 | "message" => "Transaction Completed",
161 | ]);
162 | }
163 |
164 | return response()->json([
165 | "message" => "Transaction not verified",
166 | ], 400);
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/app/Http/Controllers/RideController.php:
--------------------------------------------------------------------------------
1 | rideCompletionService = $rideCompletionService;
22 | }
23 |
24 | /**
25 | * Completes a ride and logs the total time.
26 | *
27 | * @OA\Post(
28 | * path="/api/ride/complete",
29 | * summary="Complete a ride",
30 | * description="Marks a ride as complete using the provided trip ID and returns the total time taken in HH:MM:SS format.",
31 | * tags={"Ride"},
32 | * security={{"Bearer": {}}},
33 | * @OA\RequestBody(
34 | * required=true,
35 | * @OA\JsonContent(
36 | * required={"trip_id"},
37 | * @OA\Property(property="trip_id", type="integer", example=1, description="The ID of the trip to complete")
38 | * )
39 | * ),
40 | * @OA\Response(
41 | * response=200,
42 | * description="Ride completed successfully",
43 | * @OA\JsonContent(
44 | * @OA\Property(property="message", type="string", example="Ride completed successfully"),
45 | * @OA\Property(
46 | * property="data",
47 | * type="object",
48 | * @OA\Property(property="total_time", type="string", example="00:45:30", description="Total ride duration in HH:MM:SS format")
49 | * )
50 | * )
51 | * ),
52 | * @OA\Response(
53 | * response=400,
54 | * description="Invalid or already completed trip",
55 | * @OA\JsonContent(
56 | * @OA\Property(property="message", type="string", example="Invalid trip ID or ride already completed")
57 | * )
58 | * ),
59 | * @OA\Response(
60 | * response=401,
61 | * description="User not authenticated",
62 | * @OA\JsonContent(
63 | * @OA\Property(property="message", type="string", example="Unauthorized. Please log in.")
64 | * )
65 | * ),
66 | * @OA\Response(
67 | * response=500,
68 | * description="Server error",
69 | * @OA\JsonContent(
70 | * @OA\Property(property="message", type="string", example="An error occurred while completing the ride"),
71 | * @OA\Property(property="error", type="string", example="Detailed error message", nullable=true)
72 | * )
73 | * )
74 | * )
75 | */
76 | public function complete(CompleteRideRequest $request): JsonResponse
77 | {
78 | try {
79 | $total_time = $this->rideCompletionService->completeRide($request->trip_id);
80 |
81 | return response()->json([
82 | 'message' => 'Ride completed successfully',
83 | 'data' => [
84 | 'total_time' => $total_time
85 | ],
86 | ], 201);
87 | } catch (\Exception $e) {
88 | Log::error("An error occurred while completing the ride: ", [
89 | 'error' => $e->getMessage(),
90 | 'trip_id' => $request->trip_id ?? null,
91 | ]);
92 |
93 | return response()->json([
94 | 'message' => "An error occurred while completing the ride",
95 | 'error' => config('app.debug') ? $e->getMessage() : null,
96 | ], 500);
97 | }
98 | }
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/app/Http/Controllers/RideRequestController.php:
--------------------------------------------------------------------------------
1 | ridePreparationService = $ridePreparationService;
24 | }
25 |
26 |
27 | /**
28 | * @OA\Post(
29 | * path="/api/ride-requests/store",
30 | * summary="Create a new ride request",
31 | * tags={"Ride Requests"},
32 | * security={{"Bearer": {}}},
33 | * @OA\RequestBody(
34 | * required=true,
35 | * @OA\JsonContent(
36 | * type="object",
37 | * required={"pickup_latitude", "pickup_longitude", "dest_latitude", "dest_longitude"},
38 | * @OA\Property(property="pickup_latitude", type="number", format="float", example=35.6892, description="Latitude of the pickup location"),
39 | * @OA\Property(property="pickup_longitude", type="number", format="float", example=51.3890, description="Longitude of the pickup location"),
40 | * @OA\Property(property="dest_latitude", type="number", format="float", example=35.7000, description="Latitude of the destination"),
41 | * @OA\Property(property="dest_longitude", type="number", format="float", example=51.4000, description="Longitude of the destination")
42 | * )
43 | * ),
44 | * @OA\Response(
45 | * response=201,
46 | * description="Ride request created successfully",
47 | * @OA\JsonContent(
48 | * @OA\Property(property="message", type="string", example="Ride request created successfully"),
49 | * @OA\Property(property="request_id", type="integer", example=1),
50 | * @OA\Property(
51 | * property="data",
52 | * type="object",
53 | * @OA\Property(property="id", type="integer", example=1, description="Ride request ID"),
54 | * @OA\Property(property="user_id", type="integer", example=1, description="User ID"),
55 | * @OA\Property(property="cost", type="number", format="float", example=15000.50, description="Cost of the ride"),
56 | * @OA\Property(property="distance_km", type="number", format="float", example=5.2, description="Distance in kilometers")
57 | * )
58 | * )
59 | * ),
60 | * @OA\Response(
61 | * response=400,
62 | * description="Bad request due to existing pending request or current ride",
63 | * @OA\JsonContent(
64 | * oneOf={
65 | * @OA\Schema(
66 | * @OA\Property(property="message", type="string", example="You already have a pending request!")
67 | * ),
68 | * @OA\Schema(
69 | * @OA\Property(property="message", type="string", example="You are already assigned to another ride")
70 | * )
71 | * }
72 | * )
73 | * ),
74 | * @OA\Response(
75 | * response=401,
76 | * description="User not authenticated",
77 | * @OA\JsonContent(
78 | * @OA\Property(property="message", type="string", example="Unauthorized. Please log in.")
79 | * )
80 | * ),
81 | * @OA\Response(
82 | * response=500,
83 | * description="Server error",
84 | * @OA\JsonContent(
85 | * @OA\Property(property="message", type="string", example="An error occurred while creating the ride request"),
86 | * @OA\Property(property="error", type="string", example="Detailed error message", nullable=true)
87 | * )
88 | * )
89 | * )
90 | */
91 | public function store(NewRideRequestRequest $request): JsonResponse
92 | {
93 | try {
94 | $trip = $this->ridePreparationService->calculateTripCost(
95 | $request['pickup_latitude'],
96 | $request['pickup_longitude'],
97 | $request['dest_latitude'],
98 | $request['dest_longitude']
99 | );
100 |
101 | $validatedData = collect($request->validated())
102 | ->put('user_id', $request->user('user')->id)
103 | ->put('cost', $trip['cost'])
104 | ->put('distance_km', $trip['distance_km'])
105 | ->all();
106 |
107 |
108 | if (RideRequest::where('user_id', $validatedData['user_id'])->where('isPending', true)->exists()) {
109 | return response()->json([
110 | 'message' => "You already have a pending request!",
111 | ], 400);
112 | }
113 |
114 | if (CurrentRide::where('user_id', $validatedData['user_id'])->exists()) {
115 | return response()->json([
116 | 'message' => 'you are already assigned to another ride',
117 | ], 400);
118 | }
119 |
120 | $rideRequest = RideRequest::create($validatedData);
121 |
122 | Log::info('Ride request created successfully', [
123 | 'request_id' => $rideRequest->id,
124 | 'user_id' => $request->user('user')->id ?? null,
125 | 'data' => $request->validated(),
126 | ]);
127 |
128 | event(new RideRequestCreated($rideRequest));
129 |
130 | return response()->json([
131 | 'message' => 'Ride request created successfully',
132 | 'request_id' => $rideRequest->id,
133 | 'data' => $rideRequest->only(['id', 'user_id', 'cost', 'distance_km']),
134 | ], 201);
135 | } catch (\Exception $e) {
136 | Log::error('Failed to create ride request', [
137 | 'error' => $e->getMessage(),
138 | 'trace' => $e->getTraceAsString(),
139 | 'data' => $request->all(),
140 | ]);
141 |
142 | return response()->json([
143 | 'message' => 'An error occurred while creating the ride request',
144 | 'error' => config('app.debug') ? $e->getMessage() : null,
145 | ], 500);
146 | }
147 | }
148 |
149 | /**
150 | * @OA\Get (
151 | * path="/api/ride-requests/cancel",
152 | * summary="Cancel a pending ride request",
153 | * tags={"Ride Requests"},
154 | * security={{"Bearer": {}}},
155 | * @OA\Response(
156 | * response=204,
157 | * description="Pending ride request cancelled successfully."
158 | * ),
159 | * @OA\Response(
160 | * response=404,
161 | * description="No Pending request found.",
162 | * @OA\JsonContent(
163 | * @OA\Property(property="message", type="string", example="No Pending Request!")
164 | * )
165 | * ),
166 | * @OA\Response(
167 | * response=401,
168 | * description="User not authenticated",
169 | * @OA\JsonContent(
170 | * @OA\Property(property="message", type="string", example="Unauthorized. Please log in.")
171 | * )
172 | * ),
173 | * )
174 | */
175 | public function cancel(Request $request): JsonResponse
176 | {
177 | $userId = $request->user('user')->id;
178 |
179 | $pendingRideRequest = RideRequest::where('user_id', $userId)
180 | ->where('isPending', true)
181 | ->first();
182 |
183 | if ($pendingRideRequest) {
184 | $pendingRideRequest->delete();
185 |
186 | return response()->json(status: 204);
187 | }
188 |
189 | return response()->json([
190 | "message" => "No Pending Request!",
191 | ], 404);
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/app/Http/Controllers/UserController.php:
--------------------------------------------------------------------------------
1 | user('user')->id)->where('isPending' , true)->exists()) {
57 | return response()->json([
58 | 'message' => "Your request is pending, please wait!",
59 | ], 200);
60 | }
61 | if (CurrentRide::where('user_id', $request->user('user')->id)->where('isArrived', false)->exists()) {
62 | return response()->json([
63 | 'message' => "Your request has been accepted, and the driver is on the way. Please wait!",
64 | ], 200);
65 | }
66 | if (CurrentRide::where('user_id', $request->user('user')->id)->where('isArrived', true)->exists()) {
67 | return response()->json([
68 | 'message' => "You are currently on a ride, please end it!",
69 | ], 200);
70 | }
71 | return response()->json([
72 | 'message' => "You are currently idle."
73 | ]);
74 |
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/Http/Requests/AcceptRideRequestRequest.php:
--------------------------------------------------------------------------------
1 | |string>
21 | */
22 | public function rules(): array
23 | {
24 | return [
25 | 'request_id' => 'required|exists:ride_requests,id'
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Http/Requests/CompleteRideRequest.php:
--------------------------------------------------------------------------------
1 | |string>
21 | */
22 | public function rules(): array
23 | {
24 | return [
25 | "trip_id" => "required|exists:current_rides,id"
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Http/Requests/NewRideRequestRequest.php:
--------------------------------------------------------------------------------
1 | |string>
21 | */
22 | public function rules(): array
23 | {
24 | return [
25 | 'pickup_latitude'=> 'required|numeric',
26 | 'pickup_longitude'=> 'required|numeric',
27 | 'dest_latitude'=> 'required|numeric',
28 | 'dest_longitude'=> 'required|numeric',
29 | ];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/Http/Requests/UpdateLocationDriverRequest.php:
--------------------------------------------------------------------------------
1 | |string>
21 | */
22 | public function rules(): array
23 | {
24 | return [
25 | 'latitude' => 'required|numeric',
26 | 'longitude' => 'required|numeric',
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/Http/Resources/DriverLocationResource.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | public function toArray(Request $request): array
16 | {
17 | return [
18 | 'driver_id' => $this['driver_id'],
19 | 'location' => $this['location'],
20 | 'updated_at' => $this['updated_at'],
21 | ];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Interfaces/Controllers/DriverInterface.php:
--------------------------------------------------------------------------------
1 | rideRequestId = $rideRequestId;
30 | $this->radius = $radius;
31 | $this->nextRadiusIndex = $nextRadiusIndex;
32 | }
33 |
34 | /**
35 | * Execute the job.
36 | */
37 | public function handle(RidePreparationService $ridePreparationService): void
38 | {
39 | $rideRequest = RideRequest::find($this->rideRequestId);
40 | if ($rideRequest->isPending == 0) {
41 | return;
42 | }
43 |
44 | $nearbyDrivers = $ridePreparationService->findNearbyDrivers(
45 | $rideRequest->pickup_latitude,
46 | $rideRequest->pickup_longitude,
47 | $this->radius
48 | );
49 |
50 | $driverIds = collect([]);
51 | foreach ($nearbyDrivers as $nearbyDriver)
52 | {
53 | $exists = RequestDriver::where('request_id', $rideRequest->id)
54 | ->where('driver_id', $nearbyDriver->id)
55 | ->exists();
56 |
57 | if (!$exists) {
58 | $driverIds->push($nearbyDriver->id);
59 | RequestDriver::create([
60 | 'request_id' => $rideRequest->id,
61 | 'driver_id' => $nearbyDriver->id,
62 | ]);
63 | }
64 | }
65 |
66 | if ($driverIds->isNotEmpty())
67 | {
68 | event(new RideRequestBroadcast(
69 | $rideRequest->id,
70 | $rideRequest->pickup_latitude,
71 | $rideRequest->pickup_longitude,
72 | $driverIds->all(),
73 | $rideRequest->cost
74 | ));
75 | }
76 |
77 | $radiuses = [50, 100, 200, 500, 999999999];
78 | if($this->nextRadiusIndex + 1 < count($radiuses))
79 | {
80 | SendRideRequestToDrivers::dispatch(
81 | $this->rideRequestId,
82 | $radiuses[$this->nextRadiusIndex + 1],
83 | $this->nextRadiusIndex + 1
84 | )->delay(now()->addSeconds(15));
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/Jobs/SyncDriverLocationsToDatabase.php:
--------------------------------------------------------------------------------
1 | update([
38 | 'latitude' => $locationData['latitude'] ?? null,
39 | 'longitude' => $locationData['longitude'] ?? null,
40 | ]);
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/Listeners/ProcessRiderAcceptRequest.php:
--------------------------------------------------------------------------------
1 | message);
25 | $data = $message->data;
26 |
27 | // check if rider accepts a ride request
28 | if ($message->event !== 'riderAcceptRequest' || $message->channel !== 'private-driver.' . $data->driverId) {
29 | return;
30 | }
31 |
32 | // Accept Ride Request Logic
33 | $this->ride_acceptation_service->handle($data->driverId, $data->requestId);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/Listeners/ProcessUserRideRequest.php:
--------------------------------------------------------------------------------
1 | rideRequest->id, 5, 0);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Models/CompleteRide.php:
--------------------------------------------------------------------------------
1 | hasOne(Driver::class,"id","driver_id");
17 | }
18 |
19 | public function Request(): HasOne
20 | {
21 | return $this->hasOne(RideRequest::class,"id","request_id");
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/Models/Driver.php:
--------------------------------------------------------------------------------
1 |
23 | */
24 | protected $hidden = [
25 | 'password',
26 | 'remember_token',
27 | ];
28 |
29 | public function Request(): BelongsToMany
30 | {
31 | return $this->belongsToMany(RideRequest::class, RequestDriver::class, 'driver_id', 'request_id');
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Models/Invoice.php:
--------------------------------------------------------------------------------
1 | 'boolean',
26 | ];
27 | }
28 |
29 | public function user(): BelongsTo
30 | {
31 | return $this->belongsTo(User::class);
32 | }
33 |
34 | public function rideRequest(): BelongsTo
35 | {
36 | return $this->belongsTo(RideRequest::class);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Models/RequestDriver.php:
--------------------------------------------------------------------------------
1 | 'float',
20 | 'pickup_longitude' => 'float',
21 | 'dest_latitude' => 'float',
22 | 'dest_longitude' => 'float',
23 | ];
24 |
25 | public function CurrentRide(): BelongsTo
26 | {
27 | return $this->belongsTo(CurrentRide::class,'id','request_id');
28 | }
29 |
30 | public function invoice(): HasOne
31 | {
32 | return $this->hasOne(Invoice::class);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/Models/User.php:
--------------------------------------------------------------------------------
1 | */
15 | use HasFactory, Notifiable, HasApiTokens;
16 |
17 | /**
18 | * The attributes that are mass assignable.
19 | *
20 | * @var list
21 | */
22 | protected $fillable = [
23 | 'name',
24 | 'email',
25 | 'password',
26 | ];
27 |
28 | /**
29 | * The attributes that should be hidden for serialization.
30 | *
31 | * @var list
32 | */
33 | protected $hidden = [
34 | 'password',
35 | 'remember_token',
36 | ];
37 |
38 | /**
39 | * Get the attributes that should be cast.
40 | *
41 | * @return array
42 | */
43 | protected function casts(): array
44 | {
45 | return [
46 | 'email_verified_at' => 'datetime',
47 | 'password' => 'hashed',
48 | ];
49 | }
50 |
51 | public function invoices(): HasMany
52 | {
53 | return $this->hasMany(Invoice::class);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/Providers/AppServiceProvider.php:
--------------------------------------------------------------------------------
1 | DriverRepository::class,
27 | DistanceCalculatorServiceInterface::class => HaversineDistanceCalculatorService::class,
28 | RideCompletionServiceInterface::class => RideCompletionService::class,
29 | RideAcceptationServiceInterface::class => RideAcceptationService::class,
30 | PaymentGatewayInterface::class => ZarinpalGateway::class
31 | ];
32 |
33 | public function register()
34 | {
35 | }
36 |
37 | /**
38 | * Bootstrap any application services.
39 | */
40 | public function boot(): void
41 | {
42 | //
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/Repositories/DriverRepository.php:
--------------------------------------------------------------------------------
1 | where('is_active', true)
17 | ->whereRaw("distance <= ?", [$radius]) // استفاده از whereRaw به جای having
18 | ->orderBy('distance')
19 | ->get();
20 |
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Services/HaversineDistanceCalculatorService.php:
--------------------------------------------------------------------------------
1 | merchantId = config('services.zarinpal.merchant_id');
28 | $this->sandbox = config('services.zarinpal.sandbox');
29 |
30 | $isSandboxPreFix = $this->sandbox ? 'sandbox' : 'payment';
31 | $this->zpPaymentEndPoint = "https://{$isSandboxPreFix}.zarinpal.com/pg/v4/payment/";
32 | $this->zpStartPgEndPoint = "https://{$isSandboxPreFix}.zarinpal.com/pg/StartPay/";
33 | }
34 |
35 | public function request(Invoice $invoice, array $metadata = []): array
36 | {
37 | $paymentData = [
38 | 'merchant_id' => $this->merchantId,
39 | 'amount' => $invoice->amount,
40 | 'callback_url' => route('payment-verify', $invoice),
41 | 'description' => 'پرداخت سفر : ' . $invoice->id,
42 | ];
43 | if (isset($metadata['phone'])) {
44 | $paymentData['metadata']['phone'] = $metadata['phone'];
45 | }
46 | if (isset($metadata['email'])) {
47 | $paymentData['metadata']['email'] = $metadata['email'];
48 | }
49 | try {
50 | $response = Http::post($this->zpPaymentEndPoint . 'request.json', $paymentData);
51 | if (empty($response['errors'])) {
52 | $paymentUrl = $this->zpStartPgEndPoint . $response['data']["authority"];
53 | $invoice->update([
54 | 'authority' => $response['data']["authority"],
55 | ]);
56 |
57 | Log::info('payment created for invoice :' . $invoice->id, [
58 | "data" => $response,
59 | ]);
60 |
61 | return ['status' => true, 'payment_url' => $paymentUrl];
62 | } else {
63 | Log::error('payment failed to create for invoice :' . $invoice->id, [
64 | "error" => $response['errors'],
65 | ]);
66 |
67 | return ['status' => false, 'message' => $response['errors']];
68 | }
69 | } catch (\Exception $e) {
70 | Log::error('exception caught for payment for invoice :' . $invoice->id, [
71 | "error" => $e->getMessage(),
72 | "errorType" => $e,
73 | ]);
74 |
75 | return ['status' => false, 'message' => $e->getMessage()];
76 | }
77 | }
78 |
79 | public function verify(Invoice $invoice, array $verifyData = []): bool
80 | {
81 | $status = $verifyData['status'];
82 | $authority = $verifyData['authority'];
83 | if ($status === 'NOK') {
84 | Log::error('payment for invoice :' . $invoice->id, [
85 | "error" => "Transaction was cancelled or failed.",
86 | ]);
87 |
88 | return false;
89 | }
90 | // check authority is for this invoice
91 | if ($invoice->authority !== $authority){
92 | Log::error("Payment verification failed: authority mismatch", [
93 | 'invoice_id' => $invoice->id,
94 | 'expected' => $invoice->authority,
95 | 'received' => $authority,
96 | ]);
97 | return false;
98 | }
99 |
100 | $verifyData = [
101 | 'merchant_id' => $this->merchantId,
102 | 'amount' => $invoice->amount,
103 | 'authority' => $authority,
104 | ];
105 | try {
106 | $response = Http::post($this->zpPaymentEndPoint . 'verify.json', $verifyData);
107 |
108 | if ($response['data']['code'] === 100) {
109 | Log::info('payment for invoice :' . $invoice->id, [
110 | "Reference ID: " => $response['data']['ref_id'],
111 | "Card PAN: " => $response['data']['card_pan'],
112 | "Fee: " => $response['data']['fee'],
113 | ]);
114 |
115 | // update invoice
116 | $invoice->isPaid = true;
117 | $invoice->paid_at = now();
118 | $invoice->save();
119 |
120 | return true;
121 | } elseif ($response['data']['code'] === 101) {
122 | return true;
123 | } else {
124 | Log::error('payment for invoice :' . $invoice->id, [
125 | "error" => "Transaction failed with code: " . $response['data']['code'],
126 | ]);
127 |
128 | return false;
129 | }
130 | } catch (\Exception $e) {
131 | Log::error('payment for invoice :' . $invoice->id, [
132 | "error" => "Transaction failed with error message: ",
133 | "error data" => $e->getMessage()
134 | ]);
135 |
136 | return false;
137 | }
138 | }
139 |
140 | }
141 |
--------------------------------------------------------------------------------
/app/Services/RideAcceptationService.php:
--------------------------------------------------------------------------------
1 | $driverId,
27 | 'request_id' => $requestId,
28 | 'user_id' => $rideRequest->user_id ?? null,
29 | ];
30 |
31 | // Check if the driver is already assigned to another ride
32 | if (CurrentRide::where('driver_id', $validatedData['driver_id'])->exists()) {
33 | return;
34 | }
35 |
36 | DB::transaction(function () use ($rideRequest, $validatedData) {
37 | // Update the ride request status
38 | $rideRequest->update(['isPending' => false]);
39 |
40 | // Update driver id status
41 | Driver::findOrFail($validatedData['driver_id'])->update(['is_active' => false]);
42 |
43 | // Create a new current ride
44 | CurrentRide::create($validatedData);
45 |
46 | // Trigger event (will triggered after transaction commit)
47 | event(new RideRequestConfirmed($validatedData['driver_id']));
48 | });
49 |
50 | // Create Invoice for this Ride
51 | Invoice::create([
52 | 'user_id' => $validatedData['user_id'],
53 | 'ride_request_id' => $validatedData['request_id'],
54 | 'amount' => $rideRequest->cost,
55 | ]);
56 |
57 | // tell other drivers that this ride request is taken.
58 | $otherDriverIds = RequestDriver::where('request_id', $validatedData['request_id'])
59 | ->where('driver_id', '!=', $validatedData['driver_id'])
60 | ->pluck('driver_id')
61 | ->toArray();
62 | event(new RideRequestConfirmedByOthers($otherDriverIds));
63 |
64 | Log::info('Ride Request Accepted :', [
65 | 'requestId' => $requestId,
66 | 'driverId' => $driverId,
67 | ]);
68 |
69 | return;
70 | } catch (\Exception $e) {
71 | Log::error('Failed to accept ride request', [
72 | 'error' => $e->getMessage(),
73 | 'trace' => $e->getTraceAsString(),
74 | 'data' => [
75 | 'requestId' => $requestId,
76 | 'driverId' => $driverId,
77 | ],
78 | ]);
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/Services/RideCompletionService.php:
--------------------------------------------------------------------------------
1 | diffInHours($currentRide->created_at);
19 |
20 | $rideData = collect($currentRide->getAttributes())
21 | ->put('total_time', $totalTimeInHours)
22 | ->except(['updated_at', 'created_at', 'isArrived'])
23 | ->all();
24 |
25 | Driver::findOrFail($currentRide['driver_id'])->update(['is_active' => true]);
26 |
27 | CompleteRide::create($rideData);
28 | $currentRide->delete();
29 |
30 | \Log::info('Ride completed successfully', [
31 | 'trip_id' => $tripId,
32 | 'total_time' => $totalTimeInHours,
33 | ]);
34 |
35 | return $totalTimeInHours;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/Services/RidePreparationService.php:
--------------------------------------------------------------------------------
1 | driverRepository = $driverRepository;
21 | $this->distanceCalculatorService = $distanceCalculatorService;
22 | }
23 |
24 | public function findNearbyDrivers(float $pickupLatitude, float $pickupLongitude, float $radius = 5): Collection
25 | {
26 | return $this->driverRepository->findNearbyDrivers($pickupLatitude, $pickupLongitude, $radius);
27 | }
28 |
29 | public function calculateTripCost(float $pickupLatitude, float $pickupLongitude, float $destLatitude, float $destLongitude): array
30 | {
31 | if (!is_numeric($pickupLatitude) || !is_numeric($pickupLongitude) ||
32 | !is_numeric($destLatitude) || !is_numeric($destLongitude)) {
33 | throw new \InvalidArgumentException('Coordinates must be numeric');
34 | }
35 |
36 | $distance = $this->distanceCalculatorService->calculate($pickupLatitude, $pickupLongitude, $destLatitude, $destLongitude);
37 | $cost = self::BASE_COST + ($distance * self::COST_PER_KM);
38 |
39 | return [
40 | 'distance_km' => round($distance, 2),
41 | 'cost' => (int) round($cost),
42 | 'currency' => 'IRR',
43 | ];
44 | }
45 | }
--------------------------------------------------------------------------------
/artisan:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | handleCommand(new ArgvInput);
17 |
18 | exit($status);
19 |
--------------------------------------------------------------------------------
/bootstrap/app.php:
--------------------------------------------------------------------------------
1 | withRouting(
9 | web: __DIR__.'/../routes/web.php',
10 | api: __DIR__.'/../routes/api.php',
11 | commands: __DIR__.'/../routes/console.php',
12 | channels: __DIR__.'/../routes/channels.php',
13 | health: '/up',
14 | )
15 | ->withMiddleware(function (Middleware $middleware) {
16 | //
17 | })
18 | ->withExceptions(function (Exceptions $exceptions) {
19 | //
20 | })->create();
21 |
--------------------------------------------------------------------------------
/bootstrap/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/bootstrap/providers.php:
--------------------------------------------------------------------------------
1 | env('APP_NAME', 'Laravel'),
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Application Environment
23 | |--------------------------------------------------------------------------
24 | |
25 | | This value determines the "environment" your application is currently
26 | | running in. This may determine how you prefer to configure various
27 | | services the application utilizes. Set this in your ".env" file.
28 | |
29 | */
30 |
31 | 'env' => env('APP_ENV', 'production'),
32 |
33 | /*
34 | |--------------------------------------------------------------------------
35 | | Application Debug Mode
36 | |--------------------------------------------------------------------------
37 | |
38 | | When your application is in debug mode, detailed error messages with
39 | | stack traces will be shown on every error that occurs within your
40 | | application. If disabled, a simple generic error page is shown.
41 | |
42 | */
43 |
44 | 'debug' => (bool) env('APP_DEBUG', false),
45 |
46 | /*
47 | |--------------------------------------------------------------------------
48 | | Application URL
49 | |--------------------------------------------------------------------------
50 | |
51 | | This URL is used by the console to properly generate URLs when using
52 | | the Artisan command line tool. You should set this to the root of
53 | | the application so that it's available within Artisan commands.
54 | |
55 | */
56 |
57 | 'url' => env('APP_URL', 'http://localhost'),
58 |
59 | /*
60 | |--------------------------------------------------------------------------
61 | | Application Timezone
62 | |--------------------------------------------------------------------------
63 | |
64 | | Here you may specify the default timezone for your application, which
65 | | will be used by the PHP date and date-time functions. The timezone
66 | | is set to "UTC" by default as it is suitable for most use cases.
67 | |
68 | */
69 |
70 | 'timezone' => 'UTC',
71 |
72 | /*
73 | |--------------------------------------------------------------------------
74 | | Application Locale Configuration
75 | |--------------------------------------------------------------------------
76 | |
77 | | The application locale determines the default locale that will be used
78 | | by Laravel's translation / localization methods. This option can be
79 | | set to any locale for which you plan to have translation strings.
80 | |
81 | */
82 |
83 | 'locale' => env('APP_LOCALE', 'en'),
84 |
85 | 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
86 |
87 | 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
88 |
89 | /*
90 | |--------------------------------------------------------------------------
91 | | Encryption Key
92 | |--------------------------------------------------------------------------
93 | |
94 | | This key is utilized by Laravel's encryption services and should be set
95 | | to a random, 32 character string to ensure that all encrypted values
96 | | are secure. You should do this prior to deploying the application.
97 | |
98 | */
99 |
100 | 'cipher' => 'AES-256-CBC',
101 |
102 | 'key' => env('APP_KEY'),
103 |
104 | 'previous_keys' => [
105 | ...array_filter(
106 | explode(',', env('APP_PREVIOUS_KEYS', ''))
107 | ),
108 | ],
109 |
110 | /*
111 | |--------------------------------------------------------------------------
112 | | Maintenance Mode Driver
113 | |--------------------------------------------------------------------------
114 | |
115 | | These configuration options determine the driver used to determine and
116 | | manage Laravel's "maintenance mode" status. The "cache" driver will
117 | | allow maintenance mode to be controlled across multiple machines.
118 | |
119 | | Supported drivers: "file", "cache"
120 | |
121 | */
122 |
123 | 'maintenance' => [
124 | 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
125 | 'store' => env('APP_MAINTENANCE_STORE', 'database'),
126 | ],
127 |
128 |
129 |
130 |
131 |
132 | ];
133 |
--------------------------------------------------------------------------------
/config/auth.php:
--------------------------------------------------------------------------------
1 | [
17 | 'guard' => env('AUTH_GUARD', 'user'),
18 | 'passwords' => env('AUTH_PASSWORD_BROKER', 'driver'),
19 | ],
20 |
21 | /*
22 | |--------------------------------------------------------------------------
23 | | Authentication Guards
24 | |--------------------------------------------------------------------------
25 | |
26 | | Next, you may define every authentication guard for your application.
27 | | Of course, a great default configuration has been defined for you
28 | | which utilizes session storage plus the Eloquent user provider.
29 | |
30 | | All authentication guards have a user provider, which defines how the
31 | | users are actually retrieved out of your database or other storage
32 | | system used by the application. Typically, Eloquent is utilized.
33 | |
34 | | Supported: "session"
35 | |
36 | */
37 |
38 | 'guards' => [
39 | 'user' => [
40 | 'driver' => 'sanctum',
41 | 'provider' => 'users',
42 | ],
43 | 'driver' => [
44 | 'driver' => 'sanctum',
45 | 'provider' => 'drivers',
46 | ],
47 | ],
48 |
49 | /*
50 | |--------------------------------------------------------------------------
51 | | User Providers
52 | |--------------------------------------------------------------------------
53 | |
54 | | All authentication guards have a user provider, which defines how the
55 | | users are actually retrieved out of your database or other storage
56 | | system used by the application. Typically, Eloquent is utilized.
57 | |
58 | | If you have multiple user tables or models you may configure multiple
59 | | providers to represent the model / table. These providers may then
60 | | be assigned to any extra authentication guards you have defined.
61 | |
62 | | Supported: "database", "eloquent"
63 | |
64 | */
65 |
66 | 'providers' => [
67 | 'users' => [
68 | 'driver' => 'eloquent',
69 | 'model' => env('AUTH_MODEL', App\Models\User::class),
70 | ],
71 |
72 | 'drivers' => [
73 | 'driver' => 'eloquent',
74 | 'model' => App\Models\Driver::class,
75 | ],
76 |
77 | // 'drivers' => [
78 | // 'driver' => 'database',
79 | // 'table' => 'drivers',
80 | // ],
81 | ],
82 |
83 | /*
84 | |--------------------------------------------------------------------------
85 | | Resetting Passwords
86 | |--------------------------------------------------------------------------
87 | |
88 | | These configuration options specify the behavior of Laravel's password
89 | | reset functionality, including the table utilized for token storage
90 | | and the user provider that is invoked to actually retrieve users.
91 | |
92 | | The expiry time is the number of minutes that each reset token will be
93 | | considered valid. This security feature keeps tokens short-lived so
94 | | they have less time to be guessed. You may change this as needed.
95 | |
96 | | The throttle setting is the number of seconds a user must wait before
97 | | generating more password reset tokens. This prevents the user from
98 | | quickly generating a very large amount of password reset tokens.
99 | |
100 | */
101 |
102 | 'passwords' => [
103 | 'users' => [
104 | 'provider' => 'users',
105 | 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
106 | 'expire' => 60,
107 | 'throttle' => 60,
108 | ],
109 | ],
110 |
111 | /*
112 | |--------------------------------------------------------------------------
113 | | Password Confirmation Timeout
114 | |--------------------------------------------------------------------------
115 | |
116 | | Here you may define the amount of seconds before a password confirmation
117 | | window expires and users are asked to re-enter their password via the
118 | | confirmation screen. By default, the timeout lasts for three hours.
119 | |
120 | */
121 |
122 | 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
123 |
124 | ];
125 |
--------------------------------------------------------------------------------
/config/broadcasting.php:
--------------------------------------------------------------------------------
1 | env('BROADCAST_CONNECTION', 'null'),
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Broadcast Connections
23 | |--------------------------------------------------------------------------
24 | |
25 | | Here you may define all of the broadcast connections that will be used
26 | | to broadcast events to other systems or over WebSockets. Samples of
27 | | each available type of connection are provided inside this array.
28 | |
29 | */
30 |
31 | 'connections' => [
32 |
33 | 'reverb' => [
34 | 'driver' => 'reverb',
35 | 'key' => env('REVERB_APP_KEY'),
36 | 'secret' => env('REVERB_APP_SECRET'),
37 | 'app_id' => env('REVERB_APP_ID'),
38 | 'options' => [
39 | 'host' => env('REVERB_HOST'),
40 | 'port' => env('REVERB_PORT', 443),
41 | 'scheme' => env('REVERB_SCHEME', 'https'),
42 | 'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
43 | ],
44 | 'client_options' => [
45 | // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
46 | ],
47 | ],
48 |
49 | 'pusher' => [
50 | 'driver' => 'pusher',
51 | 'key' => env('PUSHER_APP_KEY'),
52 | 'secret' => env('PUSHER_APP_SECRET'),
53 | 'app_id' => env('PUSHER_APP_ID'),
54 | 'options' => [
55 | 'cluster' => env('PUSHER_APP_CLUSTER'),
56 | 'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',
57 | 'port' => env('PUSHER_PORT', 443),
58 | 'scheme' => env('PUSHER_SCHEME', 'https'),
59 | 'encrypted' => true,
60 | 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
61 | ],
62 | 'client_options' => [
63 | // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
64 | 'disable_auth' => true,
65 | ],
66 | ],
67 |
68 | 'ably' => [
69 | 'driver' => 'ably',
70 | 'key' => env('ABLY_KEY'),
71 | ],
72 |
73 | 'log' => [
74 | 'driver' => 'log',
75 | ],
76 |
77 | 'null' => [
78 | 'driver' => 'null',
79 | ],
80 |
81 | ],
82 |
83 | ];
84 |
--------------------------------------------------------------------------------
/config/cache.php:
--------------------------------------------------------------------------------
1 | env('CACHE_STORE', 'database'),
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Cache Stores
23 | |--------------------------------------------------------------------------
24 | |
25 | | Here you may define all of the cache "stores" for your application as
26 | | well as their drivers. You may even define multiple stores for the
27 | | same cache driver to group types of items stored in your caches.
28 | |
29 | | Supported drivers: "array", "database", "file", "memcached",
30 | | "redis", "dynamodb", "octane", "null"
31 | |
32 | */
33 |
34 | 'stores' => [
35 |
36 | 'array' => [
37 | 'driver' => 'array',
38 | 'serialize' => false,
39 | ],
40 |
41 | 'database' => [
42 | 'driver' => 'database',
43 | 'connection' => env('DB_CACHE_CONNECTION'),
44 | 'table' => env('DB_CACHE_TABLE', 'cache'),
45 | 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
46 | 'lock_table' => env('DB_CACHE_LOCK_TABLE'),
47 | ],
48 |
49 | 'file' => [
50 | 'driver' => 'file',
51 | 'path' => storage_path('framework/cache/data'),
52 | 'lock_path' => storage_path('framework/cache/data'),
53 | ],
54 |
55 | 'memcached' => [
56 | 'driver' => 'memcached',
57 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
58 | 'sasl' => [
59 | env('MEMCACHED_USERNAME'),
60 | env('MEMCACHED_PASSWORD'),
61 | ],
62 | 'options' => [
63 | // Memcached::OPT_CONNECT_TIMEOUT => 2000,
64 | ],
65 | 'servers' => [
66 | [
67 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'),
68 | 'port' => env('MEMCACHED_PORT', 11211),
69 | 'weight' => 100,
70 | ],
71 | ],
72 | ],
73 |
74 | 'redis' => [
75 | 'driver' => 'redis',
76 | 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
77 | 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
78 | ],
79 |
80 | 'dynamodb' => [
81 | 'driver' => 'dynamodb',
82 | 'key' => env('AWS_ACCESS_KEY_ID'),
83 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
84 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
85 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
86 | 'endpoint' => env('DYNAMODB_ENDPOINT'),
87 | ],
88 |
89 | 'octane' => [
90 | 'driver' => 'octane',
91 | ],
92 |
93 | ],
94 |
95 | /*
96 | |--------------------------------------------------------------------------
97 | | Cache Key Prefix
98 | |--------------------------------------------------------------------------
99 | |
100 | | When utilizing the APC, database, memcached, Redis, and DynamoDB cache
101 | | stores, there might be other applications using the same cache. For
102 | | that reason, you may prefix every cache key to avoid collisions.
103 | |
104 | */
105 |
106 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
107 |
108 | ];
109 |
--------------------------------------------------------------------------------
/config/database.php:
--------------------------------------------------------------------------------
1 | env('DB_CONNECTION', 'sqlite'),
20 |
21 | /*
22 | |--------------------------------------------------------------------------
23 | | Database Connections
24 | |--------------------------------------------------------------------------
25 | |
26 | | Below are all of the database connections defined for your application.
27 | | An example configuration is provided for each database system which
28 | | is supported by Laravel. You're free to add / remove connections.
29 | |
30 | */
31 |
32 | 'connections' => [
33 |
34 | 'sqlite' => [
35 | 'driver' => 'sqlite',
36 | 'url' => env('DB_URL'),
37 | 'database' => env('DB_DATABASE', database_path('database.sqlite')),
38 | 'prefix' => '',
39 | 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
40 | 'busy_timeout' => null,
41 | 'journal_mode' => null,
42 | 'synchronous' => null,
43 | ],
44 |
45 | 'mysql' => [
46 | 'driver' => 'mysql',
47 | 'url' => env('DB_URL'),
48 | 'host' => env('DB_HOST', '127.0.0.1'),
49 | 'port' => env('DB_PORT', '3306'),
50 | 'database' => env('DB_DATABASE', 'laravel'),
51 | 'username' => env('DB_USERNAME', 'root'),
52 | 'password' => env('DB_PASSWORD', ''),
53 | 'unix_socket' => env('DB_SOCKET', ''),
54 | 'charset' => env('DB_CHARSET', 'utf8mb4'),
55 | 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
56 | 'prefix' => '',
57 | 'prefix_indexes' => true,
58 | 'strict' => true,
59 | 'engine' => null,
60 | 'options' => extension_loaded('pdo_mysql') ? array_filter([
61 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
62 | ]) : [],
63 | ],
64 |
65 | 'mariadb' => [
66 | 'driver' => 'mariadb',
67 | 'url' => env('DB_URL'),
68 | 'host' => env('DB_HOST', '127.0.0.1'),
69 | 'port' => env('DB_PORT', '3306'),
70 | 'database' => env('DB_DATABASE', 'laravel'),
71 | 'username' => env('DB_USERNAME', 'root'),
72 | 'password' => env('DB_PASSWORD', ''),
73 | 'unix_socket' => env('DB_SOCKET', ''),
74 | 'charset' => env('DB_CHARSET', 'utf8mb4'),
75 | 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
76 | 'prefix' => '',
77 | 'prefix_indexes' => true,
78 | 'strict' => true,
79 | 'engine' => null,
80 | 'options' => extension_loaded('pdo_mysql') ? array_filter([
81 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
82 | ]) : [],
83 | ],
84 |
85 | 'pgsql' => [
86 | 'driver' => 'pgsql',
87 | 'url' => env('DB_URL'),
88 | 'host' => env('DB_HOST', '127.0.0.1'),
89 | 'port' => env('DB_PORT', '5432'),
90 | 'database' => env('DB_DATABASE', 'laravel'),
91 | 'username' => env('DB_USERNAME', 'root'),
92 | 'password' => env('DB_PASSWORD', ''),
93 | 'charset' => env('DB_CHARSET', 'utf8'),
94 | 'prefix' => '',
95 | 'prefix_indexes' => true,
96 | 'search_path' => 'public',
97 | 'sslmode' => 'prefer',
98 | ],
99 |
100 | 'sqlsrv' => [
101 | 'driver' => 'sqlsrv',
102 | 'url' => env('DB_URL'),
103 | 'host' => env('DB_HOST', 'localhost'),
104 | 'port' => env('DB_PORT', '1433'),
105 | 'database' => env('DB_DATABASE', 'laravel'),
106 | 'username' => env('DB_USERNAME', 'root'),
107 | 'password' => env('DB_PASSWORD', ''),
108 | 'charset' => env('DB_CHARSET', 'utf8'),
109 | 'prefix' => '',
110 | 'prefix_indexes' => true,
111 | // 'encrypt' => env('DB_ENCRYPT', 'yes'),
112 | // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
113 | ],
114 |
115 | ],
116 |
117 | /*
118 | |--------------------------------------------------------------------------
119 | | Migration Repository Table
120 | |--------------------------------------------------------------------------
121 | |
122 | | This table keeps track of all the migrations that have already run for
123 | | your application. Using this information, we can determine which of
124 | | the migrations on disk haven't actually been run on the database.
125 | |
126 | */
127 |
128 | 'migrations' => [
129 | 'table' => 'migrations',
130 | 'update_date_on_publish' => true,
131 | ],
132 |
133 | /*
134 | |--------------------------------------------------------------------------
135 | | Redis Databases
136 | |--------------------------------------------------------------------------
137 | |
138 | | Redis is an open source, fast, and advanced key-value store that also
139 | | provides a richer body of commands than a typical key-value system
140 | | such as Memcached. You may define your connection settings here.
141 | |
142 | */
143 |
144 | 'redis' => [
145 |
146 | 'client' => env('REDIS_CLIENT', 'phpredis'),
147 |
148 | 'options' => [
149 | 'cluster' => env('REDIS_CLUSTER', 'redis'),
150 | 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
151 | 'persistent' => env('REDIS_PERSISTENT', false),
152 | ],
153 |
154 | 'default' => [
155 | 'url' => env('REDIS_URL'),
156 | 'host' => env('REDIS_HOST', '127.0.0.1'),
157 | 'username' => env('REDIS_USERNAME'),
158 | 'password' => env('REDIS_PASSWORD'),
159 | 'port' => env('REDIS_PORT', '6379'),
160 | 'database' => env('REDIS_DB', '0'),
161 | ],
162 |
163 | 'cache' => [
164 | 'url' => env('REDIS_URL'),
165 | 'host' => env('REDIS_HOST', '127.0.0.1'),
166 | 'username' => env('REDIS_USERNAME'),
167 | 'password' => env('REDIS_PASSWORD'),
168 | 'port' => env('REDIS_PORT', '6379'),
169 | 'database' => env('REDIS_CACHE_DB', '1'),
170 | ],
171 |
172 | ],
173 |
174 | ];
175 |
--------------------------------------------------------------------------------
/config/filesystems.php:
--------------------------------------------------------------------------------
1 | env('FILESYSTEM_DISK', 'local'),
17 |
18 | /*
19 | |--------------------------------------------------------------------------
20 | | Filesystem Disks
21 | |--------------------------------------------------------------------------
22 | |
23 | | Below you may configure as many filesystem disks as necessary, and you
24 | | may even configure multiple disks for the same driver. Examples for
25 | | most supported storage drivers are configured here for reference.
26 | |
27 | | Supported drivers: "local", "ftp", "sftp", "s3"
28 | |
29 | */
30 |
31 | 'disks' => [
32 |
33 | 'local' => [
34 | 'driver' => 'local',
35 | 'root' => storage_path('app/private'),
36 | 'serve' => true,
37 | 'throw' => false,
38 | 'report' => false,
39 | ],
40 |
41 | 'public' => [
42 | 'driver' => 'local',
43 | 'root' => storage_path('app/public'),
44 | 'url' => env('APP_URL').'/storage',
45 | 'visibility' => 'public',
46 | 'throw' => false,
47 | 'report' => false,
48 | ],
49 |
50 | 's3' => [
51 | 'driver' => 's3',
52 | 'key' => env('AWS_ACCESS_KEY_ID'),
53 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
54 | 'region' => env('AWS_DEFAULT_REGION'),
55 | 'bucket' => env('AWS_BUCKET'),
56 | 'url' => env('AWS_URL'),
57 | 'endpoint' => env('AWS_ENDPOINT'),
58 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
59 | 'throw' => false,
60 | 'report' => false,
61 | ],
62 |
63 | ],
64 |
65 | /*
66 | |--------------------------------------------------------------------------
67 | | Symbolic Links
68 | |--------------------------------------------------------------------------
69 | |
70 | | Here you may configure the symbolic links that will be created when the
71 | | `storage:link` Artisan command is executed. The array keys should be
72 | | the locations of the links and the values should be their targets.
73 | |
74 | */
75 |
76 | 'links' => [
77 | public_path('storage') => storage_path('app/public'),
78 | ],
79 |
80 | ];
81 |
--------------------------------------------------------------------------------
/config/logging.php:
--------------------------------------------------------------------------------
1 | env('LOG_CHANNEL', 'stack'),
22 |
23 | /*
24 | |--------------------------------------------------------------------------
25 | | Deprecations Log Channel
26 | |--------------------------------------------------------------------------
27 | |
28 | | This option controls the log channel that should be used to log warnings
29 | | regarding deprecated PHP and library features. This allows you to get
30 | | your application ready for upcoming major versions of dependencies.
31 | |
32 | */
33 |
34 | 'deprecations' => [
35 | 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
36 | 'trace' => env('LOG_DEPRECATIONS_TRACE', false),
37 | ],
38 |
39 | /*
40 | |--------------------------------------------------------------------------
41 | | Log Channels
42 | |--------------------------------------------------------------------------
43 | |
44 | | Here you may configure the log channels for your application. Laravel
45 | | utilizes the Monolog PHP logging library, which includes a variety
46 | | of powerful log handlers and formatters that you're free to use.
47 | |
48 | | Available drivers: "single", "daily", "slack", "syslog",
49 | | "errorlog", "monolog", "custom", "stack"
50 | |
51 | */
52 |
53 | 'channels' => [
54 |
55 | 'stack' => [
56 | 'driver' => 'stack',
57 | 'channels' => explode(',', env('LOG_STACK', 'single')),
58 | 'ignore_exceptions' => false,
59 | ],
60 |
61 | 'single' => [
62 | 'driver' => 'single',
63 | 'path' => storage_path('logs/laravel.log'),
64 | 'level' => env('LOG_LEVEL', 'debug'),
65 | 'replace_placeholders' => true,
66 | ],
67 |
68 | 'daily' => [
69 | 'driver' => 'daily',
70 | 'path' => storage_path('logs/laravel.log'),
71 | 'level' => env('LOG_LEVEL', 'debug'),
72 | 'days' => env('LOG_DAILY_DAYS', 14),
73 | 'replace_placeholders' => true,
74 | ],
75 |
76 | 'slack' => [
77 | 'driver' => 'slack',
78 | 'url' => env('LOG_SLACK_WEBHOOK_URL'),
79 | 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
80 | 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
81 | 'level' => env('LOG_LEVEL', 'critical'),
82 | 'replace_placeholders' => true,
83 | ],
84 |
85 | 'papertrail' => [
86 | 'driver' => 'monolog',
87 | 'level' => env('LOG_LEVEL', 'debug'),
88 | 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
89 | 'handler_with' => [
90 | 'host' => env('PAPERTRAIL_URL'),
91 | 'port' => env('PAPERTRAIL_PORT'),
92 | 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
93 | ],
94 | 'processors' => [PsrLogMessageProcessor::class],
95 | ],
96 |
97 | 'stderr' => [
98 | 'driver' => 'monolog',
99 | 'level' => env('LOG_LEVEL', 'debug'),
100 | 'handler' => StreamHandler::class,
101 | 'handler_with' => [
102 | 'stream' => 'php://stderr',
103 | ],
104 | 'formatter' => env('LOG_STDERR_FORMATTER'),
105 | 'processors' => [PsrLogMessageProcessor::class],
106 | ],
107 |
108 | 'syslog' => [
109 | 'driver' => 'syslog',
110 | 'level' => env('LOG_LEVEL', 'debug'),
111 | 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
112 | 'replace_placeholders' => true,
113 | ],
114 |
115 | 'errorlog' => [
116 | 'driver' => 'errorlog',
117 | 'level' => env('LOG_LEVEL', 'debug'),
118 | 'replace_placeholders' => true,
119 | ],
120 |
121 | 'null' => [
122 | 'driver' => 'monolog',
123 | 'handler' => NullHandler::class,
124 | ],
125 |
126 | 'emergency' => [
127 | 'path' => storage_path('logs/laravel.log'),
128 | ],
129 |
130 | ],
131 |
132 | ];
133 |
--------------------------------------------------------------------------------
/config/mail.php:
--------------------------------------------------------------------------------
1 | env('MAIL_MAILER', 'log'),
18 |
19 | /*
20 | |--------------------------------------------------------------------------
21 | | Mailer Configurations
22 | |--------------------------------------------------------------------------
23 | |
24 | | Here you may configure all of the mailers used by your application plus
25 | | their respective settings. Several examples have been configured for
26 | | you and you are free to add your own as your application requires.
27 | |
28 | | Laravel supports a variety of mail "transport" drivers that can be used
29 | | when delivering an email. You may specify which one you're using for
30 | | your mailers below. You may also add additional mailers if needed.
31 | |
32 | | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
33 | | "postmark", "resend", "log", "array",
34 | | "failover", "roundrobin"
35 | |
36 | */
37 |
38 | 'mailers' => [
39 |
40 | 'smtp' => [
41 | 'transport' => 'smtp',
42 | 'scheme' => env('MAIL_SCHEME'),
43 | 'url' => env('MAIL_URL'),
44 | 'host' => env('MAIL_HOST', '127.0.0.1'),
45 | 'port' => env('MAIL_PORT', 2525),
46 | 'username' => env('MAIL_USERNAME'),
47 | 'password' => env('MAIL_PASSWORD'),
48 | 'timeout' => null,
49 | 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
50 | ],
51 |
52 | 'ses' => [
53 | 'transport' => 'ses',
54 | ],
55 |
56 | 'postmark' => [
57 | 'transport' => 'postmark',
58 | // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
59 | // 'client' => [
60 | // 'timeout' => 5,
61 | // ],
62 | ],
63 |
64 | 'resend' => [
65 | 'transport' => 'resend',
66 | ],
67 |
68 | 'sendmail' => [
69 | 'transport' => 'sendmail',
70 | 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
71 | ],
72 |
73 | 'log' => [
74 | 'transport' => 'log',
75 | 'channel' => env('MAIL_LOG_CHANNEL'),
76 | ],
77 |
78 | 'array' => [
79 | 'transport' => 'array',
80 | ],
81 |
82 | 'failover' => [
83 | 'transport' => 'failover',
84 | 'mailers' => [
85 | 'smtp',
86 | 'log',
87 | ],
88 | ],
89 |
90 | 'roundrobin' => [
91 | 'transport' => 'roundrobin',
92 | 'mailers' => [
93 | 'ses',
94 | 'postmark',
95 | ],
96 | ],
97 |
98 | ],
99 |
100 | /*
101 | |--------------------------------------------------------------------------
102 | | Global "From" Address
103 | |--------------------------------------------------------------------------
104 | |
105 | | You may wish for all emails sent by your application to be sent from
106 | | the same address. Here you may specify a name and address that is
107 | | used globally for all emails that are sent by your application.
108 | |
109 | */
110 |
111 | 'from' => [
112 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
113 | 'name' => env('MAIL_FROM_NAME', 'Example'),
114 | ],
115 |
116 | ];
117 |
--------------------------------------------------------------------------------
/config/queue.php:
--------------------------------------------------------------------------------
1 | env('QUEUE_CONNECTION', 'database'),
17 |
18 | /*
19 | |--------------------------------------------------------------------------
20 | | Queue Connections
21 | |--------------------------------------------------------------------------
22 | |
23 | | Here you may configure the connection options for every queue backend
24 | | used by your application. An example configuration is provided for
25 | | each backend supported by Laravel. You're also free to add more.
26 | |
27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
28 | |
29 | */
30 |
31 | 'connections' => [
32 |
33 | 'sync' => [
34 | 'driver' => 'sync',
35 | ],
36 |
37 | 'database' => [
38 | 'driver' => 'database',
39 | 'connection' => env('DB_QUEUE_CONNECTION'),
40 | 'table' => env('DB_QUEUE_TABLE', 'jobs'),
41 | 'queue' => env('DB_QUEUE', 'default'),
42 | 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
43 | 'after_commit' => false,
44 | ],
45 |
46 | 'beanstalkd' => [
47 | 'driver' => 'beanstalkd',
48 | 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
49 | 'queue' => env('BEANSTALKD_QUEUE', 'default'),
50 | 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
51 | 'block_for' => 0,
52 | 'after_commit' => false,
53 | ],
54 |
55 | 'sqs' => [
56 | 'driver' => 'sqs',
57 | 'key' => env('AWS_ACCESS_KEY_ID'),
58 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
59 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
60 | 'queue' => env('SQS_QUEUE', 'default'),
61 | 'suffix' => env('SQS_SUFFIX'),
62 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
63 | 'after_commit' => false,
64 | ],
65 |
66 | 'redis' => [
67 | 'driver' => 'redis',
68 | 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
69 | 'queue' => env('REDIS_QUEUE', 'default'),
70 | 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
71 | 'block_for' => null,
72 | 'after_commit' => false,
73 | ],
74 |
75 | ],
76 |
77 | /*
78 | |--------------------------------------------------------------------------
79 | | Job Batching
80 | |--------------------------------------------------------------------------
81 | |
82 | | The following options configure the database and table that store job
83 | | batching information. These options can be updated to any database
84 | | connection and table which has been defined by your application.
85 | |
86 | */
87 |
88 | 'batching' => [
89 | 'database' => env('DB_CONNECTION', 'sqlite'),
90 | 'table' => 'job_batches',
91 | ],
92 |
93 | /*
94 | |--------------------------------------------------------------------------
95 | | Failed Queue Jobs
96 | |--------------------------------------------------------------------------
97 | |
98 | | These options configure the behavior of failed queue job logging so you
99 | | can control how and where failed jobs are stored. Laravel ships with
100 | | support for storing failed jobs in a simple file or in a database.
101 | |
102 | | Supported drivers: "database-uuids", "dynamodb", "file", "null"
103 | |
104 | */
105 |
106 | 'failed' => [
107 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
108 | 'database' => env('DB_CONNECTION', 'sqlite'),
109 | 'table' => 'failed_jobs',
110 | ],
111 |
112 | ];
113 |
--------------------------------------------------------------------------------
/config/reverb.php:
--------------------------------------------------------------------------------
1 | env('REVERB_SERVER', 'reverb'),
17 |
18 | /*
19 | |--------------------------------------------------------------------------
20 | | Reverb Servers
21 | |--------------------------------------------------------------------------
22 | |
23 | | Here you may define details for each of the supported Reverb servers.
24 | | Each server has its own configuration options that are defined in
25 | | the array below. You should ensure all the options are present.
26 | |
27 | */
28 |
29 | 'servers' => [
30 |
31 | 'reverb' => [
32 | 'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
33 | 'port' => env('REVERB_SERVER_PORT', 8080),
34 | 'path' => env('REVERB_SERVER_PATH', ''),
35 | 'hostname' => env('REVERB_HOST'),
36 | 'options' => [
37 | 'tls' => [],
38 | ],
39 | 'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000),
40 | 'scaling' => [
41 | 'enabled' => env('REVERB_SCALING_ENABLED', false),
42 | 'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
43 | 'server' => [
44 | 'url' => env('REDIS_URL'),
45 | 'host' => env('REDIS_HOST', '127.0.0.1'),
46 | 'port' => env('REDIS_PORT', '6379'),
47 | 'username' => env('REDIS_USERNAME'),
48 | 'password' => env('REDIS_PASSWORD'),
49 | 'database' => env('REDIS_DB', '0'),
50 | ],
51 | ],
52 | 'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
53 | 'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
54 | ],
55 |
56 | ],
57 |
58 | /*
59 | |--------------------------------------------------------------------------
60 | | Reverb Applications
61 | |--------------------------------------------------------------------------
62 | |
63 | | Here you may define how Reverb applications are managed. If you choose
64 | | to use the "config" provider, you may define an array of apps which
65 | | your server will support, including their connection credentials.
66 | |
67 | */
68 |
69 | 'apps' => [
70 |
71 | 'provider' => 'config',
72 |
73 | 'apps' => [
74 | [
75 | 'key' => env('REVERB_APP_KEY'),
76 | 'secret' => env('REVERB_APP_SECRET'),
77 | 'app_id' => env('REVERB_APP_ID'),
78 | 'options' => [
79 | 'host' => env('REVERB_HOST'),
80 | 'port' => env('REVERB_PORT', 443),
81 | 'scheme' => env('REVERB_SCHEME', 'https'),
82 | 'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
83 | ],
84 | 'allowed_origins' => ['*'],
85 | 'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
86 | 'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
87 | 'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000),
88 | ],
89 | ],
90 |
91 | ],
92 |
93 | ];
94 |
--------------------------------------------------------------------------------
/config/sanctum.php:
--------------------------------------------------------------------------------
1 | explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
19 | '%s%s',
20 | 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
21 | Sanctum::currentApplicationUrlWithPort()
22 | ))),
23 |
24 | /*
25 | |--------------------------------------------------------------------------
26 | | Sanctum Guards
27 | |--------------------------------------------------------------------------
28 | |
29 | | This array contains the authentication guards that will be checked when
30 | | Sanctum is trying to authenticate a request. If none of these guards
31 | | are able to authenticate the request, Sanctum will use the bearer
32 | | token that's present on an incoming request for authentication.
33 | |
34 | */
35 |
36 | 'guard' => ['web'],
37 |
38 | /*
39 | |--------------------------------------------------------------------------
40 | | Expiration Minutes
41 | |--------------------------------------------------------------------------
42 | |
43 | | This value controls the number of minutes until an issued token will be
44 | | considered expired. This will override any values set in the token's
45 | | "expires_at" attribute, but first-party sessions are not affected.
46 | |
47 | */
48 |
49 | 'expiration' => null,
50 |
51 | /*
52 | |--------------------------------------------------------------------------
53 | | Token Prefix
54 | |--------------------------------------------------------------------------
55 | |
56 | | Sanctum can prefix new tokens in order to take advantage of numerous
57 | | security scanning initiatives maintained by open source platforms
58 | | that notify developers if they commit tokens into repositories.
59 | |
60 | | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
61 | |
62 | */
63 |
64 | 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
65 |
66 | /*
67 | |--------------------------------------------------------------------------
68 | | Sanctum Middleware
69 | |--------------------------------------------------------------------------
70 | |
71 | | When authenticating your first-party SPA with Sanctum you may need to
72 | | customize some of the middleware Sanctum uses while processing the
73 | | request. You may change the middleware listed below as required.
74 | |
75 | */
76 |
77 | 'middleware' => [
78 | 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
79 | 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
80 | 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
81 | ],
82 |
83 | ];
84 |
--------------------------------------------------------------------------------
/config/services.php:
--------------------------------------------------------------------------------
1 | [
18 | 'token' => env('POSTMARK_TOKEN'),
19 | ],
20 |
21 | 'ses' => [
22 | 'key' => env('AWS_ACCESS_KEY_ID'),
23 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
24 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
25 | ],
26 |
27 | 'resend' => [
28 | 'key' => env('RESEND_KEY'),
29 | ],
30 |
31 | 'slack' => [
32 | 'notifications' => [
33 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
34 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
35 | ],
36 | ],
37 |
38 | 'zarinpal' => [
39 | 'merchant_id' => env('ZP_MERCHANT_ID', 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'),
40 | 'sandbox' => env('ZP_SANDBOX', false)
41 | ]
42 |
43 | ];
44 |
--------------------------------------------------------------------------------
/database/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite*
2 |
--------------------------------------------------------------------------------
/database/factories/CurrentRideFactory.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class CurrentRideFactory extends Factory
13 | {
14 | /**
15 | * Define the model's default state.
16 | *
17 | * @return array
18 | */
19 | public function definition(): array
20 | {
21 | return [
22 | 'driver_id' => Driver::factory(),
23 | 'request_id' => RideRequest::factory(),
24 | ];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/database/factories/DriverFactory.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class DriverFactory extends Factory
14 | {
15 | /**
16 | * Define the model's default state.
17 | *
18 | * @return array
19 | */
20 | public function definition(): array
21 | {
22 | return [
23 | 'name' => fake()->name(),
24 | 'email' => fake()->unique()->safeEmail(),
25 | 'email_verified_at' => now(),
26 | 'password' => Hash::make('password'),
27 | 'remember_token' => Str::random(10),
28 | 'latitude' => rand(34.0000, 35.0000),
29 | 'longitude' => rand(50.0000, 51.0000),
30 | 'is_active' => fake()->boolean()
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/database/factories/InvoiceFactory.php:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class InvoiceFactory extends Factory
11 | {
12 | /**
13 | * Define the model's default state.
14 | *
15 | * @return array
16 | */
17 | public function definition(): array
18 | {
19 | return [
20 | //
21 | ];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/database/factories/RideRequestFactory.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class RideRequestFactory extends Factory
14 | {
15 |
16 | /**
17 | * Define the model's default state.
18 | *
19 | * @return array
20 | */
21 | public function definition(): array
22 | {
23 | return [
24 | 'pickup_latitude' => rand(34.0000, 35.0000),
25 | 'pickup_longitude' => rand(50.0000, 51.0000),
26 | 'dest_latitude' => rand(34.0000, 35.0000),
27 | 'dest_longitude' => rand(50.0000, 51.0000),
28 | 'user_id' => User::factory(),
29 | 'cost' => fake()->numberBetween(1,1000),
30 | 'distance_km' => fake()->numberBetween(1,1000),
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/database/factories/UserFactory.php:
--------------------------------------------------------------------------------
1 |
11 | */
12 | class UserFactory extends Factory
13 | {
14 | /**
15 | * The current password being used by the factory.
16 | */
17 | protected static ?string $password;
18 |
19 | /**
20 | * Define the model's default state.
21 | *
22 | * @return array
23 | */
24 | public function definition(): array
25 | {
26 | return [
27 | 'name' => fake()->name(),
28 | 'email' => fake()->unique()->safeEmail(),
29 | 'email_verified_at' => now(),
30 | 'password' => static::$password ??= Hash::make('password'),
31 | 'remember_token' => Str::random(10),
32 | ];
33 | }
34 |
35 | /**
36 | * Indicate that the model's email address should be unverified.
37 | */
38 | public function unverified(): static
39 | {
40 | return $this->state(fn (array $attributes) => [
41 | 'email_verified_at' => null,
42 | ]);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/database/migrations/0001_01_01_000000_create_users_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->string('name');
17 | $table->string('email')->unique();
18 | $table->timestamp('email_verified_at')->nullable();
19 | $table->string('password');
20 | $table->rememberToken();
21 | $table->timestamps();
22 | });
23 |
24 | Schema::create('password_reset_tokens', function (Blueprint $table) {
25 | $table->string('email')->primary();
26 | $table->string('token');
27 | $table->timestamp('created_at')->nullable();
28 | });
29 |
30 | Schema::create('sessions', function (Blueprint $table) {
31 | $table->string('id')->primary();
32 | $table->foreignId('user_id')->nullable()->index();
33 | $table->string('ip_address', 45)->nullable();
34 | $table->text('user_agent')->nullable();
35 | $table->longText('payload');
36 | $table->integer('last_activity')->index();
37 | });
38 | }
39 |
40 | /**
41 | * Reverse the migrations.
42 | */
43 | public function down(): void
44 | {
45 | Schema::dropIfExists('users');
46 | Schema::dropIfExists('password_reset_tokens');
47 | Schema::dropIfExists('sessions');
48 | }
49 | };
50 |
--------------------------------------------------------------------------------
/database/migrations/0001_01_01_000001_create_cache_table.php:
--------------------------------------------------------------------------------
1 | string('key')->primary();
16 | $table->mediumText('value');
17 | $table->integer('expiration');
18 | });
19 |
20 | Schema::create('cache_locks', function (Blueprint $table) {
21 | $table->string('key')->primary();
22 | $table->string('owner');
23 | $table->integer('expiration');
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | */
30 | public function down(): void
31 | {
32 | Schema::dropIfExists('cache');
33 | Schema::dropIfExists('cache_locks');
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/database/migrations/0001_01_01_000002_create_jobs_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->string('queue')->index();
17 | $table->longText('payload');
18 | $table->unsignedTinyInteger('attempts');
19 | $table->unsignedInteger('reserved_at')->nullable();
20 | $table->unsignedInteger('available_at');
21 | $table->unsignedInteger('created_at');
22 | });
23 |
24 | Schema::create('job_batches', function (Blueprint $table) {
25 | $table->string('id')->primary();
26 | $table->string('name');
27 | $table->integer('total_jobs');
28 | $table->integer('pending_jobs');
29 | $table->integer('failed_jobs');
30 | $table->longText('failed_job_ids');
31 | $table->mediumText('options')->nullable();
32 | $table->integer('cancelled_at')->nullable();
33 | $table->integer('created_at');
34 | $table->integer('finished_at')->nullable();
35 | });
36 |
37 | Schema::create('failed_jobs', function (Blueprint $table) {
38 | $table->id();
39 | $table->string('uuid')->unique();
40 | $table->text('connection');
41 | $table->text('queue');
42 | $table->longText('payload');
43 | $table->longText('exception');
44 | $table->timestamp('failed_at')->useCurrent();
45 | });
46 | }
47 |
48 | /**
49 | * Reverse the migrations.
50 | */
51 | public function down(): void
52 | {
53 | Schema::dropIfExists('jobs');
54 | Schema::dropIfExists('job_batches');
55 | Schema::dropIfExists('failed_jobs');
56 | }
57 | };
58 |
--------------------------------------------------------------------------------
/database/migrations/2025_03_22_113439_create_ride_requests_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->decimal('pickup_latitude', 10, 6);
17 | $table->decimal('pickup_longitude', 10, 6);
18 | $table->decimal('dest_latitude', 10, 6);
19 | $table->decimal('dest_longitude', 10, 6);
20 | $table->unsignedBigInteger('user_id');
21 | $table->boolean('isPending')->default(true);
22 | $table->unsignedBigInteger('cost');
23 | $table->unsignedBigInteger('distance_km');
24 | $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
25 |
26 |
27 |
28 | });
29 | }
30 |
31 | /**
32 | * Reverse the migrations.
33 | */
34 | public function down(): void
35 | {
36 | Schema::dropIfExists('requests');
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/database/migrations/2025_03_22_113521_create_drivers_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->string('name');
17 | $table->string('email')->unique();
18 | $table->timestamp('email_verified_at')->nullable();
19 | $table->string('password');
20 | $table->rememberToken();
21 | $table->timestamps();
22 | $table->boolean('is_active')->default(true);
23 | $table->decimal('latitude', 10, 6)->nullable();
24 | $table->decimal('longitude', 10, 6)->nullable();
25 | });
26 | }
27 |
28 | /**
29 | * Reverse the migrations.
30 | */
31 | public function down(): void
32 | {
33 | Schema::dropIfExists('drivers');
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/database/migrations/2025_03_22_113525_create_req_dri_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->unsignedBigInteger('request_id');
17 | $table->unsignedBigInteger('driver_id');
18 | $table->foreign('request_id')->references('id')->on('ride_requests')->cascadeOnDelete();
19 | $table->foreign('driver_id')->references('id')->on('drivers')->cascadeOnDelete();
20 |
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | */
27 | public function down(): void
28 | {
29 | Schema::dropIfExists('req_dri');
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/database/migrations/2025_03_22_113540_create_current_rides_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->unsignedBigInteger('driver_id')->unique();
17 | $table->foreign('driver_id')->references('id')->on('drivers')->cascadeOnDelete();
18 | $table->unsignedBigInteger('request_id')->unique();
19 | $table->foreign('request_id')->references('id')->on('ride_requests')->cascadeOnDelete();
20 | $table->unsignedBigInteger('user_id')->unique();
21 | $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
22 | $table->boolean('isArrived')->default(false);
23 | $table->timestamps();
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | */
30 | public function down(): void
31 | {
32 | Schema::dropIfExists('current_rides');
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/database/migrations/2025_03_22_113553_create_complete_rides_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->time('total_time');
17 | $table->unsignedBigInteger('driver_id');
18 | $table->foreign('driver_id')->references('id')->on('drivers')->cascadeOnDelete();
19 | $table->unsignedBigInteger('request_id')->unique();
20 | $table->foreign('request_id')->references('id')->on('ride_requests')->cascadeOnDelete();
21 | $table->unsignedBigInteger('user_id');
22 | $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
23 | $table->unsignedInteger('star')->nullable();
24 |
25 | });
26 | }
27 |
28 | /**
29 | * Reverse the migrations.
30 | */
31 | public function down(): void
32 | {
33 | Schema::dropIfExists('complete_rides');
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/database/migrations/2025_03_22_151923_create_personal_access_tokens_table.php:
--------------------------------------------------------------------------------
1 | id();
16 | $table->morphs('tokenable');
17 | $table->string('name');
18 | $table->string('token', 64)->unique();
19 | $table->text('abilities')->nullable();
20 | $table->timestamp('last_used_at')->nullable();
21 | $table->timestamp('expires_at')->nullable();
22 | $table->timestamps();
23 | });
24 | }
25 |
26 | /**
27 | * Reverse the migrations.
28 | */
29 | public function down(): void
30 | {
31 | Schema::dropIfExists('personal_access_tokens');
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/database/migrations/2025_04_18_092450_create_invoices_table.php:
--------------------------------------------------------------------------------
1 | id();
17 | $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
18 | $table->foreignId('ride_request_id')->nullable()->constrained()->nullOnDelete();
19 | $table->unsignedInteger('amount');
20 | $table->boolean('isPaid')->default(false);
21 | $table->enum('payment_type', [PaymentType::ONLINE->value, PaymentType::CASH->value])->default(PaymentType::ONLINE->value);
22 | $table->timestamp('paid_at')->nullable();
23 | $table->string('authority')->nullable();
24 | $table->timestamps();
25 | });
26 | }
27 |
28 | /**
29 | * Reverse the migrations.
30 | */
31 | public function down(): void
32 | {
33 | Schema::dropIfExists('invoices');
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/database/seeders/DatabaseSeeder.php:
--------------------------------------------------------------------------------
1 | create();
20 | Driver::factory(10)->create();
21 | RideRequest::factory(10)->create();
22 |
23 | User::factory(['email' => 'user@example.com', 'password'=> Hash::make('password123')])->create();
24 | Driver::factory(["email" => "driver@example.com" ,'password'=> Hash::make('password123')])->create();
25 |
26 |
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/drawSQL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pourya-azad/SnappAPI/340ae7241c579e0f970955eb618892a21192c49c/drawSQL.png
--------------------------------------------------------------------------------
/eer Diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pourya-azad/SnappAPI/340ae7241c579e0f970955eb618892a21192c49c/eer Diagram.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "type": "module",
4 | "scripts": {
5 | "build": "vite build",
6 | "dev": "vite"
7 | },
8 | "devDependencies": {
9 | "@tailwindcss/vite": "^4.0.0",
10 | "axios": "^1.8.2",
11 | "concurrently": "^9.0.1",
12 | "laravel-echo": "^2.0.2",
13 | "laravel-vite-plugin": "^1.2.0",
14 | "pusher-js": "^8.4.0",
15 | "tailwindcss": "^4.0.0",
16 | "vite": "^6.0.11"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | tests/Unit
10 |
11 |
12 | tests/Feature
13 |
14 |
15 |
16 |
17 | app
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/public/.htaccess:
--------------------------------------------------------------------------------
1 |
2 |
3 | Options -MultiViews -Indexes
4 |
5 |
6 | RewriteEngine On
7 |
8 | # Handle Authorization Header
9 | RewriteCond %{HTTP:Authorization} .
10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
11 |
12 | # Handle X-XSRF-Token Header
13 | RewriteCond %{HTTP:x-xsrf-token} .
14 | RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
15 |
16 | # Redirect Trailing Slashes If Not A Folder...
17 | RewriteCond %{REQUEST_FILENAME} !-d
18 | RewriteCond %{REQUEST_URI} (.+)/$
19 | RewriteRule ^ %1 [L,R=301]
20 |
21 | # Send Requests To Front Controller...
22 | RewriteCond %{REQUEST_FILENAME} !-d
23 | RewriteCond %{REQUEST_FILENAME} !-f
24 | RewriteRule ^ index.php [L]
25 |
26 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pourya-azad/SnappAPI/340ae7241c579e0f970955eb618892a21192c49c/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 | handleRequest(Request::capture());
21 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/resources/css/app.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 |
3 | @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
4 | @source '../../storage/framework/views/*.php';
5 | @source '../**/*.blade.php';
6 | @source '../**/*.js';
7 |
8 | @theme {
9 | --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
10 | 'Segoe UI Symbol', 'Noto Color Emoji';
11 | }
12 |
--------------------------------------------------------------------------------
/resources/js/app.js:
--------------------------------------------------------------------------------
1 | import './bootstrap';
2 |
--------------------------------------------------------------------------------
/resources/js/bootstrap.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | window.axios = axios;
3 |
4 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
5 |
6 | /**
7 | * Echo exposes an expressive API for subscribing to channels and listening
8 | * for events that are broadcast by Laravel. Echo and event broadcasting
9 | * allow your team to quickly build robust real-time web applications.
10 | */
11 |
12 | import './echo';
13 |
--------------------------------------------------------------------------------
/resources/js/echo.js:
--------------------------------------------------------------------------------
1 | import Echo from 'laravel-echo';
2 |
3 | import Pusher from 'pusher-js';
4 | window.Pusher = Pusher;
5 |
6 | window.Echo = new Echo({
7 | broadcaster: 'reverb',
8 | key: import.meta.env.VITE_REVERB_APP_KEY,
9 | wsHost: import.meta.env.VITE_REVERB_HOST,
10 | wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
11 | wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
12 | forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
13 | enabledTransports: ['ws', 'wss'],
14 | });
15 |
--------------------------------------------------------------------------------
/resources/views/reverb.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | تست Pusher
8 |
9 | @vite(['resources/js/app.js'])
10 |
11 |
12 |
13 |
14 | تست اتصال به Pusher
15 |
16 | قبول درخواست
17 |
18 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/resources/views/reverb2.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | تست Pusher
8 |
9 | @vite(['resources/js/app.js'])
10 |
11 |
12 |
13 |
14 | تست اتصال به Pusher
15 |
16 | قبول درخواست
17 |
18 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/resources/views/vendor/l5-swagger/index.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ $documentationTitle }}
6 |
7 |
8 |
9 |
28 | @if(config('l5-swagger.defaults.ui.display.dark_mode'))
29 |
116 | @endif
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
173 |
174 |
175 |
--------------------------------------------------------------------------------
/routes/api.php:
--------------------------------------------------------------------------------
1 | "users"], function () {
13 | Route::get('status', [UserController::class, 'status'])->middleware('auth:sanctum,user');
14 |
15 | Route::post('login', [UserAuthController::class,'login']);
16 |
17 | Route::post('logout', [UserAuthController::class,'logout'])->middleware('auth:sanctum,user');
18 | });
19 |
20 | Route::group(['prefix'=> 'drivers'], function () {
21 | Route::post('/location', [DriverController::class,'updateLocation'])->middleware('auth:sanctum,driver');
22 |
23 | Route::post('login', [DriverAuthController::class,'login']);
24 |
25 | Route::post('logout', [DriverAuthController::class,'logout'])->middleware('auth:sanctum,driver');
26 | });
27 |
28 | Route::post('ride-requests/store', [RideRequestController::class, 'store'])->middleware('auth:sanctum,user');
29 | Route::get('ride-requests/cancel', [RideRequestController::class, 'cancel'])->middleware('auth:user');
30 |
31 | Route::post('ride/complete', [RideController::class,'complete'])->middleware('auth:sanctum,driver');
32 | Route::post('pay', [PaymentController::class, 'pay'])->middleware('auth:sanctum,user')
33 | ->name('pay');
34 | Route::get('payment-verify/{invoice}', [PaymentController::class, 'verify'])->middleware('auth:sanctum,user')
35 | ->name('payment-verify');
36 |
--------------------------------------------------------------------------------
/routes/channels.php:
--------------------------------------------------------------------------------
1 | id === (int) $id;
8 | });
9 |
10 | //Broadcast::channel('riderequests', function ($user) {
11 | // return true;
12 | //});
13 |
14 | Broadcast::channel('drivers.{id}', function ($driver, $id) {
15 | return (int) $driver->id === (int) $id;
16 | // return true;
17 | }, ['guards' => ['driver']]);
18 |
19 |
--------------------------------------------------------------------------------
/routes/console.php:
--------------------------------------------------------------------------------
1 | comment(Inspiring::quote());
8 | })->purpose('Display an inspiring quote');
9 |
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 | rideCompletionService = $this->mock(RideCompletionService::class);
23 |
24 | $this->app->instance(RideCompletionService::class, $this->rideCompletionService);
25 | }
26 |
27 | public function test_complete_ride_successfully()
28 | {
29 | $fakeTotalTime = 3;
30 |
31 | $request = new CompleteRideRequest([
32 | "driver_id" => 1,
33 | "trip_id" => 2,
34 | ]);
35 |
36 | $this->rideCompletionService
37 | ->shouldReceive('completeRide')
38 | ->with(2) // مقدار trip_id که به متد میفرستیم
39 | ->once()
40 | ->andReturn($fakeTotalTime);
41 |
42 | $controller = app(RideController::class);
43 | $response = $controller->complete($request);
44 |
45 | $this->assertInstanceOf(JsonResponse::class, $response);
46 | $this->assertEquals(201, $response->getStatusCode());
47 | $this->assertJsonStringEqualsJsonString(
48 | json_encode([
49 | 'message' => 'Ride completed successfully',
50 | 'data' => [
51 | 'total_time' => $fakeTotalTime
52 | ],
53 | ]),
54 | $response->getContent()
55 | );
56 | }
57 |
58 | public function test_complete_ride_fails_with_exception()
59 | {
60 | $request = new CompleteRideRequest([
61 | "driver_id" => 1,
62 | "trip_id" => 2,
63 | ]);
64 |
65 | $this->rideCompletionService
66 | ->shouldReceive('completeRide')
67 | ->with(2)
68 | ->once()
69 | ->andThrow(new \Exception('Something went wrong'));
70 |
71 | \Log::shouldReceive('error')
72 | ->once()
73 | ->with('An error occurred while completing the ride: ', Mockery::on(function ($context) {
74 | return isset($context['error']) && $context['error'] === 'Something went wrong'
75 | && $context['trip_id'] === 2;
76 | }));
77 |
78 | $controller = app(RideController::class);
79 | $response = $controller->complete($request);
80 |
81 | $this->assertInstanceOf(JsonResponse::class, $response);
82 | $this->assertEquals(500, $response->getStatusCode());
83 | $this->assertJsonStringEqualsJsonString(
84 | json_encode([
85 | 'message' => 'An error occurred while completing the ride',
86 | 'error' => config('app.debug') ? 'Something went wrong' : null,
87 | ]),
88 | $response->getContent()
89 | );
90 | }
91 |
92 | protected function tearDown(): void
93 | {
94 | Mockery::close();
95 | parent::tearDown();
96 | }
97 | }
--------------------------------------------------------------------------------
/tests/Feature/DriverControllerTest.php:
--------------------------------------------------------------------------------
1 | create();
27 | $this->actingAs($driver, 'driver');
28 |
29 | $locationData = [
30 | 'latitude' => 35.6892,
31 | 'longitude' => 51.3890,
32 | ];
33 |
34 | $validatedData = array_merge($locationData, ['driver_id' => $driver->id]);
35 |
36 | Redis::shouldReceive('setex')
37 | ->once()
38 | ->with("driver:location:{$driver->id}", 3600, json_encode($validatedData));
39 |
40 | \Log::shouldReceive('info')
41 | ->once()
42 | ->with('Driver location updated successfully: ', [$driver->id]);
43 |
44 | $response = $this->postJson('/api/drivers/location', $locationData);
45 |
46 | $response->assertStatus(200)
47 | ->assertJsonStructure([
48 | 'data' => [
49 | 'driver_id',
50 | 'location' => ['latitude', 'longitude'],
51 | 'updated_at',
52 | ],
53 | 'message',
54 | ])
55 | ->assertJsonFragment([
56 | 'message' => 'Driver location updated successfully in Redis',
57 | 'driver_id' => $driver->id,
58 | 'location' => $locationData,
59 | ]);
60 | }
61 |
62 | /**
63 | * تست شکست به دلیل دادههای نامعتبر
64 | */
65 | public function test_update_location_fails_with_invalid_data(): void
66 | {
67 | $driver = Driver::factory()->create();
68 | $this->actingAs($driver, 'driver');
69 |
70 | $invalidData = [
71 | 'latitude' => 'invalid',
72 | 'longitude' => 51.3890,
73 | ];
74 |
75 | $response = $this->postJson('/api/drivers/location', $invalidData);
76 |
77 | $response->assertStatus(422)
78 | ->assertJsonValidationErrors(['latitude']);
79 | }
80 |
81 | /**
82 | * تست شکست به دلیل عدم احراز هویت راننده
83 | */
84 | public function test_update_location_fails_with_unauthenticated_driver(): void
85 | {
86 | $locationData = [
87 | 'latitude' => 35.6892,
88 | 'longitude' => 51.3890,
89 | ];
90 |
91 | $response = $this->postJson('/api/drivers/location', $locationData);
92 |
93 | $response->assertStatus(401)
94 | ->assertJson([
95 | 'message' => 'Unauthenticated.',
96 | ]);
97 | }
98 |
99 | /**
100 | * تست شکست به دلیل خطای Redis
101 | */
102 | public function test_update_location_fails_with_redis_error(): void
103 | {
104 | $driver = Driver::factory()->create();
105 | $this->actingAs($driver, 'driver');
106 |
107 | $locationData = [
108 | 'latitude' => 35.6892,
109 | 'longitude' => 51.3890,
110 | ];
111 |
112 | $validatedData = array_merge($locationData, ['driver_id' => $driver->id]);
113 |
114 | Redis::shouldReceive('setex')
115 | ->once()
116 | ->with("driver:location:{$driver->id}", 3600, json_encode($validatedData))
117 | ->andThrow(new \Exception('Redis connection failed'));
118 |
119 | \Log::shouldReceive('error')
120 | ->once()
121 | ->with('Failed to update driver location: ', ['Redis connection failed']);
122 |
123 | $response = $this->postJson('/api/drivers/location', $locationData);
124 |
125 | $response->assertStatus(500)
126 | ->assertJson([
127 | 'message' => 'An error occurred while updating driver location',
128 | 'error' => config('app.debug') ? 'Redis connection failed' : null,
129 | ]);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/tests/Feature/PaymentTest.php:
--------------------------------------------------------------------------------
1 | create();
27 | $this->actingAs($user, 'user');
28 |
29 | // ساخت RideRequest و Invoice مرتبط
30 | $rideRequest = RideRequest::factory()->create([
31 | 'cost' => 11000
32 | ]);
33 | $invoice = Invoice::factory()->create([
34 | 'ride_request_id' => $rideRequest->id,
35 | 'user_id' => $user->id,
36 | 'isPaid' => false,
37 | 'amount' => $rideRequest->cost,
38 | ]);
39 |
40 | // ماک کردن PaymentService
41 | $mockPaymentService = Mockery::mock(PaymentGatewayInterface::class);
42 | $mockPaymentService
43 | ->shouldReceive('request')
44 | ->once()
45 | ->withArgs(function ($passedInvoice, $data) use ($invoice, $user) {
46 | return $passedInvoice->id === $invoice->id && $data['email'] === $user->email;
47 | })
48 | ->andReturn([
49 | 'status' => true,
50 | 'payment_url' => 'https://zarinpal.com/pg/StartPay/XXXX',
51 | ]);
52 | $this->app->instance(PaymentGatewayInterface::class, $mockPaymentService);
53 |
54 | // ارسال درخواست به کنترلر
55 | $response = $this->postJson(route('pay'), [
56 | 'ride_request_id' => $rideRequest->id,
57 | ]);
58 | // بررسی پاسخ
59 | $response->assertStatus(200)
60 | ->assertJsonStructure(['payment_url'])
61 | ->assertExactJson([
62 | 'payment_url' => 'https://zarinpal.com/pg/StartPay/XXXX',
63 | ]);
64 | }
65 |
66 | public function test_pay_returns_404_if_no_unpaid_invoice()
67 | {
68 | $user = User::factory()->create();
69 | $this->actingAs($user, 'user');
70 |
71 | $rideRequest = RideRequest::factory()->create([
72 | 'cost' => 11000
73 | ]);
74 | Invoice::factory()->create([
75 | 'ride_request_id' => $rideRequest->id,
76 | 'user_id' => $user->id,
77 | 'isPaid' => true, // قبلاً پرداخت شده
78 | 'amount' => $rideRequest->cost,
79 | ]);
80 |
81 | $response = $this->postJson(route('pay'), [
82 | 'ride_request_id' => $rideRequest->id,
83 | ]);
84 |
85 | $response->assertStatus(404)
86 | ->assertExactJson(['message' => 'No unpaid ride request!']);
87 | }
88 |
89 | public function test_pay_returns_400_if_payment_request_fails()
90 | {
91 | $user = User::factory()->create();
92 | $this->actingAs($user, 'user');
93 |
94 | $rideRequest = RideRequest::factory()->create([
95 | 'cost' => 11000
96 | ]);
97 | $invoice = Invoice::factory()->create([
98 | 'ride_request_id' => $rideRequest->id,
99 | 'user_id' => $user->id,
100 | 'isPaid' => false,
101 | 'amount' => $rideRequest->cost,
102 | ]);
103 |
104 | $mockPaymentService = \Mockery::mock(PaymentGatewayInterface::class);
105 | $mockPaymentService
106 | ->shouldReceive('request')
107 | ->once()
108 | ->andReturn([
109 | 'status' => false,
110 | 'message' => 'Something went wrong!',
111 | ]);
112 |
113 | $this->app->instance(PaymentGatewayInterface::class, $mockPaymentService);
114 |
115 | $response = $this->postJson(route('pay'), [
116 | 'ride_request_id' => $rideRequest->id,
117 | ]);
118 |
119 | $response->assertStatus(400)
120 | ->assertExactJson(['message' => 'Something went wrong!']);
121 | }
122 |
123 | public function test_user_can_verify_payment_successfully()
124 | {
125 | // ایجاد کاربر و لاگین
126 | $user = User::factory()->create();
127 | $this->actingAs($user, 'user');
128 |
129 | // ساخت RideRequest و Invoice
130 | $rideRequest = RideRequest::factory()->create(['cost' => 11000]);
131 | $invoice = Invoice::factory()->create([
132 | 'ride_request_id' => $rideRequest->id,
133 | 'user_id' => $user->id,
134 | 'isPaid' => false,
135 | 'amount' => $rideRequest->cost,
136 | 'authority' => 'A000000000000000000000000000jjrx31yv',
137 | ]);
138 | $isSandboxMode = config('services.zarinpal.sandbox');
139 | $sandboxPrefix = $isSandboxMode ? 'sandbox' : 'payment';
140 | // فیک کردن پاسخ HTTP زرینپال
141 | Http::fake([
142 | "https://{$sandboxPrefix}.zarinpal.com/pg/v4/payment/verify.json" => Http::response([
143 | 'data' => [
144 | 'code' => 100,
145 | 'ref_id' => '1234567890',
146 | 'card_pan' => '4321 **** **** 1234',
147 | 'fee' => 1000,
148 | ],
149 | 'errors' => null,
150 | ], 200)
151 | ]);
152 |
153 | // ارسال ریکوئست به verify endpoint
154 | $response = $this->getJson(route('payment-verify', [
155 | 'invoice' => $invoice->id,
156 | 'Authority' => $invoice->authority,
157 | 'Status' => 'OK',
158 | ]));
159 |
160 | // بررسی پاسخ
161 | $response->assertStatus(200)
162 | ->assertJson([
163 | 'message' => 'Transaction Completed',
164 | ]);
165 |
166 | $invoice->refresh();
167 | $this->assertTrue($invoice->isPaid);
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/tests/Feature/StatusDriverControllerTest.php:
--------------------------------------------------------------------------------
1 | create();
32 | $this->actingAs($driver, 'driver');
33 |
34 | $user = User::factory()->create();
35 | $rideRequest = RideRequest::factory()->create();
36 |
37 | CurrentRide::factory()->create([
38 | 'driver_id' => $driver->id,
39 | 'user_id' => $user->id,
40 | 'request_id' => $rideRequest->id,
41 | 'isArrived' => false,
42 | ]);
43 |
44 | $request = Request::create('/api/driver/status', 'GET');
45 | $request->setUserResolver(function () use ($driver) {
46 | return $driver;
47 | });
48 |
49 | $controller = app(DriverController::class);
50 | $response = $controller->status($request);
51 |
52 | $this->assertInstanceOf(JsonResponse::class, $response);
53 | $this->assertEquals(200, $response->getStatusCode());
54 | $this->assertJsonStringEqualsJsonString(
55 | json_encode([
56 | 'message' => 'You have accepted a request and are heading to the user!',
57 | ]),
58 | $response->getContent()
59 | );
60 | }
61 |
62 | /**
63 | * تست وضعیت وقتی راننده در حال انجام سفر است
64 | */
65 | public function test_status_when_driver_is_on_ride(): void
66 | {
67 | $driver = Driver::factory()->create();
68 | $this->actingAs($driver, 'driver');
69 |
70 | $user = User::factory()->create();
71 | $rideRequest = RideRequest::factory()->create();
72 |
73 | CurrentRide::factory()->create([
74 | 'driver_id' => $driver->id,
75 | 'user_id' => $user->id,
76 | 'request_id' => $rideRequest->id,
77 | 'isArrived' => true,
78 | ]);
79 |
80 | $request = Request::create('/api/driver/status', 'GET');
81 | $request->setUserResolver(function () use ($driver) {
82 | return $driver;
83 | });
84 |
85 | $controller = app(DriverController::class);
86 | $response = $controller->status($request);
87 |
88 | $this->assertInstanceOf(JsonResponse::class, $response);
89 | $this->assertEquals(200, $response->getStatusCode());
90 | $this->assertJsonStringEqualsJsonString(
91 | json_encode([
92 | 'message' => 'You are currently on a ride, please end it!',
93 | ]),
94 | $response->getContent()
95 | );
96 | }
97 |
98 | /**
99 | * تست وضعیت وقتی راننده بیکار است
100 | */
101 | public function test_status_when_driver_is_idle(): void
102 | {
103 | $driver = Driver::factory()->create();
104 | $this->actingAs($driver, 'driver');
105 |
106 | $request = Request::create('/api/driver/status', 'GET');
107 | $request->setUserResolver(function () use ($driver) {
108 | return $driver;
109 | });
110 |
111 | $controller = app(DriverController::class);
112 | $response = $controller->status($request);
113 |
114 | $this->assertInstanceOf(JsonResponse::class, $response);
115 | $this->assertEquals(200, $response->getStatusCode());
116 | $this->assertJsonStringEqualsJsonString(
117 | json_encode([
118 | 'message' => 'You are currently idle.',
119 | ]),
120 | $response->getContent()
121 | );
122 | }
123 |
124 | /**
125 | * تست وضعیت وقتی راننده احراز هویت نشده است
126 | */
127 | public function test_status_fails_when_driver_is_not_authenticated(): void
128 | {
129 | $request = Request::create('/api/driver/status', 'GET');
130 |
131 | $controller = app(DriverController::class);
132 |
133 | try {
134 | $response = $controller->status($request);
135 | $this->fail('Expected an exception due to unauthenticated driver, but none was thrown.');
136 | } catch (\ErrorException $e) {
137 | $this->assertStringContainsString('Attempt to read property "id" on null', $e->getMessage());
138 | }
139 | }
140 | }
--------------------------------------------------------------------------------
/tests/Feature/StatusUserContorllerTest.php:
--------------------------------------------------------------------------------
1 | create();
31 | $this->actingAs($user, 'user');
32 |
33 | RideRequest::factory()->create([
34 | 'user_id' => $user->id,
35 | 'isPending' => true,
36 | ]);
37 |
38 | $request = Request::create('/api/user/status', 'GET');
39 | $request->setUserResolver(fn() => $user);
40 |
41 | $controller = app(UserController::class);
42 | $response = $controller->status($request);
43 |
44 | $this->assertInstanceOf(JsonResponse::class, $response);
45 | $this->assertEquals(200, $response->getStatusCode());
46 | $this->assertJsonStringEqualsJsonString(
47 | json_encode(['message' => 'Your request is pending, please wait!']),
48 | $response->getContent()
49 | );
50 | }
51 |
52 | /**
53 | * تست وضعیت وقتی راننده درخواست را پذیرفته و در راه است
54 | */
55 | public function test_status_when_driver_is_on_the_way(): void
56 | {
57 | $user = User::factory()->create();
58 | $this->actingAs($user, 'user');
59 |
60 | CurrentRide::factory()->create([
61 | 'user_id' => $user->id,
62 | 'isArrived' => false,
63 | ]);
64 |
65 | $request = Request::create('/api/user/status', 'GET');
66 | $request->setUserResolver(fn() => $user);
67 |
68 | $controller = app(UserController::class);
69 | $response = $controller->status($request);
70 |
71 | $this->assertInstanceOf(JsonResponse::class, $response);
72 | $this->assertEquals(200, $response->getStatusCode());
73 | $this->assertJsonStringEqualsJsonString(
74 | json_encode(['message' => 'Your request has been accepted, and the driver is on the way. Please wait!']),
75 | $response->getContent()
76 | );
77 | }
78 |
79 | /**
80 | * تست وضعیت وقتی کاربر در حال انجام سفر است
81 | */
82 | public function test_status_when_user_is_on_ride(): void
83 | {
84 | $user = User::factory()->create();
85 | $this->actingAs($user, 'user');
86 |
87 | CurrentRide::factory()->create([
88 | 'user_id' => $user->id,
89 | 'isArrived' => true,
90 | ]);
91 |
92 | $request = Request::create('/api/user/status', 'GET');
93 | $request->setUserResolver(fn() => $user);
94 |
95 | $controller = app(UserController::class);
96 | $response = $controller->status($request);
97 |
98 | $this->assertInstanceOf(JsonResponse::class, $response);
99 | $this->assertEquals(200, $response->getStatusCode());
100 | $this->assertJsonStringEqualsJsonString(
101 | json_encode(['message' => 'You are currently on a ride, please end it!']),
102 | $response->getContent()
103 | );
104 | }
105 |
106 | /**
107 | * تست وضعیت وقتی کاربر هیچ سفری ندارد (بیکار است)
108 | */
109 | public function test_status_when_user_is_idle(): void
110 | {
111 | $user = User::factory()->create();
112 | $this->actingAs($user, 'user');
113 |
114 | $request = Request::create('/api/user/status', 'GET');
115 | $request->setUserResolver(fn() => $user);
116 |
117 | $controller = app(UserController::class);
118 | $response = $controller->status($request);
119 |
120 | $this->assertInstanceOf(JsonResponse::class, $response);
121 | $this->assertEquals(200, $response->getStatusCode());
122 | $this->assertJsonStringEqualsJsonString(
123 | json_encode(['message' => 'You are currently idle.']),
124 | $response->getContent()
125 | );
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/tests/Feature/StoreRideRequestControllerTest.php:
--------------------------------------------------------------------------------
1 | create();
28 | $requestData = [
29 | 'pickup_latitude' => 35.6892,
30 | 'pickup_longitude' => 51.3890,
31 | 'dest_latitude' => 35.6892,
32 | 'dest_longitude' => 51.3890,
33 | ];
34 |
35 | $mockRequest = $this->mock(NewRideRequestRequest::class, function ($mock) use ($requestData, $user) {
36 | $mock->shouldReceive('validated')->andReturn($requestData);
37 | $mock->shouldReceive('user')->andReturn($user);
38 | $mock->shouldReceive('all')->andReturn($requestData);
39 | });
40 |
41 | \Log::shouldReceive('info')
42 | ->once()
43 | ->withArgs(function ($message, $context) use ($user, $requestData) {
44 | return $message === 'Ride request created successfully' &&
45 | isset($context['request_id']) &&
46 | $context['user_id'] === $user->id &&
47 | $context['data'] === $requestData;
48 | });
49 |
50 | \Log::shouldReceive('error')
51 | ->never();
52 |
53 | $controller = new RideRequestController();
54 | $response = $controller->store($mockRequest);
55 |
56 | $this->assertInstanceOf(JsonResponse::class, $response);
57 | $this->assertEquals(201, $response->getStatusCode());
58 |
59 | $responseData = json_decode($response->getContent(), true);
60 | $this->assertEquals('Ride request created successfully', $responseData['message']);
61 | $this->assertArrayHasKey('request_id', $responseData);
62 | $this->assertEquals(['id', 'user_id'], array_keys($responseData['data']));
63 |
64 | $this->assertDatabaseHas('ride_requests', [
65 | 'user_id' => $user->id,
66 | 'pickup_latitude' => 35.6892,
67 | 'pickup_longitude' => 51.3890,
68 | 'dest_latitude' => 35.6892,
69 | 'dest_longitude' => 51.3890,
70 | ]);
71 |
72 | Event::assertDispatched(RideRequestCreated::class, function ($event) use ($responseData) {
73 | return $event->rideRequest->id === $responseData['request_id'];
74 | });
75 | }
76 |
77 | public function it_handles_creation_failure_and_logs_error()
78 | {
79 | $user = User::factory()->create();
80 | $requestData = [
81 | 'pickup_latitude' => 35.6892,
82 | 'pickup_longitude' => 51.3890,
83 | 'dest_latitude' => 35.6892,
84 | 'dest_longitude' => 51.3890,
85 | ];
86 |
87 | $mockRequest = $this->mock(NewRideRequestRequest::class, function ($mock) use ($requestData, $user) {
88 | $mock->shouldReceive('validated')->andReturn($requestData);
89 | $mock->shouldReceive('user')->andReturn($user);
90 | $mock->shouldReceive('all')->andReturn($requestData);
91 | });
92 |
93 | RideRequest::shouldReceive('create')
94 | ->with($requestData)
95 | ->andThrow(new \Exception('Database error'));
96 |
97 | \Log::shouldReceive('error')
98 | ->once()
99 | ->withArgs(function ($message, $context) use ($requestData) {
100 | return $message === 'Failed to create ride request' &&
101 | $context['error'] === 'Database error' &&
102 | is_string($context['trace']) &&
103 | $context['data'] === $requestData;
104 | });
105 |
106 | $controller = new RideRequestController();
107 | $response = $controller->store($mockRequest);
108 |
109 | $this->assertInstanceOf(JsonResponse::class, $response);
110 | $this->assertEquals(500, $response->getStatusCode());
111 |
112 | $responseData = json_decode($response->getContent(), true);
113 | $this->assertEquals('An error occurred while creating the ride request', $responseData['message']);
114 |
115 | if (config('app.debug')) {
116 | $this->assertEquals('Database error', $responseData['error']);
117 | } else {
118 | $this->assertNull($responseData['error']);
119 | }
120 |
121 | $this->assertDatabaseMissing('ride_requests', [
122 | 'pickup_latitude' => 35.6892,
123 | 'pickup_longitude' => 51.3890,
124 | 'dest_latitude' => 35.6892,
125 | 'dest_longitude' => 51.3890,
126 | ]);
127 |
128 | Event::assertNotDispatched(RideRequestCreated::class);
129 | }
130 |
131 |
132 | protected function tearDown(): void
133 | {
134 | Mockery::close();
135 | parent::tearDown();
136 | }
137 | }
138 |
139 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 |