Skip to content

PyroRadius — Phase 2 Eloquent Model Reference

Version: 2.0.0 Date: 2026-07-13 Scope: Complete documentation for every Eloquent model in the PyroRadius application. Stack: Laravel 13 · PostgreSQL 16


Table of Contents

  1. Customer — Core entity; ISP subscriber record
  2. User — Authentication, roles, permissions
  3. Invoice — Billing period invoice
  4. Payment — Payment transaction record
  5. LedgerEntry — Double-entry financial ledger
  6. Package — PPPoE speed package / RADIUS group
  7. Nas — Network Access Server (router)
  8. RadCheck — FreeRADIUS check attributes
  9. RadGroupReply — FreeRADIUS group reply attributes
  10. RadUserGroup — FreeRADIUS user-to-group mapping
  11. RadAcct — FreeRADIUS accounting sessions (read-only)
  12. WhatsappTemplate — WhatsApp message templates
  13. WhatsappLog — WhatsApp send history
  14. WhatsappSession — WhatsApp connection state
  15. AuditLog — Immutable change audit trail
  16. Setting — Application key-value settings
  17. CompanyProfile — ISP branding and identity
  18. Olt — Optical Line Terminal device
  19. OntCache — ONT/ONU discovery cache
  20. CronJob — Schedulable command registry
  21. Ticket — Support / NOC ticket
  22. DealerPackage — Dealer-Package pivot with custom pricing

1. Customer

File: app/Models/Customer.php Table: customers Soft Deletes: Yes (deleted_at, deleted_by, delete_reason) Auth: No

The central entity of PyroRadius. Every PPPoE subscriber is a Customer. All billing, RADIUS, and support flows reference this model.

Fillable Fields

Field Type Cast Purpose
customer_code string Human-readable ID (e.g., CUS-000042); generated from PostgreSQL sequence
full_name string Subscriber display name
mobile string Primary phone number; used for SMS/WhatsApp fallback
whatsapp string WhatsApp number if different from mobile
address string Physical service address
area string Free-text area name (legacy field; prefer area_id)
pppoe_username string PPPoE login; must be unique; synced to FreeRADIUS
pppoe_password string hidden PPPoE password; excluded from JSON serialization
package_id integer FK → packages.id
dealer_id integer FK → users.id (dealer role)
sub_dealer_id integer FK → users.id (sub_dealer role)
nas_id integer FK → nas.id; which router authenticates this customer
static_ip string Static Framed-IP-Address sent via RADIUS; null = dynamic pool
ont_serial string ONT/ONU serial number (decoded format, e.g., HWTC1234ABCD)
monthly_price decimal float Customer-specific price override; null = use package.price
expiry_date date Carbon Service expiry; renewal extends this
status string active / expired / suspended / temp_extended / disabled
comments string Dealer-visible notes
operator_note string Internal staff notes (hidden from dealers)
created_by string Name of user who created the record
pool_name string RADIUS IP pool name for dynamic IP allocation
rx_power decimal float Latest ONT RX optical power reading (dBm × 100 for precision)
rx_power_at timestamp Carbon When rx_power was last updated
cnic string Customer's national ID number
device_type string CPE device model (e.g., TP-Link WR841N)
device_ownership string isp_owned / customer_owned
country_id integer FK → countries.id
city_id integer FK → cities.id
area_id integer FK → areas.id (structured area)
sub_area_id integer FK → areas.id (sub-area under area)
customer_type_id integer FK → customer_types.id
connection_type_id integer FK → connection_types.id
grace_days integer Extra days after expiry before auto-suspension
gateway string Static route gateway IP (for static IP setups)
subnet_mask string Subnet mask for static IP
vlan integer VLAN tag if applicable
committed_bandwidth string SLA guaranteed bandwidth (informational)
burst_bandwidth string SLA burst bandwidth (informational)
sla_notes string Free-text SLA terms for this customer
static_ip_pool string RADIUS pool to use when static IP is assigned
contract_price decimal float Contracted package price (may differ from monthly_price)
contract_expiry date Carbon When the current contract expires
deleted_by integer FK → users.id; who soft-deleted this record
delete_reason string Mandatory reason recorded at deletion
restored_at timestamp Carbon When the customer was restored from soft-delete
restored_by integer FK → users.id; who restored the record

Relationships

