Skip to content

PHASE 2 — Route Feature Map (Complete Vertical Slice Reference)

System: PyroRadius ISP Billing & RADIUS Management
Stack: Laravel 13 · Inertia.js v2 · React 18 · PostgreSQL 16
Last Updated: 2026-07-13
Document: PHASE2_01

This is the definitive route-to-feature reference. Every route is documented as a full vertical slice: HTTP layer → controller → service → model → database → frontend. Use this when tracing a feature end-to-end or onboarding into a new module.


Table of Contents

  1. Auth Group
  2. Dashboard Group
  3. Customers Group
  4. Billing Group
  5. Packages Group
  6. NAS Group
  7. OLT Group
  8. WhatsApp Group
  9. Campaign Group
  10. Reports Group
  11. Health & Cron Group
  12. Settings Group
  13. Users Group
  14. Import Group
  15. Audit Group
  16. Tickets Group
  17. TR-069 Group
  18. Graphs Group
  19. Engineer Group
  20. Customer Portal Group
  21. Mobile API Group
  22. Internal Ajax API Group
  23. Route Statistics

1. Auth Group

Routes in routes/auth.php. No auth middleware — publicly accessible (except logout).


GET /login

Field Value
Method GET
URI /login
Controller AuthController@showLogin
Middleware web (no auth)
Permission None (public)
Inertia Page Pages/Auth/Login.jsx
React Components LoginForm, LogoHeader, FlashMessage

Request Parameters: None

Business Logic: 1. If user is already authenticated, redirect to /dashboard 2. Otherwise render the Inertia login page 3. Flash errors from previous failed attempt are injected via shared props

Services Called: None

Models Touched: None

Database Tables: None

Related APIs: None

Related Features: Session management, remember-me cookie flow


POST /login

Field Value
Method POST
URI /login
Controller AuthController@login
Middleware web, throttle:5,1 (5 attempts per minute per IP+email)
Permission None (public)
Inertia Page Redirects to /dashboard on success
React Components N/A (server redirect)

Request Parameters: - email — required, string, email format - password — required, string, min 8 - remember — optional, boolean

Business Logic: 1. Validate input (email required, password required) 2. Check rate limiter: if > 5 failures for IP+email combo in 1 min → return 429 with lockout message 3. Auth::attempt(['email' => $email, 'password' => $password], $remember) - Laravel queries users WHERE email = ? LIMIT 1 - Verifies password_verify($password, $user->password) 4. On FAILURE: - Increment rate limiter counter - Redirect back with validation error: "These credentials do not match our records." 5. On SUCCESS: - Session::regenerate() — prevents session fixation - Store user ID in encrypted session under key login_web_* - If $remember = true → set long-lived remember_web_* cookie with hashed remember_token - Redirect to /dashboard (or intended URL if user was redirected to login) - AuditLogger records login event with IP and user_agent

Services Called: AuditLogger::log('user.login', ...)

Models Touched: User

Database Tables: users (read), audit_logs (write)

Related APIs: None

Related Features: Remember-me, session security, rate limiting


POST /logout

Field Value
Method POST
URI /logout
Controller AuthController@logout
Middleware web, auth
Permission None (any authenticated user)
Inertia Page Redirects to /login
React Components N/A

Request Parameters: None (CSRF token via header)

Business Logic: 1. Auth::logout() — invalidates session, clears remember token from DB 2. Session::invalidate() — destroys the session 3. Session::regenerateToken() — new CSRF token 4. Clear viewing_as_dealer from session if set 5. Redirect to /login

Services Called: None

Models Touched: User (clears remember_token)

Database Tables: users (remember_token nulled), Redis session deleted

Related APIs: None

Related Features: View As session cleanup


2. Dashboard Group

Prefix: / — Middleware: auth


GET /dashboard (or GET /)

Field Value
Method GET
URI / or /dashboard
Controller DashboardController@index
Middleware auth
Permission All authenticated users
Inertia Page Pages/Dashboard/Index.jsx
React Components StatCard, OnlineNowWidget, RevenueChart, ExpiryAlertBanner, QuickActions

Request Parameters: None

Business Logic: 1. Determine scope: if viewing_as_dealer in session → scope as that dealer; else use auth()->user() scope 2. Compute stats (admin sees all, dealer sees own scope): - total_customers — COUNT from customers (active scope) - active_customers — COUNT WHERE status = 'active' - expired_customers — COUNT WHERE expiry_date < NOW() - online_now — read from Redis cache key online_count (set by RADIUS auth listener); fallback to COUNT radacct WHERE acctstoptime IS NULL - monthly_revenue — SUM of payments in current calendar month (dealer_id scoped) - outstanding_balance — SUM of unpaid invoices (dealer_id scoped) 3. Expiry alerts — customers expiring in next 3 days 4. Return all stats to Inertia as page props

Services Called: RedisService (read online_count), BillingService::getOutstanding()

Models Touched: Customer, Payment, Invoice

Database Tables: customers (read), payments (read), invoices (read), Redis (read)

Related APIs: GET /api/dashboard/online (polled every 30s by frontend)

Related Features: Online Now widget, Expiry alerts, View As


POST /dashboard/view-as

Field Value
Method POST
URI /dashboard/view-as
Controller DashboardController@viewAs
Middleware auth
Permission super_admin or admin only
Inertia Page Redirects to /dashboard
React Components N/A

Request Parameters: - dealer_id — required, integer, must exist in users WHERE role = 'dealer'

Business Logic: 1. Abort 403 if current user is not admin or super_admin 2. Validate dealer_id exists 3. Store dealer_id in session: session(['viewing_as_dealer' => $dealerId]) 4. AuditLogger logs: admin.view_as with {admin_id, dealer_id} 5. Redirect to /dashboard — all subsequent requests now scoped as that dealer

Services Called: AuditLogger::log('admin.view_as', ...)

Models Touched: User

Database Tables: users (read to validate dealer), audit_logs (write), Redis session (write)

Related APIs: None

Related Features: View As banner in UI, dealer impersonation, session scoping


GET /api/dashboard/online

Field Value
Method GET
URI /api/dashboard/online
Controller Api\DashboardController@online
Middleware auth
Permission All authenticated users
Inertia Page None (JSON response)
React Components Consumed by OnlineNowWidget

Request Parameters: None

Business Logic: 1. Read Redis key online_count (integer — maintained by RADIUS auth/acct listener) 2. For admin: return system-wide count 3. For dealer: return count of online customers WHERE dealer_id = $user->id (from radacct JOIN customers) 4. Response: {"count": N, "timestamp": "..."}

Services Called: RedisService

Models Touched: None directly

Database Tables: radacct (read if cache miss), Redis (read)

Related APIs:

Related Features: Dashboard Online Now widget (polled every 30s)


3. Customers Group

Prefix: /customers — Middleware: auth


GET /customers

