All articles

API / Route Reference

2026-06-16
laravelapi

This is a **Laravel** recruitment management system with **4 major route groups

မြန်မာဘာသာဖြင့် ဖတ်ရန်

API / Route Reference — Architecture Explanation & Implementation Guide

Overview

This is a Laravel recruitment management system with 4 major route groups:

| Group | Purpose | Middleware | |---|---|---| | Auth Routes | Login, logout, password reset | guest (unauthenticated users only) | | Admin Routes | Dashboard, accounts, mail templates, applications | auth + EnsureAdmin | | Webhook Routes | Google Form integration (API) | VerifyGoogleFormWebhook + throttle | | Flash Messages | UI feedback after actions | N/A (reference table) |

The system manages a candidate recruitment pipeline: Apply → Shortlist → IQ Test → Interview 1 → Interview 2 → Hired/Rejected.


Architecture Diagram

graph TB
    subgraph "External"
        GF["Google Forms"]
    end

    subgraph "Middleware Layer"
        Guest["guest middleware"]
        Auth["auth middleware"]
        EA["EnsureAdmin"]
        EAR["EnsureAdminRole"]
        VGF["VerifyGoogleFormWebhook"]
    end

    subgraph "Auth Routes"
        Login["LoginController"]
        Forgot["ForgotPasswordController"]
        Reset["ResetPasswordController"]
    end

    subgraph "Admin Routes"
        Dashboard["DashboardController"]
        Accounts["AccountController"]
        MailTpl["MailTemplateController"]
        Apps["ApplicationController"]
    end

    subgraph "Webhook Routes"
        WH1["GoogleFormWebhookController"]
        WH2["GoogleFormBasicInfoWebhookController"]
    end

    subgraph "Models"
        Admin["Admin model"]
        App["Application model"]
        MT["MailTemplate model"]
        Note["Note model"]
        Log["ApplicationLog model"]
    end

    Guest --> Login
    Guest --> Forgot
    Guest --> Reset
    Auth --> EA --> Dashboard
    Auth --> EA --> MailTpl
    Auth --> EA --> EAR --> Accounts
    Auth --> EA --> Apps
    GF --> VGF --> WH1
    GF --> VGF --> WH2

1. Auth Routes — Explanation & Implementation

What it does

Standard Laravel authentication: login page, password reset flow, logout.

Key Points

  • All routes except logout use guest middleware (redirects already-logged-in users away)
  • logout uses auth middleware (only logged-in users can log out)
  • Uses the admins table (not the default users table)

Implementation

Route Definition (routes/web.php)

<?php

use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\ForgotPasswordController;
use App\Http\Controllers\Auth\ResetPasswordController;

// Root redirect
Route::get('/', fn() => redirect()->route('login'));

// Guest-only routes
Route::middleware('guest')->group(function () {
    Route::get('/login', [LoginController::class, 'show'])->name('login');
    Route::post('/login', [LoginController::class, 'login']);

    Route::get('/forgot-password', [ForgotPasswordController::class, 'show'])
        ->name('password.request');
    Route::post('/forgot-password', [ForgotPasswordController::class, 'sendResetLink'])
        ->name('password.email');

    Route::get('/reset-password/{token}', [ResetPasswordController::class, 'show'])
        ->name('password.reset');
    Route::post('/reset-password', [ResetPasswordController::class, 'reset'])
        ->name('password.update');
});

// Auth-only
Route::post('/logout', [LoginController::class, 'logout'])
    ->middleware('auth')
    ->name('logout');

LoginController (app/Http/Controllers/Auth/LoginController.php)

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    public function show()
    {
        return view('auth.login');
    }

    public function login(Request $request)
    {
        $credentials = $request->validate([
            'email'    => ['required', 'email'],
            'password' => ['required'],
        ]);

        if (Auth::attempt($credentials, $request->boolean('remember'))) {
            $request->session()->regenerate();
            return redirect()->intended(route('admin.dashboard'));
        }

        return back()->withErrors([
            'email' => 'The provided credentials do not match our records.',
        ])->onlyInput('email');
    }

    public function logout(Request $request)
    {
        Auth::logout();
        $request->session()->invalidate();
        $request->session()->regenerateToken();
        return redirect()->route('login');
    }
}

2. Admin Routes — The Core System

2.1 Middleware Architecture