Method Type Related Model Foreign Key Notes
package BelongsTo Package package_id Eager-loaded in most queries
dealer BelongsTo User dealer_id The primary dealer responsible
subDealer BelongsTo User sub_dealer_id Sub-dealer under the primary dealer
invoices HasMany Invoice customer_id All billing invoices
payments HasMany Payment customer_id All payment records
country BelongsTo Country country_id
city BelongsTo City city_id
areaRef BelongsTo Area area_id Named areaRef to avoid collision with free-text area field
subArea BelongsTo Area sub_area_id
customerType BelongsTo CustomerType customer_type_id
connectionType BelongsTo ConnectionType connection_type_id
nas BelongsTo Nas nas_id The authenticating router
deletedBy BelongsTo User deleted_by Who performed the soft delete
restoredBy BelongsTo User restored_by Who restored the record

Scopes

Scope Parameters Purpose
visibleTo(User $user) $user Multi-role access control; the most complex scope in the codebase

visibleTo() logic in detail:

super_admin, admin, noc_admin, billing_admin, auditor:
  → No filter; see all non-deleted customers

dealer:
  → WHERE dealer_id = $user->id

sub_dealer:
  → WHERE sub_dealer_id = $user->id

collection_officer:
  → WHERE dealer_id IN (SELECT id FROM users WHERE parent_id = $user->id)
    OR assigned collection areas match

support_agent, field_engineer:
  → All customers (read-only access; editing restricted by permissions)

Business Methods

isExpired(): bool - Returns true if status is expired OR (expiry_date < today AND status is not disabled or suspended). - Important rule: disabled and suspended customers are not considered "expired" regardless of their expiry date — they have been administratively blocked, not merely lapsed.

nextCode(): string (static) - Calls SELECT nextval('customer_code_seq') via DB::selectOne(). - Returns formatted string: 'CUS-' . str_pad($next, 6, '0', STR_PAD_LEFT). - Example: CUS-000042. - Called only from CustomerController::store().

Notes

  • pppoe_password is in the $hidden array — excluded from toArray(), toJson(), and API responses. Always access via direct property or use makeVisible() when explicit password display is required.
  • The area (free-text) and areaRef (FK) fields coexist for backward compatibility. New installs should use area_id / areaRef exclusively.
  • rx_power is stored as dBm × 100 (integer-safe storage), so -23.5 dBm is stored as -2350. The OntCache model follows the same convention (rx_raw).
  • Soft deletes: the SoftDeletes trait adds deleted_at. Records with deleted_at IS NOT NULL are excluded from all standard queries unless withTrashed() is explicitly added.

2. User

File: app/Models/User.php Table: users Soft Deletes: No Auth: Yes (extends Authenticatable)

Represents every type of system user: staff (admin, NOC, billing, support), dealers, sub-dealers, collection officers, and field engineers. Roles and permissions are stored in JSONB columns.

Fillable Fields

Field Type Cast Purpose
name string Display name
email string Login email; unique
password string hidden Bcrypt-hashed
role string One of the ROLES constants below
permissions array array (JSONB) Array of granted permission strings
parent_id integer FK → users.id; for sub-dealers (parent = their dealer)
phone string Contact phone
address string Office/home address
active boolean bool Account enabled/disabled
cnic string National ID
commission_rate decimal float Dealer's collection commission percentage
whatsapp string WhatsApp contact number
balance decimal float Dealer ledger balance (cached; authoritative data is in LedgerEntry)

Constants

ROLES (array of role metadata):

public const ROLES = [
    'super_admin'        => ['label' => 'Super Admin',         'color' => 'red',    'assignable_by' => []],
    'admin'              => ['label' => 'Admin',               'color' => 'orange', 'assignable_by' => ['super_admin']],
    'noc_admin'          => ['label' => 'NOC Admin',           'color' => 'blue',   'assignable_by' => ['super_admin', 'admin']],
    'billing_admin'      => ['label' => 'Billing Admin',       'color' => 'green',  'assignable_by' => ['super_admin', 'admin']],
    'support_agent'      => ['label' => 'Support Agent',       'color' => 'cyan',   'assignable_by' => ['super_admin', 'admin']],
    'dealer'             => ['label' => 'Dealer',              'color' => 'purple', 'assignable_by' => ['super_admin', 'admin']],
    'sub_dealer'         => ['label' => 'Sub Dealer',          'color' => 'violet', 'assignable_by' => ['super_admin', 'admin', 'dealer']],
    'field_engineer'     => ['label' => 'Field Engineer',      'color' => 'yellow', 'assignable_by' => ['super_admin', 'admin']],
    'auditor'            => ['label' => 'Auditor',             'color' => 'gray',   'assignable_by' => ['super_admin']],
    'collection_officer' => ['label' => 'Collection Officer',  'color' => 'teal',   'assignable_by' => ['super_admin', 'admin', 'dealer']],
];