Field Value
Method GET
URI /customers
Controller CustomerController@index
Middleware auth
Permission customers.view
Inertia Page Pages/Customers/Index.jsx
React Components CustomerTable, CustomerFilters, BulkToolbar, StatusBadge, PaginationBar

Request Parameters (query): - search — string, searches name / pppoe_username / phone / address - status — enum: active, expired, suspended, disconnected - dealer_id — integer (admin only filter) - package_id — integer - area_id — integer - connection_type — string: pppoe, static, ftth - page — integer (pagination) - per_page — integer (default 25)

Business Logic: 1. Check customers.view permission → abort 403 if missing 2. Build base query: Customer::with(['package', 'dealer', 'connectionType', 'areaRef']) 3. Apply dealer scope via visibleTo($user) scope: - admin/super_admin → no filter - dealer → WHERE dealer_id = $user->id - sub_dealer → WHERE sub_dealer_id = $user->id - field_engineer → WHERE dealer_id = $user->parent_id - collection_officer → WHERE dealer_id IN (assigned) OR area_id IN (assigned) 4. Apply search filters from query params 5. Paginate with ->paginate($perPage)->withQueryString() 6. Return to Inertia with filters (for URL state preservation)

Services Called: None

Models Touched: Customer, Package, User (dealer), Area, ConnectionType

Database Tables: customers, packages, users, areas, connection_types (all read)

Related APIs: GET /api/customers/filter (used for advanced filter UI), GET /api/customers (search typeahead)

Related Features: Bulk toolbar (bulk status change), CSV export, filter persistence


GET /customers/create

Field Value
Method GET
URI /customers/create
Controller CustomerController@create
Middleware auth
Permission customers.create
Inertia Page Pages/Customers/Create.jsx
React Components CustomerForm, PackageSelect, AreaSelect, NasSelect, ConnectionTypeSelect, DealerSelect (admin only)

Request Parameters: None

Business Logic: 1. Check customers.create permission → abort 403 2. Load form data: - packages — dealer's assigned packages (via dealer_package pivot) or all packages for admin - areas — all areas or dealer's areas - dealers — all dealers (admin only) - nas_devices — active NAS list - connection_types — all connection types - olts — GPON OLTs if FTTH feature enabled 3. Return Inertia with form data as props

Services Called: None

Models Touched: Package, Area, Nas, ConnectionType, Olt, User

Database Tables: packages, dealer_package, areas, nas, connection_types, olts (all read)

Related APIs: None

Related Features: FTTH ONT provisioning, PPPoE credential generation


POST /customers

Field Value
Method POST
URI /customers
Controller CustomerController@store
Middleware auth
Permission customers.create
Inertia Page Redirects to customers.show on success
React Components N/A

Request Parameters (body): - name — required, string - phone — required, string - email — optional, email - address — required, string - area_id — required, integer - package_id — required, integer - connection_type_id — required, integer - nas_id — required, integer - pppoe_username — required if PPPoE, unique in customers - pppoe_password — required if PPPoE - static_ip — optional, for static type - expiry_date — required, date - dealer_id — admin only (otherwise set to auth()->id()) - sub_dealer_id — optional - olt_id, ont_serial — if FTTH - notes — optional

Business Logic: 1. Validate all fields 2. For dealers: force dealer_id = auth()->id() (cannot assign to another dealer) 3. Wrap in DB transaction: a. Customer::create($validated) with explicit dealer_id b. RadiusService::syncCustomer($customer) → writes to radcheck, radreply, radusergroup in RADIUS DB c. If FTTH: OltService::provisionOnt($customer, $ont_serial) 4. AuditLogger::log('customer.created', ['customer_id' => $customer->id, ...]) 5. Optionally: WhatsAppService::sendWelcome($customer) if setting enabled 6. Redirect to customers.show with success flash

Services Called: RadiusService::syncCustomer(), AuditLogger::log(), WhatsAppService::sendWelcome() (optional)

Models Touched: Customer

Database Tables: customers (insert), radcheck (insert/update), radreply (insert/update), radusergroup (insert/update), audit_logs (insert), whatsapp_outbox (insert if welcome enabled)

Related APIs: None

Related Features: RADIUS provisioning, FTTH ONT provisioning, WhatsApp welcome message


GET /customers/{id}

Field Value
Method GET
URI /customers/{customer}
Controller CustomerController@show
Middleware auth
Permission customers.view
Inertia Page Pages/Customers/Show.jsx
React Components CustomerHeader, CustomerInfoTab, CustomerLedgerTab, CustomerInvoicesTab, CustomerOntTab, RadiusStatusCard, ActionButtons

Request Parameters: None (route model binding: {customer}Customer::find($id))

Business Logic: 1. Check customers.view permission → abort 403 2. IDOR check: if not admin, verify customer->dealer_id === $user->id (or sub_dealer logic) 3. Load relationships: - $customer->load(['package', 'dealer', 'subDealer', 'area', 'nas', 'connectionType']) - Recent invoices: Invoice::where('customer_id', $id)->latest()->take(10)->get() - Recent payments: Payment::where('customer_id', $id)->latest()->take(10)->get() - Ledger summary: running balance from ledger_entries - RADIUS status: RadiusService::getCustomerStatus($customer->pppoe_username) → checks radcheck + radacct - ONT info (if FTTH): OltService::getOntStatus($customer->ont_serial) 4. Pass to Inertia with permission flags: canEdit, canDelete, canRecharge, canDisconnect

Services Called: RadiusService::getCustomerStatus(), OltService::getOntStatus() (if FTTH)

Models Touched: Customer, Invoice, Payment, LedgerEntry, Package, Area, Nas

Database Tables: customers, invoices, payments, ledger_entries, radcheck, radacct, radreply (all read)

Related APIs: GET /api/customers/{customer}/ledger, GET /api/radius/{username}/status

Related Features: Recharge button, Kick/disconnect, PPPoE credential view, ONT signal tab, traffic graphs tab


GET /customers/{id}/edit

Field Value
Method GET
URI /customers/{customer}/edit
Controller CustomerController@edit
Middleware auth
Permission customers.edit
Inertia Page Pages/Customers/Edit.jsx
React Components CustomerForm (edit mode), same as Create

Business Logic: Same as create but pre-populates form with existing customer data. IDOR check applied.


PUT /customers/{id}

Field Value
Method PUT
URI /customers/{customer}
Controller CustomerController@update
Middleware auth
Permission customers.edit
Inertia Page Redirects to customers.show
React Components N/A

Request Parameters: Same as POST /customers (partial update allowed)

Business Logic: 1. Validate, check permission, IDOR check 2. Lock-for-update with retry loop to handle PostgreSQL 55P03 (lock not available) errors — up to 3 retries with 50ms sleep 3. $customer->update($validated) 4. If pppoe_username, pppoe_password, package_id, status, or expiry_date changed: - RadiusService::syncCustomer($customer) → update radcheck/radreply/radusergroup 5. AuditLogger::log('customer.updated', ['before' => $old, 'after' => $new]) 6. Redirect with success flash

