Skip to content

13 — Dealer System

Overview

PyroRadius supports a three-tier hierarchy: administrators manage dealers, dealers manage sub-dealers and customers, sub-dealers assist dealers in managing customers. All roles share the users table with different role values and different permission scopes enforced throughout every controller.


Hierarchy Structure

super_admin / admin
        ├─── Dealer A (role=dealer)
        │         │
        │         ├─── Sub-Dealer A1 (role=sub_dealer, parent_id=Dealer A)
        │         │         └─── Customers (dealer_id=A, sub_dealer_id=A1)
        │         │
        │         ├─── Sub-Dealer A2 (role=sub_dealer, parent_id=Dealer A)
        │         │         └─── Customers (dealer_id=A, sub_dealer_id=A2)
        │         │
        │         └─── Customers directly under Dealer A (sub_dealer_id=null)
        └─── Dealer B (role=dealer)
                  └─── Customers (dealer_id=B)

Role Values in users.role

Role Description
super_admin Full access to everything; can manage admins and dealers
admin Same as super_admin operationally; created by super_admin
dealer Manages own customers; limited to their own scope
sub_dealer Acts under a dealer; scope further restricted to their own customers

User Model — Relevant Fields

Table: users

Column Type Description
id bigint PK
name varchar Display name
email varchar Login email (unique)
password varchar Bcrypt hashed
role enum super_admin / admin / dealer / sub_dealer
parent_id bigint FK (self) For sub_dealers: points to their parent dealer's user ID
phone varchar Contact number
address text Business address
status enum active / inactive
balance decimal Dealer's credit balance (for prepaid dealer billing model)
deleted_at timestamp Soft delete

How Dealers Own Customers

Every customers row carries two foreign keys:

  • dealer_id — the dealer (User) responsible for this customer
  • sub_dealer_id — the sub-dealer who added/manages this customer (nullable)

Scope enforcement is applied in every query:

// Applied automatically via Customer model scope or controller filter:
if (auth()->user()->isDealer()) {
    $query->where('dealer_id', auth()->id());
} elseif (auth()->user()->isSubDealer()) {
    $query->where('sub_dealer_id', auth()->id());
}

Admins and super_admins see all customers across all dealers.


Sub-Dealer Relationship

Sub-dealers are User records with role = 'sub_dealer' and parent_id set to their dealer's user ID.

Key rules: - A sub-dealer can only be created by their parent dealer (or admin). - Customers owned by a sub-dealer still have dealer_id set to the parent dealer. - A sub-dealer cannot see other sub-dealers' customers or the dealer's direct customers. - When a sub-dealer is deactivated, their customers remain under the dealer.

Sub-Dealer Scope Query Example

Customer::where('dealer_id', $subDealer->parent_id)
         ->where('sub_dealer_id', $subDealer->id)
         ->get();

Creating Dealers

Route

POST /users  (with role=dealer in payload)
Handled by UserController::store()

Required Fields

Field Notes
Name Full business name
Email Unique login email
Password Min 8 chars
Phone Contact number
Address Business address
Status active (default)
Role Set to dealer

After creation, the dealer can log in at the main login page. They immediately see a scoped dashboard showing only their customers.


Creating Sub-Dealers

Sub-dealers are created via the same UserController::store() but with: - role = sub_dealer - parent_id = <dealer's user ID>

Admin selects the parent dealer from a dropdown. When a dealer creates a sub-dealer, their own ID is automatically used as parent_id.


Dealer Package Assignments

The dealer_package Pivot Table

