# NOWILLS ISP — cPanel installation guide

**Connect. Manage. Grow.** · A product of Nowills Group

Enterprise ISP business management, network operations, billing and subscription
platform. This package installs on ordinary cPanel shared hosting: **no Node, no
Docker, no build step, no daemonised queue worker.**

---

## 1. Requirements

| | |
|---|---|
| PHP | **8.3 or newer** (8.4 recommended) |
| PHP extensions | `pdo_mysql`, `mbstring`, `openssl`, `tokenizer`, `xml`, `ctype`, `json`, `bcmath`, `curl`, `fileinfo`, `zip` |
| MySQL / MariaDB | MySQL 5.7.8+ / MariaDB 10.3+ (utf8mb4) |
| Web server | Apache with `mod_rewrite` (LiteSpeed also works) |
| Cron | Required — billing, dunning and monitoring all run from the scheduler |
| Composer | **Not required.** The `vendor/` directory is included |

Check your PHP version under **cPanel → MultiPHP Manager** and the extensions under
**cPanel → Select PHP Version → Extensions**.

Optional, only for network integration: outbound UDP 161 (SNMP), TCP 8728/8729
(MikroTik API), UDP 3799 (RADIUS CoA/DM). Many shared hosts block these; the
platform runs fully without them.

---

## 2. Upload and extract

1. Upload `nowills-isp-cpanel.zip` to your home directory via **cPanel → File
   Manager**.
2. Extract it. You should end up with `/home/USER/nowills-isp/`.
3. The directory that must become your website root is
   **`/home/USER/nowills-isp/public`** — not `nowills-isp` itself.

Set the document root in **cPanel → Domains** (or *Subdomains* / *Addon Domains*),
editing the domain's *Document Root* to `/home/USER/nowills-isp/public`.

> **If your host will not let you change the document root**, copy the contents of
> `nowills-isp/public/` into `public_html/` and edit `public_html/index.php`,
> changing both `__DIR__.'/../` paths to `__DIR__.'/../nowills-isp/`. Nothing else
> needs to change. Note that `php artisan storage:link` then has to be created
> by hand — see step 8.

---

## 3. Create the database

In **cPanel → MySQL Databases**:

1. Create a database, e.g. `cpaneluser_nowills`.
2. Create a user with a strong generated password.
3. **Add the user to the database** and grant **ALL PRIVILEGES** on that one
   database only.

Skip this step if your host already provisioned a database for you — just note the
name, user and password.

---

## 4. Configure the environment

In **File Manager**, inside `/home/USER/nowills-isp/`:

1. Copy `.env.production.example` to `.env`.
2. Replace every `CHANGE_ME`. The important ones:

```ini
APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-domain.com
APP_TIMEZONE=Africa/Accra

DB_HOST=          # usually localhost on cPanel
DB_DATABASE=cpaneluser_nowills
DB_USERNAME=cpaneluser_nowills
DB_PASSWORD=

NOWILLS_ORG_SLUG=your-isp-slug
NOWILLS_ORG_NAME="Your ISP Name"
NOWILLS_ORG_LEGAL_NAME="Your ISP Legal Name Ltd"
NOWILLS_ORG_COUNTRY=GH
NOWILLS_ORG_SUPPORT_EMAIL=support@your-domain.com

NOWILLS_ADMIN_EMAIL=admin@your-domain.com
NOWILLS_ADMIN_PASSWORD="..."   # 12+ chars, quoted; delete this line after step 6

MAIL_MAILER=smtp
MAIL_HOST=
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM_ADDRESS=billing@your-domain.com

RADIUS_SHARED_SECRET=         # openssl rand -hex 32
```

> **Quote any value containing `#`, a space or a comma.** In a `.env` file an
> unquoted `#` starts a comment, so `NOWILLS_ADMIN_PASSWORD=Pass#word1` is read as
> `Pass`. The installer refuses a password shorter than 12 characters precisely to
> catch this, but the same trap applies to every other value.

`APP_DEBUG=false` is not optional. With it enabled, any error page prints your
database credentials to the browser.

`.env` must never be readable over HTTP. It sits outside `public/`, and the
included `.htaccess` also denies it as a second line of defence.

