Skip to main content
Tripfy Africa organises its controllers into two main namespaces:
  • App\Http\Controllers\Api — JSON endpoints consumed by the mobile app
  • App\Http\Controllers\User — Blade-rendered web dashboard for travellers and vendors
All API controllers live in app/Http/Controllers/Api/. They return JSON and use Laravel Sanctum token authentication on protected routes.

AuthController

File: app/Http/Controllers/Api/AuthController.phpHandles mobile/SPA authentication via Sanctum tokens.
MethodRouteAuthDescription
POST/api/v1/auth/registerRegister a new traveller account
POST/api/v1/auth/loginAuthenticate and receive a Sanctum token
GET/api/v1/auth/meRequiredReturn the authenticated user’s profile
POST/api/v1/auth/logoutRequiredRevoke the current device token
POST/api/v1/auth/logout-allRequiredRevoke tokens on all devices
Register validation
public function register(Request $request): JsonResponse
{
    $validated = $request->validate([
        'firstname' => ['required', 'string', 'max:100'],
        'lastname'  => ['required', 'string', 'max:100'],
        'email'     => ['required', 'email', 'max:255', 'unique:users,email'],
        'password'  => ['required', 'confirmed', Password::min(8)->mixedCase()->numbers()],
        'phone'     => ['nullable', 'string', 'max:20'],
    ]);
    // Creates user, issues 'mobile-app' token
    $token = $user->createToken('mobile-app')->plainTextToken;
    return response()->json(['token' => $token, ...], 201);
}
Login behaviour
public function login(Request $request): JsonResponse
{
    // Revokes previous mobile tokens before issuing a new one
    $user->tokens()->where('name', 'mobile-app')->delete();
    $token = $user->createToken('mobile-app')->plainTextToken;
    // Returns 403 if user status == 0 (suspended)
}

PackageController

File: app/Http/Controllers/Api/PackageController.phpProvides read-only tour listings. Write operations remain in the web dashboard.
MethodRouteAuthDescription
GET/api/v1/packagesList active packages with filtering and pagination
GET/api/v1/packages/{slug}Full detail for a single package
GET/api/v1/packages/categoriesList active package categories
GET/api/v1/packages/featuredFeatured packages for the home screen carousel
Supported query parameters for index
ParameterTypeDescription
destination_idintegerFilter by destination
category_idintegerFilter by category
min_pricefloatMinimum adult price
max_pricefloatMaximum adult price
searchstringKeyword match on title or description
sortstringprice_asc, price_desc, rating, or newest (default)
per_pageintegerItems per page (max 50, default 12)
Listing filter example
public function index(Request $request): JsonResponse
{
    $query = Package::with(['category:id,name', 'destination:id,title,slug', 'owner:id,firstname,lastname,image,image_driver', 'media'])
        ->where('status', 1)
        ->whereHas('owner', fn($q) => $q->whereHas('activePlan'));

    if ($request->filled('destination_id')) {
        $query->where('destination_id', (int) $request->destination_id);
    }
    // ... additional filters
    match ($request->input('sort', 'newest')) {
        'price_asc'  => $query->orderBy('adult_price', 'asc'),
        'price_desc' => $query->orderBy('adult_price', 'desc'),
        'rating'     => $query->orderByDesc('is_featured'),
        default      => $query->orderByDesc('created_at'),
    };
}
Only packages whose owner has an active plan (activePlan relationship) appear in API listings.

BookingController

File: app/Http/Controllers/Api/BookingController.phpAllows travellers to view and manage their bookings. Payment processing happens through the web checkout flow.
MethodRouteAuthDescription
GET/api/v1/bookingsRequiredPaginated list of the authenticated user’s bookings
GET/api/v1/bookings/{uid}RequiredSingle booking detail (scoped to owner)
POST/api/v1/bookings/{uid}/cancelRequiredCancel a pending (0) or confirmed (1) booking
GET/api/v1/bookings/availabilityCheck remaining spaces for a package on a date
Cancel logic
public function cancel(Request $request, string $uid): JsonResponse
{
    $booking = Booking::where('uid', $uid)
        ->where('user_id', $request->user()->id)  // scoped to owner
        ->firstOrFail();

    if (!in_array($booking->status, [0, 1])) {
        return response()->json(['message' => 'This booking cannot be cancelled in its current status.'], 422);
    }

    $booking->update(['status' => 3]); // 3 = cancelled
}
Availability check query parameters
ParameterValidation
package_slugrequired, string
daterequired, date, after_or_equal:today

ReviewController

File: app/Http/Controllers/Api/ReviewController.phpOnly users with a confirmed or completed booking can submit a review.
MethodRouteAuthDescription
GET/api/v1/packages/{slug}/reviewsPaginated approved reviews for a package
POST/api/v1/reviewsRequiredSubmit a review (requires qualifying booking)
Review submission validation
public function store(Request $request): JsonResponse
{
    $validated = $request->validate([
        'package_id' => ['required', 'integer', 'exists:packages,id'],
        'rating'     => ['required', 'integer', 'min:1', 'max:5'],
        'comment'    => ['required', 'string', 'min:10', 'max:1000'],
    ]);

    // Verify qualifying booking (status 1 or 2)
    $hasBooking = Booking::where('user_id', $user->id)
        ->where('package_id', $validated['package_id'])
        ->whereIn('status', [1, 2])
        ->exists();

    // Submitted reviews start with status=0 (pending admin approval)
    Review::create([..., 'status' => 0]);
}

UserController

File: app/Http/Controllers/Api/UserController.phpLets the mobile app read and update the authenticated user’s profile.
MethodRouteAuthDescription
GET/api/v1/user/profileRequiredFull profile including vendor info and active plan
PUT/api/v1/user/profileRequiredUpdate profile fields
PUT/api/v1/user/passwordRequiredChange password and revoke all tokens
GET/api/v1/user/statsRequiredBooking statistics for the dashboard summary
Password change — revokes all tokens
public function changePassword(Request $request): JsonResponse
{
    // Validates current password, then:
    $user->update(['password' => Hash::make($validated['password'])]);
    $user->tokens()->delete(); // force re-login on all devices
}

DestinationController

File: app/Http/Controllers/Api/DestinationController.php
MethodRouteAuthDescription
GET/api/v1/destinationsAll active destinations ordered by visitor count
GET/api/v1/destinations/{slug}Destination detail with up to 20 featured packages
public function index(Request $request): JsonResponse
{
    $destinations = Destination::where('status', 1)
        ->withCount('visitor')
        ->orderByDesc('visitor_count')
        ->get();
}

Middleware

MiddlewareApplied to
auth:sanctumAll protected API routes
authAll User controller routes
throttle:60,1Public API endpoints (60 requests per minute)

Response format

API controllers return a consistent JSON envelope:
// Success
return response()->json([
    'data'    => $resource,
    'message' => 'Operation successful.',
], 200);

// Paginated
return response()->json([
    'data' => $items,
    'meta' => [
        'current_page' => $paginator->currentPage(),
        'last_page'    => $paginator->lastPage(),
        'per_page'     => $paginator->perPage(),
        'total'        => $paginator->total(),
    ],
]);

// Error
return response()->json(['message' => 'Reason for failure.'], 422);
Web controllers use Laravel’s back()->with('success', ...) and back()->with('error', ...) flash conventions for form feedback, and DataTables::of(...) for AJAX table responses.

Build docs developers (and LLMs) love