Services Called: RadiusService::syncCustomer(), AuditLogger::log()

Models Touched: Customer

Database Tables: customers (update), radcheck, radreply, radusergroup (update), audit_logs (insert)

Related APIs: None

Related Features: PPPoE credential change, package change (triggers RADIUS group update), expiry edit


DELETE /customers/{id}

Field Value
Method DELETE
URI /customers/{customer}
Controller CustomerController@destroy
Middleware auth
Permission customers.delete
Inertia Page Redirects to customers.index
React Components N/A

Request Parameters (body): - delete_reason — required, string

Business Logic: 1. Check permission and IDOR 2. Soft delete: set deleted_at = NOW(), deleted_by = $user->id, delete_reason = $reason 3. RadiusService::deleteCustomerRadius($customer->pppoe_username) → removes rows from radcheck, radreply, radusergroup in RADIUS DB (customer can no longer authenticate) 4. If active RADIUS session: send CoA Disconnect (kick session) 5. AuditLogger::log('customer.deleted', [...])

Services Called: RadiusService::deleteCustomerRadius(), RadiusService::kickSession() (if online)

Models Touched: Customer

Database Tables: customers (soft delete update), radcheck, radreply, radusergroup (delete), audit_logs (insert)

Related APIs: None

Related Features: Deleted customers list, restore flow


POST /customers/{id}/recharge

Field Value
Method POST
URI /customers/{customer}/recharge
Controller CustomerController@recharge
Middleware auth
Permission customers.recharge (or billing.collect)
Inertia Page Redirects back to customers.show
React Components N/A (called from RechargeModal)

Request Parameters (body): - amount — required, numeric - payment_method — required, enum: cash, bank_transfer, easypaisa, jazzcash - months — optional, integer (default 1) - notes — optional

Business Logic: 1. Check permission and IDOR 2. Wrap in DB transaction: a. BillingService::generateInvoice($customer, $months) → creates Invoice record b. BillingService::recordPayment($invoice, $amount, $method) → creates Payment record, updates LedgerEntry c. BillingService::renewCustomer($customer, $months) → advances expiry_date by N months, sets status = 'active' 3. RadiusService::syncCustomer($customer) → updates expiry attribute in radcheck so RADIUS picks up new expiry 4. WhatsAppService::sendRechargeConfirmation($customer, $payment) → queues WhatsApp message if enabled 5. AuditLogger::log('customer.recharged', [...]) 6. Redirect with success flash

Services Called: BillingService::generateInvoice(), BillingService::recordPayment(), BillingService::renewCustomer(), RadiusService::syncCustomer(), WhatsAppService::sendRechargeConfirmation()

Models Touched: Customer, Invoice, Payment, LedgerEntry

Database Tables: customers (expiry_date update), invoices (insert), payments (insert), ledger_entries (insert), radcheck (update), whatsapp_outbox (insert), audit_logs (insert)

Related APIs: None

Related Features: Billing cycle, WhatsApp receipt, RADIUS re-authentication trigger


POST /customers/{id}/kick (disconnect)

Field Value
Method POST
URI /customers/{customer}/kick
Controller CustomerController@kick
Middleware auth
Permission customers.disconnect
Inertia Page Redirects back
React Components N/A

Business Logic: 1. Check permission and IDOR 2. RadiusService::kickSession($customer->pppoe_username) → - Finds active session in radacct for username - Sends RADIUS CoA Disconnect-Request packet to NAS IP via radclient - Tries port 3799 first (standard), falls back to 1700 (MikroTik CoA) - Waits for Disconnect-ACK or times out after 5s 3. Log result to audit_logs 4. Flash success or failure message

Services Called: RadiusService::kickSession()

Models Touched: Customer

Database Tables: radacct (read), audit_logs (insert)

Related APIs: None

Related Features: Real-time session management, NAS CoA


GET /customers/deleted

Field Value
Method GET
URI /customers/deleted
Controller CustomerController@deleted
Middleware auth
Permission customers.deleted.view
Inertia Page Pages/Customers/Deleted.jsx
React Components DeletedCustomerTable, RestoreButton

Business Logic: 1. Customer::onlyTrashed()->with([...])->visibleTo($user)->paginate() 2. Shows: name, pppoe_username, deleted_at, deleted_by (user name), delete_reason


POST /customers/{id}/restore

Field Value
Method POST
URI /customers/{customer}/restore
Controller CustomerController@restore
Middleware auth
Permission customers.restore
Inertia Page Redirects to customers.show
React Components N/A

Business Logic: 1. Customer::withTrashed()->findOrFail($id)->restore() — clears deleted_at, deleted_by, delete_reason 2. RadiusService::syncCustomer($customer) — re-creates RADIUS entries 3. AuditLogger::log('customer.restored', [...])


GET /customers/{id}/graphs

Field Value
Method GET
URI /customers/{customer}/graphs
Controller GraphController@customer
Middleware auth
Permission network.graphs
Inertia Page Pages/Graphs/Customer.jsx
React Components TrafficChart, SessionHistoryTable

Business Logic: 1. IDOR check 2. RadiusService::getTrafficHistory($customer->pppoe_username, $days) — queries radacct for upload/download bytes per session 3. Returns time-series data for chart rendering


GET /customers/{id}/troubleshoot

Field Value
Method GET
URI /customers/{customer}/troubleshoot
Controller CustomerController@troubleshoot
Middleware auth
Permission customers.view
Inertia Page Pages/Customers/Troubleshoot.jsx
React Components DiagnosticPanel, RadiusInfoCard, SessionCard, OntSignalCard

Business Logic: 1. Aggregate diagnostic data: - RADIUS radcheck/radreply entries for this user - Current radacct session (if any) - Last 5 accounting records - If FTTH: ONT signal (rx/tx optical power) - NAS reachability status 2. Return all diagnostic info as props


4. Billing Group

Prefix: /billing — Middleware: auth


GET /billing/invoices

Field Value
Method GET
URI /billing/invoices
Controller InvoiceController@index
Middleware auth
Permission billing.invoices
Inertia Page Pages/Billing/Invoices/Index.jsx
React Components InvoiceTable, InvoiceFilters, StatusBadge, PaginationBar

Request Parameters (query): search, status (paid/unpaid/partial), from_date, to_date, dealer_id (admin), page

Business Logic: 1. Check billing.invoices permission 2. Invoice::with(['customer', 'payments'])->dealerScoped($user)->filter($request)->paginate(25) 3. Dealer-scoped: WHERE dealer_id = $user->id

Database Tables: invoices, payments, customers (read)


POST /billing/invoices

