# 📚 TemseeEdu — Complete Deployment Guide

> Two models covered:
> - **SaaS Model** — You host everything; schools get `school.yourdomain.com`
> - **School Edition** — Each school hosts on their own server/domain

---

## Table of Contents

1. [Server Requirements](#1-server-requirements)
2. [SaaS Model — Plesk Setup](#2-saas-model--plesk-setup)
3. [SaaS — Environment Config](#3-saas--environment-config)
4. [SaaS — First Deploy Steps](#4-saas--first-deploy-steps)
5. [SaaS — Paystack Webhooks](#5-saas--paystack-webhooks)
6. [School Edition — Per-School Hosting](#6-school-edition--per-school-hosting)
7. [Provisioning a School (Both Models)](#7-provisioning-a-school)
8. [Queue Workers & Cron](#8-queue-workers--cron)
9. [Zero-Downtime Updates](#9-zero-downtime-updates)
10. [Common Issues](#10-common-issues)

---

## 1. Server Requirements

Confirm in **Plesk → PHP Settings**:

| Setting | Value |
|---|---|
| PHP Version | **8.3** or **8.4** |
| Extensions | `pdo_mysql`, `mbstring`, `openssl`, `bcmath`, `gd`, `zip`, `redis`, `intl`, `fileinfo`, `sodium` |
| `memory_limit` | `256M` |
| `max_execution_time` | `120` |
| `upload_max_filesize` | `50M` |
| `post_max_size` | `50M` |

Also needed on the server:
- **MySQL 8.0+** (DB user must have `CREATE DATABASE` privilege for tenancy)
- **Redis** (`apt install redis-server`)
- **Composer** (`composer --version`)
- **Node.js 20+** (`node --version`)

### OPcache

Enable PHP OPcache in production — without it, every request recompiles the full Laravel/Filament/module class tree from source, which is the single biggest avoidable performance cost on shared/VPS hosting. In `php.ini` (or your Plesk PHP settings panel):

```ini
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=256
opcache.max_accelerated_files=40000
opcache.validate_timestamps=0
opcache.jit_buffer_size=64M
```

`validate_timestamps=0` means OPcache won't notice new deploys on its own — restart PHP-FPM (or `service php8.3-fpm reload`) as part of every deploy when this is set.

---

## 2. SaaS Model — Plesk Setup

### Wildcard DNS
In **Plesk → Domains → yourdomain.com → DNS Settings**, add:

```
Type: A    Host: *    Points to: <server IP>
Type: A    Host: @    Points to: <server IP>
```

### Wildcard SSL
1. Plesk → **SSL/TLS Certificates** → Let's Encrypt
2. Tick **"Wildcard domain"** (requires DNS-01 challenge)
3. Covers `yourdomain.com` AND `*.yourdomain.com`

### Document Root
In **Plesk → Hosting Settings**:
- Document Root → `httpdocs/public`

### Nginx Additional Directives
**Plesk → Apache & Nginx Settings → Additional Nginx**:

```nginx
client_max_body_size 50M;

location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

location ~ /\.(?!well-known) {
    deny all;
}
```

---

## 3. SaaS — Environment Config

```bash
cp .env.example .env
nano .env
```

```dotenv
APP_NAME="TemseeEdu"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com

# Central Database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=temsee_central
DB_USERNAME=temsee_user
DB_PASSWORD=strong_password_here

# Tenant DB prefix (each school → temsee_tenant_schoolname)
TENANCY_DB_PREFIX=temsee_tenant_

# Redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

# Paystack — USE LIVE KEYS IN PRODUCTION
PAYSTACK_PUBLIC_KEY=pk_live_xxxxxxxxxxxxxxxxxx
PAYSTACK_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxx

# Mail (SMTP)
MAIL_MAILER=smtp
MAIL_HOST=smtp.yourprovider.com
MAIL_PORT=587
MAIL_USERNAME=noreply@yourdomain.com
MAIL_PASSWORD=your_mail_password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=noreply@yourdomain.com
MAIL_FROM_NAME="TemseeEdu"
```

---

## 4. SaaS — First Deploy Steps

```bash
# 1. Upload code (Git recommended)
cd /var/www/vhosts/yourdomain.com/httpdocs
git clone https://your-repo.git .

# 2. Install PHP dependencies
composer install --optimize-autoloader --no-dev

# 3. Install & build frontend
npm install
npm run build

# 4. Generate app key
php artisan key:generate

# 5. Run central migrations
php artisan migrate --force

# 6. Run all tenant migrations (for existing schools)
php artisan tenants:migrate --force

# 7. Link storage
php artisan storage:link

# 8. Set permissions
chmod -R 775 storage bootstrap/cache
chown -R www-data:www-data storage bootstrap/cache

# 9. Optimize caches
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan filament:cache-components
```

---

## 5. SaaS — Paystack Webhooks

In **Paystack Dashboard → Settings → API Keys & Webhooks**:

- **Webhook URL:** `https://yourdomain.com/paystack/webhook`
- **Events to enable:**
  - `charge.success` → creates new school tenant automatically
  - `charge.failed` → marks subscription `payment_failed`, emails owner
  - `subscription.disable` → suspends tenant subscription
  - `subscription.not_renew` → suspends tenant subscription

---

## 6. School Edition — Per-School Hosting

This is for schools that **provide their own domain and Plesk/cPanel hosting**.

### Architecture for a school's own domain

```
schoolname.com           → TemseeEdu landing / redirect
admin.schoolname.com     → Admin panel (Filament)
```

Or if they only have one domain, use a path proxy — but subdomain is cleaner.

### Steps

**1. Deploy the app** on the school's hosting using steps 2–4 above, but set:

```dotenv
APP_URL=https://schoolname.com
APP_DEBUG=false
```

**2. DNS on school's domain** (in their Plesk → DNS):
```
Type: A    Host: @      Points to: <school's server IP>
Type: A    Host: admin  Points to: <school's server IP>
```

**3. SSL** — Let's Encrypt in Plesk for:
- `schoolname.com`
- `admin.schoolname.com`

No wildcard needed — only one subdomain.

**4. Provision the school** (see Section 7 below)

**5. No Paystack registration needed** — the school bypasses the SaaS signup flow. The installer/provisioning command creates the local School Edition subscription record and syncs default integration blueprints.

### Shared cPanel hosting specifically

Most budget/shared hosting is cPanel, not a VPS — and cPanel mostly does **not** allow `CREATE DATABASE` from the app itself. If that's this school's hosting, the flow above (which assumes automatic tenant-database creation) will fail at the tenant-creation step. Use the cPanel-aware path instead:

1. **Create two MySQL databases in cPanel's "MySQL Databases" tool**, both owned by the same database user:
   - A **central** database (any name — holds platform-level tables: tenants, domains, settings).
   - A **tenant** database named exactly `{TENANCY_DB_PREFIX}{school-code}{TENANCY_DB_SUFFIX}`. This depends on which `.env.example` the install started from — they differ:
     - Deploying via `git clone` + `./deploy.sh --fresh` (or the web `/install` wizard on a fresh clone): uses the repo's own `.env.example`, where `TENANCY_DB_PREFIX=tenant` and `SUFFIX` is empty — a school code of `amezion` needs a database named `tenantamezion`.
     - Deploying a package built by `scripts/build-school-edition.sh`: that script blanks both `TENANCY_DB_PREFIX` and `SUFFIX` in the shipped `.env.example` — the same school code needs a database named just `amezion`.
     - Either way, the web installer's cPanel notice computes and displays the exact name live from whatever `.env` is actually in place — trust that over memorizing the rule above.
   - Add the database user to **both** databases with full privileges.
2. Either:
   - SSH/Terminal in and run `./deploy.sh --fresh --cpanel --repo=https://github.com/Linxford/TemseeEdu.git --dir=/home/USER/school` (adjust `--dir`) — clones, installs Composer dependencies, runs the interactive installer with cPanel hosting mode already selected (`--cpanel` now correctly implies `--hosting-mode=cpanel` for the installer step), builds assets, migrates, runs `temsee:cpanel-check --fix`, and sets up the cron scheduler, all in one command. Answer its prompts (school name, admin email, the two database names from step 1, etc.), or pass them all as flags for a fully non-interactive run — see `php artisan temsee:install --help`.
   - Or visit `https://yourdomain/install` in a browser — the wizard has a "Hosting Type" step; choosing "Shared cPanel hosting" shows the exact tenant database name to create (live, as you type the school code) and validates it exists before it lets the install proceed.
   - Or run `php artisan temsee:cpanel-install-school --domain=... --name=... --email=...` directly if the codebase is already deployed and you just need to (re)provision the tenant.
3. If you didn't use `./deploy.sh --fresh --cpanel` (which already runs this), run `php artisan temsee:cpanel-check --fix` after install — it verifies `.env` is in file-cache/sync-queue mode (no queue worker needed), all required writable directories and vendor/Livewire assets exist, and reports exactly what's missing rather than a blank 500 page.
4. **License activation requires `TEMSEEEDU_LICENSE_SERVER_URL` (and `TEMSEEEDU_LICENSE_PUBLIC_KEY`) to be set to the real vendor central server** before the school activates their key — `build-school-edition.sh` does not currently populate these in the shipped `.env.example`. Left blank, the School Edition install falls back to activating against its own domain, which 404s (`LicenseServerController::activate()` only runs on the vendor's own `edition=cloud` instance) — the school sees a generic "Activation failed" with no indication the real cause is a missing server-side env var. Set both explicitly in `.env` before handing off the install.

---

## 7. Provisioning a School

### ✅ Method A — Artisan Command (Recommended)

Use the built-in provisioning command:

```bash
php artisan temsee:provision-school \
  --subdomain=admin \
  --domain=admin.schoolname.com \
  --name="Accra Academy" \
  --email="principal@schoolname.com" \
  --plan=school_edition \
  --years=1
```

The command creates or updates the central subscription record, writes basic school profile settings into the tenant database, and syncs default integration blueprints. Tenant creation already triggers database creation, tenant migrations, tenant seeders, tenant storage setup, and module migrations.

Output:
```
─────────────────────────────────────────────
✅  School provisioned successfully
─────────────────────────────────────────────
  Admin URL  : https://admin.schoolname.com/admin
  Email      : principal@schoolname.com
  Password   : xK9mP2vQ4r  ← Save this now!
  Expires    : 2026-06-08 (1 yr)
─────────────────────────────────────────────
```

**Options:**
| Option | Default | Description |
|---|---|---|
| `--subdomain` | `admin` | Subdomain prefix |
| `--domain` | auto from APP_URL | Full domain override |
| `--name` | prompted | School/admin name |
| `--email` | prompted | Login email |
| `--password` | auto-generated | Leave blank for random password |
| `--plan` | `school_edition` | Plan label |
| `--years` | `1` | Subscription length |
| `--tenant-id` | same as subdomain | Tenant DB slug |

**Renew / extend a school:**
```bash
php artisan temsee:provision-school \
  --tenant-id=accra-academy \
  --plan=school_edition \
  --years=1
# → Will prompt to update subscription only
```

---

### Method B — Manual (Tinker)

If you need fine control:

```bash
php artisan tinker
```

```php
// Create the tenant (triggers DB create + migrate automatically)
$tenant = \App\Models\Tenant::create([
    'id'             => 'accra-academy',
    'admin_name'     => 'Principal Mensah',
    'admin_email'    => 'principal@accraacademy.edu.gh',
    'admin_password' => bcrypt('ChangeMe123!'),
]);

// Attach the domain
$tenant->domains()->create([
    'domain' => 'admin.accraacademy.edu.gh',
]);

// Create subscription record
\App\Models\Subscription::create([
    'tenant_id'                  => 'accra-academy',
    'plan_name'                  => 'school_edition',
    'paystack_subscription_code' => 'SCHOOL-EDITION-' . strtoupper(uniqid()),
    'paystack_customer_code'     => 'SCHOOL-CUST-' . strtoupper(uniqid()),
    'status'                     => 'active',
    'ends_at'                    => now()->addYear(),
]);
```

---

## 8. Queue Workers & Cron

### Cron (Plesk Scheduled Tasks)

Add in **Plesk → Scheduled Tasks**:

```bash
* * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1
```

### Queue Worker

**Option A — Cron-based (simplest for shared Plesk):**
```bash
* * * * * cd /path/to/app && php artisan queue:work --max-time=55 --stop-when-empty redis >> /dev/null 2>&1
```

**Option B — Supervisor (if available):**
```ini
[program:temsee-worker]
command=php /path/to/app/artisan queue:work redis --timeout=90 --tries=3
autostart=true
autorestart=true
user=www-data
numprocs=2
stdout_logfile=/path/to/app/storage/logs/worker.log
```

**Option C — Background nohup (quick and dirty):**
```bash
nohup php artisan queue:work redis --timeout=90 --tries=3 &
```

---

## 9. Zero-Downtime Updates

Run this after every code push:

```bash
#!/bin/bash
set -e

cd /path/to/app

echo "→ Pulling latest code..."
git pull origin main

echo "→ Installing dependencies..."
composer install --optimize-autoloader --no-dev

echo "→ Building frontend..."
npm ci && npm run build

echo "→ Running central migrations..."
php artisan migrate --force

echo "→ Running tenant migrations..."
php artisan tenants:migrate --force

echo "→ Clearing & rebuilding caches..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan filament:cache-components

echo "→ Restarting queue workers..."
php artisan queue:restart

echo "✅ Deploy complete."
```

Save as `deploy.sh`, `chmod +x deploy.sh`, run with `./deploy.sh`.

---

## 10. Common Issues

| Problem | Fix |
|---|---|
| 500 error after deploy | Check `storage/logs/laravel.log`; run `php artisan config:clear` |
| Subdomain not loading | Check DNS A record; check Plesk has SSL for that subdomain |
| `storage/` files not showing | Run `php artisan storage:link` |
| Queue jobs not running | Restart worker; check Redis is running: `redis-cli ping` |
| `Class not found` | Run `composer dump-autoload` |
| Tenant migration fails | Check DB user has `CREATE DATABASE` privilege |
| School portal locked/suspended | Check `subscriptions` table — `status` and `ends_at` |
| `APP_KEY` error | Run `php artisan key:generate` |
| Paystack webhook not firing | Check webhook URL in Paystack dashboard; ensure no firewall blocking POST |
| Session not persisting | Check Redis connection; set `SESSION_DRIVER=file` as fallback |

---

## Quick Reference — Most Used Commands

```bash
# Provision a new school (school edition)
php artisan temsee:provision-school --domain=admin.school.com --name="School" --email=admin@school.com --years=1

# Run all tenant migrations after update
php artisan tenants:migrate --force

# Rebuild all caches after update
php artisan config:cache && php artisan route:cache && php artisan view:cache && php artisan filament:cache-components

# Restart queue workers
php artisan queue:restart

# Check application key
php artisan key:generate --show

# Clear all caches (debug)
php artisan config:clear && php artisan cache:clear && php artisan view:clear && php artisan route:clear

# Link storage
php artisan storage:link

# Tail live logs
tail -f storage/logs/laravel.log
```
