# Axioma

PHP-based management system for an education center: students, teachers, groups, schedules,
tariffs/payments, and role-based dashboards. See [README.md](README.md) for local setup.

## Stack

Laravel (PHP 8.4), MySQL 8.0, Redis (sessions/cache), Docker/docker-compose. Composer-managed
(`laravel/composer.json`). The app used to be a strangler-fig migration running a custom PHP
framework and Laravel side by side; the legacy half was fully decommissioned once every route had
moved over — everything under `laravel/` is now the whole app.

## Request flow

`.htaccess` at the repo root serves real static files directly (`css/`, `js/`, `images/`) and
routes everything else to [laravel/public/index.php](laravel/public/index.php) — standard Laravel
front-controller dispatch from there. Every route lives in
[laravel/routes/web.php](laravel/routes/web.php) and runs under Laravel's own `web` middleware
group uniformly (sessions, CSRF, `$errors`/`old()`) — the earlier two-file split
(`legacy_bridge.php`/`legacy_bridge_web.php`, session-only vs. full `web` group) existed only
because authentication itself used to run on a separate bridged session; once auth moved onto
Laravel's own session (Phase 62), that split had no more reason to exist and both files were
merged into this one. URL paths still look like the original legacy ones (e.g.
`/group_admin_info.php?group_id=5`, `/admin/teacher_admin_info.php`) — they were preserved exactly
across the migration rather than redesigned, so bookmarks/links didn't need to change.

**Adding a route:** add the controller method, then register it in `laravel/routes/web.php`.

## Layered structure

```
laravel/app/Http/Controllers/   One class per resource, extends the (empty) base Controller
laravel/app/Models/              Eloquent models
laravel/resources/views/         Blade templates, mirror controller/resource names
laravel/app/Support/             Small per-consumer classes (SessionAuth, Iin, Weekdays, XlsxReader)
                                  — copied rather than shared across controllers in some cases,
                                  since each was ported independently; check for an existing copy
                                  before adding a new one
laravel/app/Services/            Cross-cutting logic (Gamification, AdviserTaskService, LessonSubmissionService)
laravel/app/Console/Commands/    Scheduled/CLI jobs (billing:charge-students, billing:send-payment-reminders)
scripts/                         One-off SQL migrations (not a migration framework — see below)
```

Roles: `admin`, `teacher`, `adviser`, `manager`, `parent`, `student`. Entry points per role are
listed in [README.md](README.md#roles) — note that `manager`'s dashboard URL doesn't currently
resolve to a real page (flagged, not yet fixed; see the README table). The authoritative
role→dashboard mapping is `AuthController::ROLE_DASHBOARDS` in
[laravel/app/Http/Controllers/AuthController.php](laravel/app/Http/Controllers/AuthController.php).

## Controller conventions

Controllers extend an empty base `Controller` — no shared helper methods (`render()`,
`verifyCsrf()`, etc. from the old framework don't exist here; use Laravel's own equivalents
directly). The dominant pattern, consistent across most controllers (see
[laravel/app/Http/Controllers/HolidayController.php](laravel/app/Http/Controllers/HolidayController.php)
for a representative example):

1. `SessionAuth::check()` — redirect to `/login.php` if not authenticated
2. an inline role guard (`if (! in_array(SessionAuth::role(), [...], true))`) — soft-redirect with
   a flash message on failure, not a hard 403, matching this app's established style
3. Laravel's own `$request->validate([...])` for pages that read Laravel's own `$errors`/`old()`
   bag, or a manual `Illuminate\Support\Facades\Validator::make(...)` + explicit
   `session()->flash('error', ...)` for pages whose view doesn't render that bag (check which the
   page you're editing actually does — using `$request->validate()` where the view doesn't display
   `$errors` silently swallows validation failures)