Field Value
Method POST
URI /billing/invoices
Controller InvoiceController@store
Middleware auth
Permission billing.invoices
Inertia Page Redirects to billing.invoices.show
React Components N/A

Business Logic: 1. BillingService::generateInvoice($customer, $months, $options) — creates Invoice with line items 2. AuditLogger::log('invoice.created', [...])


GET /billing/invoices/{id}

Field Value
Method GET
URI /billing/invoices/{invoice}
Controller InvoiceController@show
Middleware auth
Permission billing.invoices
Inertia Page Pages/Billing/Invoices/Show.jsx
React Components InvoiceDetail, PaymentHistory, PrintButton, PDFDownloadButton

Business Logic: Load invoice with customer, payments, line items. IDOR check via dealer_id. Render full invoice detail.


GET /billing/invoices/{id}/pdf

Field Value
Method GET
URI /billing/invoices/{invoice}/pdf
Controller InvoiceController@pdf
Middleware auth
Permission billing.invoices
Inertia Page None (PDF binary response)
React Components N/A

Business Logic: Generate PDF using Laravel DomPDF or Snappy. Returns Content-Type: application/pdf response for download/print.


GET /billing/defaulters

Field Value
Method GET
URI /billing/defaulters
Controller InvoiceController@defaulters
Middleware auth
Permission billing.view
Inertia Page Pages/Billing/Defaulters.jsx
React Components DefaulterTable, SendReminderButton, BulkWhatsAppButton

Business Logic: 1. Customer::with(['package', 'invoices'])->whereHas('invoices', fn($q) => $q->unpaid())->visibleTo($user)->get() 2. Calculate: days_overdue, outstanding_amount per customer 3. Admins/dealers can trigger WhatsApp reminders from this view


GET /billing/outbox

Field Value
Method GET
URI /billing/outbox
Controller OutboxController@index
Middleware auth
Permission whatsapp.queue
Inertia Page Pages/Billing/Outbox.jsx
React Components OutboxTable, RetryButton, StatusBadge

Business Logic: Paginated list of whatsapp_outbox records. Shows: recipient, message preview, status (pending/sent/failed), created_at, attempts.


GET /billing/archive

Field Value
Method GET
URI /billing/archive
Controller BillingArchiveController@index
Middleware auth
Permission billing.view
Inertia Page Pages/Billing/Archive/Index.jsx
React Components ArchiveMonthList, ArchiveStatCard

POST /billing/archive

Field Value
Method POST
URI /billing/archive
Controller BillingArchiveController@store
Middleware auth
Permission billing.manage (admin only)
Inertia Page Redirects back
React Components N/A

Business Logic: 1. Run month-end archive: snapshot current month's invoices/payments into billing_archives table 2. Generate aggregate summary: total invoices, total collected, total outstanding, per-dealer breakdown 3. Mark month as archived — cannot be re-archived


POST /billing/archive/{id}/restore

Field Value
Method POST
URI /billing/archive/{month}/restore
Controller BillingArchiveController@restore
Middleware auth
Permission billing.manage (super_admin only)
Inertia Page Redirects back

Business Logic: Un-archive a billing month. Restores archived records to active state. Logs to audit_logs.


5. Packages Group

Prefix: /packages — Middleware: auth


GET /packages

Field Value
Method GET
URI /packages
Controller PackageController@index
Middleware auth
Permission admin.packages
Inertia Page Pages/Packages/Index.jsx
React Components PackageTable, PackageCard, AddPackageButton

Business Logic: List all packages with: name, speed_download, speed_upload, price, type (PPPoE/FTTH), group_name (RADIUS group), active customer count.


POST /packages

Field Value
Method POST
URI /packages
Controller PackageController@store
Middleware auth
Permission admin.packages
Inertia Page Redirects to packages.index

Request Parameters: name, speed_download, speed_upload, burst_download, burst_limit_threshold, price, connection_type, radius_group_name, description

Business Logic: 1. Validate and create Package record 2. RadiusService::syncPackage($package) → creates/updates radgroupreply entries for all registered NAS vendors (MikroTik: rate-limit, Cisco: Cisco-AVPair, Huawei: HW-Data-Filter) 3. AuditLogger::log('package.created', [...])

Database Tables: packages (insert), radgroupreply (insert/update), audit_logs (insert)


PUT /packages/{id}

Field Value
Method PUT
URI /packages/{package}
Controller PackageController@update
Middleware auth
Permission admin.packages
Inertia Page Redirects to packages.index

Business Logic: 1. Update Package record 2. RadiusService::syncPackage($package) → updates radgroupreply for ALL vendors that have this group — so existing customers on this package immediately get the new speed on next auth 3. AuditLogger::log('package.updated', ['before' => $old, 'after' => $new])


6. NAS Group

Prefix: /nas — Middleware: auth


GET /nas

Field Value
Method GET
URI /nas
Controller NasController@index
Middleware auth
Permission network.nas_view
Inertia Page Pages/Nas/Index.jsx
React Components NasTable, NasStatusCard, AddNasButton

Business Logic: List all NAS devices from nas table. For each: name, shortname, type, nastype, ports, server (RADIUS secret), active session count from radacct.


POST /nas

Field Value
Method POST
URI /nas
Controller NasController@store
Middleware auth
Permission network.nas_manage
Inertia Page Redirects to nas.index

Request Parameters: nasname (IP), shortname, type, ports, secret, server, community, description, vendor (mikrotik/cisco/huawei)

Business Logic: Create NAS record in both nas table (local config) and FreeRADIUS nas table (for RADIUS auth). The secret is the shared secret for RADIUS communication.


GET /nas/{id}/sessions

Field Value
Method GET
URI /nas/{nas}/sessions
Controller NasController@sessions
Middleware auth
Permission network.nas_view
Inertia Page Pages/Nas/Sessions.jsx
React Components SessionTable, DisconnectButton

Business Logic: Query radacct WHERE nasipaddress = $nas->nasname AND acctstoptime IS NULL to get live sessions.


GET /api/nas/{id}/traffic

Field Value
Method GET
URI /api/nas/{nas}/traffic
Controller Api\NasController@traffic
Middleware auth
Permission network.nas_view
Inertia Page None (JSON)

Business Logic: MikroTikService::liveTraffic($nas) → connects to MikroTik API on port 8728 or 8729 (SSL), fetches /interface/monitor-traffic for WAN interface. Returns real-time bps/pps data. Used by NAS detail page charts.


7. OLT Group

Prefix: /olt — Middleware: auth


GET /olt

Field Value
Method GET
URI /olt
Controller OltController@index
Middleware auth
Permission network.gpon_view
Inertia Page Pages/Olt/Index.jsx
React Components OltTable, OltStatusCard

Business Logic: List all OLT devices. Show: name, IP, vendor, total ONTs, registered ONTs, unregistered ONTs count.


GET /olt/{id}/onts