---

## 5. Open a terminal

Use **cPanel → Terminal**, or SSH. Every command below runs from the application
directory:

```bash
cd /home/USER/nowills-isp
```

If your host has no terminal, ask them to run the commands in step 6, or use the
cPanel **Cron Jobs** entry from step 9 as a one-off by pointing it at
`php artisan migrate --force && php artisan db:seed --class=Database\\Seeders\\ProductionSeeder --force`.

---

## 6. Install

```bash
php artisan key:generate --force
php artisan migrate --force
php artisan db:seed --class=Database\\Seeders\\ProductionSeeder --force
php artisan storage:link
```

`ProductionSeeder` creates your organisation, the 8 roles and 58 permissions, the
billing catalog (service types, plans, taxes, add-ons), the chart of accounts and
**one administrator** from `NOWILLS_ADMIN_EMAIL` / `NOWILLS_ADMIN_PASSWORD`.
It creates no demo data — no sample customers, services or invoices.

It refuses to run without an admin password. That is deliberate: there is no
built-in default credential, because a published one would work on every
installation of this package.

> **Want the demonstration estate instead?** `php artisan db:seed --force` loads
> ~543 invoices, 60 customers and eight staff accounts with published passwords.
> Useful for evaluation. `DatabaseSeeder` **refuses to run while
> `APP_ENV=production`**, so an accidental run cannot put known credentials on a
> live system. Set `APP_ENV=local` first if you genuinely want the demo data.

Then remove the password from the environment:

```bash
sed -i 's/^NOWILLS_ADMIN_PASSWORD=.*/NOWILLS_ADMIN_PASSWORD=/' .env
```

---

## 7. Cache and optimise

```bash
php artisan config:cache
php artisan route:cache
php artisan event:cache
php artisan view:cache
```

Or simply `composer run-script nowills:optimize` if Composer is available.

**Re-run these after every code update and after any `.env` change.** A stale
config cache is the most common cause of "I changed .env and nothing happened".

---

## 8. Permissions

`storage/` and `bootstrap/cache/` must be writable by the web server user:

```bash
chmod -R 775 storage bootstrap/cache
find storage -type d -exec chmod 775 {} \;
```

If you copied `public/` into `public_html` instead of changing the document root,
create the storage symlink by hand:

```bash
ln -s /home/USER/nowills-isp/storage/app/public /home/USER/public_html/storage
```

---

## 9. Cron jobs

Add these two entries in **cPanel → Cron Jobs**. They replace the supervisor
daemons a conventional Laravel deployment would need.

**Scheduler** — every minute:

```
* * * * * cd /home/USER/nowills-isp && /usr/local/bin/php artisan schedule:run >> /dev/null 2>&1
```

**Queue drain** — every minute, exits when the queue is empty:

```
* * * * * cd /home/USER/nowills-isp && /usr/local/bin/php artisan queue:work --stop-when-empty >> /dev/null 2>&1
```

Use the full path to PHP (`which php` tells you; on many cPanel hosts it is
`/usr/local/bin/php` or `/opt/cpanel/ea-php84/root/usr/bin/php`).

The scheduler drives:

| Time | Task |
|---|---|
| every 5 min | `nowills:poll`, `nowills:alert-sweep` — ICMP/SNMP sweep and alert evaluation |
| every 10 min | `nowills:radius-sync` — pulls `radacct` / `radpostauth` |
| hourly :15 | `nowills:fair-use` — quota enforcement |
| 00:10 | `nowills:billing-cycle` — recurring invoicing |
| 01:30 | `nowills:dunning` — reminders, suspension, termination |
| 02:00 | `nowills:snapshot` — analytics rollups |
| 02:30 | `nowills:send-reports` — scheduled report delivery |
| 03:00 | `nowills:expire-vouchers` |
| 04:00 / 04:30 | `queue:prune-batches`, `model:prune` |

Without the scheduler cron entry, **no invoices are ever raised**. This is the
single most important step in the installation.

---

## 10. First login

Browse to `https://your-domain.com` and sign in with the administrator you
configured. You will be sent straight to the password change screen — change it
before doing anything else.

Then work through these pages:

| Page | URI | What to do |
|---|---|---|
| Company profile | `/settings/profile` | Legal name, tax ID, logo, invoice footer, bank details |
| Branches | `/settings/branches` | The seeder creates one head office; add the rest |
| Catalog | `/settings/catalog` | Plans, service types, taxes, add-ons, speed profiles |
| Payment gateways | `/settings/gateways` | Mobile money and card processors |
| Roles | `/settings/roles` | The 58 permissions across 8 roles |
| Users | `/settings/users` | Add staff and assign roles |
| Audit log | `/settings/audit` | Who did what |

Set your tax rates under **Catalog** before raising the first invoice.

---

## 11. FreeRADIUS and MikroTik

The full FreeRADIUS SQL schema is included and populated: `radcheck`, `radreply`,
`radgroupcheck`, `radgroupreply`, `radusergroup`, `radacct`, `radpostauth`, `nas`.

- Point FreeRADIUS's `sql` module at the **same database** the application uses.
- Set `RADIUS_SHARED_SECRET` to match the secret configured on every NAS.
- **RADIUS group names are namespaced per tenant** as `org<id>-<group>`, e.g.
  `org1-speed-50`. This is what keeps two tenants' subscribers from colliding on a
  shared NAS. If you are migrating an existing FreeRADIUS database, existing group
  names must be renamed to match — migration
  `2026_01_01_001600_namespace_radius_group_names` does this for rows written by
  this platform.
- CoA/DM disconnects are sent as real RFC 3576 UDP packets. Verify with
  `php artisan nowills:radius-sync` and by watching the FreeRADIUS log in debug
  mode.

For MikroTik, enable the API service on the router (port 8728, or 8729 for TLS)
and allow the web host's IP in the router's firewall.

---

## 12. Verify the installation

```bash
php artisan nowills:billing-cycle --no-interaction
php artisan nowills:dunning --no-interaction
php artisan nowills:snapshot --no-interaction
php artisan nowills:poll --no-interaction
```

Each should complete without error. Then check:

- [ ] Login works and redirects to the dashboard
- [ ] Dashboard charts render (inline SVG — no external assets)
- [ ] A customer can be created and a service activated
- [ ] An invoice can be raised and a payment recorded against it
- [ ] `/up` returns 200 (health endpoint)
- [ ] `storage/logs/laravel.log` shows no errors after the above

---

## 13. Updating

```bash
cd /home/USER/nowills-isp
php artisan down
# upload the new files, keeping .env and storage/
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache
php artisan up
```

**Always take a database dump first** — cPanel → phpMyAdmin → Export.

---

## 14. Troubleshooting

| Symptom | Cause and fix |
|---|---|
| 500 error, blank page | Check `storage/logs/laravel.log`. Almost always `storage/` is not writable, or `APP_KEY` is empty. |
| 404 on every page except the homepage | `mod_rewrite` is off, or the document root is not `public/`. |
| Directory listing instead of the app | The document root is the application directory, not `public/`. |
| "No application encryption key" | `php artisan key:generate --force` |
| SQLSTATE\[HY000\] \[2002\] | Wrong `DB_HOST` — on cPanel it is usually `localhost`, not `127.0.0.1`. |
| `.env` changes ignored | Config is cached. Run `php artisan config:cache` again. |
| No invoices appear | The scheduler cron is missing. See step 9. |
| Emails not arriving | `MAIL_MAILER=log` writes to `storage/logs` instead of sending. Set it to `smtp`. |
| `php artisan` not found | You are not in the application directory, or PHP is not on PATH — use the full path. |
| Queue jobs never run | The `queue:work --stop-when-empty` cron entry is missing. |

---

## 15. Support and licensing

NOWILLS ISP is a product of **Nowills Group**.

This package ships without the automated verification harness (`tools_test_*.php`)
used during development. The tenant-isolation, transactional-integrity and
financial-accuracy suites were run against this codebase before packaging; they
are available from Nowills Group on request.

**Unverified in this environment:** live CoA/DM packets against real NAS hardware,
MikroTik API sessions against physical RouterOS devices, SNMP polling of live
equipment, and actual email/SMS delivery. These require hardware and provider
accounts that a staging sandbox cannot supply.