Column Type Description
id bigint PK
dealer_id bigint FK References users.id
package_id bigint FK References packages.id
cost_price decimal What this dealer pays per month (dealer's cost)
sale_price decimal What this dealer charges customers (suggested retail)
is_commercial boolean True = commercial/corporate terms apply
notes text Free-form notes on the commercial arrangement
created_at timestamp
updated_at timestamp

Purpose

Not all packages are available to all dealers. This table defines: 1. Which packages a dealer can sell. 2. The dealer's cost price (what they owe PyroNet). 3. The suggested sale price (what customers are charged). 4. Whether commercial terms apply.

When a dealer creates a customer, the package dropdown is filtered to only show packages in this pivot table for that dealer.

DealerPackageController

Method Route Description
index() GET /dealer-packages List all dealer-package assignments (admin)
store() POST /dealer-packages Assign a package to a dealer with pricing
update() PUT /dealer-packages/{id} Update cost/sale price or commercial flag
destroy() DELETE /dealer-packages/{id} Remove package from dealer (existing customers unaffected)

Removing a dealer-package assignment does not affect existing customers on that package — it only prevents new customers from being enrolled in it.


Assigning New Packages to a Dealer

Steps (performed by admin): 1. Go to Dealers → select dealer → Packages tab. 2. Click "Assign Package." 3. Select the package from the global packages list. 4. Set cost_price (what dealer pays), sale_price (retail), check is_commercial if applicable. 5. Save — creates a dealer_package row.

The dealer can now see and select this package when creating customers.


View As Feature (Admin Impersonates Dealer)

Admins can switch their dashboard view to any dealer to see exactly what that dealer sees.

How It Works

  1. Admin visits Dealers list, clicks "View As" next to a dealer.
  2. A session variable view_as_dealer_id is set to the dealer's user ID.
  3. All subsequent queries use this ID as the scoping dealer.
  4. A banner appears at the top of every page: "Viewing as [Dealer Name] — Exit View."
  5. Clicking "Exit View" clears the session variable and returns to admin scope.

What Changes Under View As

  • Customer list shows only that dealer's customers.
  • Dashboard stats (active, expired, revenue) reflect only that dealer.
  • Package dropdown shows only that dealer's assigned packages.
  • NAS dropdown shows only that dealer's assigned NAS.

Admin privileges (edit/delete dealer, manage users) remain in admin scope and are hidden when in View As mode.


Dealer Scoping in Controllers

Every controller that works with customer or financial data applies dealer scoping. The pattern used throughout:

protected function scopeQuery(Builder $query): Builder
{
    $user = auth()->user();

    if ($user->isDealer()) {
        return $query->where('dealer_id', $user->id);
    }

    if ($user->isSubDealer()) {
        return $query->where('sub_dealer_id', $user->id);
    }

    // If admin is in View As mode
    if (session()->has('view_as_dealer_id')) {
        return $query->where('dealer_id', session('view_as_dealer_id'));
    }

    return $query; // admin/super_admin sees everything
}

This pattern is applied in: CustomerController, TransactionController, DashboardController, ReportController, TicketController.


Permission Matrix

Action super_admin admin dealer sub_dealer
View all customers Yes Yes Own only Own only
Create customer Yes Yes Yes Yes (under their dealer)
Edit customer Yes Yes Own only Own only
Delete customer Yes Yes No No
View all dealers Yes Yes No No
Create dealer Yes Yes No No
Create sub-dealer Yes Yes Yes (own) No
Assign packages to dealer Yes Yes No No
View As dealer Yes Yes No No
Manage NAS Yes Yes No No
View financial reports (all) Yes Yes No No
View own revenue report Yes Yes Yes No
Manage packages (global) Yes Yes No No
Kick PPPoE session Yes Yes Yes No
Recharge customer Yes Yes Yes Yes
Access TR-069 management Yes Yes No No
Manage OLT Yes Yes No No

Dealer Dashboard Stats

When a dealer logs in, their dashboard shows:

  • Total customers (their scope)
  • Active customers count
  • Expired customers count
  • Suspended customers count
  • Revenue this month (sum of transactions where dealer_id = their ID)
  • Customers expiring in next 7 days (needs action list)
  • Recent payments

These numbers are fetched from CustomerController::statusCounts() and DashboardController::index(), both of which apply dealer scoping.


Dealer Balance (Prepaid Model)

If PyroNet operates dealers on a prepaid basis, the users.balance field tracks their credit.

  • When a dealer recharges a customer, their balance is debited by cost_price for that package.
  • If balance is insufficient, the recharge may be blocked (configurable).
  • Admin can top up a dealer's balance via the Dealer edit form.
  • Balance movements are logged in a dealer_transactions table (dealer_id, amount, type, note, created_by).

Method Route Description
index() GET /users List all users (admin filters by role)
create() GET /users/create Show user creation form
store() POST /users Create dealer, sub-dealer, or admin
show() GET /users/{id} User detail / dealer profile
edit() GET /users/{id}/edit Edit form
update() PUT /users/{id} Update user details
destroy() DELETE /users/{id} Soft delete user
viewAs() POST /users/{id}/view-as Set View As session (admin only)
exitViewAs() POST /users/exit-view-as Clear View As session
topupBalance() POST /users/{id}/topup Add dealer balance (admin only)

  • UserhasMany Customer (as dealer)
  • UserhasMany Customer (as sub_dealer)
  • UserhasMany User (sub_dealers, via parent_id)
  • UserbelongsTo User (parent dealer, via parent_id)
  • UserbelongsToMany Package (via dealer_package)
  • UserbelongsToMany NAS (via dealer_nas)