Field Value
Method GET
URI /olt/{olt}/onts
Controller OltController@onts
Middleware auth
Permission network.gpon_view
Inertia Page Pages/Customers/OntList.jsx
React Components OntTable, OntStatusBadge, SignalLevel

Business Logic: List all ONTs registered on this OLT, joined with customers to show which customer each ONT belongs to. Show: serial, status, rx_power, tx_power, distance, customer name.


GET /olt/{id}/unregistered

Field Value
Method GET
URI /olt/{olt}/unregistered
Controller OltController@unregistered
Middleware auth
Permission network.gpon_view
Inertia Page Pages/Customers/UnregisteredOnts.jsx
React Components UnregisteredOntTable, ProvisionButton

Business Logic: Fetch ONTs from OLT that are not in customers.ont_serial column. These are physically connected but not yet provisioned. Shows: serial, PON port, signal. Admin can click Provision to assign to a customer.


8. WhatsApp Group

Prefix: /whatsapp — Middleware: auth


GET /whatsapp

Field Value
Method GET
URI /whatsapp
Controller WhatsAppController@index
Middleware auth
Permission whatsapp.view
Inertia Page Pages/WhatsApp/Index.jsx
React Components WhatsAppDashboard, SessionStatusCard, OutboxSummary, QRCodeModal

Business Logic: Dashboard showing WhatsApp session status (connected/disconnected), pending outbox count, today's sent count, failed count.


GET /whatsapp/templates

Field Value
Method GET
URI /whatsapp/templates
Controller WhatsAppController@templates
Middleware auth
Permission whatsapp.templates
Inertia Page Pages/WhatsApp/Templates.jsx
React Components TemplateList, TemplateEditor, VariableReference

Business Logic: List all message templates from whatsapp_templates. Templates use {name}, {expiry}, {amount}, {package} placeholders.


POST /whatsapp/templates

Field Value
Method POST
URI /whatsapp/templates
Controller WhatsAppController@storeTemplate
Middleware auth
Permission whatsapp.templates
Inertia Page Redirects back

Request Parameters: name, body (message text with placeholders), trigger_event (recharge/expiry_reminder/welcome/custom)


POST /whatsapp/campaigns

Field Value
Method POST
URI /campaigns
Controller CampaignController@store
Middleware auth
Permission whatsapp.campaigns
Inertia Page Redirects to campaigns.show

Request Parameters: name, template_id, recipient_filter (all/expired/active/custom), scheduled_at, dealer_ids (admin scoping)

Business Logic: 1. Create Campaign record 2. Dispatch SendCampaignJob to queue → job resolves recipients → creates one whatsapp_outbox record per recipient → WhatsAppService::send() processes queue


GET /whatsapp/sessions

Field Value
Method GET
URI /whatsapp/sessions
Controller WhatsAppController@sessions
Middleware auth
Permission whatsapp.sessions
Inertia Page Pages/WhatsApp/Sessions.jsx
React Components QRCodeScanner, SessionStatusCard, RegenerateButton

Business Logic: Shows active Baileys/WPPConnect session status. If disconnected, generates QR code for re-linking WhatsApp. QR code is fetched from the Node.js WhatsApp bridge service.


9. Campaign Group

Prefix: /campaigns — Middleware: auth


GET /campaigns

Field Value
Method GET
URI /campaigns
Controller CampaignController@index
Middleware auth
Permission whatsapp.campaigns
Inertia Page Pages/Campaigns/Index.jsx
React Components CampaignTable, CampaignStatusBadge

GET /campaigns/{id}

Field Value
Method GET
URI /campaigns/{campaign}
Controller CampaignController@show
Middleware auth
Permission whatsapp.campaigns
Inertia Page Pages/Campaigns/Show.jsx
React Components CampaignProgress, RecipientList, DeliveryStats

Business Logic: Show campaign progress: total recipients, sent count, delivered count, failed count. Polls /api/campaigns/{id}/progress for live update.


10. Reports Group

Prefix: /reports — Middleware: auth


GET /reports

Field Value
Method GET
URI /reports
Controller ReportController@index
Middleware auth
Permission reports.view
Inertia Page Pages/Reports/Index.jsx
React Components ReportCard, ReportNavigationList

Business Logic: Landing page showing available reports for the current user's permission level.


GET /reports/revenue

Field Value
Method GET
URI /reports/revenue
Controller ReportController@revenue
Middleware auth
Permission reports.billing
Inertia Page Pages/Reports/Revenue.jsx
React Components RevenueChart, MonthlyTable, ExportButton

Business Logic: Monthly revenue by dealer. Sums payments.amount grouped by DATE_TRUNC('month', paid_at) and dealer_id. Admin sees all; dealer sees own.


GET /reports/defaulters

Field Value
Method GET
URI /reports/defaulters
Controller ReportController@defaulters
Middleware auth
Permission reports.recovery
Inertia Page Pages/Reports/Defaulters.jsx
React Components DefaulterTable, ExportButton

GET /reports/collections

Field Value
Method GET
URI /reports/collections
Controller ReportController@collections
Middleware auth
Permission reports.billing
Inertia Page Pages/Reports/Collections.jsx
React Components CollectionTable, PeriodSelector, DealerFilter

Request Parameters (query): from_date, to_date, dealer_id, group_by (day/week/month)


GET /reports/{type}/export

Field Value
Method GET
URI /reports/{type}/export
Controller ReportController@export
Middleware auth
Permission reports.export
Inertia Page None (file download)

Business Logic: Generates Excel/CSV export using Laravel Excel (Maatwebsite). Dealer-scoped. Types: customers, revenue, defaulters, collections, radius, ftth, area, package.


11. Health & Cron Group

Prefix: /health and /settings/cron — Middleware: auth


GET /health

Field Value
Method GET
URI /health
Controller HealthController@index
Middleware auth
Permission admin.health
Inertia Page Pages/Health/Index.jsx
React Components ServerStatsCard, QueueStatusCard, NasHealthList, CronJobList, RedisStatusCard

Business Logic: 1. Server stats: CPU, RAM, disk (via sys_getloadavg() and df) 2. Queue: check Laravel Horizon or queue worker status via Artisan queue:size 3. Redis: Redis::ping() → check connectivity 4. Cron jobs: CronJob::all() → list with last_run_at, next_run_at, status 5. NAS health: ping each NAS IP → record response time


GET /health/cron

Field Value
Method GET
URI /health/cron (or /settings/cron)
Controller CronJobController@index
Middleware auth
Permission admin.health
Inertia Page Pages/Health/CronJobs.jsx
React Components CronJobTable, CronJobForm, EnableToggle

Business Logic: List all records from cron_jobs table. Shows: name, schedule (cron expression), last_run_at, next_run_at, enabled, last_status.


POST /health/cron

Field Value
Method POST
URI /health/cron
Controller CronJobController@store
Middleware auth
Permission admin.health

