Combined single-file version of the documentation suite.
Welcome to the Turista API documentation. This guide covers everything you need to develop against, operate, and contribute to the project.
Turista is a Laravel 13 API for managing vacation rentals, reservations, payments, and on-arrival guest bookings. It supports multiple roles — super admin, admin, owner, employee, and customer — with role-based access control and a stateless Sanctum authentication layer.
| If you are... | Start with... |
|---|---|
| A new developer joining the team | development/setup.md |
| An API consumer building a client | api/overview.md |
| An owner or employee using the API | api/roles/owner.md or api/roles/employee.md |
| A DevOps engineer deploying the app | operations/deployment.md |
| A security reviewer | security/overview.md |
architecture/overview.md — high-level architecture and design principles.architecture/directory-structure.md — what lives where.architecture/authentication.md — Sanctum, roles, and permissions.architecture/data-flow.md — request lifecycle and service layer.architecture/error-handling.md — exception mapping and response format.api/overview.md — base URL, versioning, auth headers, rate limits.api/authentication.md — login, registration, OTP, password reset.api/roles/admin.md — admin endpoints.api/roles/owner.md — owner endpoints.api/roles/employee.md — employee endpoints.api/roles/customer.md — customer endpoints.api/auto-generated.md — Laravel Request Docs setup.domains/users-and-profiles.mddomains/buildings-and-units.mddomains/reservations.mddomains/billing.mddomains/promo-codes.mddomains/locations.mddomains/notifications.mdoperations/deployment.mdoperations/environment-variables.mdoperations/scheduled-tasks.mdoperations/monitoring.mdTurista is an API-first Laravel 13 application. It exposes a JSON REST API under /api/v1 and keeps frontend assets minimal.
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Client │────▶│ Nginx/PHP │────▶│ Laravel App │
│ (SPA/App) │◀────│ (Laravel) │◀────│ │
└─────────────┘ └──────────────┘ └─────────────────┘
│
┌────────────┬────────────┬────────────┼────────────┬────────────┐
▼ ▼ ▼ ▼ ▼ ▼
Routes/ Controllers/ Services/ Models/ Policies/ Jobs/
Middleware Requests/ Facades Eloquent Gates Notifications
Resources MySQL
| Layer | Technology |
|---|---|
| Framework | Laravel 13 |
| Language | PHP 8.4 |
| Database | MySQL 8.0 (dev), SQLite in-memory (tests) |
| API authentication | Laravel Sanctum |
| Roles & permissions | Spatie Laravel Permission |
| Media uploads | Spatie Media Library |
| Auditing | OwenIt Auditing |
| Frontend build | Vite + Tailwind CSS 4 |
| Testing | Pest PHP 4 |
| Queues | Database (default) |
Thin controllers, fat services. Controllers validate input, authorize actions, and delegate business logic to service classes in app/Services/. Services are exposed through facades in app/Facades/.
Policy-based authorization. Every resource has a policy in app/Policies/. Route middleware checks roles; policies check ownership and state. super_admin bypasses all policy checks via Gate::before.
Availability ledger. The unit_availabilities table stores one row per unit per night. This makes date-range conflict checks simple and reliable.
Invoice/receipt mirroring. When a reservation is created or modified, the system generates an owner-facing invoice and a customer-facing receipt with matching financial totals.
Scheduled notifications. Reminders are stored as ScheduledNotification rows and dispatched by a scheduled command through queue jobs.
Morph relationships. Reservation and PendingReservation are polymorphically linked to either a Customer or a PendingCustomer, supporting both online and on-arrival booking flows.
routes/api.phproutes/console.phpbootstrap/app.phpapp/Providers/AppServiceProvider.phpThis document explains the purpose of each major directory and file group in the Turista project.
| Path | Purpose |
|---|---|
app/ |
Application code (controllers, models, services, etc.). |
bootstrap/ |
Laravel bootstrapping and exception handling. |
config/ |
Configuration files. |
database/ |
Migrations, seeders, factories, and location data. |
docs/ |
Project documentation (this suite). |
public/ |
Web server document root. |
resources/ |
CSS, JS, and email views. |
routes/ |
Route definitions. |
storage/ |
Logs, cache, uploads, and compiled files. |
tests/ |
Pest feature and unit tests. |
vendor/ |
Composer dependencies. |
node_modules/ |
NPM dependencies. |
app/| Path | Purpose |
|---|---|
Console/Commands/ |
Artisan commands (CreateSuperAdmin, DownloadLocations, ReleaseExpiredPendingReservations, SendDueScheduledNotifications). |
Exceptions/ |
Custom exceptions such as ReservationUnavailableException. |
Facades/ |
Laravel facades for services (ReservationService, PaymentService, FilterService, etc.). |
Http/Controllers/ |
HTTP controllers grouped by area (API auth, admin, owner, customer, core resources). |
Http/Middleware/ |
Custom middleware (EnsureUserIsVerified, SecurityHeadersMiddleware). |
Http/Requests/ |
Form request classes for validation. |
Http/Resources/ |
API resource transformers. |
Jobs/ |
Queue jobs (DispatchScheduledNotification). |
Mail/ |
Mailable classes (PasswordResetMail). |
Models/ |
Eloquent models. |
Notifications/ |
Notification classes and custom channels (database, WhatsApp). |
Policies/ |
Authorization policies for every major model. |
Providers/ |
Service providers (AppServiceProvider, etc.). |
Rules/ |
Custom validation rules (PhoneNumber, WhatsAppNumber, PhotoFileRules). |
Services/ |
Business logic services (ReservationService, PaymentService, FilterService, etc.). |
Traits/ |
Reusable traits (HandlesMediaPhotos). |
database/| Path | Purpose |
|---|---|
data/ |
JSON/PHP location data used by seeders. |
factories/ |
Model factories for tests and seeding. |
migrations/ |
All database migrations, ordered by timestamp. |
seeders/ |
DatabaseSeeder, RolesAndPermissionsSeeder, LocationSeeder. |
routes/| File | Purpose |
|---|---|
api.php |
All API routes (prefixed with /api/v1). |
console.php |
Scheduled console commands. |
web.php |
Minimal web routes (health check, password-reset email view). |
resources/| Path | Purpose |
|---|---|
css/app.css |
Tailwind CSS import. |
js/app.js |
Minimal Vite entry point. |
views/emails/password-reset.blade.php |
Password reset email template. |
tests/| Path | Purpose |
|---|---|
Feature/ |
High-level HTTP/feature tests. |
Unit/ |
Isolated unit tests for services, policies, and models. |
Pest.php |
Pest configuration; applies RefreshDatabase to feature/unit tests. |
TestCase.php |
Base test case with shared helpers and CoreVerde fakes. |
config/Key files include:
app.php — application name, environment, timezone, locale.auth.php — default api guard and password reset settings.sanctum.php — token expiration and stateful domains.audit.php — OwenIt Auditing configuration.request-docs.php — Laravel Request Docs settings.locations.php — countries to seed.Turista uses a stateless token architecture built on Laravel Sanctum. Roles and permissions are managed by Spatie Laravel Permission.
User record plus the matching profile (Owner or Customer) and sends an OTP.is_verified is set to true.Authorization: Bearer {token} on subsequent requests.api60 * 24 * 7 minutes in config/sanctum.php)SANCTUM_STATEFUL_DOMAINS.RolesAndPermissionsSeeder creates the following roles against the api guard:
super_adminadminowneremployeecustomerKey permissions include:
manage_platformapprove_buildingsmanage_employeesmanage_buildingsmanage_unitsview_reservationsmanage_reservationsmanage_customersapply_promo_codesRoles map to permissions in the seeder. Controllers and routes use role middleware (role:owner, role:owner|employee, etc.) for coarse access and policies for fine-grained authorization.
| Middleware | Alias | Purpose |
|---|---|---|
EnsureUserIsVerified |
verified |
Returns 403 if the authenticated user is not verified. |
Spatie RoleMiddleware |
role |
Restricts routes by role. |
Spatie PermissionMiddleware |
permission |
Restricts routes by permission. |
Spatie RoleOrPermissionMiddleware |
role_or_permission |
Restricts by role or permission. |
AppServiceProvider registers a Gate::before callback that grants super_admin users access to every policy check. This is the only role that bypasses policies.
Several routes use both auth:api and verified middleware. Unverified users can register, verify OTP, and resend OTP, but cannot access protected resources.
Owners have an additional status field (pending, active, suspended) and an is_verified flag on the Owner model. Admin users can verify owners via POST /api/v1/owners/{owner}/verify. Many owner actions require the owner to be approved (status = active).
Employees belong to an Owner and optionally to a Building. Their status can be active or suspended. Active employees can perform owner-delegated actions such as managing reservations.
This document describes how a typical HTTP request moves through the Turista application.
Request
▼
Route (routes/api.php)
▼
Middleware (auth, verified, role, throttle)
▼
Form Request validation (app/Http/Requests/)
▼
Controller action (app/Http/Controllers/)
▼
Policy authorization ($this->authorize('view', $model))
▼
Service layer (app/Services/ via Facades)
▼
Eloquent models & database
▼
API Resource (app/Http/Resources/)
▼
JSON response
POST /api/v1/customer/reservations hits ReservationController@store.auth:api and role:customer ensure an authenticated customer.ReservationRequest validates dates, guest counts, unit_id, and promo code.ReservationService::createReservationForCustomer().UnitAvailabilityService.PromoCodeService.Reservation record.Invoice for the owner and a Receipt for the customer.ReservationReminderScheduler.ReservationResource is returned with the new reservation, invoice, and receipt.Business logic lives in app/Services/ and is consumed through facades in app/Facades/. This keeps controllers thin and makes the logic testable in isolation.
| Service | Responsibility |
|---|---|
ReservationService |
Reservation lifecycle, pricing, availability booking, documents. |
PaymentService |
Payments, refunds, wallet updates. |
UnitAvailabilityService |
Date ledger operations (book, hold, release, block). |
PromoCodeService |
Bulk generation and redemption validation. |
OtpService |
OTP generation, caching, and verification. |
WhatsAppService |
CoreVerde WhatsApp HTTP integration. |
DashboardMetrics |
Revenue, occupancy, and user-growth aggregations. |
FilterService |
Query filtering for list endpoints. |
SequenceService |
Atomic document number generation. |
ReservationReminderScheduler |
Schedules reminder notifications. |
unit_availabilities has one row per unit per night with a status of available, blocked, or booked. When a reservation is created, the service changes the matching rows to booked and links them to the reservation. Cancellations reverse this process.
When a reservation is confirmed, ReservationReminderScheduler creates ScheduledNotification rows for check-in, check-out, and evacuate reminders. The notifications:send-due scheduler command runs every minute and dispatches DispatchScheduledNotification jobs for due rows. The job sends the notification through the database channel (in-app notification) and WhatsApp channel.
SequenceService increments counters stored in the sequences table to produce zero-padded document numbers such as INV-00001, REC-00001, and RES-00001.
Controllers catch domain exceptions (e.g., ReservationUnavailableException) and return structured JSON errors. Unexpected exceptions are rendered as JSON by the handler in bootstrap/app.php.
Turista is an API-first application, so all errors are returned as JSON. Exception rendering is configured in bootstrap/app.php.
A typical error response follows this shape:
{
"message": "The given data was invalid.",
"errors": {
"email": ["The email field is required."]
}
}
For non-validation errors the response may contain only message:
{
"message": "Reservation dates are not available."
}
| Exception type | HTTP status | Notes |
|---|---|---|
| ValidationException | 422 | Returned when Form Request validation fails. |
| AuthenticationException | 401 | Returned by auth:api middleware. |
| AuthorizationException | 403 | Returned by policies or verified middleware. |
| ModelNotFoundException | 404 | Returned when a route-bound model is missing. |
| ReservationUnavailableException | 422 | Returned when requested dates cannot be booked or blocked. |
| HttpException | as set | Generic HTTP exceptions. |
bootstrap/app.php configures the exception handler to render API exceptions as JSON. Validation, authentication, authorization, and model-not-found exceptions are mapped to clean responses. In production, detailed stack traces are hidden (APP_DEBUG=false).
App\Exceptions\ReservationUnavailableException is thrown by the reservation and availability services when a unit cannot be booked or blocked for the requested dates. Controllers catch this and return a 422 response with a clear message.
Form Request classes in app/Http/Requests/ centralize validation rules. When validation fails, Laravel returns a 422 response with the errors object keyed by field name.
Unexpected exceptions are logged to storage/logs/laravel.log. Operators can tail these logs or forward them to a centralized logging service.
API clients should:
message for a human-readable description.errors for field-level validation feedback.Turista exposes a JSON REST API under the base URL /api/v1.
https://{your-domain}/api/v1
The current API version is v1. Versioning is path-based. Future versions will use a new path prefix (e.g., /api/v2).
All requests should include:
Accept: application/json
Content-Type: application/json
Authenticated requests must also include:
Authorization: Bearer {sanctum_token}
See authentication.md for details on login, registration, OTP verification, password reset, and logout.
Public authentication endpoints (login, register, OTP) are rate-limited by IP and contact information. Authenticated endpoints generally use the default Laravel throttle. Specific limits are configured in RouteServiceProvider or route middleware.
Successful responses return a 2xx status and a JSON body. The shape depends on the endpoint; see auto-generated.md for full schemas.
List endpoints typically return paginated data:
{
"data": [...],
"links": {...},
"meta": {...}
}
Single-resource endpoints return a resource object:
{
"data": {...}
}
Errors are returned as JSON with an HTTP 4xx/5xx status. See architecture/error-handling.md for details.
List endpoints accept query parameters for filtering, sorting, and pagination. Common patterns:
?page=2?per_page=20?status=active?sort=-created_atExact parameter names vary by endpoint. Use Laravel Request Docs for the full list.
The API is organized by actor:
For exhaustive request/response schemas, see auto-generated.md on how to use Laravel Request Docs.
This document covers the public and authenticated auth endpoints.
POST /api/v1/login
Request:
{
"email": "user@example.com",
"password": "secret"
}
Response includes a Sanctum token:
{
"data": {
"user": {...},
"token": "{sanctum_token}"
}
}
Unverified accounts receive a 403 response.
POST /api/v1/register/owner
Creates a User and an Owner profile. Sends an OTP.
POST /api/v1/register/customer
Creates a User and a Customer profile. Links any prior PendingCustomer reservations.
POST /api/v1/verify-otp
Validates the OTP and marks the user as verified.
POST /api/v1/verify-account
Alternative verification endpoint.
POST /api/v1/resend-otp
Resends the OTP if the user exists and is unverified.
POST /api/v1/forgot-password
Creates a password-reset token and sends a reset email.
POST /api/v1/reset-password
Validates the reset token and updates the password.
These require a valid bearer token.
POST /api/v1/logout
Deletes the current access token.
PUT /api/v1/update-password
Changes the authenticated user's password after verifying the current password.
OTP codes are exposed in API responses only in local and testing environments for development convenience. In production, OTPs are sent through the configured channel only.
After login, include the token in all subsequent requests:
Authorization: Bearer {sanctum_token}
Tokens expire after one week by default.
Admins manage the platform, locations, and owner verification.
admin or super_admin role.super_admin role.| Method | Path | Description |
|---|---|---|
| GET | /api/v1/admin/dashboard |
Revenue and user-growth summary with filters. |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/admin/reservations |
List all reservations. |
| GET | /api/v1/admin/buildings |
List all buildings. |
Admins have full CRUD over locations. Public read-only endpoints are documented in customer.md.
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/admin/cities |
Create a city. |
| PUT | /api/v1/admin/cities/{city} |
Update a city. |
| DELETE | /api/v1/admin/cities/{city} |
Delete a city. |
| POST | /api/v1/admin/countries |
Create a country. |
| PUT | /api/v1/admin/countries/{country} |
Update a country. |
| DELETE | /api/v1/admin/countries/{country} |
Delete a country. |
| POST | /api/v1/admin/currencies |
Create a currency. |
| PUT | /api/v1/admin/currencies/{currency} |
Update a currency. |
| DELETE | /api/v1/admin/currencies/{currency} |
Delete a currency. |
| POST | /api/v1/admin/regions |
Create a region. |
| PUT | /api/v1/admin/regions/{region} |
Update a region. |
| DELETE | /api/v1/admin/regions/{region} |
Delete a region. |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/owners |
List owners. |
| POST | /api/v1/owners/{owner}/verify |
Verify an owner profile. |
| GET | /api/v1/admin/employees |
List all employees. |
| GET | /api/v1/admin/notifications |
List all notifications. |
| POST | /api/v1/admins |
Create a new admin (super_admin only). |
| GET | /api/v1/customers |
List customers. |
| DELETE | /api/v1/customers/{customer} |
Delete a customer. |
For detailed request/response schemas, generate Laravel Request Docs (see auto-generated.md).
Owners manage buildings, units, employees, reservations, billing, and promo codes.
owner role.status = active).| Method | Path | Description |
|---|---|---|
| GET | /api/v1/owner/dashboard |
Revenue and occupancy dashboard. |
| GET | /api/v1/owner/profile |
Get owner profile. |
| PUT | /api/v1/owner/profile |
Update owner profile. |
| GET | /api/v1/owner/calendar |
Unit availability calendar. |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/owner/buildings |
List owner's buildings. |
| POST | /api/v1/owner/buildings |
Create a building. |
| GET | /api/v1/owner/buildings/{building} |
Show a building. |
| PUT | /api/v1/owner/buildings/{building} |
Update a building. |
| DELETE | /api/v1/owner/buildings/{building} |
Delete a building. |
| POST | /api/v1/owner/buildings/{building}/photos |
Upload building photos. |
| POST | /api/v1/owner/buildings/{building}/facilities |
Assign facilities. |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/owner/units |
List owner's units. |
| POST | /api/v1/owner/units |
Create a unit. |
| GET | /api/v1/owner/units/{unit} |
Show a unit. |
| PUT | /api/v1/owner/units/{unit} |
Update a unit. |
| DELETE | /api/v1/owner/units/{unit} |
Delete a unit. |
| POST | /api/v1/owner/buildings/{building}/units/bulk |
Bulk-create units. |
| POST | /api/v1/owner/units/{unit}/photos |
Upload unit photos. |
| POST | /api/v1/owner/units/{unit}/facilities |
Assign facilities. |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/owner/employees |
List employees. |
| POST | /api/v1/owner/employees |
Create an employee. |
| GET | /api/v1/owner/employees/{employee} |
Show an employee. |
| DELETE | /api/v1/owner/employees/{employee} |
Delete an employee. |
Note: employees update their own profiles through the shared employee endpoint.
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/owner/reservations |
List reservations. |
| POST | /api/v1/owner/reservations |
Create a reservation. |
| GET | /api/v1/owner/reservations/{reservation} |
Show a reservation. |
| PUT | /api/v1/owner/reservations/{reservation} |
Update a reservation. |
| DELETE | /api/v1/owner/reservations/{reservation} |
Cancel a reservation. |
| POST | /api/v1/owner/reservations/{reservation}/check-in |
Check in a guest. |
| POST | /api/v1/owner/reservations/{reservation}/check-out |
Check out a guest. |
| GET | /api/v1/about-to-end |
Reservations about to end. |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/owner/invoices |
List invoices. |
| POST | /api/v1/owner/invoices |
Create an invoice. |
| GET | /api/v1/owner/invoices/{invoice} |
Show an invoice. |
| DELETE | /api/v1/owner/invoices/{invoice} |
Delete an invoice (guarded by transactions). |
| GET | /api/v1/owner/transactions |
List transactions. |
| POST | /api/v1/owner/transactions |
Record a payment or refund. |
| GET | /api/v1/owner/receipts |
List receipts. |
| POST | /api/v1/owner/receipts |
Generate a receipt. |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/owner/promo-codes |
List promo codes. |
| POST | /api/v1/owner/promo-codes |
Bulk-generate promo codes. |
| GET | /api/v1/owner/promo-codes/{promo_code} |
Show a promo code. |
| PUT | /api/v1/owner/promo-codes/{promo_code} |
Update a promo code. |
| DELETE | /api/v1/owner/promo-codes/{promo_code} |
Delete a promo code. |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/owner/unit-availabilities |
List availability. |
| POST | /api/v1/owner/unit-availabilities/block |
Block dates. |
| POST | /api/v1/owner/unit-availabilities/unblock |
Unblock dates. |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/owner/facilities |
List facilities. |
| POST | /api/v1/owner/facilities |
Create a facility. |
| GET | /api/v1/owner/facilities/{facility} |
Show a facility. |
| PUT | /api/v1/owner/facilities/{facility} |
Update a facility. |
| DELETE | /api/v1/owner/facilities/{facility} |
Delete a facility. |
For detailed schemas, see auto-generated.md.
Employees act on behalf of an owner. They can manage reservations and view related data.
employee role.active.Owner and optionally to a specific Building.The following routes accept both owner and employee roles:
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/reservations |
List reservations scoped to the owner/employee. |
| POST | /api/v1/reservations/on-arrival/prepare |
Start an on-arrival booking. |
| POST | /api/v1/reservations/on-arrival/validate |
Validate the on-arrival OTP. |
| POST | /api/v1/reservations/{reservation}/check-in |
Check in a guest. |
| POST | /api/v1/reservations/{reservation}/check-out |
Check out a guest. |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/employee/profile |
Get own employee profile. |
| PUT | /api/v1/employee/profile |
Update own employee profile. |
Employees see:
They cannot create or delete buildings, units, or employees.
For detailed schemas, see auto-generated.md.
Customers browse the public catalog and manage their own reservations and profile.
customer role.| Method | Path | Description |
|---|---|---|
| GET | /api/v1/units |
List available units with filters. |
| GET | /api/v1/units/{unit} |
Show a unit. |
| POST | /api/v1/promo-codes/preview |
Preview promo-code discount. |
| GET | /api/v1/countries |
List countries. |
| GET | /api/v1/countries/{country} |
Show a country. |
| GET | /api/v1/cities |
List cities. |
| GET | /api/v1/cities/{city} |
Show a city. |
| GET | /api/v1/regions |
List regions. |
| GET | /api/v1/regions/{region} |
Show a region. |
| GET | /api/v1/currencies |
List currencies. |
| GET | /api/v1/currencies/{currency} |
Show a currency. |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/customer/profile |
Get own profile. |
| PUT | /api/v1/customer/profile |
Update own profile. |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/customer/reservations |
List customer reservations. |
| POST | /api/v1/customer/reservations |
Create a reservation. |
| GET | /api/v1/customer/reservations/{reservation} |
Show a reservation. |
| PUT | /api/v1/customer/reservations/{reservation} |
Update a reservation. |
| DELETE | /api/v1/customer/reservations/{reservation} |
Cancel a reservation. |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/customer/invoices |
List customer invoices. |
| GET | /api/v1/customer/receipts |
List customer receipts. |
| GET | /api/v1/customer/transactions |
List customer transactions. |
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/notifications |
List own notifications. |
| POST | /api/v1/notifications/{notification}/mark-as-read |
Mark a notification as read. |
| DELETE | /api/v1/notifications/{notification} |
Delete a notification. |
For detailed schemas, see auto-generated.md.
Turista includes Laravel Request Docs, which generates interactive API documentation from your routes and Form Request classes.
Laravel Request Docs can display:
Set the environment variable:
REQUEST_DOCS_ENABLED=true
Then clear the config cache if running in production:
php artisan config:clear
When enabled, visit:
https://{your-domain}/request-docs
The UI allows you to authenticate with a bearer token and make test requests against the API.
Laravel Request Docs can generate api.json and routes.json for external consumers. To regenerate them:
php artisan route:docs
Check the package documentation for the exact command if this differs.
Keep REQUEST_DOCS_ENABLED=false in production. The generated api.json and routes.json files reveal endpoint structure, middleware, and controllers. Consider adding them to .gitignore and generating them only in build pipelines.
The documentation in this docs/api/ directory explains concepts, roles, and workflows. Laravel Request Docs provides the exhaustive endpoint-level schemas. Use both together: start here for context, then use Request Docs for precise payloads.
Turista uses a single users table as the shared identity for all actors. Role-specific data lives in profile tables.
App\Models\User stores:
nameemail (unique)phone (unique)password (hashed)is_verifiedA user has one of the following profiles:
OwnerEmployeeCustomerSpatie Laravel Permission assigns one role per user:
super_adminadminowneremployeecustomerApp\Models\Owner stores:
status — pending, active, or suspendedwhatsapp_numberis_verifiedThe primary key id is also a foreign key to users.id. An owner has many Buildings, Employees, and Units through buildings.
Owners must be approved (status = active) before they can list properties or accept reservations.
App\Models\Employee stores:
owner_id — the owner they work forbuilding_id — optional building assignmentstatus — active or suspendedEmployees act on behalf of an owner and can manage reservations and related data. Their access is scoped to their owner (and optionally building).
App\Models\Customer stores:
whatsapp_numberwallet (decimal)Customers make online bookings and receive receipts. The wallet can be used for payments or receive refunds.
App\Models\PendingCustomer supports the on-arrival booking flow. When a guest arrives without a prior account, the owner/employee can create a pending reservation linked to a pending customer. The guest verifies their phone number via OTP, at which point the pending customer is converted or linked to a real customer.
User + Owner created; OTP sent; admin verifies owner after activation.User + Customer created; OTP sent.PendingCustomer created with phone; OTP verified; reservation confirmed.Buildings and units are the core inventory of the platform.
App\Models\Building represents a physical property owned by an owner.
Key fields:
owner_idregion_idcurrency_idnamecheck_in_time / check_out_timemap_lat / map_lngstatus — e.g., active, inactiveslug (unique)payment_methods (JSON)booking_conditions (JSON)Relationships:
ownerregioncurrencyunitsfacilities (BelongsToMany)App\Models\Unit represents a rentable space within a building.
Key fields:
building_idfloorname_or_numberroomsbase_priceoffer_priceguest_typemax_adults / max_children / max_child_agestatusslug (unique)booking_conditions (JSON)Relationships:
buildingreservationsavailabilitiesfacilities (BelongsToMany)App\Models\Facility represents amenities such as Wi-Fi, parking, or a pool. Facilities can be attached to buildings and units.
App\Models\UnitAvailability stores one row per unit per night.
Fields:
unit_iddatestatus — available, blocked, or bookedreservation_idpending_reservation_idThis ledger enables reliable date-range conflict detection. When a reservation is confirmed, the matching rows become booked. When canceled, they revert to available. Owners can block dates to take units off the market.
Unauthenticated users can list and view units. The catalog supports filters for:
Owners can bulk-create units under a building. This is useful for hotels or apartment blocks with many similar units.
Buildings and units support photo uploads via Spatie MediaLibrary. The HandlesMediaPhotos trait centralizes upload and replacement logic. Allowed formats are typically JPG, JPEG, PNG, and WebP with size and dimension limits.
Reservations are the central transaction in Turista. They link a customer (or pending customer) to a unit for a date range.
App\Models\Reservation stores:
reservation_number (unique)customer_type / customer_id (morph to Customer or PendingCustomer)unit_idpromo_code_idcheck_in_date / check_out_dateadults_count / children_countpayment_statusstatus — lifecycle statesource — online or on-arrivaltotal_pricenotespending → confirmed → checked_in → checked_out
↓
canceled
A verified customer selects a unit and dates. The system checks availability, applies any promo code, calculates the total, and creates a confirmed reservation.
When a guest arrives without a prior booking:
POST /api/v1/reservations/on-arrival/prepare).PendingReservation linked to a PendingCustomer and sends an OTP.POST /api/v1/reservations/on-arrival/validate).Reservation.Customers and owners can update reservation dates. The service:
Owner/employee users can transition a confirmed reservation to checked_in and later to checked_out. Checked-out and canceled reservations cannot be edited or canceled again.
The reservation_companions table was removed; companion information is stored in notes or handled by the client application.
ReservationService — create, update, confirm, cancel, recalculate.UnitAvailabilityService — book, release, block availability dates.ReservationReminderScheduler — schedule reminder notifications.Turista's billing model separates owner-facing invoices from customer-facing receipts and tracks all money movement through transactions.
App\Models\Invoice is generated for the owner when a reservation is confirmed.
Key fields:
reservation_idreceived enum (invoice type/role)document_numberprice, total_price, discount, net_pricepaid_amount, remaining_amountpaid_at, due_atInvoices cannot be deleted if transactions exist against them.
App\Models\Receipt mirrors the invoice for the customer.
Key fields:
reservation_idinvoice_id (nullable)Receipts give customers a record of what they owe or have paid.
App\Models\Transaction records each payment or refund.
Key fields:
invoice_idtype — payment or refundpayment_method — cash, card, or walletamountreasonThe PaymentService processes payments and refunds atomically to avoid double-credits or race conditions.
Customers have a wallet balance on their profile. Refunds can be credited to the wallet, and wallet balance can be used as a payment method.
SequenceService maintains atomic counters in the sequences table. Each invoice, receipt, and reservation gets a zero-padded number such as:
RES-00001INV-00001REC-00001When a reservation is updated (e.g., date change), ReservationService::recalculateReservation updates the invoice and receipt totals and triggers any required refund.
Financial operations use row-level locking and atomic updates to prevent race conditions. See the security audit for additional hardening notes.
Promo codes let owners offer discounts on unit bookings.
App\Models\PromoCode stores:
owner_id (nullable; null means platform-wide)code (encrypted)code_hash (unique, used for lookups)usage_limitper_customer_limituses_countdiscount_valuediscount_type — e.g., fixed or percentagestarts_at, expires_at, used_atThe actual code is encrypted; a hash is used for validation.
Owners can generate many promo codes at once. The PromoCodeService::generateBulk method:
A promo code is valid when:
usage_limit.per_customer_limit.During reservation creation or recalculation, the system:
uses_count.Unauthenticated users can preview the discount a promo code would apply to a unit without actually redeeming it.
Owners can list, update, and delete their own promo codes. Admins can manage platform-wide codes (owner_id = null).
The location catalog provides country, city, region, and currency data used by buildings and the public search.
| Model | Purpose |
|---|---|
Country |
Top-level country. |
City |
City within a country. |
Region |
Region/neighborhood within a city. |
Currency |
Currency used by buildings. |
Country has many CityCity belongs to Country and has many RegionRegion belongs to City and has many BuildingCountry belongs to CurrencyBuilding belongs to Region and CurrencyAdmins can create, update, and delete locations. The admin endpoints are prefixed with /api/v1/admin/.
Unauthenticated users can list and show countries, cities, regions, and currencies. These endpoints power the public catalog filters.
The LocationSeeder seeds the configured countries (default: Libya) from database/data/locations.json. To download fresh location data:
php artisan locations:download
This command fetches an external dataset, filters it by config('locations.seed_countries'), and writes database/data/locations.json.
config/locations.php contains:
'seed_countries' => explode(',', env('SEED_COUNTRIES', 'Libya')),
Use the SEED_COUNTRIES environment variable to control which countries are seeded.
Turista sends reservation reminders through in-app database notifications and WhatsApp messages.
App\Models\ScheduledNotification represents a reminder that should be sent at a specific time.
Key fields:
notifiable_type / notifiable_id (morph)reservation_idtype — e.g., check_in, check_out, evacuatesend_atqueued_at, sent_at, cancelled_atpayload (array)Scopes:
due — notifications whose send_at has passed and are not yet sent/cancelled.pendingForReservation — unsent notifications for a given reservation.| Type | When it fires |
|---|---|
check_in |
Before the guest arrives. |
check_out |
Before the guest departs. |
evacuate |
When the stay should end. |
The notifications:send-due command runs every minute via routes/console.php:
Schedule::command('notifications:send-due')->everyMinute();
It claims due notifications and dispatches DispatchScheduledNotification jobs.
App\Jobs\DispatchScheduledNotification:
ReservationReminder notification.App\Notifications\ReservationReminder is sent through two channels:
AppDatabaseChannel — writes to the notifications table for in-app display.WhatsAppChannel — sends a WhatsApp message via WhatsAppService / CoreVerde.Authenticated users can list their notifications, mark them as read, and delete them.
When a reservation is canceled or checked out, pending scheduled notifications for that reservation are canceled to avoid sending irrelevant reminders.
This guide walks you through running Turista on your local machine.
bash
composer install
bash
npm install
bash
cp .env.example .env
Update .env with your database credentials and any required third-party API keys (CoreVerde WhatsApp, mail, etc.).
bash
php artisan key:generate
bash
php artisan storage:link
bash
php artisan migrate --seed
This creates roles, permissions, and the default location data (Libya by default).
bash
npm run build
composer run dev
This starts the Laravel development server, a queue worker, and the Vite dev server concurrently.
After seeding, create a super admin via CLI:
php artisan admin:create-super admin@example.com
You will be prompted for a secure password.
If you need to seed additional countries:
SEED_COUNTRIES in .env.bash
php artisan locations:download
php artisan db:seed --class=LocationSeeder
Run the verification commands from the top-level README:
php artisan test
vendor/bin/pint --test
npm run build
php artisan route:cache
php artisan optimize
All tests should pass and the build should complete without errors.
Turista uses Pest PHP 4 for testing. Tests run against an in-memory SQLite database.
php artisan test
To run a specific test file:
php artisan test tests/Feature/Auth/LoginTest.php
To run with verbose output:
php artisan test --verbose
phpunit.xml configures SQLite in-memory mode:
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
The RefreshDatabase trait is applied to all feature and unit tests via tests/Pest.php.
| Directory | Purpose |
|---|---|
tests/Feature/ |
End-to-end HTTP and feature tests. |
tests/Unit/ |
Isolated tests for services, policies, and models. |
tests/TestCase.php provides shared helpers and sets up:
A typical feature test:
it('allows a verified customer to create a reservation', function () {
$customer = User::factory()
->has(Customer::factory())
->create()
->assignRole('customer');
$unit = Unit::factory()->create();
actingAs($customer, 'api')
->postJson('/api/v1/customer/reservations', [
'unit_id' => $unit->id,
'check_in_date' => now()->addDay()->toDateString(),
'check_out_date' => now()->addDays(3)->toDateString(),
'adults_count' => 2,
])
->assertCreated();
});
Unit tests focus on a single class or method without HTTP:
it('blocks dates in the availability ledger', function () {
$unit = Unit::factory()->create();
UnitAvailabilityService::blockDates($unit, ['2026-07-01', '2026-07-02']);
expect($unit->availabilities()->where('status', 'blocked')->count())->toBe(2);
});
As of the latest project progress report, the suite has 297 tests and 1111 assertions. Aim to maintain or improve this coverage when adding features.
Turista follows Laravel conventions and uses Laravel Pint for code-style enforcement.
Pint is included as a dev dependency. To check style without making changes:
vendor/bin/pint --test
To apply fixes:
vendor/bin/pint
Run Pint before committing:
vendor/bin/pint --test
If it reports issues, run vendor/bin/pint and review the changes.
PascalCase, singular where possible (BuildingController).PascalCase, singular (Building, Unit).PascalCase + Request suffix (BuildingRequest).PascalCase + Resource suffix (BuildingResource).PascalCase + Service suffix (ReservationService).PascalCase + Policy suffix (BuildingPolicy).buildings, unit_availabilities).Keep controllers thin:
$this->authorize(...)).Services live in app/Services/ and are exposed through facades in app/Facades/. Public methods should have clear, single responsibilities.
Model::unguard() is enabled globally by project decision. This means every model accepts any database column during mass assignment. Be extremely careful to whitelist input in controllers and Form Requests.
See security/overview.md for the rationale and risks.
This document collects frequently used commands and workflows.
php artisan admin:create-super admin@example.com
You will be prompted for a password.
php artisan locations:download
Downloads and filters location data into database/data/locations.json.
php artisan pending-reservations:release-expired
Finds expired pending reservations and releases held availability. This also runs every minute via the scheduler.
php artisan notifications:send-due
Dispatches reminder notifications that are due. Runs every minute via the scheduler.
php artisan migrate:fresh --seed
php artisan db:seed --class=LocationSeeder
Tests use an in-memory SQLite database, so no manual reset is needed.
php artisan optimize:clear
In some local environments this may report a missing MySQL
cachetable. This is an environment quirk and does not affect the test suite.
php artisan config:cache
php artisan route:cache
php artisan optimize
For local development, the queue worker is started by:
composer run dev
To run it manually:
php artisan queue:listen --tries=1
Ensure phpunit.xml uses SQLite in-memory and that the SQLite extension is enabled.
npm run build failsMake sure resources/js/app.js and resources/css/app.css exist. They are minimal but required for Vite.
Ensure the storage link exists:
php artisan storage:link
Verify CoreVerde configuration in .env and that COREVERDE_ENABLED is true.
This checklist covers deploying Turista to a production environment.
APP_ENV=production.APP_DEBUG=false.APP_KEY (php artisan key:generate).REQUEST_DOCS_ENABLED=false.CORS_ALLOWED_ORIGINS to known frontend domains.bash
composer install --no-dev --optimize-autoloader
npm ci
npm run build
bash
php artisan migrate --force
bash
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize
bash
php artisan storage:link
Ensure storage/ and bootstrap/cache/ are writable by the web server.
Add the scheduler cron entry:
* * * * * cd /path/to/turista && php artisan schedule:run >> /dev/null 2>&1
Run a queue worker using Supervisor or systemd:
php artisan queue:work --sleep=3 --tries=3 --max-time=3600
Point the document root to public/. Use HTTPS in production.
200 OK on /.storage/logs/.If a deployment fails:
php artisan migrate:rollback if migrations were applied.php artisan optimize:clear.php artisan optimize.This reference describes the key environment variables used by Turista.
| Variable | Default | Description |
|---|---|---|
APP_NAME |
Laravel | Application name. |
APP_ENV |
local | Environment: local, testing, production. |
APP_DEBUG |
false | Enables debug responses. Must be false in production. |
APP_KEY |
Encryption key. Generate with php artisan key:generate. |
|
APP_URL |
http://localhost | Public URL of the application. |
APP_TIMEZONE |
UTC | Application timezone. |
APP_LOCALE |
en | Application locale. |
| Variable | Default | Description |
|---|---|---|
DB_CONNECTION |
mysql | Database driver. Tests use SQLite. |
DB_HOST |
127.0.0.1 | Database host. |
DB_PORT |
3306 | Database port. |
DB_DATABASE |
turista | Database name. |
DB_USERNAME |
root | Database user. |
DB_PASSWORD |
Database password. |
| Variable | Default | Description |
|---|---|---|
SANCTUM_TOKEN_EXPIRATION |
10080 | Token lifetime in minutes (1 week). |
SANCTUM_STATEFUL_DOMAINS |
Comma-separated list of stateful domains. |
| Variable | Description |
|---|---|
MAIL_MAILER |
Mail driver (smtp, log, etc.). |
MAIL_HOST |
SMTP host. |
MAIL_PORT |
SMTP port. |
MAIL_USERNAME |
SMTP username. |
MAIL_PASSWORD |
SMTP password. |
MAIL_ENCRYPTION |
TLS/SSL. |
MAIL_FROM_ADDRESS |
Default from address. |
MAIL_FROM_NAME |
Default from name. |
| Variable | Description |
|---|---|
COREVERDE_BASE_URL |
CoreVerde API base URL. |
COREVERDE_API_TOKEN |
API token. |
COREVERDE_DEVICE_ID |
Device ID. |
COREVERDE_ENABLED |
Set to true to enable WhatsApp sending. |
| Variable | Default | Description |
|---|---|---|
REQUEST_DOCS_ENABLED |
false | Enables /request-docs UI. Disable in production. |
| Variable | Description |
|---|---|
CORS_ALLOWED_ORIGINS |
Comma-separated allowed origins. |
| Variable | Default | Description |
|---|---|---|
SEED_COUNTRIES |
Libya | Comma-separated countries to seed. |
| Variable | Default | Description |
|---|---|---|
QUEUE_CONNECTION |
database | Queue driver. |
| Variable | Default | Description |
|---|---|---|
CACHE_STORE |
database | Cache driver. |
| Variable | Default | Description |
|---|---|---|
LOG_CHANNEL |
stack | Log channel. |
LOG_LEVEL |
debug | Minimum log level. |
Turista relies on Laravel's task scheduler for background jobs. A cron entry must call schedule:run every minute.
* * * * * cd /path/to/turista && php artisan schedule:run >> /dev/null 2>&1
Commands are defined in routes/console.php.
Schedule::command('pending-reservations:release-expired')->everyMinute();
Finds pending reservations whose expires_at has passed and releases their held availability so the units can be booked again.
Schedule::command('notifications:send-due')->everyMinute();
Claims ScheduledNotification rows whose send_at has passed and dispatches DispatchScheduledNotification jobs to the queue.
Scheduled commands dispatch jobs to the queue. Ensure a queue worker is running:
php artisan queue:work --sleep=3 --tries=3
For production, use Supervisor or systemd to keep the worker alive.
storage/logs/laravel.log for scheduler or worker errors.php artisan queue:monitor or a monitoring service to alert on queue length.This document covers observability points for operating Turista in production.
Laravel logs to storage/logs/laravel.log by default. Tail logs in real time:
tail -f storage/logs/laravel.log
For production, forward logs to a centralized system such as ELK, Datadog, or CloudWatch.
OwenIt Auditing writes model changes to the audits table. You can query it to trace:
Example:
SELECT * FROM audits WHERE auditable_type = 'App\\Models\\Reservation' AND auditable_id = 123 ORDER BY created_at DESC;
Note: password hashes are excluded from audit logs via User::$auditExclude.
Monitor queue health by:
failed_jobs.routes/web.php exposes a simple health-check endpoint at /:
GET /
A 200 OK response indicates the application is reachable.
Ensure the cron entry is active:
crontab -l
Verify scheduler activity in logs.
Consider integrating an error-tracking service such as Sentry, Bugsnag, or Flare to capture production exceptions.
config, route, and view caches enabled in production.unit_availabilities and reservations tables.Turista's security model combines Laravel's built-in protections, Sanctum tokens, Spatie roles/permissions, policy-based authorization, and additional hardening.
super_admin, admin, owner, employee, customer).super_admin bypasses all policies via Gate::before.status = active).active and scoped to their owner/building.Model::unguard() is enabled globally in AppServiceProvider. This is an intentional project decision that removes Laravel's default mass-assignment protection. All input must be explicitly whitelisted in Form Requests and controllers.
Removing Model::unguard() requires adding $fillable or $guarded to every model first.
OTP codes are returned in API responses only in local and testing environments. In production, OTPs are sent via the configured channel (CoreVerde WhatsApp).
Photo uploads are validated by custom rules (PhotoFileRules). Allowed types typically include JPG, JPEG, PNG, and WebP with size and dimension limits.
API resources return user-supplied strings as-is. Clients must HTML-escape all strings before inserting them into the DOM to prevent stored XSS.
SecurityHeadersMiddleware is applied globally and sets:
X-Frame-Options: DENYX-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-originStrict-Transport-SecurityContent-Security-PolicyPublic auth endpoints are rate-limited. Additional rate limiting can be configured per route.
Run composer audit regularly and update dependencies with known vulnerabilities.
Document any new vulnerabilities in the security audit file and prioritize fixes before the next release.
This document summarizes the findings from docs/security-audit-2026-06-22.md.
app/, routes/, config/, database/, resources/views/The application has a moderate defensive posture with proper use of Form Requests, Eloquent parameter binding, role-based middleware, and policies on most resources.
Model::unguard()app/Providers/AppServiceProvider.php$fillable/$guarded to every model.| Issue | Status |
|---|---|
| Default super-admin password in seeder | Addressed — no default admin created by DatabaseSeeder; use admin:create-super command. |
| IDOR in bulk unit creation | Fixed — ownership verified. |
| IDOR in unit listing | Fixed — building authorization enforced. |
Cross-owner unit_id changes in reservations |
Fixed — owner boundary checks added. |
| Empty collection returns all availabilities | Fixed — whereIn applied even for empty sets. |
| Issue | Status |
|---|---|
Dependency CVEs in guzzlehttp/guzzle and guzzlehttp/psr7 |
Monitor and update via composer audit. |
| CORS defaults | Addressed — .env.example restricts origins; publish config/cors.php for production. |
| Missing security headers | Addressed — SecurityHeadersMiddleware added globally. |
| Debug mode and request docs in local config | Addressed — .env.example sets APP_DEBUG=false and REQUEST_DOCS_ENABLED=false. |
| Sanctum token expiration | Addressed — token TTL set to 1 week in config/sanctum.php. |
| OTP returned in responses in local/testing | Intentional for development; not returned in production. |
| Login enumeration | Reviewed; consider generic failure messages in future hardening. |
| Stored content output encoding | Documented — clients must HTML-escape API strings. |
api.json / routes.json files should be added to .gitignore.User::$auditExclude.resources/views/ contains only the password-reset email view)..env is gitignored.As of the latest project progress report, all tests pass and the fixable audit items have been implemented. The remaining accepted risk is the global Model::unguard() decision.
Use this checklist before deploying Turista to production or making it publicly available.
APP_ENV=productionAPP_DEBUG=falseAPP_KEY is strong and unique.env file is not readable by the web server (outside document root or denied by server config)SANCTUM_TOKEN_EXPIRATION is set to an appropriate value (default 1 week)SANCTUM_STATEFUL_DOMAINS is configured if using cookie-based SPA authphp artisan admin:create-superREQUEST_DOCS_ENABLED=falseNotFoundWhenProduction middleware enabled in config/request-docs.phpCORS_ALLOWED_ORIGINS restricted to known frontend domainsconfig/cors.php published and configuredSecurityHeadersMiddleware active and sets CSP, HSTS, X-Frame-Options, etc.composer audit reports no high/critical vulnerabilitiesnpm audit reports no high/critical vulnerabilitiescomposer install --no-devaudits table) monitoredschedule:run activepending-reservations:release-expired and notifications:send-due execute on schedule200 OK