graph LR
    Request --> Auth["auth middleware<br/>(is user logged in?)"]
    Auth --> EnsureAdmin["EnsureAdmin<br/>(is user an Admin model?)"]
    EnsureAdmin --> EnsureAdminRole["EnsureAdminRole<br/>(is role = 'admin'?)"]

    style Auth fill:#4a9eff,color:#fff
    style EnsureAdmin fill:#ff9f43,color:#fff
    style EnsureAdminRole fill:#ee5a24,color:#fff

There are two levels of admin access:

| Role | Can Access | |---|---| | member | Dashboard, view accounts list, mail templates, applications | | admin | Everything above + create/edit/delete accounts |

Middleware Implementation

// app/Http/Middleware/EnsureAdmin.php
<?php

namespace App\Http\Middleware;

use Closure;

class EnsureAdmin
{
    public function handle($request, Closure $next)
    {
        if (!auth()->check() || !auth()->user() instanceof \App\Models\Admin) {
            abort(403, 'Unauthorized');
        }
        return $next($request);
    }
}
// app/Http/Middleware/EnsureAdminRole.php
<?php

namespace App\Http\Middleware;

use Closure;
use App\Enums\AdminRole;

class EnsureAdminRole
{
    public function handle($request, Closure $next)
    {
        if (auth()->user()->role !== AdminRole::ADMIN) {
            abort(403, 'Admin role required');
        }
        return $next($request);
    }
}

2.2 Admin Routes Definition

// routes/web.php (continued)

use App\Http\Controllers\Admin\DashboardController;
use App\Http\Controllers\Admin\AccountController;
use App\Http\Controllers\Admin\MailTemplateController;
use App\Http\Controllers\Admin\ApplicationController;

Route::prefix('admin')
    ->name('admin.')
    ->middleware(['auth', EnsureAdmin::class])
    ->group(function () {

        // Dashboard
        Route::get('/dashboard', [DashboardController::class, 'index'])
            ->name('dashboard');

        // Accounts — index is accessible to members too
        Route::get('/accounts', [AccountController::class, 'index'])
            ->name('accounts.index');

        // Accounts CRUD — admin role only
        Route::middleware(EnsureAdminRole::class)->group(function () {
            Route::get('/accounts/create', [AccountController::class, 'create'])
                ->name('accounts.create');
            Route::post('/accounts', [AccountController::class, 'store'])
                ->name('accounts.store');
            Route::get('/accounts/{admin}/edit', [AccountController::class, 'edit'])
                ->name('accounts.edit');
            Route::put('/accounts/{admin}', [AccountController::class, 'update'])
                ->name('accounts.update');
            Route::delete('/accounts/{admin}', [AccountController::class, 'destroy'])
                ->name('accounts.destroy');
        });

        // Mail Templates (full resource)
        Route::resource('mail-templates', MailTemplateController::class);
        Route::get('/mail-templates/{mailTemplate}/get', [MailTemplateController::class, 'get'])
            ->name('mail-templates.get');

        // Applications
        Route::get('/applications', [ApplicationController::class, 'index'])
            ->name('applications.index');
        Route::get('/applications/{application}', [ApplicationController::class, 'show'])
            ->name('applications.show');
        Route::get('/applications/{application}/export', [ApplicationController::class, 'export'])
            ->name('applications.export');

        // Application Actions (status transitions)
        Route::post('/applications/{application}/shortlist', [ApplicationController::class, 'shortlist'])
            ->name('applications.shortlist');
        Route::post('/applications/{application}/send-iq-invite', [ApplicationController::class, 'sendIqInvite'])
            ->name('applications.send-iq-invite');
        Route::post('/applications/{application}/reject', [ApplicationController::class, 'reject'])
            ->name('applications.reject');
        Route::post('/applications/{application}/iq-result', [ApplicationController::class, 'iqResult'])
            ->name('applications.iq-result');
        Route::post('/applications/{application}/interview-1-result', [ApplicationController::class, 'interview1Result'])
            ->name('applications.interview-1-result');
        Route::post('/applications/{application}/interview-2-result', [ApplicationController::class, 'interview2Result'])
            ->name('applications.interview-2-result');
        Route::post('/applications/{application}/hired', [ApplicationController::class, 'hired'])
            ->name('applications.hired');

        // Notes
        Route::post('/applications/{application}/notes', [ApplicationController::class, 'storeNote'])
            ->name('applications.notes');
        Route::delete('/applications/{application}/notes/{note}', [ApplicationController::class, 'destroyNote'])
            ->name('applications.notes.destroy');

        // Profile / Info Updates
        Route::put('/applications/{application}/basic-info', [ApplicationController::class, 'updateBasicInfo'])
            ->name('applications.basic-info.update');
        Route::put('/applications/{application}/update-details', [ApplicationController::class, 'updateDetails'])
            ->name('applications.update-details');
        Route::put('/applications/{application}/basic-profile', [ApplicationController::class, 'updateBasicProfile'])
            ->name('applications.update-basic-profile');
    });