Request Parameters: name, schedule (cron expression), command (Artisan command), enabled


PUT /health/cron/{id}

Field Value
Method PUT
URI /health/cron/{job}
Controller CronJobController@update
Middleware auth
Permission admin.health

Business Logic: Update schedule, enable/disable. Laravel Scheduler reads this table to determine which jobs to run.


DELETE /health/cron/{id}

Field Value
Method DELETE
URI /health/cron/{job}
Controller CronJobController@destroy
Middleware auth
Permission admin.health

12. Settings Group

Prefix: /settings — Middleware: auth


GET /settings

Field Value
Method GET
URI /settings
Controller SettingsController@general
Middleware auth
Permission admin.masters (super_admin or admin)
Inertia Page Pages/Settings/General.jsx
React Components SettingsForm, SettingGroup, SaveButton

Business Logic: Load all settings from settings key-value table via Setting::all(). Group into sections: General, Billing, RADIUS, WhatsApp, Notifications.


PUT /settings

Field Value
Method PUT
URI /settings
Controller SettingsController@updateGeneral
Middleware auth
Permission admin.masters
Inertia Page Redirects back

Business Logic: 1. Validate each setting key/value pair 2. Setting::set($key, $value) for each changed setting → upsert in settings table 3. Clear app_settings Redis cache (so HandleInertiaRequests picks up new values) 4. AuditLogger::log('settings.updated', [...])


GET /settings/company

Field Value
Method GET
URI /settings/company
Controller SettingsController@company
Middleware auth
Permission admin.masters
Inertia Page Pages/Settings/Company.jsx
React Components CompanyForm, LogoUploader

PUT /settings/company

Field Value
Method PUT
URI /settings/company
Controller SettingsController@updateCompany
Middleware auth
Permission admin.masters

Business Logic: Update company profile (name, address, phone, email, logo). Logo uploaded to storage/app/public/logo. Settings saved to settings table under company.* keys.


GET /settings/radius

Field Value
Method GET
URI /settings/radius
Controller SettingsController@radius
Middleware auth
Permission admin.masters
Inertia Page Pages/Settings/Radius.jsx

Business Logic: RADIUS server configuration: host, port, DB connection, CoA port, retry count.


13. Users Group

Prefix: /users — Middleware: auth (admin only)


GET /users

Field Value
Method GET
URI /users
Controller UserController@index
Middleware auth
Permission admin.users
Inertia Page Pages/Users/Index.jsx
React Components UserTable, RoleBadge, CreateUserButton

Business Logic: List all users (dealers, sub_dealers, admins). Admin sees only dealers/sub_dealers they manage. super_admin sees all including other admins.


POST /users

Field Value
Method POST
URI /users
Controller UserController@store
Middleware auth
Permission admin.users
Inertia Page Redirects to users.edit

Request Parameters: name, email, password, role (dealer/sub_dealer/noc_admin/billing_admin/etc.), dealer_id (if sub_dealer), permissions (JSONB object), phone, address

Business Logic: 1. Validate: email unique, role valid, for sub_dealer → dealer_id required 2. User::create([...]) with hashed password 3. AuditLogger::log('user.created', [...])


PUT /users/{id}

Field Value
Method PUT
URI /users/{user}
Controller UserController@update
Middleware auth
Permission admin.users
Inertia Page Redirects back

Request Parameters: Same as store. permissions JSONB replaces existing permissions atomically.

Business Logic: 1. Validate. Cannot change own role (prevents lockout). Cannot grant a sub_dealer permissions the parent dealer doesn't have. 2. Update users record 3. AuditLogger::log('user.permissions_updated', ['before' => $oldPermissions, 'after' => $newPermissions])


GET /users/{id}/packages

Field Value
Method GET
URI /users/{user}/packages
Controller DealerPackageController@index
Middleware auth
Permission admin.users
Inertia Page Pages/Users/Packages.jsx
React Components DealerPackageTable, PriceOverrideInput, AssignPackageButton

Business Logic: Manages the dealer_package pivot table. Shows: which packages are assigned to this dealer, dealer-specific price per package. Admin can assign/unassign packages and set custom prices.


14. Import Group

Prefix: /import — Middleware: auth


GET /import

Field Value
Method GET
URI /import
Controller ImportController@index
Middleware auth
Permission admin.import.customers
Inertia Page Pages/Import/Index.jsx
React Components ImportDropzone, TemplateDownloadButton, ImportLogList

POST /import/customers

Field Value
Method POST
URI /import/customers
Controller ImportController@importCustomers
Middleware auth
Permission admin.import.customers
Inertia Page Redirects to import.logs

Request Parameters: file (XLS/XLSX/CSV), dealer_id (required for admin), dry_run (boolean — validate without saving)

Business Logic: 1. Upload file to temp storage 2. Parse using Laravel Excel (Maatwebsite) 3. Validate each row: required fields, unique pppoe_username, valid package_id, valid area 4. If dry_run: return validation report (N valid, M errors) without saving 5. If valid: dispatch ImportCustomersJob to queue 6. Job processes rows in batches of 50: create customer + RadiusService::syncCustomer() per row 7. Log results to import_logs table

Database Tables: customers (insert), radcheck, radreply, radusergroup (insert), import_logs (insert)


15. Audit Group

Prefix: /audit — Middleware: auth


GET /audit

Field Value
Method GET
URI /audit
Controller AuditController@index
Middleware auth
Permission admin.audit
Inertia Page Pages/Audit/Index.jsx
React Components AuditLogTable, AuditFilters, AuditEntryModal

Request Parameters (query): event, user_id, from_date, to_date, search (target description)

Business Logic: 1. AuditLog::with(['user'])->filter($request)->latest()->paginate(50) 2. Admin sees all audit logs. super_admin only can prune. 3. Each row: event, user who did it, target (customer/invoice/user name), old_values, new_values (JSONB diff), ip_address, user_agent, created_at

Database Tables: audit_logs (read), users (join for user name)


GET /audit/export

Field Value
Method GET
URI /audit/export
Controller AuditController@export
Middleware auth
Permission admin.audit
Inertia Page None (CSV download)

16. Tickets Group

Prefix: /tickets — Middleware: auth


GET /tickets

Field Value
Method GET
URI /tickets
Controller TicketController@index
Middleware auth
Permission tickets.view
Inertia Page Pages/Tickets/Index.jsx
React Components TicketTable, TicketFilters, StatusBadge, PriorityBadge

Request Parameters (query): status (open/in_progress/resolved/closed), priority, assigned_to, dealer_id


POST /tickets

Field Value
Method POST
URI /tickets
Controller TicketController@store
Middleware auth
Permission tickets.create
Inertia Page Redirects to tickets.show

Request Parameters: customer_id, title, description, priority (low/medium/high/critical), category


PUT /tickets/{id}

