# E-Internet Management Service — Project Plan

A Laravel 12 SaaS platform for ISP billing management, migrated and expanded from
the legacy `isinternet` PHP project.

---

## Tech Stack

| Layer | Choice |
|---|---|
| Framework | Laravel 12.12.2 (PHP 8.2) |
| Database | MySQL — single shared database, tenant isolation via `tenant_id` |
| Multi-tenancy | stancl/tenancy v3 — subdomain identification, **no** per-tenant DB |
| Permissions | spatie/laravel-permission v6 — teams mode (`team = tenant`) |
| Activity log | spatie/laravel-activitylog v4 |
| API auth | laravel/sanctum v4 |
| PDF bills | barryvdh/laravel-dompdf v3 |
| Excel export | maatwebsite/excel v3 |
| Auth scaffolding | laravel/breeze v2 (Blade stack) |
| Primary keys | ULID everywhere (`HasUlids`) — no integer IDs in URLs |
| Timezone | `Asia/Karachi` |

---

## Architecture

### Multi-Tenancy (Single-Database)
- Each tenant is identified by subdomain: `{slug}.eims.test`
- `stancl/tenancy` resolves the tenant from the subdomain via `InitializeTenancyBySubdomain` middleware
- **No separate database per tenant** — `DatabaseTenancyBootstrapper` is removed
- All tenant-scoped models use the `HasTenant` trait which:
  - Applies a `WHERE tenant_id = tenant('id')` global scope automatically
  - Auto-sets `tenant_id` on `creating`
  - Provides `withoutTenantScope()` for super-admin cross-tenant queries

### Permission System (Teams Mode)
- Spatie's "teams" = tenants. `team_id` in pivot tables = `tenant_id`
- Super-admin has `team_id = null` (platform level, no tenant)
- `Gate::before()` in `AppServiceProvider` grants super-admin all abilities
- `setPermissionsTeamId($tenant->id)` must be called before any permission check within a tenant request
- Tenancy middleware triggers this via `BootstrapPermissionForTenant` listener on `TenancyInitialized`

### Customer Registry (Shared + Per-Tenant)
- `customers` table: global, no `tenant_id` — stores identity (CNIC, contact, name)
- CNIC and contact are globally unique — used for **deduplication** on entry/import
- `tenant_customer_profiles` table: per-tenant ISP data (package, village, status, internet_id)
- One customer can be on multiple tenants; identity is never duplicated

### URL Design
- All resource URLs use ULID route keys: `/customers/01HWZP2J3K...`
- Integer IDs are **never** exposed in URLs or API responses
- Route model binding uses `getRouteKeyName()` returning `'id'` (ULID)

---

## Roles & Permissions

| Role | Scope | Description |
|---|---|---|
| `super-admin` | Platform | Full access to all tenants and settings. Bypasses all checks. |
| `admin` | Tenant | Full access within their tenant |
| `manager` | Tenant | Manage customers, bills, payments; cannot manage users/roles |
| `billing-staff` | Tenant | Create/edit bills and payments only |
| `support-staff` | Tenant | View customers and bills; cannot modify billing |
| `viewer` | Tenant | Read-only access to dashboard and reports |

Permission groups: `tenant`, `user`, `customer`, `package`, `village`, `bill`,
`payment`, `report`, `configuration`, `log`

---

## Data Models