[!IMPORTANT] The Accounts resource uses {admin} as the route parameter (not {account}). This means the route model binding will look for the Admin model.


2.3 Database Schema (Implied)

Based on the routes, validations, and fields, here's the implied schema:

erDiagram
    ADMINS {
        bigint id PK
        string name
        string email UK
        string password
        enum role "admin | member"
        timestamp created_at
        timestamp updated_at
    }

    APPLICATIONS {
        bigint id PK
        string google_response_id
        string full_name
        string email
        string phone
        string nrc
        date date_of_birth
        string address
        string degree
        string position
        string expected_salary
        string cv_original_name
        string cv_link
        string cv_folder_path
        string cv_mime
        string cv_folder_link
        text basic_info
        integer iq_test_score
        date iq_test_date
        date first_interview_date
        date second_interview_date
        string status
        string reject_reason
        json other_files
        timestamp applied_at
        timestamp basic_info_submitted_at
        timestamp created_at
        timestamp updated_at
    }

    MAIL_TEMPLATES {
        bigint id PK
        string template_name
        enum mail_type
        string title
        text body
        boolean is_active
        timestamp created_at
        timestamp updated_at
    }

    NOTES {
        bigint id PK
        bigint application_id FK
        bigint admin_id FK
        text body
        timestamp created_at
        timestamp updated_at
    }

    APPLICATION_LOGS {
        bigint id PK
        bigint application_id FK
        bigint admin_id FK
        string action
        text note
        timestamp created_at
    }

    APPLICATIONS ||--o{ NOTES : "has many"
    APPLICATIONS ||--o{ APPLICATION_LOGS : "has many"
    ADMINS ||--o{ NOTES : "created by"
    ADMINS ||--o{ APPLICATION_LOGS : "performed by"

2.4 Enums

// app/Enums/AdminRole.php
<?php

namespace App\Enums;

enum AdminRole: string
{
    case ADMIN = 'admin';
    case MEMBER = 'member';
}
// app/Enums/MailType.php
<?php

namespace App\Enums;

enum MailType: string
{
    case IQ_TEST = 'iq_test';
    case SHORTLIST = 'shortlist';
    case INTERVIEW_1 = 'interview_1';
    case INTERVIEW_2 = 'interview_2';
    case HIRED = 'hired';
    case REJECTED = 'rejected';
    // Add more as needed
}
// app/Enums/ApplicationStatus.php
<?php

namespace App\Enums;

enum ApplicationStatus: string
{
    case APPLIED = 'applied';
    case SHORTLISTED = 'shortlisted';
    case IQ_INVITED = 'iq_invited';
    case IQ_PASS = 'iq_pass';
    case IQ_FAIL = 'iq_fail';
    case INTERVIEW_1_PASS = 'interview_1_pass';
    case INTERVIEW_1_FAIL = 'interview_1_fail';
    case INTERVIEW_2_PASS = 'interview_2_pass';
    case INTERVIEW_2_FAIL = 'interview_2_fail';
    case HIRED = 'hired';
    case REJECTED = 'rejected';
}

2.5 Key Request Validation Examples

// app/Http/Requests/Admin/StoreAccountRequest.php
<?php

namespace App\Http\Requests\Admin;

use App\Enums\AdminRole;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\Rules\Password;

class StoreAccountRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // Middleware handles authorization
    }

    public function rules(): array
    {
        return [
            'name'     => ['required', 'string', 'max:255'],
            'email'    => ['required', 'email', 'max:255', 'unique:admins,email'],
            'role'     => ['required', new Enum(AdminRole::class)],
            'password' => ['required', 'confirmed', Password::defaults()],
        ];
    }
}
// app/Http/Requests/Admin/UpdateAccountRequest.php
<?php

namespace App\Http\Requests\Admin;

use App\Enums\AdminRole;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\Rules\Password;

class UpdateAccountRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name'     => ['required', 'string', 'max:255'],
            'email'    => ['required', 'email', 'max:255',
                           'unique:admins,email,' . $this->route('admin')->id],
            'role'     => ['required', new Enum(AdminRole::class)],
            'password' => ['nullable', 'confirmed', Password::defaults()],
        ];
    }
}