ALL_PERMISSIONS (array ~80 permission strings):

Grouped by domain:

customers.*       — view, create, edit, delete, recharge, disconnect, export
invoices.*        — view, create, edit, delete, mark_paid
payments.*        — view, create, delete, reverse
ledger.*          — view, create, reverse
dealers.*         — view, create, edit, delete, view_balance
packages.*        — view, create, edit, delete, assign_dealers
nas.*             — view, create, edit, delete, sync
tickets.*         — view, create, edit, close, assign, delete
reports.*         — view, export, financial, operational
audit.*           — view
settings.*        — view, edit
whatsapp.*        — view, send, manage_templates
olt.*             — view, sync, manage
users.*           — view, create, edit, delete, manage_permissions
cron.*            — view, edit, run

PERMISSION_GROUPS (10 groups):

Maps display group names to the permissions they contain (used by the permissions UI to group checkboxes).

Relationships

Method Type Related Model Foreign Key Notes
parent BelongsTo User parent_id Sub-dealer's parent dealer
children HasMany User parent_id Dealer's sub-dealers
engineerTeams BelongsToMany EngineerTeam pivot: engineer_team_user Teams this engineer belongs to
assignedTickets HasMany Ticket assigned_to Tickets assigned to this user
nasDevices BelongsToMany Nas pivot: dealer_nas NAS devices a dealer has access to
packages BelongsToMany Package pivot: dealer_package Packages a dealer can sell; pivot includes rate, active

Business Methods

can(string $ability): bool - Overrides Laravel's default Gate check. - super_admin role: always returns true (bypasses all permission checks). - All other roles: checks if $ability is in $this->permissions array (JSONB column). - This is the primary authorization method used throughout the app — all $user->can('permission.key') calls go here.

defaultPermissions(string $role): array - Returns the default permission array for a given role. - Called when creating a new user to pre-populate their permissions. - Each role has a curated default set; super_admin by convention gets an empty array (since can() always returns true for them).

Role helper methods (all return bool):

Method Condition
isSuperAdmin() role === 'super_admin'
isAdmin() role === 'admin'
isStaff() role in: super_admin, admin, noc_admin, billing_admin, support_agent, auditor
isDealer() role === 'dealer'
isSubDealer() role === 'sub_dealer'
isCollectionOfficer() role === 'collection_officer'
isFieldEngineer() role === 'field_engineer'

accessibleNasIds(User $user): array (static) - Returns array of NAS IDs the given user is allowed to access. - super_admin / admin / noc_admin: returns all NAS IDs. - dealer: returns IDs from the dealer_nas pivot table for $user->id. - Other roles: returns [] (no direct NAS access).

Notes

  • The permissions column is JSONB in PostgreSQL. Laravel casts it as array. Always access via $user->permissions — never query the raw JSON column in application code.
  • super_admin is a singleton role; only one super_admin account should exist per installation. The seeder creates admin@pyronet.com.pk / change-me-on-first-login.
  • Role assignable_by defines who can create accounts of each role. Enforced at the UserController level, not at the model level.
  • Password hashing: handled by Laravel's User model Authenticatable trait using Bcrypt. Never store plain text passwords.

3. Invoice

File: app/Models/Invoice.php Table: invoices Soft Deletes: No

Represents a billing period invoice for a customer. Created by BillingService::generateInvoice().

Fillable Fields

Field Type Cast Purpose
invoice_no string Unique reference number (e.g., INV202607-00042)
customer_id integer FK → customers.id
dealer_id integer FK → users.id; denormalized for reporting
amount decimal float Total amount billed
paid_amount decimal float Amount received to date (partial payments accumulate here)
status string unpaid / partial / paid
period_start date Carbon Start of the billing period
period_end date Carbon End of the billing period
due_date date Carbon Payment due date (typically period_start + 7 days)
notes string Optional invoice notes or memo
created_by string Name of user who generated the invoice

Relationships

Method Type Related Model Foreign Key Notes
customer BelongsTo Customer customer_id
dealer BelongsTo User dealer_id Denormalized for dealer reporting
payments HasMany Payment invoice_id All payments applied to this invoice

Notes

  • Idempotency: BillingService::generateInvoice() checks for an existing invoice with the same customer_id, period_start, and period_end before creating a new one. Duplicates are never created by the service layer.
  • paid_amount is the running total of all payments applied. Status is derived: paid_amount == 0unpaid; 0 < paid_amount < amountpartial; paid_amount >= amountpaid.
  • dealer_id is denormalized (copied from customer.dealer_id at invoice creation time). It does not update if the customer changes dealers later.