4. the actual DB work (Eloquent, or raw `DB::table()`/`DB::select()` for bespoke report-style
   queries that don't map cleanly onto Eloquent)
5. `session()->flash(...)` + `redirect(...)`

Follow the existing pattern in the controller you're editing rather than introducing a new one.

## Database

Eloquent, configured via [laravel/config/database.php](laravel/config/database.php) +
`laravel/.env`. No raw PDO singleton — use models or the `DB` facade.

There's no migration framework in the Laravel sense either — schema changes still ship as
numbered/described `.sql` files in `scripts/` (e.g. `add_category_to_expenses.sql`), applied
manually. When adding a column/table, add a new `scripts/*.sql` file rather than editing an
existing one, and mention the manual-apply step in the commit/PR.

## Auth & security

- `App\Support\SessionAuth` ([laravel/app/Support/SessionAuth.php](laravel/app/Support/SessionAuth.php))
  — a thin wrapper around Laravel's own `Auth` facade/session guard (Phase 62). `check()`/`id()`/
  `role()`/`user()` delegate straight to `Auth::`; `login()`/`logout()` are the only writers. Kept
  as a separate class (rather than switching every call site to `Auth::` directly) purely so its
  ~167 read-only call sites across the app stay untouched — there's no other reason to route
  through it now that it isn't bridging a separate legacy session anymore.
- `App\Models\User` is Laravel's stock `Authenticatable` model, with `getRememberTokenName()`
  overridden to return `''` — this app's 30-day remember-me cookie (`AuthController`'s own
  `bin2hex(random_bytes(32))` token + `remember_expires` column, entirely hand-rolled) manages
  `users.remember_token` itself, so Laravel's native remember-token cycling is disabled to avoid
  the two fighting over the same column. Laravel's native remember-me (`Auth::login($user,
  remember: true)`, ~5-year cookie, rotates on every use) is deliberately not used — different
  semantics than the existing 30-day hard cutoff.
- Cookie is scoped to `.axioma-study.kz` in production so the session is shared across subdomains
  — `game.axioma-study.kz` (where students land after login) is the same app under a different
  vhost, and needs the same session cookie to keep them logged in. Controlled by
  `SESSION_COOKIE_DOMAIN` (blank for local dev), read by `laravel/config/session.php`'s `domain`
  key.
- CSRF: Laravel's own `VerifyCsrfToken` middleware (part of the `web` group, applied uniformly to
  every route) + `@csrf` in real forms, or an `X-CSRF-TOKEN` header (read from the
  `<meta name="csrf-token">` tag in `layouts/app.blade.php`) for JS-only `fetch`/`$.ajax` call
  sites with no backing form.
- Passwords: `password_hash()`/`password_verify()` via Laravel's `Hash` facade (`Auth::attempt()`
  for login).
- All queries are parameterized (Eloquent/query builder) — never interpolate user input into SQL.

## Validation

Laravel's own `$request->validate([...])` / `Illuminate\Support\Facades\Validator::make(...)` —
see Controller conventions above for which one to use depending on the page. Error messages are in
Russian (the whole UI is Russian-facing) — match that when adding new rules or flash messages.

## Views

Blade templates under `laravel/resources/views/{resource}/{action}.blade.php`, extending
`layouts/app.blade.php` for shared chrome (header/sidebar/footer, via `HeaderComposer`) and using
Bootstrap 5 classes.

## Tests

`laravel/tests/Unit` + `laravel/tests/Feature` (PHPUnit). Coverage is thin — mostly framework
scaffolding plus `Unit/Support/WeekdaysTest.php` (ported from the old framework's test suite when
it was decommissioned; the old suite's `ValidatorTest` had no Laravel-side equivalent to port to,
since Laravel's own validator replaced the custom one it tested — that coverage was dropped, not
silently, see the decommission commit). CI ([.gitlab-ci.yml](.gitlab-ci.yml)) only runs `php -l`
lint + a Docker build, not the test suite. When adding non-trivial logic to a Model or Support
class, prefer adding a test over relying on manual verification.

## Timezone

Business operates in `Asia/Almaty` (UTC+5). Set via `laravel/config/app.php`'s `timezone` key —
don't call `date_default_timezone_set()` elsewhere.

Scheduled jobs (daily billing, payment reminders) run via Laravel's scheduler
([laravel/bootstrap/app.php](laravel/bootstrap/app.php)'s `->withSchedule()`), fired by a
production crontab entry invoking `php artisan schedule:run` every minute inside the app
container.

## Related repo

The `bot` service in [docker-compose.yml](docker-compose.yml) builds from a sibling checkout of
`teacher_reminder_bot` (path configurable via `BOT_BUILD_CONTEXT`) — not part of this repo.
