# KCU Digital — King Ceasor University Platform

A Laravel monolith covering the public university website, the applicant portal,
online admissions and the administrative back office.

Built to the KCU Digital Technical PRD v1.0. KCU is the first implementation, but
institutional identity, programmes, intakes, fees and content all live in the
database rather than in code, so the same engine can serve another university.

- **Stack:** PHP 8.3 · Laravel 13 · MySQL 8 · Blade + Livewire 4 · Tailwind CSS 4 · Alpine.js
- **Brand:** deep green `#0B6232` with gold `#FFC66B`, taken from kcu.ac.ug
- **Currency:** UGX · **Timezone:** Africa/Kampala

---

## Quick start

```bash
composer install
npm install

cp .env.example .env
php artisan key:generate

php artisan migrate --seed
npm run build

php artisan serve          # http://127.0.0.1:8000
php artisan queue:work     # notifications + PDF generation
```

### Development sign-in

Seeded accounts, password `password` for all of them. **Replace these before production.**

| Role                    | Email                      |
| ----------------------- | -------------------------- |
| Super Administrator     | `admin@example.test`       |
| Admissions Manager      | `manager@example.test`     |
| Admissions Officer      | `officer@example.test`     |
| Finance Officer         | `finance@example.test`     |
| Programme Administrator | `programmes@example.test`  |
| Content Editor          | `editor@example.test`      |

Applicants register themselves at `/applicant/register`.

### Database

`.env.example` targets **MySQL 8**, which is the production target. For local work
without a MySQL server, SQLite runs the whole suite unchanged:

```dotenv
DB_CONNECTION=sqlite
```

```bash
touch database/database.sqlite && php artisan migrate:fresh --seed
```

Migrations use the schema builder throughout and run on both engines.

---

## Architecture

Controllers stay thin. Business logic lives in actions and services:

```
Controller → Action → Service → Model → Database
```

```
app/
├── Actions/           Admissions/, Applications/   one job per class
├── Console/Commands/  scheduled tasks
├── Enums/             the domain vocabulary + the status machine
├── Http/
│   ├── Controllers/   Admin/, Applicant/, Auth/, PublicSite/
│   └── Middleware/    active, staff, permission
├── Jobs/              queued PDF generation
├── Livewire/          Applicant/  the application wizard, one component per step
├── Models/
├── Notifications/     one class per applicant-facing event
├── Services/
│   ├── Admissions/    status machine, eligibility, reference numbers
│   ├── Documents/     private upload handling
│   ├── Notifications/ portal/email/SMS fan-out, SMS provider abstraction
│   ├── Payments/      gateway interface + providers + PaymentService
│   └── Pdf/           admission letter, joining instructions
└── Support/           helpers, Ugandan reference data
```

### Key design decisions

**One status machine.** `ApplicationStatus` owns the legal transitions;
`ApplicationStatusService` is the only place status changes. Every transition writes
to `application_status_history` and `audit_logs`, and notifies the applicant when the
new state is one they should hear about. Internal review states (shortlisted,
recommended) are deliberately invisible to applicants.

**One definition of "ready to submit."** `ApplicationValidator` produces the checklist
that the wizard renders *and* that `SubmitApplication` enforces, so an applicant is
never told they are ready and then refused.

**Payments are never trusted from the browser.** A payment settles only via a verified
provider response, a signature-checked webhook, or an explicit act by a Finance Officer.
Webhook events are persisted with a unique `(provider, event_id)` so a retried
notification cannot settle a payment twice, and an amount lower than the fee is refused.

**Documents are private.** Uploads are sniffed by content (not extension), stored on the
`private` disk under a random filename, and served only through an authorised controller.

**Reference numbers are transactional.** `KCU-2026-000001` and `KCU/2026/000001` are drawn
from a locked counter row, never from an auto-increment id.

**Permissions, not role checks.** 32 permissions across 6 roles; every permission is
registered as a Gate and available in Blade as `@permission('applications.review')`.

---

## Testing

```bash
php artisan test
```

87 tests / 237 assertions, covering authentication, the full application lifecycle,
payments (including duplicate-webhook and underpayment protection), admissions and PDF
generation, every role's authorization boundaries, security (document access, upload
validation, privilege escalation, rate limiting), and that every page renders.

---

## Production deployment

Ubuntu · Nginx · PHP-FPM · MySQL · Supervisor · Cron.

```bash
# 1. Code
cd /var/www/kcu
git pull origin main
composer install --no-dev --optimize-autoloader
npm ci && npm run build

# 2. Database
php artisan migrate --force

# 3. Cache
php artisan optimize
php artisan queue:restart
```

**Nginx** must serve from `/var/www/kcu/public` — never the project root.

**Queue worker** (Supervisor keeps it alive):

```ini
[program:kcu-worker]
command=php /var/www/kcu/artisan queue:work --queue=notifications,documents,default --sleep=3 --tries=3 --max-time=3600
directory=/var/www/kcu
autostart=true
autorestart=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/kcu/storage/logs/worker.log
stopwaitsecs=3600
```

**Scheduler** (one cron entry drives everything in `routes/console.php`):

```cron
* * * * * cd /var/www/kcu && php artisan schedule:run >> /dev/null 2>&1
```

**Permissions:**

```bash
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache
```

`storage/app/private` holds applicant documents and generated letters. It must never be
inside the web root and should be included in backups.

### Go-live checklist

- [ ] `APP_ENV=production`, `APP_DEBUG=false`, `APP_KEY` generated
- [ ] `SESSION_SECURE_COOKIE=true` and HTTPS enforced
- [ ] Seeded `@example.test` accounts deleted; real staff accounts created
- [ ] SMTP configured and a test email delivered
- [ ] Payment provider credentials and `PAYMENT_WEBHOOK_SECRET` set; webhook URL registered
- [ ] Institutional settings reviewed in **Admin → Settings** (bank details, fees, contacts)
- [ ] Tuition figures confirmed with the Finance Office (seeded values are placeholders)
- [ ] Supervisor worker running; `schedule:run` cron installed
- [ ] Daily off-server database backup verified by restoring it

---

## Configuration

Institutional values are database-driven and editable at **Admin → Settings**: university
name, tagline, motto, contacts, social links, brand colours, application fee, currency,
number prefixes, bank details, and the email/SMS toggles.

Deployment-level settings that must be known before the database is available live in
`config/kcu.php`: number formats, declaration version, upload limits and provider selection.

Swapping providers is configuration, not code — `PaymentGatewayInterface` and
`SmsServiceInterface` are the seams.

---

## Attribution

Content, brand colours, logo, programme names and the Vice Chancellor's message are
King Ceasor University's own, taken from [kcu.ac.ug](https://www.kcu.ac.ug/).

Tuition amounts are **indicative placeholders** — KCU does not publish fees publicly.
Correct them in Admin → Programmes before launch.