4. Payment

File: app/Models/Payment.php Table: payments Soft Deletes: No

Records a single payment transaction. Created by BillingService::recordPayment().

Fillable Fields

Field Type Cast Purpose
receipt_no string Unique receipt number (e.g., RCP202607-00017)
customer_id integer FK → customers.id
invoice_id integer FK → invoices.id; nullable for unattributed payments
dealer_id integer FK → users.id; denormalized
amount decimal float Amount paid
method string Payment method (e.g., cash, easypaisa, jazzcash, bank_transfer)
received_by string Name of collector (staff name or dealer name)
notes string Reference number, bank slip number, or memo
paid_at timestamp Carbon When the payment was collected

Relationships

Method Type Related Model Foreign Key Notes
customer BelongsTo Customer customer_id
invoice BelongsTo Invoice invoice_id Nullable; null = unattributed payment
dealer BelongsTo User dealer_id

Notes

  • Payments are immutable after creation. Reversals are handled via LedgerEntry reversal, not by modifying the payment record.
  • invoice_id may be null for advance payments or unattributed cash collections. These are applied to outstanding invoices by applyExcessToUnpaid().
  • receipt_no uses the seq_receipt_no PostgreSQL sequence — globally unique across all dealers.

5. LedgerEntry

File: app/Models/LedgerEntry.php Table: ledger_entries Soft Deletes: No

Implements a simplified double-entry ledger. Every financial movement (charge or payment) creates at least one ledger entry. Used for dealer balance tracking and financial reporting.

Fillable Fields

Field Type Cast Purpose
customer_id integer FK → customers.id
account_type string customer / dealer — which ledger account this entry belongs to
account_id integer ID of the account (customer ID or dealer/user ID)
type string Entry type (see CHARGE_TYPES / CREDIT_TYPES below)
status string active / reversed
description string Human-readable description of the transaction
debit decimal float Debit amount (money owed by account)
credit decimal float Credit amount (money paid or credited to account)
effective_date date Carbon Date this entry is effective for
created_by string Name of creating user
invoice_id integer FK → invoices.id; links entry to an invoice
remarks string Additional notes
reversed_by string Name of user who reversed this entry
reversed_at timestamp Carbon When the reversal occurred

LedgerService Constants

CHARGE_TYPES (debit-creating entry types):

Type Key Display Label
monthly_charge Monthly Subscription
installation Installation Fee
device Device/Equipment
repair Repair Charge
fiber Fiber Pulling
activation Activation Fee
late_fee Late Payment Fee
penalty Penalty
other_charge Other Charge

CREDIT_TYPES (credit-creating entry types):

Type Key Display Label
credit Payment / Credit
waiver Fee Waiver
discount Discount
write_off Write-Off
adjustment Adjustment
refund Refund

Notes

  • Each payment recorded by BillingService::recordPayment() creates two ledger entries: one for the customer account (account_type = customer) and one for the dealer account (account_type = dealer).
  • Reversal sets status = 'reversed' and records reversed_by / reversed_at. The reversed entry is never deleted.
  • debit and credit are not mutually exclusive per row — a row will have one non-zero and one zero. This matches simplified bookkeeping (not strict double-entry where each entry is a separate row).
  • Dealer balance is derived by summing credit - debit for account_type = 'dealer' and account_id = dealer.id across all active entries.

6. Package

File: app/Models/Package.php Table: packages Soft Deletes: No

Represents a PPPoE speed tier. Each package corresponds to a FreeRADIUS group (radgroupreply) that delivers rate-limit attributes to the router.

Fillable Fields

Field Type Cast Purpose
name string Display name (e.g., 10 Mbps Basic)
radius_group string FreeRADIUS group name; must match radgroupreply.groupname exactly
download_speed integer Committed download speed (Mbps)
upload_speed integer Committed upload speed (Mbps)
burst_download integer Burst download limit (Mbps); 0 = no burst
burst_upload integer Burst upload limit (Mbps)
burst_threshold_down integer Download threshold before burst kicks in
burst_threshold_up integer Upload threshold before burst kicks in
burst_time integer Seconds of burst allowed per burst cycle
rate_limit string Computed MikroTik rate-limit string (e.g., 10M/10M 20M/20M 10M/10M 8/8 10)
price decimal float Standard monthly price; overridable per customer or per dealer
active boolean bool Whether new customers can be assigned this package
description string Optional notes about the package

Relationships