```
tenants (id:ulid, slug, name, phone, email, address, logo, prop_name_1/2, prop_phone_1/2, status, subscription_plan, subscription_expires_at)
domains (id, domain, tenant_id)

users (id:ulid, tenant_id, full_name, username, email, contact, cnic, address, status, photo, docs:json, softDeletes)

customers (id:ulid, name, father_name, cnic:unique, contact:unique, email, photo)
tenant_customer_profiles (id:ulid, tenant_id, customer_id, internet_id, package_id, village_id, address, status, due_amount, docs:json)

packages (id:ulid, tenant_id, name, speed_mbps, price, description, status)
villages (id:ulid, tenant_id, name)
departments (id:ulid, tenant_id, name, description)

tax_configurations (id:ulid, tenant_id, name, rate, type:percentage|fixed, applies_to, compound_on:json, is_default, status)
discount_configurations (id:ulid, tenant_id, name, value, type, condition:always|before_due_date|manual, applies_to, is_default, status)
payment_method_configurations (id:ulid, tenant_id, method:cash|easypaisa|jazzcash|bank_transfer|cheque|other, is_enabled, display_name, account_number, account_title, instructions, surcharge_type, surcharge_value, minimum_amount, maximum_amount, has_live_gateway, gateway_credentials:encrypted:array)

bills (id:ulid, tenant_id, customer_id, profile_id, bill_number, billing_month, billing_year, due_date, subtotal, tax_amount, discount_amount, total_amount, paid_amount, status:not-paid|partial-paid|paid|installment|waived, notes)
bill_items (id:ulid, tenant_id, bill_id:nullable, profile_id, type:package|tax|discount|arrears|late_fee|misc|credit, label, amount, quantity, is_recurring, billing_months, billed_count)

installment_plans (id:ulid, tenant_id, bill_id, total_installments, installment_amount, notes)
installment_schedules (id:ulid, plan_id, installment_number, due_date, amount, paid_at, status:pending|paid|overdue|waived)

payments (id:ulid, tenant_id, bill_id, customer_id, amount, surcharge_amount, net_amount, method, payment_date, reference_number, payment_proof, gateway_transaction_id, notes, received_by)
payment_gateway_transactions (id:ulid, tenant_id, payment_id, gateway, transaction_id, status, request_payload:encrypted:array, response_payload:encrypted:array, webhook_payload:encrypted:array, verified_at)

ledger_entries (id:ulid, tenant_id, type:debit|credit, amount, description, reference_type, reference_id, entry_date)
session_logs (id:ulid, user_id, tenant_id, ip_address, user_agent, logged_in_at, logged_out_at, notes)
```

---

## Phase Plan

### ✅ Phase 1 — Foundation
- [x] Laravel 12 scaffolded in `beta/`
- [x] All packages installed and configured
- [x] Single-DB tenancy configured (no per-tenant DB)
- [x] ULID primary keys on all models
- [x] `HasTenant` trait with global scopes
- [x] All 16 models created
- [x] All migrations created and verified
- [x] Roles & permissions seeded
- [x] Demo tenant seeded (IS Internet Services)
- [x] `TenancyServiceProvider` — DB jobs removed
- [x] `AppServiceProvider` — `Gate::before()` for super-admin
- [x] `migrate:fresh --seed` runs clean

### ✅ Phase 2 — Auth & Tenant Bootstrap
- [x] `BootstrapPermissionForTenant` listener on `TenancyInitialized` event (sets `setPermissionsTeamId`)
- [x] Login page (subdomain-aware: tenant login vs platform login — `credential` field: email/username/contact)
- [x] Auth middleware groups: `EnsureTenantUser` middleware for tenant routes
- [x] Session tracking (SessionLog created on login, updated on logout)
- [x] `routes/tenant.php` — all tenant routes with `InitializeTenancyBySubdomain` + auth + tenant.user middleware
- [x] Navigation: tenant-aware nav links (Dashboard, Customers, Packages, More dropdown)
- [x] Profile page (updated for `full_name`, `username`, `contact` fields)
- [x] `.env` / `.env.example` updated: `APP_URL`, `CENTRAL_DOMAIN`, `SESSION_DOMAIN`

### ✅ Phase 3 — Core Tenant Modules
- [x] **Dashboard** — stats: active customers, monthly revenue, unpaid bills, package distribution
- [x] **Customers** — list (with search/filter), create (with CNIC dedup), view, edit, status toggle
- [x] **Packages** — CRUD (with delete guard — cannot delete if customers assigned)
- [x] **Villages** — inline-edit CRUD
- [x] **Departments** — inline-edit CRUD
- [x] **Users** — CRUD with role assignment (tenant-scoped via Spatie teams)