Field Value
Method PUT
URI /tickets/{ticket}
Controller TicketController@update
Middleware auth
Permission tickets.manage
Inertia Page Redirects back

Business Logic: Update status, assigned_to (engineer), resolution notes. Triggers notification to assigned engineer if assignment changes.


POST /tickets/{id}/comments

Field Value
Method POST
URI /tickets/{ticket}/comments
Controller TicketController@comment
Middleware auth
Permission tickets.view (any authenticated user can comment on tickets they can view)
Inertia Page Redirects back to tickets.show

Request Parameters: body (comment text), is_internal (boolean — internal note or customer-visible)


17. TR-069 Group

Prefix: /tr069 — Middleware: auth


GET /tr069

Field Value
Method GET
URI /tr069
Controller Tr069Controller@index
Middleware auth
Permission tr069.view
Inertia Page Pages/Tr069/Index.jsx
React Components DeviceList, DeviceStatusCard, GenieACSLink

Business Logic: Lists CPE devices managed via TR-069/GenieACS. Shows: device ID, model, IP, last contact, firmware version, status.


GET /tr069/devices/{id}

Field Value
Method GET
URI /tr069/devices/{device}
Controller Tr069Controller@show
Middleware auth
Permission tr069.view
Inertia Page Pages/Tr069/Device.jsx
React Components DeviceInfoCard, ParameterTree, RebootButton, FactoryResetButton, FirmwareUpgradeButton

POST /tr069/devices/{id}/provision

Field Value
Method POST
URI /tr069/devices/{device}/provision
Controller Tr069Controller@provision
Middleware auth
Permission tr069.manage
Inertia Page Redirects back

Business Logic: Push provisioning parameters to CPE via GenieACS API: SSID, WiFi password, PPPoE credentials, DNS settings. Dispatches Tr069ProvisionJob to queue.


18. Graphs Group

Prefix: /graphs — Middleware: auth


GET /graphs/nas/{id}

Field Value
Method GET
URI /graphs/nas/{nas}
Controller GraphController@nas
Middleware auth
Permission network.graphs
Inertia Page Pages/Graphs/Nas.jsx
React Components TrafficChart, SessionCountChart, TimeRangeSelector

Business Logic: Fetches traffic history for a NAS device. Queries nas_traffic_log table (populated by background SNMP/MikroTik polling job). Returns time-series: timestamp, bytes_in, bytes_out, session_count.


GET /graphs/customer/{id}

Field Value
Method GET
URI /graphs/customer/{customer}
Controller GraphController@customer
Middleware auth
Permission network.graphs
Inertia Page Pages/Graphs/Customer.jsx
React Components TrafficChart, SessionHistoryTable

Business Logic: IDOR check. Queries radacct for customer's session history: acctinputoctets, acctoutputoctets per session. Aggregates by day for chart.


19. Engineer Group

Prefix: /engineer — Middleware: auth


GET /engineer

Field Value
Method GET
URI /engineer
Controller EngineerController@dashboard
Middleware auth
Permission admin.field_engineers
Inertia Page Pages/Engineer/Dashboard.jsx
React Components TaskList, EngineerMap, AssignmentCard

GET /engineer/tasks

Field Value
Method GET
URI /engineer/tasks
Controller EngineerController@tasks
Middleware auth
Permission admin.field_engineers
Inertia Page Pages/Engineer/Tasks.jsx
React Components TaskTable, TaskStatusBadge

Business Logic: Lists engineer_tasks for this engineer's team. Tasks linked to tickets or standalone installation/fault jobs.


20. Customer Portal Group

Routes in routes/portal.php. Guard: portal. Prefix: /portal.


GET /portal/login

Field Value
Method GET
URI /portal/login
Controller PortalAuthController@showLogin
Middleware web (no portal auth)
Permission Public
Inertia Page Pages/Auth/PortalLogin.jsx

POST /portal/login

Field Value
Method POST
URI /portal/login
Controller PortalAuthController@login
Middleware web, throttle:5,1
Permission Public
Inertia Page Redirects to /portal/dashboard

Request Parameters: username (pppoe_username or phone), password

Business Logic: 1. Auth::guard('portal')->attempt([...]) — authenticates against customers table (portal_password field) 2. On success: session under portal guard namespace 3. Session never shares space with admin sessions


GET /portal/dashboard

Field Value
Method GET
URI /portal/dashboard
Controller PortalController@dashboard
Middleware auth:portal
Permission Authenticated portal user
Inertia Page Pages/Portal/Dashboard.jsx
React Components AccountStatusCard, ExpiryCountdown, RecentInvoices, SupportTicketButton

Business Logic: 1. Auth::guard('portal')->user() → returns the Customer model 2. Load: package name, expiry date, current status, outstanding balance, last payment 3. All data scoped strictly to customer->id — no other customer's data is accessible


GET /portal/invoices

Field Value
Method GET
URI /portal/invoices
Controller PortalController@invoices
Middleware auth:portal
Permission Portal user
Inertia Page Pages/Portal/Invoices.jsx

Business Logic: Invoice::where('customer_id', $portalUser->id)->latest()->paginate(10)


GET /portal/profile

Field Value
Method GET
URI /portal/profile
Controller PortalController@profile
Middleware auth:portal
Permission Portal user
Inertia Page Pages/Portal/Profile.jsx
React Components ProfileForm, PasswordChangeForm

Business Logic: Customer can update: phone, email, portal password. Cannot change pppoe_username, package, expiry — those are admin-only.


21. Mobile API Group

Prefix: /api/mobile — Auth: Sanctum token auth. All routes: throttle:60,1 unless noted.


POST /api/mobile/login

Field Value
Method POST
URI /api/mobile/login
Controller MobileAuthController@login
Middleware throttle:5,1
Permission Public

Request Parameters: email, password, device_name

Business Logic: 1. Auth::attempt([...]) against users table 2. On success: $user->createToken($device_name)->plainTextToken → Sanctum personal access token 3. Return: {"token": "...", "user": {...}, "permissions": {...}} 4. Token stored in personal_access_tokens table


GET /api/mobile/me

Field Value
Method GET
URI /api/mobile/me
Controller MobileAuthController@me
Middleware auth:sanctum
Permission Authenticated

Returns: User profile, role, permissions JSONB, assigned dealer info


POST /api/mobile/logout

Field Value
Method POST
URI /api/mobile/logout
Controller MobileAuthController@logout
Middleware auth:sanctum
Permission Authenticated

Business Logic: $request->user()->currentAccessToken()->delete() — revokes current Sanctum token only (not all tokens).


GET /api/mobile/dashboard

Field Value
Method GET
URI /api/mobile/dashboard
Controller MobileDashboardController@index
Middleware auth:sanctum
Permission All authenticated mobile users

Returns: JSON: {total_customers, active, expired, online_now, monthly_revenue, outstanding}


GET /api/mobile/customers