Method Type Related Model Foreign Key Notes
customers HasMany Customer package_id All customers on this package
dealers BelongsToMany User pivot: dealer_package Dealers authorized to sell this package; pivot has rate, active

Notes

  • rate_limit is the MikroTik Mikrotik-Rate-Limit attribute value. Format: rx/tx burst_rx/burst_tx burst_threshold_rx/burst_threshold_tx burst_time/burst_time priority. This is written to radgroupreply by RadiusService::syncPackage().
  • radius_group must exactly match the groupname in radgroupreply and radusergroup. Changing it without a RADIUS sync will break authentication for all customers on this package.
  • active = false hides the package from dealer and staff creation forms; existing customers on the package are unaffected.

7. Nas

File: app/Models/Nas.php Table: nas Soft Deletes: No

Represents a Network Access Server — a MikroTik (or compatible) router that authenticates PPPoE sessions via FreeRADIUS.

Fillable Fields

Field Type Cast Purpose
nasname string NAS IP address (IPv4); used by FreeRADIUS for NAS lookup
shortname string Display name (e.g., Core-Router-1)
type string Device type (e.g., mikrotik, cisco)
ports integer Number of ports (informational; FreeRADIUS default: 1812)
community string SNMP community string for read access
description string Human-readable description
status string active / inactive
api_user string MikroTik RouterOS API username
api_password string encrypted RouterOS API password; stored encrypted via Laravel cast
api_port integer RouterOS API port (default: 8728 plaintext, 8729 SSL)
device_class string router / olt / switch

Casts

Field Cast
api_password encrypted — uses Laravel's built-in encryption (APP_KEY)

Relationships

  • Dealers access NAS via dealer_nas pivot (User::nasDevices()).
  • Customers reference NAS via nas_id FK.

Notes

  • The nas table is also read by FreeRADIUS itself (via the rlm_sql module) to validate that the NAS IP is authorized. The nasname field must match the router's actual IP.
  • api_password uses Laravel encryption — never stored in plaintext. Decrypted in memory only when making RouterOS API calls.
  • RADIUS secret for this NAS is stored in /etc/freeradius/3.0/clients.conf on the server, NOT in this table. The DB nas table does not contain RADIUS shared secrets.

8. RadCheck

File: app/Models/RadCheck.php Table: radcheck Soft Deletes: No

FreeRADIUS check attribute table. Stores the credential that FreeRADIUS verifies during PPPoE authentication.

Fields

Field Type Purpose
id integer Primary key
username string PPPoE username (matches customers.pppoe_username)
attribute string RADIUS attribute name
op string Operator (==, :=, =~, etc.)
value string Attribute value

How it is Used

For each customer, RadiusService::syncCustomer() writes one row:

username  = pppoe_username
attribute = 'Cleartext-Password'
op        = ':='
value     = pppoe_password

Optionally, for disabled/suspended customers, it may write:

attribute = 'Auth-Type'
op        = ':='
value     = 'Reject'

This immediately blocks authentication without deleting the account.

Notes

  • This table is shared between the PyroRadius application and FreeRADIUS. FreeRADIUS reads it directly during every authentication attempt.
  • The PyroRadius app reads it only during RADIUS sync operations; all authentication is handled by FreeRADIUS.
  • No Laravel soft deletes — rows are hard-deleted when a customer is removed.

9. RadGroupReply

File: app/Models/RadGroupReply.php Table: radgroupreply Soft Deletes: No

FreeRADIUS group reply attribute table. Stores RADIUS attributes that are sent back to the NAS (router) when a PPPoE session is accepted.

Fields

Field Type Purpose
id integer Primary key
groupname string Group name (matches packages.radius_group)
attribute string RADIUS reply attribute
op string Operator (typically :=)
value string Attribute value

How it is Used

For each package, RadiusService::syncPackage() writes rows like:

groupname = 'pkg_10mbps_basic'
attribute = 'Mikrotik-Rate-Limit'
op        = ':='
value     = '10M/10M 20M/20M 10M/10M 8/8 10'

Multiple attributes per group are possible (e.g., Session-Timeout, Idle-Timeout, Framed-IP-Netmask).

Notes

  • Group names are defined by packages.radius_group. They must be consistent between radgroupreply and radusergroup.
  • RadiusService::syncPackage() is called when a package's speed is edited — it updates the radgroupreply rows, which affects ALL customers on that package immediately (on their next authentication or CoA refresh).

10. RadUserGroup

File: app/Models/RadUserGroup.php Table: radusergroup Soft Deletes: No

Links a PPPoE username to a FreeRADIUS group. This is how a customer is assigned their speed package.