### ✅ Phase 4 — Billing
- [x] **Bill generation** — auto-generate monthly bills from active profiles + default taxes/discounts + pending BillItems
- [x] **Bill list** — filterable by status, month, year, customer/CNIC search
- [x] **Bill detail** — line items display with paid/balance summary
- [x] **Manual bill items** — add misc charges, credits, arrears, late fees; remove non-package items
- [x] **Tax & Discount config** — CRUD for TaxConfiguration and DiscountConfiguration (shared configurations page)
- [x] **A4 Bill PDF** — dompdf Blade template (company header, customer info, line items, payment history)
- [x] **Waive bill** — mark bill as waived, adjusts customer due_amount

### ✅ Phase 5 — Payments (Core Manual)
- [x] **Record payment** — manual (cash, EasyPaisa, JazzCash, bank, cheque, other); surcharge support
- [x] **Payment list** — filterable by method, date range with running total
- [x] **Payment receipt** — detail view with reverse payment action
- [x] **Bill status sync** — auto updates bill status (not-paid → partial-paid → paid) on payment record/void
- [x] **Profile due_amount sync** — recalculated from unpaid bills on every payment change

### ✅ Phase 6 — Reports & Accounting
- [x] Monthly income summary (filterable by year/month, Excel export)
- [x] Outstanding dues report (searchable, Excel export)
- [x] Package-wise revenue (by year, Excel export)
- [x] Ledger view (income/expense entries, date/type filter)
- [x] Excel export via `maatwebsite/excel` — `ReportExport` class

### ✅ Phase 7 — Platform / Super-Admin
- [x] Platform dashboard (all tenants overview with per-tenant stats)
- [x] Tenant management (create, suspend, activate, view, edit)
- [x] Cross-tenant customer lookup (CNIC / name / contact search)
- [x] Subscription management (plan + expires_at editable per tenant)

### ✅ Phase 8 — REST API (Mobile Apps)
- [x] Sanctum token issuance (`POST /api/v1/auth/login`) — email/username/contact
- [x] `GET/POST /api/v1/auth/logout`, `GET /api/v1/auth/me`
- [x] Customer self-service: `GET /api/v1/customers`, `GET /api/v1/customers/{id}`
- [x] Bills: `GET/POST /api/v1/bills`, `GET /api/v1/bills/{id}`
- [x] Payments: `GET/POST /api/v1/payments`, `GET /api/v1/payments/{id}`
- [x] API versioning under `api/v1/`
- [x] Rate limiting on auth endpoints (throttle:10,1)

### ✅ Phase 9 — Polish
- [x] Form Requests: `StoreCustomerRequest`, `UpdateCustomerRequest`, `StorePaymentRequest`, `StoreBillItemRequest`
- [x] Error pages: 404, 403, 500 (custom Blade templates)
- [x] Activity log viewer (Spatie activitylog, filterable by log_name/date/search)
- [x] User session logs viewer (filterable by user/IP/date)

---

## Key Technical Decisions

| Decision | Rationale |
|---|---|
| Single DB (not separate DB per tenant) | Simpler ops, no per-tenant migration overhead, ULID tenant_id provides isolation |
| ULID PKs everywhere | Non-sequential, URL-safe, no exposure of record counts |
| Global customer registry | Prevents duplicate customer records across tenants; CNIC/contact as dedup keys |
| Configurable taxes/discounts | Replaces hardcoded 16% GST and 15% early payment discount |
| BillItem line-item engine | Supports misc charges, recurring items, installments without bill-formula coupling |
| gateway_credentials encrypted | Sensitive EasyPaisa/JazzCash API keys never stored in plaintext |
| Webhook HMAC verification | EasyPaisa and JazzCash callbacks verified before processing to prevent spoofing |

---

## Environment

- **Dev URL**: `http://eims.test` (central), `http://isinternet.eims.test` (demo tenant)
- **XAMPP MySQL**: root / (no password), database `eims_beta`
- **PHP**: 8.2.12 (`C:\xampp\php\php.exe`)
- **Central domain**: `eims.test` (set in `.env` as `CENTRAL_DOMAIN`)
- **Demo credentials**:
  - Super-admin: `superadmin@eims.test` / `password`
  - Tenant admin: `admin@isinternet.test` / `password`