Field Value
Method GET
URI /api/mobile/customers
Controller MobileCustomerController@index
Middleware auth:sanctum
Permission customers.view

Request Parameters: page, per_page, search, status, package_id

Returns: Paginated customer list (dealer-scoped)


POST /api/mobile/customers

Field Value
Method POST
URI /api/mobile/customers
Controller MobileCustomerController@store
Middleware auth:sanctum
Permission customers.create

Business Logic: Same as web CustomerController@store — full RADIUS sync.


GET /api/mobile/customers/search

Field Value
Method GET
URI /api/mobile/customers/search
Controller MobileCustomerController@search
Middleware auth:sanctum
Permission customers.view

Request Parameters: q — search term

Returns: Array of matching customers (dealer-scoped, max 20 results)


GET /api/mobile/customers/{id}

Field Value
Method GET
URI /api/mobile/customers/{id}
Controller MobileCustomerController@show
Middleware auth:sanctum
Permission customers.view

Returns: Full customer detail JSON + RADIUS status + recent invoices


POST /api/mobile/customers/{id}/recharge

Field Value
Method POST
URI /api/mobile/customers/{id}/recharge
Controller MobileCustomerController@recharge
Middleware auth:sanctum, throttle:10,1
Permission customers.recharge

Business Logic: Same as web recharge — BillingService + RADIUS sync + WhatsApp confirmation.


POST /api/mobile/customers/{id}/extend

Field Value
Method POST
URI /api/mobile/customers/{id}/extend
Controller MobileCustomerController@extend
Middleware auth:sanctum
Permission customers.extend

Business Logic: Extend expiry without generating a new invoice — advance expiry_date only. Used for grace extensions. RADIUS sync updated.


POST /api/mobile/customers/{id}/toggle

Field Value
Method POST
URI /api/mobile/customers/{id}/toggle
Controller MobileCustomerController@toggle
Middleware auth:sanctum
Permission customers.status

Business Logic: Toggle customer status between active and suspended. RadiusService::syncCustomer() updates radcheck Auth-Type attribute accordingly.


POST /api/mobile/customers/{id}/package

Field Value
Method POST
URI /api/mobile/customers/{id}/package
Controller MobileCustomerController@changePackage
Middleware auth:sanctum
Permission customers.edit

Request Parameters: package_id

Business Logic: Change customer->package_id. RadiusService::syncCustomer() updates radusergroup so next authentication uses the new speed group.


GET /api/mobile/customers/{id}/live-traffic

Field Value
Method GET
URI /api/mobile/customers/{id}/live-traffic
Controller MobileCustomerController@liveTraffic
Middleware auth:sanctum
Permission network.graphs

Business Logic: MikroTikService::liveTraffic($customer) → connects to customer's NAS MikroTik API → fetches current bandwidth usage for this PPPoE user via /ppp/active + /interface/monitor-traffic. Returns real-time bytes_in/bytes_out per second.


GET /api/mobile/customers/{id}/traffic-history

Field Value
Method GET
URI /api/mobile/customers/{id}/traffic-history
Controller MobileCustomerController@trafficHistory
Middleware auth:sanctum
Permission network.graphs

Request Parameters: days (default 7, max 90)

Returns: Daily aggregated upload/download bytes from radacct


GET /api/mobile/tickets

Field Value
Method GET
URI /api/mobile/tickets
Controller MobileTicketController@index
Middleware auth:sanctum
Permission tickets.view

Returns: Paginated tickets (dealer-scoped)


GET /api/mobile/packages

Field Value
Method GET
URI /api/mobile/packages
Controller MobilePackageController@index
Middleware auth:sanctum
Permission All authenticated

Returns: List of packages available to this user (dealer-filtered via dealer_package)


GET /api/mobile/reports

Field Value
Method GET
URI /api/mobile/reports
Controller MobileReportController@index
Middleware auth:sanctum
Permission reports.view

Returns: Summary report JSON: revenue, collections, defaulters count


GET /api/mobile/network

Field Value
Method GET
URI /api/mobile/network
Controller MobileNetworkController@index
Middleware auth:sanctum
Permission network.nas_view

Returns: NAS list with session counts, online customer count per NAS


22. Internal Ajax API Group

Routes in routes/api.php. Session-cookie auth (Sanctum stateful). No API token required for web frontend AJAX.

Route Controller Notes
GET /api/customers Api\CustomerController@search Typeahead search, returns 10 results
GET /api/customers/filter Api\CustomerController@filter Advanced filter — paginated JSON
GET /api/customers/{id}/ledger Api\CustomerController@ledger Ledger entries JSON for customer detail tabs
GET /api/nas/{id}/traffic Api\NasController@traffic MikroTik live traffic (polled by chart)
GET /api/nas/{id}/online Api\NasController@online Active sessions on NAS
GET /api/dashboard/online Api\DashboardController@online Online count from Redis cache
GET /api/packages/{id}/availability Api\PackageController@availability {available: bool, count: N}
GET /api/whatsapp/outbox Api\WhatsAppController@outbox Outbox JSON for outbox page polling
GET /api/invoices Api\InvoiceController@search Invoice typeahead for payment forms
GET /api/radius/{username}/status Api\RadiusController@status Current radcheck + radacct status JSON
GET /api/health/ping Api\HealthController@ping Returns {"status":"ok"} — monitoring endpoint

23. Route Statistics

Metric Count
Total web routes (routes/web.php + routes/auth.php) ~95
Total portal routes (routes/portal.php) ~10
Total internal AJAX API routes (routes/api.php) ~12
Total mobile API routes (/api/mobile/*) ~20
Grand total ~137
Routes requiring super_admin or admin only ~35
Routes with per-permission JSONB check ~60
Routes with rate limiting (throttle middleware) ~8
Routes that trigger RADIUS sync ~10
Routes that trigger WhatsApp ~4
Routes with dealer scope enforcement ~70

Routes Requiring Admin Only (selected)

  • All /settings/* routes
  • All /users/* routes
  • /billing/archive (POST/PUT)
  • /import/* routes
  • /audit/* routes
  • /health/cron (POST/PUT/DELETE)
  • /packages (POST/PUT/DELETE)
  • /nas (POST/PUT/DELETE)
  • /olt (POST/PUT/DELETE)
  • /tr069/*/provision

Routes With Rate Limiting

  • POST /login — throttle:5,1 (5 per minute per IP+email)
  • POST /portal/login — throttle:5,1
  • POST /api/mobile/login — throttle:5,1
  • POST /api/mobile/customers/{id}/recharge — throttle:10,1
  • GET /api/nas/{id}/traffic — throttle:30,1 (to protect MikroTik API)

Cross-reference: For service implementations see 08_Backend_Architecture.md. For RADIUS sync details see 11_Radius_System.md. For billing flow see 10_Billing_System.md. For the permission system see PHASE2_02_Permission_Matrix.md.