Fields

Field Type Purpose
id integer Primary key
username string PPPoE username
groupname string RADIUS group name (matches packages.radius_group)
priority integer Priority when multiple groups apply (lower = higher priority)

How it is Used

RadiusService::syncCustomer() writes one row:

username  = customer.pppoe_username
groupname = customer.package.radius_group
priority  = 1

When FreeRADIUS authenticates a PPPoE session, it looks up the group from radusergroup and fetches reply attributes from radgroupreply for that group.

Notes

  • Changing a customer's package updates this row to the new package's radius_group.
  • Username renames (via CustomerController::update()) delete the old row and insert a new one with the new username.

11. RadAcct

File: app/Models/RadAcct.php Table: radacct Soft Deletes: No Application Access: Read-only

FreeRADIUS accounting table. Written exclusively by FreeRADIUS; the PyroRadius application only reads it.

Key Fields

Field Type Purpose
radacctid bigint Primary key (FreeRADIUS auto-increments)
acctsessionid string FreeRADIUS session ID
username string PPPoE username
nasipaddress string NAS (router) IP address
framedipaddress string IP assigned to the PPPoE client
acctstarttime timestamp Session start time
acctstoptime timestamp Session end time; NULL = session is active
acctinputoctets bigint Bytes uploaded by client
acctoutputoctets bigint Bytes downloaded by client
acctterminatecause string How the session ended (e.g., User-Request, Admin-Reset)
callingstationid string MAC address of the PPPoE client (if provided by router)

How it is Used by PyroRadius

Use Case Query
Online detection (dashboard, customer list) WHERE acctstoptime IS NULL → collect username set
Session kick WHERE username = ? AND acctstoptime IS NULL → get nasipaddress + acctsessionid
Session history on customer show WHERE username = ? ORDER BY acctstarttime DESC LIMIT 20

Notes

  • Never write to this table from the application. It is owned by FreeRADIUS.
  • acctstoptime IS NULL is the reliable online indicator. Do not use session IDs or start times alone.
  • On very busy systems, this table can have millions of rows. Queries against it should always use indexed columns (username, acctstoptime, nasipaddress).
  • FreeRADIUS does not expire or archive old sessions automatically. Old rows must be pruned via the F7 Prune cron job (Batch A fix).

12. WhatsappTemplate

File: app/Models/WhatsappTemplate.php Table: whatsapp_templates Soft Deletes: No

Stores the body text for WhatsApp messages. Supports placeholder substitution.

Fillable Fields

Field Type Purpose
key string Unique template key (e.g., payment_received, expiry_reminder)
body string Message body with {placeholder} variables
dealer_id integer FK → users.id; null = global template; non-null = dealer override
enabled boolean Whether this template is active

How Placeholders Work

The {placeholder} tokens in body are replaced at send time. Common placeholders:

{customer_name}   → customer.full_name
{amount}          → payment amount
{receipt_no}      → payment.receipt_no
{expiry_date}     → customer.expiry_date
{package_name}    → package.name
{company_name}    → CompanyProfile.name

Notes

  • Dealer overrides: if a WhatsappTemplate exists with dealer_id = X and key = 'payment_received', it is used for that dealer's customers instead of the global template.
  • Templates are resolved by WhatsAppService at send time: dealer-specific first, global fallback.

13. WhatsappLog

File: app/Models/WhatsappLog.php Table: whatsapp_logs Soft Deletes: No

Audit trail for every WhatsApp message sent.

Fillable Fields

Field Type Purpose
to string Recipient WhatsApp number (international format)
body string Actual message body after placeholder substitution
status string sent / failed / pending
customer_id integer FK → customers.id; nullable
template_key string Which template was used
driver string Which WhatsApp driver was used (e.g., baileys, ultramsg)
response string Raw API response or error message
sent_at timestamp When the send was attempted

14. WhatsappSession

File: app/Models/WhatsappSession.php Table: whatsapp_sessions Soft Deletes: No

Tracks the connection state of WhatsApp session(s).

Fillable Fields

Field Type Purpose
session_id string Unique session identifier
name string Human label for this session
status string ready / connecting / disconnected

Notes

  • status = 'ready' means QR code has been scanned and the session is authenticated.
  • WhatsAppService checks this before attempting to send; messages are queued or dropped if status != 'ready'.

15. AuditLog

File: app/Models/AuditLog.php Table: audit_logs Soft Deletes: No (audit logs are immutable — never deleted in normal operation)

Immutable record of every significant action taken in the system. Written by AuditLogger::log().

