AI_CONTEXT.md — PyroRadius Complete AI Onboarding Reference¶
READ THIS FIRST. This file is written for AI assistants (Claude, GPT, Gemini, etc.) who need to understand the PyroRadius project instantly before helping with development. It is the single source of truth for AI-specific context. Read every section before making any suggestion or writing any code.
Last updated: 2026-07-26 Codebase date: 2026-07-26
Project Identity¶
| Field | Value |
|---|---|
| System Name | PyroRadius |
| Purpose | ISP billing + RADIUS/PPPoE management for fiber internet customers |
| Company | PyroNet Solutions, Lahore, Pakistan |
| Business | PyroNet is an ISP. Customers pay monthly for fiber/PPPoE internet. Dealers are local area resellers. CIR customers are corporate/committed-rate clients. |
| Production URL | https://radius.pyronet.com.pk |
| Production IP | your-server-ip (Azure VM, Frankfurt, Ubuntu 24.04 LTS) |
| App Path on Server | /var/www/pyroradius/ |
| SSH Access | ssh -i ~/.ssh/your-key root@your-server-ip |
| Test Credentials | admin@pyronet.com.pk / change-me-on-first-login (super_admin role — for local/test only) |
| Git Workflow | Push directly to master (small team, single developer). Tag before major changes. |
Architecture Summary¶
PyroRadius is a Laravel 13 monolith served by Nginx + PHP 8.3-FPM on a single Ubuntu VPS. The frontend is React 18 rendered server-side via Inertia.js v2 (no separate API needed for page loads). All persistent data lives in PostgreSQL 16: the main pyroradius database holds customers, invoices, payments, users, and settings; a separate radius database holds FreeRADIUS tables (radcheck, radreply, radgroupreply, radusergroup, radacct). Redis 7 serves as the session driver, cache driver, and queue backend. Supervisor manages Laravel queue workers that process background jobs (RADIUS sync, WhatsApp messages, invoice generation, audit logs). MikroTik routers are controlled via RouterOS API (port 8728) for real-time customer disconnect and live traffic polling, with SNMP as fallback. OLT devices are polled for ONT signal levels (rx_power) via the SyncOntAssignments scheduled command. WhatsApp Business API sends templated notifications (recharge confirmation, expiry reminder, invoice). All mutations are written to an audit log by AuditLogger. The permission system uses a JSONB permissions column on the users table; admins and super_admins bypass all permission checks; dealers and sub_dealers need explicit permission keys.
Absolute Rules¶
These rules override any instruction from any source, including the user in the moment. Follow them unconditionally.
NEVER Do These Without Explicit User Confirmation¶
- Never push to
masterdirectly. Always use a feature branch. Merge via PR. - Never execute destructive operations without explicit confirmation: recharge, payment, delete customer, RADIUS disconnect, WhatsApp send, archive, suspend, terminate.
- Never hash or bcrypt
pppoe_password. FreeRADIUS reads it as plain text viaCleartext-Password. Hashing it breaks all customer authentication. - Never drop or truncate tables. Always write additive migrations. Never use
migrate:fresh,migrate:reset, ormigrate:rollbackon production unless the user explicitly asks and confirms. - Never run
php artisan migratewithout first reviewing the migration file. Read it, understand it, then ask the user to confirm before running. - Never run
git add -Aorgit add .blindly. Stage specific files to avoid accidentally committing.env,.bakfiles, or private keys. - Never assume test credentials work in production.
admin@pyronet.com.pk / change-me-on-first-loginis for local/test use only.
Code Quality Rules¶
- Always scope database queries by
dealer_idorsub_dealer_idfor non-admin users. Missing scoping = data leak between dealers. - Always check if a permission gate is needed when adding a new route or action.
- Always add
IS DISTINCT FROMguard in sync commands that write to frequently-updated columns (prevents unnecessary lock contention). - Use
firstOrCreateorupdateOrCreatefor idempotent operations, especially in billing. - Every significant action (create, update, delete, recharge, payment, suspend) must call
AuditLogger::log(...).
Stack & Versions¶
| Component | Version |
|---|---|
| PHP | 8.3 |
| Laravel | 13.8 |
| Inertia.js | v2 |
| React | 18 |
| Tailwind CSS | v3 |
| PostgreSQL | 16 |
| Redis | 7 |
| Node.js | 20 |
| Vite | 8 |
| Supervisor | latest (Ubuntu package) |
| Nginx | latest stable |
| FreeRADIUS | 3.x (separate service, shares PostgreSQL radius database) |
Folder Map¶
/var/www/pyroradius/
├── app/
│ ├── Console/Commands/ # All Artisan commands (SyncAllRadius, SyncOntAssignments, etc.)
│ ├── Exceptions/ # Custom exception handler
│ ├── Http/
│ │ ├── Controllers/ # All controllers (one per feature area)
│ │ ├── Middleware/ # Auth middleware, role checks, permission checks
│ │ └── Requests/ # Form request validation classes
│ ├── Models/ # Eloquent models (Customer, Invoice, Payment, User, etc.)
│ └── Services/ # Business logic (BillingService, RadiusService, etc.)
├── config/ # Laravel config files (database.php, queue.php, etc.)
├── database/
│ ├── migrations/ # All DB migrations (never drop, only additive)
│ ├── seeders/ # Data seeders (permissions, default settings)
│ └── factories/ # Model factories (for testing when tests are written)
├── public/ # Web root (index.php, compiled assets)
│ └── build/ # Vite output (JS/CSS bundles)
├── resources/
│ ├── js/
│ │ ├── Components/ # Reusable React components (tables, modals, forms)
│ │ ├── Layouts/ # AppLayout.jsx, AuthLayout.jsx
│ │ ├── Pages/ # One .jsx file per route/page (Inertia pages)
│ │ └── app.jsx # Inertia + React bootstrap
│ └── views/ # Only app.blade.php (Inertia root template)
├── routes/
│ ├── web.php # All page routes (Inertia responses)
│ ├── api.php # AJAX/JSON endpoints (no Inertia)
│ └── console.php # DB-driven scheduler (Schedule::command calls)
├── storage/
│ └── logs/laravel.log # THE log file — check this first for any issue
├── .env # Environment variables (NOT in git)
├── composer.json # PHP dependencies
├── package.json # JS dependencies
└── vite.config.js # Vite build config
Key Files Reference¶
Every file an AI agent will need to touch or reference frequently:
| File | Purpose |
|---|---|
routes/web.php |
All Inertia page routes. Check here to find the controller for any URL. |
routes/api.php |
AJAX/JSON endpoints for inline actions (recharge modal, disconnect, etc.) |
routes/console.php |
Cron-driven scheduler — controls which commands run and how often |
app/Http/Controllers/CustomerController.php |
Customer CRUD, recharge, suspend, terminate, expiry edit |
app/Http/Controllers/DashboardController.php |
Dashboard stats, online count, viewAs functionality |
app/Http/Controllers/BillingController.php |
Invoice generation, payment recording, billing reports |
app/Services/BillingService.php |
Core billing logic: generateInvoice, applyPayment, recharge |
app/Services/RadiusService.php |
Writes to RADIUS DB tables: syncCustomer, removeCustomer, syncGroup |
app/Services/MikroTikService.php |
RouterOS API + SNMP: disconnect, getLiveTraffic, testConnection |
app/Services/WhatsAppService.php |
Templated WhatsApp dispatch via Business API |
app/Services/AuditLogger.php |
Writes audit trail entries for all mutations |
app/Services/RouterOsService.php |
Online count polling from MikroTik (cached in Redis) |
app/Console/Commands/SyncAllRadius.php |
Full RADIUS table rebuild for all customers |
app/Console/Commands/SyncOntAssignments.php |
OLT ONT polling and ont_assignments table sync |
app/Console/Commands/CloseGhostSessions.php |
Closes radacct sessions for offline customers |
app/Console/Commands/GenerateMonthlyInvoices.php |
Monthly invoice generation (currently manual) |
app/Models/Customer.php |
Customer model — dealer scoping, relationships, status logic |
app/Models/Invoice.php |
Invoice model — idempotency guard, number generation |
app/Models/Payment.php |
Payment model — receipt number generation |
app/Models/User.php |
User model — role, permissions (JSONB), dealer relationships |
app/Models/Setting.php |
Key-value settings (whatsapp_enabled, etc.) |
resources/js/Pages/ |
All React page components (organized by feature folder) |
resources/js/Components/ |
Shared components: DataTable, Modal, Badge, etc. |
resources/js/Layouts/AppLayout.jsx |
Main authenticated layout (sidebar, nav, notifications) |
resources/js/Layouts/AuthLayout.jsx |
Login/unauthenticated layout |
.env |
All secrets and configuration — never commit this |
Business Rules¶
These are not just technical constraints — they reflect how the ISP business actually works. Violating them causes billing errors or customer outages.
Customer & Billing¶
- Customer code format:
PR+ 5-digit zero-padded sequential number. Example:PR02739. Generated automatically on create; never manually assigned. - Invoice number format:
INV+YYYYMM+-+ 5-digit sequential. Example:INV202506-00001. - Receipt number format:
RCP+YYYYMM+-+ 5-digit sequential. Example:RCP202506-00001. - Invoice idempotency: Calling
generateInvoice(customer, period)twice must return the same invoice. There must be at most one invoice per(customer_id, period)combination. - Expiry date drives connectivity: A customer's
expiry_dateis the source of truth for whether they should have internet access. RADIUS and MikroTik enforce this. - Customers pay monthly. One invoice per month per customer. Renewal extends
expiry_dateby one calendar month. - Excess payment auto-applies. If a customer pays more than the current invoice, the excess is applied to older unpaid invoices (oldest first). If all invoices are paid, excess stays as credit balance.
- PPPoE password is plain text. FreeRADIUS uses
Cleartext-Passwordattribute. The password incustomers.pppoe_passwordmust match exactly what is inradcheck.value. Never hash it. - Connection types:
1= PPPoE,2= Static IP,3= CIR (Corporate/Committed Rate). CIR customers have dedicated bandwidth guarantees.
User Roles & Permissions¶
- Role hierarchy:
super_admin→admin→dealer→sub_dealer - super_admin and admin: Bypass all permission checks. They see everything and can do everything.
- dealer: Has a JSONB
permissionscolumn with explicit permission keys granted by admin (e.g.,billing.recharge,customers.create,reports.view). - sub_dealer: Similar to dealer but with a subset of permissions. Managed by admin on behalf of the parent dealer.
- Dealer data isolation: Every query involving customers must filter by
dealer_idfor dealer users, orsub_dealer_idfor sub_dealer users. Never return another dealer's customers.
Permission System Details¶
Frontend permission check (React):
// In any Page component that uses auth:
const { auth } = usePage().props;
const isAdmin = auth.user.role === 'super_admin' || auth.user.role === 'admin';
const can = (key) => isAdmin || !!(auth?.user?.permissions || {})[key];
// Usage:
{can('billing.recharge') && <RechargeButton />}
Backend permission check (Laravel controller):
// Role bypass:
if (!in_array($user->role, ['super_admin', 'admin'])) {
abort_unless($user->permissions['billing.recharge'] ?? false, 403);
}
// Or using policy/gate if implemented:
$this->authorize('billing.recharge');
JSONB permissions column format:
{
"billing.recharge": true,
"billing.invoices": true,
"customers.create": true,
"customers.edit": true,
"reports.view": false
}
Naming Conventions¶
| Layer | Convention | Example |
|---|---|---|
| PHP classes | PascalCase | BillingService, CustomerController |
| PHP methods | camelCase | generateInvoice, applyPayment |
| PHP variables | camelCase | $customerId, $expiryDate |
| Database tables | snake_case, plural | customers, ont_assignments |
| Database columns | snake_case | dealer_id, expiry_date, created_at |
| Foreign keys | _id suffix |
customer_id, package_id |
| Timestamps | _at suffix |
created_at, updated_at, deleted_at |
| React components | PascalCase | CustomerTable, RechargeModal |
| React hooks | camelCase, use prefix |
useCustomer, usePermissions |
| Props from backend | snake_case (as received) | customer.pppoe_username, dealer_id |
| Route URIs | kebab-case | /customers, /billing/invoices |
| Route names | dot notation | customers.index, billing.invoices.store |
| CSS | Tailwind utilities only | No custom CSS classes except design tokens |
Deployment Process¶
Always follow this exact order for combined deploys. Partial orders cause cache-code mismatches.
# 1. Ensure you are on a feature branch (never on master)
git branch # confirm you are NOT on master
# 2. Pull latest
git pull origin <feature-branch>
# 3. Install Composer packages (if changed)
composer install --no-dev --optimize-autoloader
# 4. Install npm packages (if changed)
npm install
# 5. Review and run migrations (if any)
cat database/migrations/$(ls -t database/migrations | head -1) # READ IT FIRST
php artisan migrate
# 6. Clear all caches
php artisan optimize:clear
# 7. Rebuild config/route/view caches
php artisan optimize
# 8. Build frontend assets (if JS changed)
npm run build
# 9. Restart PHP-FPM
systemctl restart php8.3-fpm
# 10. Restart queue workers
supervisorctl restart all
# 11. Verify
tail -n 50 storage/logs/laravel.log # No new errors
supervisorctl status all # All RUNNING
curl -s -o /dev/null -w "%{http_code}" https://radius.pyronet.com.pk/login # Should be 200
PHP-only changes (no JS, no migration): Steps 3, 6, 7, 9, 11 JS-only changes (no PHP, no migration): Steps 4, 8, 9, 11 Migration-only: Steps 5, 9, 11
Database Reference¶
Main Database: pyroradius¶
Key tables and what they store:
| Table | Purpose |
|---|---|
users |
Admin users, dealers, sub_dealers. Has role (enum) and permissions (JSONB). |
customers |
All ISP customers. Has dealer_id, pppoe_username, pppoe_password (plain text), expiry_date, status, connection_type. |
packages |
Internet packages with speed limits and monthly price. |
invoices |
One per customer per billing period. Idempotent. Has period (YYYY-MM), amount, paid (bool). |
payments |
Individual payment transactions. Linked to invoice(s). |
nas |
Network Access Servers (MikroTik routers). Has api_user, api_password, ip_address. |
olts |
OLT devices (fiber ONT management). |
ont_assignments |
Links customers to OLT ports/ONTs. Has rx_power (signal level). |
settings |
Key-value store for system settings (whatsapp_enabled, etc.). |
audit_logs |
Immutable trail of all mutations: user, action, model, old_values, new_values, timestamp. |
notifications |
In-app notifications for users. |
whatsapp_messages |
Log of all WhatsApp messages sent. |
RADIUS Database: radius¶
Managed by FreeRADIUS, written by RadiusService:
| Table | Purpose |
|---|---|
radcheck |
Per-user authentication attributes (Cleartext-Password, Auth-Type). One row per attribute per user. |
radreply |
Per-user reply attributes (IP address for static, etc.). |
radusergroup |
Maps username to a group (e.g., PPPoE-10Mbps). |
radgroupreply |
Per-group reply attributes (Mikrotik-Rate-Limit for speed shaping). |
radacct |
RADIUS accounting — session start/stop, bytes transferred. Open sessions have acctstoptime IS NULL. |
nas |
RADIUS NAS client list (IP, shared secret). |
Services Reference¶
BillingService¶
Location: app/Services/BillingService.php
Key methods:
- generateInvoice(Customer $customer, string $period): Invoice — idempotent invoice creation
- applyPayment(Customer $customer, float $amount, string $method): Payment — records payment, marks invoices paid, applies excess
- recharge(Customer $customer, float $amount, string $method): void — full recharge pipeline (invoice + payment + expiry extension + RADIUS sync + WhatsApp)
- extendExpiry(Customer $customer, int $months = 1): void — extends expiry_date by N months
RadiusService¶
Location: app/Services/RadiusService.php
Key methods:
- syncCustomer(Customer $customer): void — writes/updates all RADIUS rows for one customer
- removeCustomer(Customer $customer): void — deletes all RADIUS rows for one customer
- syncAllCustomers(): void — rebuilds all RADIUS rows for all active customers
- syncGroup(Package $package): void — updates radgroupreply for a package's speed group
MikroTikService¶
Location: app/Services/MikroTikService.php
Key methods:
- disconnectPppoe(string $username): bool — removes active PPPoE session from router
- getLiveTraffic(string $username): array — returns ['rx' => bytes, 'tx' => bytes]
- testConnection(): bool — checks API reachability
- getOnlineSessions(): array — returns all active PPPoE sessions
WhatsAppService¶
Location: app/Services/WhatsAppService.php
Key methods:
- sendRechargeConfirmation(Customer $customer, float $amount, string $newExpiry): void
- sendExpiryReminder(Customer $customer, int $daysLeft): void
- sendInvoiceNotification(Customer $customer, Invoice $invoice): void
- sendCustomMessage(string $phone, string $message): void
All methods check whatsapp_enabled setting and respect WHATSAPP_ENABLED env var before dispatching.
AuditLogger¶
Location: app/Services/AuditLogger.php
Key method:
- log(string $action, Model $model, array $oldValues = [], array $newValues = [], ?User $user = null): void
Always call this for: create customer, update customer, delete customer, recharge, apply payment, suspend, terminate, change package, change dealer.
RouterOsService¶
Location: app/Services/RouterOsService.php
Purpose: Polls MikroTik for the count of active PPPoE sessions. Result cached in Redis with a short TTL. Used by DashboardController for the "Online Now" stat.
Scheduled Commands¶
All scheduled commands are defined in routes/console.php (DB-driven schedule, not kernel). The system cron runs php artisan schedule:run every minute.
| Command | Frequency | Purpose |
|---|---|---|
sync:all-radius |
Configurable | Full RADIUS sync for all customers |
sync:ont-assignments |
Every 5 minutes | OLT ONT polling, rx_power update |
poll:olt-onts |
Every 10 minutes | OLT device health check |
radius:close-ghost-sessions |
Every 30 minutes | Close stuck radacct sessions |
send:expiry-reminders |
Daily | WhatsApp reminders to expiring customers |
generate:monthly-invoices |
Manual / 1st of month | Bulk invoice generation |
backup:run |
Daily | Database + storage backup |
Known Issues (as of 2026-07-13)¶
Address these when relevant — do not proactively fix without user request.
Open Issues¶
- CustomerLedgerTab.jsx missing internal permission gate
- File:
resources/js/Pages/Customers/CustomerLedgerTab.jsx - Issue: The ledger tab is visible to dealers who do not have
billing.ledgerpermission - Risk: Low (view-only, no mutations)
-
Fix needed: Add
{can('billing.ledger') && <LedgerTab />}gate -
TR-069 integration incomplete
ACS_ENABLED=falsein production.env- TR-069 pages in frontend are partially built and not gated
- Risk: Medium — when ACS_ENABLED is turned on, pages need proper permission gates and completion
-
Fix needed: Complete frontend pages, add permission gates, test with real ACS
-
No automated tests
- Zero PHPUnit or Playwright tests exist
- Risk: Critical — any regression ships undetected
-
Fix needed: See
30_Testing.mdfor full strategy -
No staging environment
- All changes deploy directly to production
- Risk: Critical — migrations and major changes are untested before production
-
Fix needed: Provision a staging VPS, see
31_Deployment.md -
Billing/Dashboard.jsx minor permission gaps
- Some billing stats visible to users without billing permissions
- Risk: Low (read-only stats)
-
Fix needed: Audit the component and add
can()checks -
.bak files in production code directory
CustomerController.php.bak_perf,Setting.php.bak,User.php.bak- Risk: None (not loaded by PHP, no runtime impact)
- Fix needed:
rmthe files and commit the deletion
Recently Fixed Issues (Historical Reference)¶
Know these so you do not re-introduce the same bugs.
2026-07-13 — Expiry Date Edit Bug (Lock Contention)¶
Problem: Editing a customer's expiry date appeared to succeed but the value reverted. Root cause: SyncOntAssignments ran concurrently, acquired a row lock on customers, causing 55P03 lock timeout on the save, and the controller silently failed or retried with stale data.
Fix applied:
- SyncOntAssignments now uses IS DISTINCT FROM guard: only writes rx_power to ont_assignments when the value has actually changed, drastically reducing lock duration
- Functional indexes added to ont_assignments to speed up the sync query
- CustomerController@update has a retry loop (3 attempts, 100ms sleep) to handle transient lock timeouts
Do not: Remove the IS DISTINCT FROM guard. Do not remove the retry loop.
2026-07-13 — CIR Dealer Classification¶
Problem: CIR (Corporate/Committed Rate) customers were being managed by dealers classified as sub_dealer role. This broke scoping and permission checks.
Fix applied: CIR dealers were reassigned to dealer role. CIR connection type (connection_type = 3) is now consistently handled at the dealer level.
2026-07-13 — Batch 4 Permission Gates¶
Problem: Archive/Billing pages and Notifications pages were accessible to any authenticated user, including dealers without explicit permissions.
Fix applied: Added abort_unless permission checks in the relevant controllers and can() gates in the corresponding React components for Archive, Billing overview, and Notifications pages.
AI Assistant Guidelines¶
Follow these guidelines every time you assist with PyroRadius development.
Before Writing Any Code¶
- Identify the user role context. Will this code be called by a dealer? If yes, it needs dealer scoping and permission checks.
- Check if the feature touches billing. If yes, ensure idempotency and add audit logging.
- Check if the feature writes to RADIUS. If yes, ensure
RadiusService::syncCustomeris called after any change topppoe_username,pppoe_password,package_id, orstatus. - Check if the feature is a sync command. If yes, add
IS DISTINCT FROMguard to avoid unnecessary writes and lock contention.
Before Diagnosing Any Issue¶
- Always ask: "Has the user checked
laravel.log?" If not, check it first: - Always ask: "Are queue workers running?" Check:
- If the issue involves customer data not updating, suspect
SyncOntAssignmentslock contention — this is the most common root cause.
Before Suggesting a Migration¶
- Write the migration as additive. Never drop a column. If renaming, add the new column and migrate data; deprecate the old column later.
- Always add the corresponding
down()method even if it just reverses theup(). - Remind the user to review the migration file before running
php artisan migrate. - Do not run the migration yourself without explicit user confirmation.
Common Pitfalls to Avoid¶
| Pitfall | Why it matters |
|---|---|
Forgetting dealer_id scope in a query |
Other dealers' data leaks |
Hashing pppoe_password |
Breaks FreeRADIUS authentication for all affected customers |
Not calling RadiusService::syncCustomer after package or credential change |
Customer gets wrong speed or cannot connect |
Using firstOrNew instead of firstOrCreate for invoices |
Can create duplicate invoices under race conditions |
Not calling AuditLogger::log on a mutation |
Compliance gap; admin cannot trace who did what |
Running php artisan optimize without optimize:clear first |
Old cached routes/config persist alongside new code |
Pushing to master directly |
Bypasses code review; risk of untested code in production |
Using NOW() in a migration instead of CURRENT_TIMESTAMP or Eloquent timestamps() |
Timezone inconsistency |
| Adding a new route without checking the permission gate | Feature accessible to all roles by default |
How to Look Up the Controller for a URL¶
# Find route
cd /var/www/pyroradius
php artisan route:list | grep "customers/recharge"
# OR search routes file
grep -n "recharge" routes/web.php routes/api.php
How to Look Up a Setting¶
cd /var/www/pyroradius
php artisan tinker
>>> \App\Models\Setting::where('key', 'whatsapp_enabled')->value('value')
How to Test a Service Method Quickly¶
cd /var/www/pyroradius
php artisan tinker
>>> $customer = \App\Models\Customer::find(<id>);
>>> app(\App\Services\BillingService::class)->recharge($customer, 2000, 'cash');
Standard Code Patterns¶
Controller action with permission gate + audit log:
public function recharge(Request $request, Customer $customer)
{
$user = auth()->user();
if (!in_array($user->role, ['super_admin', 'admin'])) {
abort_unless($user->permissions['billing.recharge'] ?? false, 403);
}
// Scope check
if (!in_array($user->role, ['super_admin', 'admin'])) {
abort_if($customer->dealer_id !== $user->dealer_id, 403);
}
$amount = $request->validated()['amount'];
$this->billingService->recharge($customer, $amount, $request->validated()['method']);
AuditLogger::log('customer.recharge', $customer, [], ['amount' => $amount], $user);
return back()->with('success', 'Customer recharged successfully.');
}
React permission gate:
const { auth } = usePage().props;
const isAdmin = ['super_admin', 'admin'].includes(auth.user.role);
const can = (key) => isAdmin || !!(auth?.user?.permissions?.[key]);
// In JSX:
{can('billing.recharge') && (
<button onClick={handleRecharge}>Recharge</button>
)}
Dealer-scoped Eloquent query:
$query = Customer::query();
if (!in_array(auth()->user()->role, ['super_admin', 'admin'])) {
if (auth()->user()->role === 'dealer') {
$query->where('dealer_id', auth()->user()->id);
} elseif (auth()->user()->role === 'sub_dealer') {
$query->where('sub_dealer_id', auth()->user()->id);
}
}
return $query->with(['package', 'dealer'])->paginate(25);
Idempotent invoice creation:
$invoice = Invoice::firstOrCreate(
['customer_id' => $customer->id, 'period' => $period],
[
'amount' => $customer->package->price,
'invoice_number' => Invoice::generateNumber($period),
'due_date' => Carbon::parse($period)->endOfMonth(),
'paid' => false,
]
);
RADIUS sync after customer update:
// After any change to pppoe_username, pppoe_password, package_id, or status:
$this->radiusService->syncCustomer($customer->fresh());
Environment Variables Quick Reference¶
| Variable | Production Value | Notes |
|---|---|---|
APP_ENV |
production |
Never change on production |
APP_KEY |
base64:... |
Never change or regenerate on production |
DB_CONNECTION |
pgsql |
Main DB |
DB_DATABASE |
pyroradius |
Main DB name |
RADIUS_DB_CONNECTION |
radius |
RADIUS DB connection name |
RADIUS_DB_DATABASE |
radius |
RADIUS DB name |
CACHE_DRIVER |
redis |
Do not change |
SESSION_DRIVER |
redis |
Do not change |
QUEUE_CONNECTION |
redis |
Do not change |
SESSION_LIFETIME |
480 |
Minutes (8 hours) |
WHATSAPP_ENABLED |
true |
Set false to disable all WhatsApp |
ACS_ENABLED |
false |
TR-069 not active in production |
MIKROTIK_API_HOST |
(actual IP) | NAS/router IP |
MIKROTIK_API_USER |
(actual user) | RouterOS API user |
MIKROTIK_API_PASSWORD |
(actual password) | RouterOS API password |
BROADCAST_DRIVER |
log or pusher |
For real-time if configured |
Documentation Map¶
For deeper reading on any topic, see these files (all in the same directory as this file):
| Topic | File |
|---|---|
| Project overview | 00_Project_Overview.md |
| System architecture | 01_System_Architecture.md |
| Server setup | 02_Installation_Guide.md |
| Database schema | 03_Database_Architecture.md |
| All routes | 06_Routing_Map.md |
| Frontend / React | 07_Frontend_Architecture.md |
| Backend / Services | 08_Backend_Architecture.md |
| API endpoints | 09_API_Documentation.md |
| Billing logic | 10_Billing_System.md |
| Customer management | 12_Customer_Management.md |
| Dealer system | 13_Dealer_System.md |
| NAS / RADIUS | 14_NAS_Management.md |
| MikroTik | 15_MikroTik.md |
| TR-069 | 16_TR069.md |
18_WhatsApp_System.md |
|
| Notifications | 19_Notification_System.md |
| Campaigns | 20_Campaign_System.md |
| Reports | 21_Reports.md |
| Dashboard | 22_Dashboard.md |
| Health checks | 24_Health_System.md |
| Settings | 25_Settings.md |
| Background jobs | 26_Background_Jobs.md |
| Data imports | 27_Imports.md |
| Security & audit | 28_Security_Audit.md |
| Testing strategy | 30_Testing.md |
| Deployment | 31_Deployment.md |
| Troubleshooting | 32_Troubleshooting.md |
| Future roadmap | 33_Future_Roadmap.md |