2.6 The "Send Only" Pattern — Critical Design Pattern

This is the most complex pattern in the system. When send_only=true is passed in any status-transition request (shortlist, iq-result, interview results, hired, reject), the system does NOT change the application status — it only sends an email.

flowchart TD
    Request["POST .../shortlist"]
    Check{"send_only=true?"}
    
    Request --> Check
    Check -->|No| Normal["1. Validate normally<br/>2. Change status<br/>3. Optionally send mail<br/>4. Log transition"]
    Check -->|Yes| SendOnly["1. Validate mail fields<br/>2. Send email only<br/>3. Don't change status<br/>4. Flash success message"]
    
    style SendOnly fill:#ff9f43,color:#fff
    style Normal fill:#4a9eff,color:#fff

Implementation Pattern

// In ApplicationController

public function shortlist(ShortlistRequest $request, Application $application)
{
    // Check if this is a "send only" request
    if ($request->boolean('send_only')) {
        return $this->handleSendOnly($request, $application, MailType::SHORTLIST);
    }

    // Normal flow: change status + optionally send mail
    $application->transitionTo(ApplicationStatus::SHORTLISTED);

    if ($request->filled('mail_template_id')) {
        $this->sendMail($application, $request->mail_template_id);
    }

    $this->logAction($application, 'shortlisted');
    return back()->with('success', 'Candidate shortlisted.');
}

protected function handleSendOnly(Request $request, Application $application, MailType $type)
{
    try {
        $template = MailTemplate::where('id', $request->mail_template_id)
            ->where('mail_type', $type)
            ->where('is_active', true)
            ->firstOrFail();

        // Send the email with optional subject/body overrides and attachments
        $this->sendMailWithTemplate($application, $template, $request);

        return back()->with('success', $this->getMailSuccessMessage($type));
    } catch (\Exception $e) {
        return back()->with('error', "Failed to send email: {$e->getMessage()}");
    }
}

2.7 Application Status State Machine

stateDiagram-v2
    [*] --> applied : Google Form Webhook
    applied --> shortlisted : shortlist
    applied --> rejected : reject

    shortlisted --> iq_invited : send-iq-invite
    shortlisted --> rejected : reject

    iq_invited --> iq_pass : iq-result (pass)
    iq_invited --> iq_fail : iq-result (fail)
    iq_invited --> rejected : reject

    iq_pass --> interview_1_pass : interview-1-result (pass)
    iq_pass --> interview_1_fail : interview-1-result (fail)
    iq_pass --> hired : hired (after 1st interview)
    iq_pass --> rejected : reject

    interview_1_pass --> interview_2_pass : interview-2-result (pass)
    interview_1_pass --> interview_2_fail : interview-2-result (fail)
    interview_1_pass --> hired : hired (after 1st interview)
    interview_1_pass --> rejected : reject

    interview_2_pass --> hired : hired (after 2nd interview)
    interview_2_pass --> rejected : reject

    iq_fail --> rejected : reject
    interview_1_fail --> rejected : reject
    interview_2_fail --> rejected : reject

    hired --> [*]
    rejected --> [*]

[!WARNING] Invalid transitions should throw an exception (e.g., "Cannot transition from X to Y"). This should be implemented in the Application model's transitionTo() method.


2.8 Dashboard Controller

// app/Http/Controllers/Admin/DashboardController.php
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\DashboardRequest;
use App\Models\Application;

class DashboardController extends Controller
{
    public function index(DashboardRequest $request)
    {
        $year     = $request->input('year', now()->year);
        $month    = $request->input('month', now()->month);
        $position = $request->input('position');

        // Query applications with filters
        $query = Application::query()
            ->whereYear('applied_at', $year)
            ->whereMonth('applied_at', $month);

        if ($position) {
            $query->where('position', $position);
        }

        // Aggregate stats by status for the dashboard
        $stats = $query->get()->groupBy('status')->map->count();

        return view('admin.dashboard', compact('stats', 'year', 'month', 'position'));
    }
}

2.9 Account Controller (with Delete Guards)

// app/Http/Controllers/Admin/AccountController.php (partial — destroy method)