Fillable Fields

Field Type Cast Purpose
user_id integer FK → users.id; who performed the action
user_name string Denormalized user name (preserves name if user is later deleted)
role string User's role at time of action
action string Dot-notation action key (e.g., customer.created, customer.recharged)
subject_type string Eloquent model class name (e.g., App\Models\Customer)
subject_id integer Primary key of the affected record
old_values array JSONB State before the change; null for create actions
new_values array JSONB State after the change; null for delete actions
ip string Client IP address of the requesting session
created_at timestamp Carbon When the action occurred

Common Action Keys

Action Trigger
customer.created CustomerController::store()
customer.updated CustomerController::update()
customer.deleted CustomerController::destroy()
customer.recharged CustomerController::recharge()
customer.restored Customer restore action
invoice.created BillingService::generateInvoice()
payment.reversed Payment reversal action
user.login Authentication event
settings.updated SettingController::update()

Notes

  • old_values and new_values are JSONB columns. They store a snapshot of the relevant fields, not the entire model.
  • The AuditLog table should never have rows deleted by the application. Archival (moving old logs to cold storage) is acceptable but deletion is not.
  • user_name and role are denormalized intentionally — the audit trail must remain meaningful even if the user account is later deleted or their role changes.

16. Setting

File: app/Models/Setting.php Table: settings Soft Deletes: No

Global application key-value configuration store.

Fields

Field Type Purpose
key string Unique setting key
value string Setting value (always stored as string; cast in application code)

Static Helper

Setting::get(string $key, mixed $default = null): mixed

Reads a setting by key. Returns $default if the key does not exist. Uses caching internally (TTL: 60 seconds) to avoid repeated DB hits.

Setting::set(string $key, mixed $value): void

Upserts a setting value and clears the cache.

Common Settings Keys

Key Purpose
auto_generate_invoice 1 / 0 — Auto-generate invoice on unattributed payment
grace_days_default Default grace period in days after expiry
invoice_due_days Days after period_start that invoice is due
whatsapp_enabled Global toggle for WhatsApp notifications
payment_methods Comma-separated list of accepted payment methods
sms_enabled Toggle for SMS (if integrated)

17. CompanyProfile

File: app/Models/CompanyProfile.php Table: company_profiles Soft Deletes: No

Stores ISP branding and contact information displayed throughout the application and on invoices/receipts.

Fillable Fields