public function destroy(Admin $admin)
{
    // Guard 1: Cannot delete yourself
    if ($admin->id === auth()->id()) {
        return back()->with('error', 'You cannot delete your own account.');
    }

    // Guard 2: Must keep at least one admin
    if ($admin->role === AdminRole::ADMIN) {
        $adminCount = Admin::where('role', AdminRole::ADMIN)->count();
        if ($adminCount <= 1) {
            return back()->with('error', 'At least one admin role account must remain.');
        }
    }

    $admin->delete();
    return back()->with('success', 'Account deleted.');
}

3. Webhook Routes — Google Form Integration

What it does

Receives data from Google Forms (via Apps Script) to automatically create candidate applications.

Route Definition (routes/api.php)

<?php

use App\Http\Controllers\Api\GoogleFormWebhookController;
use App\Http\Controllers\Api\GoogleFormBasicInfoWebhookController;

Route::prefix('webhooks')
    ->middleware(['App\Http\Middleware\VerifyGoogleFormWebhook', 'throttle:60,1'])
    ->group(function () {
        Route::post('/google-form', GoogleFormWebhookController::class);
        Route::post('/google-form/basic-info', GoogleFormBasicInfoWebhookController::class);
    });

Webhook Authentication Middleware

// app/Http/Middleware/VerifyGoogleFormWebhook.php
<?php

namespace App\Http\Middleware;

use Closure;

class VerifyGoogleFormWebhook
{
    public function handle($request, Closure $next)
    {
        $secret = $request->header('X-Webhook-Secret');
        $expected = config('mtm.google_form_webhook_secret');

        if (!$secret || $secret !== $expected) {
            return response()->json(['message' => 'Unauthorized'], 401);
        }

        return $next($request);
    }
}

Main Webhook Controller

// app/Http/Controllers/Api/GoogleFormWebhookController.php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\GoogleFormWebhookRequest;
use App\Models\Application;

class GoogleFormWebhookController extends Controller
{
    public function __invoke(GoogleFormWebhookRequest $request)
    {
        $application = Application::create([
            'google_response_id' => $request->google_response_id,
            'full_name'          => $request->full_name,
            'email'              => $request->email,
            'phone'              => $request->phone,
            'nrc'                => $request->nrc,
            'date_of_birth'      => $request->date_of_birth,
            'address'            => $request->address,
            'degree'             => $request->degree,
            'position'           => $request->position,
            'expected_salary'    => $request->expected_salary,
            'cv_original_name'   => $request->cv_original_name,
            'cv_link'            => $request->cv_link,
            'cv_folder_path'     => $request->cv_folder_path,
            'cv_mime'            => $request->cv_mime,
            'cv_folder_link'     => $request->cv_folder_link,
            'other_files'        => $request->other_files,
            'applied_at'         => $request->submitted_at ?? now(),
            'status'             => 'applied',
        ]);

        return response()->json([
            'message'        => 'Application received',
            'application_id' => $application->id,
            'status'         => 'applied',
        ], 201);
    }
}

Basic Info Webhook — Lookup Logic

// app/Http/Controllers/Api/GoogleFormBasicInfoWebhookController.php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\GoogleFormBasicInfoRequest;
use App\Models\Application;

class GoogleFormBasicInfoWebhookController extends Controller
{
    public function __invoke(GoogleFormBasicInfoRequest $request)
    {
        // Find application by ID or email
        $application = null;

        if ($request->filled('application_id')) {
            $application = Application::find($request->application_id);
        } elseif ($request->filled('email')) {
            $application = Application::where('email', $request->email)
                ->latest('applied_at')
                ->first();
        }

        if (!$application) {
            return response()->json(['message' => 'Application not found'], 422);
        }

        $application->update([
            'basic_info'              => json_encode($request->basic_info_text),
            'basic_info_submitted_at' => $request->submitted_at ?? now(),
        ]);

        return response()->json([
            'message'                 => 'Basic info saved',
            'application_id'          => $application->id,
            'basic_info_submitted_at' => $application->basic_info_submitted_at,
        ], 200);
    }
}

4. Project File Structure

Here's the recommended file organization:

app/
├── Enums/
│   ├── AdminRole.php
│   ├── ApplicationStatus.php
│   └── MailType.php
├── Http/
│   ├── Controllers/
│   │   ├── Auth/
│   │   │   ├── LoginController.php
│   │   │   ├── ForgotPasswordController.php
│   │   │   └── ResetPasswordController.php
│   │   ├── Admin/
│   │   │   ├── DashboardController.php
│   │   │   ├── AccountController.php
│   │   │   ├── MailTemplateController.php
│   │   │   └── ApplicationController.php
│   │   └── Api/
│   │       ├── GoogleFormWebhookController.php
│   │       └── GoogleFormBasicInfoWebhookController.php
│   ├── Middleware/
│   │   ├── EnsureAdmin.php
│   │   ├── EnsureAdminRole.php
│   │   └── VerifyGoogleFormWebhook.php
│   └── Requests/
│       ├── Admin/
│       │   ├── DashboardRequest.php
│       │   ├── StoreAccountRequest.php
│       │   ├── UpdateAccountRequest.php
│       │   ├── StoreMailTemplateRequest.php
│       │   ├── UpdateMailTemplateRequest.php
│       │   ├── ShortlistRequest.php
│       │   ├── IqResultRequest.php
│       │   ├── InterviewResultRequest.php
│       │   ├── HiredRequest.php
│       │   ├── RejectRequest.php
│       │   ├── UpdateDetailsRequest.php
│       │   ├── UpdateBasicInfoRequest.php
│       │   ├── StoreNoteRequest.php
│       │   └── SendMailRequest.php
│       └── Api/
│           ├── GoogleFormWebhookRequest.php
│           └── GoogleFormBasicInfoRequest.php
├── Models/
│   ├── Admin.php
│   ├── Application.php
│   ├── MailTemplate.php
│   ├── Note.php
│   └── ApplicationLog.php
├── Mail/
│   └── CandidateMail.php
└── Services/
    ├── ApplicationService.php          (status transitions logic)
    └── MailService.php                 (email sending with template rendering)

config/
└── mtm.php                            (google_form_webhook_secret, etc.)

database/
└── migrations/
    ├── create_admins_table.php
    ├── create_applications_table.php
    ├── create_mail_templates_table.php
    ├── create_notes_table.php
    └── create_application_logs_table.php

resources/views/
├── auth/
│   ├── login.blade.php
│   ├── forgot-password.blade.php
│   └── reset-password.blade.php
├── admin/
│   ├── dashboard.blade.php
│   ├── accounts/
│   │   ├── index.blade.php
│   │   ├── create.blade.php
│   │   └── edit.blade.php
│   ├── mail-templates/
│   │   ├── index.blade.php
│   │   ├── create.blade.php
│   │   └── edit.blade.php
│   └── applications/
│       ├── index.blade.php
│       └── show.blade.php
└── layouts/
    └── admin.blade.php

routes/
├── web.php                            (Auth + Admin routes)
└── api.php                            (Webhook routes)

5. Implementation Order (Recommended)

[!TIP] Build in this order to avoid dependency issues:

| Step | What to Build | Why First | |---|---|---| | 1 | Migrations + Models + Enums | Everything depends on the database schema | | 2 | Config (config/mtm.php) | Webhook secret needed early | | 3 | Middleware (3 files) | Routes need middleware registered | | 4 | Auth Controllers + Views | You need to log in to test anything | | 5 | Admin Layout (layouts/admin.blade.php) | All admin views extend this | | 6 | Dashboard | Simple starting point after login | | 7 | Accounts CRUD | Manage who can access the system | | 8 | Mail Templates CRUD | Needed before application actions | | 9 | Application Index + Show | View candidates | | 10 | Status Transition Actions | The core workflow | | 11 | Send Only Pattern | Email-only actions | | 12 | Notes CRUD | Add notes to applications | | 13 | Webhook Controllers | Google Form integration | | 14 | Export Feature | PDF/Excel export |


6. Key Design Patterns Used

Pattern 1: Form Request Validation

All validation is extracted into dedicated FormRequest classes, keeping controllers clean.

Pattern 2: State Machine for Status

Applications follow a strict state machine. Invalid transitions throw exceptions. This prevents data corruption (e.g., you can't mark someone "hired" if they haven't passed an IQ test).

Pattern 3: Send Only Pattern

A single mechanism that lets admins re-send emails without changing candidate status. Useful when an email bounced or needs to be re-sent with different content.

Pattern 4: Template-based Emails

Email content comes from database-stored templates with placeholders like {{candidate_name}}. This allows admins to edit email content without code changes.

Pattern 5: Action Logging

Every status change is logged with the admin who performed it, creating an audit trail.

Pattern 6: Soft Authorization Layers

Two middleware layers (EnsureAdmin + EnsureAdminRole) provide granular access control without complex permission systems.