Field Type Purpose
name string ISP company name
tagline string Company tagline
logo_path string Path to uploaded logo file
brand_color string Primary brand color (hex, e.g., #E0330A)
address string Company address
phone string Main contact phone
email string Main contact email

Notes

  • Only one row exists in practice (singleton table). Access via CompanyProfile::first().
  • logo_path is a relative path from the storage/app/public directory; served via Storage::url().
  • brand_color is injected into invoice PDFs and email templates.

18. Olt

File: app/Models/Olt.php Table: olts Soft Deletes: No

Represents an Optical Line Terminal — a GPON/EPON device that manages ONT/ONU subscribers.

Fillable Fields

Field Type Purpose
name string Display name (e.g., OLT-Hub-1)
host string IP address of the OLT management interface
port integer SSH/Telnet port
username string Management login username
password string Management login password (stored encrypted)
type string OLT vendor/type (e.g., huawei, zte, bdcom)
status string active / inactive

Notes

  • password should use Laravel encrypted cast — verify this is applied in the actual model.
  • The olt:sync-ont-assignments Artisan command reads from this table to connect to each OLT and refresh ont_cache.

19. OntCache

File: app/Models/OntCache.php Table: ont_cache Soft Deletes: No

Ephemeral cache of ONT/ONU discovery data fetched from OLT devices. Refreshed by the olt:sync-ont-assignments command every minute.

Fields

Field Type Purpose
sn_decoded string ONT serial number in human-readable decoded format (e.g., HWTC1234ABCD)
description string Description field from OLT — typically contains the PPPoE username
rx_raw integer RX optical power × 100 (so -23.5 dBm = -2350)
port_alias string OLT port name/alias (e.g., 0/1/3)
olt_id integer FK → olts.id

How it is Used

The SyncOntAssignments command: 1. Connects to each OLT via SSH. 2. Fetches all ONT registrations (serial, description, RX power, port). 3. Upserts rows into ont_cache. 4. Then runs an UPDATE joining ont_cache.description to customers.pppoe_username (after LOWER/TRIM normalization) to populate customers.rx_power, customers.rx_power_at, and customers.ont_serial.

Performance Note

As of 2026-07-05, this join query takes 13-14 seconds due to missing functional index on LOWER(TRIM(description)). This is a known issue scheduled for optimization. See docs/SYNC_ONT_ASSIGNMENTS_OPTIMIZATION_AUDIT.md.


20. CronJob

File: app/Models/CronJob.php Table: cron_jobs Soft Deletes: No

Registry of scheduled Artisan commands, managed through the UI rather than hardcoded in app/Console/Kernel.php.

Fillable Fields

Field Type Purpose
command string Artisan command signature (e.g., olt:sync-ont-assignments)
expression string Cron expression (e.g., * * * * * for every minute)
enabled boolean Whether this job is scheduled
without_overlap boolean If true, skips if a previous run is still executing
log_file string Optional path for command output logging

Notes

  • The Laravel scheduler reads this table at runtime to dynamically build the schedule. This allows admins to enable/disable or adjust cron timing from the UI without SSH access.
  • without_overlap maps to Laravel's ->withoutOverlapping() scheduler modifier — critical for long-running commands like sync-ont-assignments.

21. Ticket

File: app/Models/Ticket.php Table: tickets Soft Deletes: No

Support and NOC incident ticket.

Fillable Fields

Field Type Purpose
title string Short description of the issue
description string Full problem description
status string open / in_progress / resolved / closed
priority string low / medium / high / critical
customer_id integer FK → customers.id; which customer is affected
assigned_to integer FK → users.id; assigned staff member
due_date date Expected resolution date
category_id integer FK → ticket_categories.id

Relationships

Method Type Related Model Foreign Key Notes
customer BelongsTo Customer customer_id
assignee BelongsTo User assigned_to
category BelongsTo TicketCategory category_id

22. DealerPackage

File: app/Models/DealerPackage.php Table: dealer_package Type: Pivot model (intermediate table for User ↔ Package many-to-many)

Stores per-dealer custom pricing for packages, and controls which packages each dealer can offer to their customers.

Fields

Field Type Purpose
dealer_id integer FK → users.id (dealer role)
package_id integer FK → packages.id
rate decimal Custom price for this dealer's customers (overrides package.price)
active boolean Whether this dealer can currently assign this package

How it is Used

  • Package::dealers() returns dealers with pivot rate and active.
  • User::packages() returns packages with pivot rate and active.
  • When a dealer creates a customer, PackageAvailabilityService checks that dealer_package.active = true for the chosen package.
  • When generating an invoice, BillingService uses customer.monthly_price (if set) → dealer_package.rate (if set) → package.price as the price resolution chain.

Notes

  • This is a pivot model with extra columns. It extends Pivot (not Model) in Laravel.
  • The rate field is optional (nullable). A null rate means the dealer sells at the package's standard price.

Appendix: Model Relationship Map

CompanyProfile    (singleton)

User ──────────────────────────────────────────────────────┐
  ├── children (User, sub-dealers)                         │
  ├── packages (BelongsToMany Package via dealer_package)  │
  ├── nasDevices (BelongsToMany Nas via dealer_nas)        │
  └── assignedTickets (Ticket)                             │
Customer ──────────────────────────────────────────────────┤
  ├── package (Package)                                    │
  ├── dealer (User) ─────────────────────────────────────>─┘
  ├── subDealer (User)
  ├── nas (Nas)
  ├── areaRef (Area)
  ├── invoices (Invoice)
  │     └── payments (Payment)
  ├── payments (Payment)
  └── [RADIUS sync] radcheck, radusergroup

Package ──────────────────────────────────────────────────
  └── [RADIUS sync] radgroupreply

Nas ───────────────────────────────────────────────────────
  └── [FreeRADIUS auth/acct] radacct

Olt ───────────────────────────────────────────────────────
  └── ont_cache (OntCache) ──→ Customer.rx_power

Setting     (key-value)
AuditLog    (immutable)
WhatsappTemplate → WhatsappLog
WhatsappSession
CronJob
Ticket
LedgerEntry

Appendix: Cast Reference

Model Field Cast Notes
User permissions array JSONB column
User active boolean
Customer pppoe_password hidden Excluded from serialization
Customer expiry_date date Carbon date
Customer monthly_price float
Customer rx_power float dBm × 100 stored as int
Customer contract_expiry date Carbon date
Nas api_password encrypted Laravel encryption
LedgerEntry old_values array JSONB
LedgerEntry new_values array JSONB
AuditLog old_values array JSONB
AuditLog new_values array JSONB
Package active boolean
CronJob enabled boolean
CronJob without_overlap boolean

End of PHASE2_04_Model_Reference.md