# Business Processes & Requirement -> Service Mapping

Reference notes distilled from the legacy Yii2 monolith at `/var/www/html/ains`.
Used to keep service boundaries aligned with real processes. See `docs/PLAN.md`
for architecture and `docs/STATUS.md` for progress.

---

## 1. Actors / roles

`frontend_user.role` in {`murid`, `teacher`, `ppd`, `jpn`, `kpm`}.

- **murid** — student: logs reading records, creates mybooks.
- **teacher** — approves student records (single + batch), approves mybooks.
- **ppd** — district officer: scoreboards/statistics scoped to PPD.
- **jpn** — state officer: scoped to state.
- **kpm** — national: everything.

Scope is `state -> ppd -> school`, resolved from `frontend_user` + `school`.

## 2. Identity & access (login)

1. Login via **Google OAuth** (`GoogleAuthController`).
2. Callback enriches the user from **IAM Delima** (`GET /user/{email}`,
   header `X-DELIMA-TOKEN`): role text, organization (school), classroom.
3. Find/create `frontend_user` (`username = email`, `provider = google.com`,
   default role `murid`), copy state/ppd/school/class.
4. Session today; target is stateless **JWT** (HttpOnly cookie for SSR/HTMX,
   `Bearer` for Vue/API).

Legacy role→redirect: `/web/dashboard` (student), `/web/{role}-dashboard`.

## 3. Reading records (NILAM core)

`reading_record` = **one student reading entry**, `record_type` in
{`book`, `digital`, `others`}; fields: title, author, isbn, publisher, year,
language, total_page, rating, summary, lesson, cover image, `record_book_id`
(optional link to catalog `book`).

**Submit** (`ReadingRecordController::actionSaverecord`):
1. Create record with `record_approval_status = 'pending'`.
2. Secure cover upload -> **GCS** (`uploads/record/<random>.<ext>`; ext + MIME +
   5 MB checks).
3. Rate limits: **>= 3 minutes** since last record; **<= 50 records/day**.
4. On save, within one transaction:
   - `ReadingRecord::updatepoints()` -> `record_points`.
   - `FrontendUser::updatetotal()` -> `frontend_user.total_books/total_points`.
   - `ReportStudentRecord::updaterecord()` -> per-student snapshot.

**Points** (`updatepoints`): `record_points = category_weight × page_points`.
- category weight from `ref` (`BOOK_CAT`/`DIGITAL_CAT`/`JURNAL_CAT`.`other_val`).
- page points by `total_page`: `>100 -> 10`, `51-100 -> 7`, `26-50 -> 5`,
  `11-25 -> 3`, `1-10 -> 1`.

**Teacher batch entry** (`actionTeachersaverecord`): teacher creates records for
many `student_list` at once with `record_approval_status = 'approved'`
immediately.

**Approve/reject** (`actionUpdatestatus`): set status + `record_approval_at/by`,
optional comment; **rejected -> `record_points = 0`**; recompute user totals.

**Historical totals** (`reading_record_v1`): imported per-student, per-year totals
(`email`, `updated_at`=year, `total_record`). Merged with live `reading_record`
when computing totals/snapshots.

## 4. Per-student snapshot & reading reporting

- **`report_student_record`** — denormalized per-student snapshot
  (student + school name/code/type/ppd/state + `total_record_current/past/all`).
  Recomputed by `updatereport()`/`updaterecord()` on record writes and by the
  nightly `GenerateReportController::actionStudentRecord` (also writes
  `report_student_record_log`).
- **`total_student`** — denominators by state/PPD/type (SK/SMK).
- **Aggregates** — `report_student_active_by_state/ppd`,
  `report_average_record_by_state/ppd`. Regenerated by
  `frontend/GenerateReportController` **delete + insert per year/month**.
- **Scoreboards / statistics** pages read these summary tables.
- **Exports** — `report_code` rows whose `content` is `eval()`'d
  (`DownloadReportController::actionDownload`). **Dangerous, must be replaced.**

## 5. Library / MyBook

`mybook` = **student-created ebook** (title, summary, language, pages, cover
image, PDF -> GCS).

**Approval** (`MybookController::actionUpdatestatus` + `SiteController`):
approved -> creates a catalog `book` row:
`type = 'student-creation'`, `url_type = 'upload'`, `file_url = mybook_file_url`,
`cover_img = mybook_cover_img`, `author = student name`, `publisher = 'AINS'`,
`page_slug = UUID`, `status = 'active'`; `book.mybook_id` links back. Rejected
with comment.

`book` = published catalog (admin reading materials, ebooks, external links,
`url_type`, `page_slug`). Ebooks read via **FlowPaper**; PDF served through
`/ebook/proxy` (GCS signed URL).

## 6. Chatbot

- Q&A: `chat_question`, `chat_answer`, `chat_answer_bot`
  (`php_status = 1` -> `answer_bot_description` is `eval()`'d; **replace**),
  `chat_common_question`, `chat_common_word`, `chat_question_tag`,
  `chat_suggestion`, `chat_greeting`, `chat_menu`.
- Live chat: `chat_live`, `chat_user`.

## 7. Content publishing

Admin CRUD for: `content_article` (+`_translation`), `content_announcement`,
`content_application` (+`_translation`), `content_slider` (+`_translation`),
`content_downloads`, `content_video`, `content_photo_*`, `content_calendar`,
`content_poll`, `frontend_menu` (menus).

## 8. Media

All uploads/PDFs/covers via **Google Cloud Storage** (`GcsHelper` ->
flysystem `gcs` component): `writeStream` to upload, `temporaryUrl` for signed
URLs; `/ebook/proxy` streams PDFs. Objects under `uploads/...`.

## 9. Dangerous `eval()` ports (must be redesigned)

| legacy | location | replacement |
|---|---|---|
| report downloads | `DownloadReportController.php:13` (`report_code.content`) | record-svc async export templates |
| public JSON API | `SiteController.php:145` (`api.api_content`) | explicit Go endpoints |
| chatbot answers | `ChatController.php:494` (`answer_bot.php_status`) | safe placeholder/template expansion |

## 10. Dropped (out of scope)

- Dynaweb **page/block builder**: `frontend_page`, `frontend_content*`,
  `frontend_layout*`, `frontend_header/footer`, `frontend_theme`,
  `uiux_builder`, `grape_component`, `module_repository`. Pages become
  code-defined Go routes + templates.
- Legacy **`record`** table (millions of rows, imported from the older system) —
  **deferred**, not part of the rewrite yet.

---

## Requirement -> service mapping

| # | Business process / requirement | Service | Owns |
|---|---|---|---|
| 1 | Login, Google OAuth + IAM Delima, users, schools, ref, roles, profile | **auth** | `frontend_user`, `school`, `ref`, `auth_*`, `frontend_role_mapping` |
| 2 | Log reading (book/digital/others), rate limits, points, teacher batch, approval, **student totals + scoring config** | **record** | `reading_record`, `reading_record_v1`, `total_student`, `student_record_total`, record scoring config |
| 3 | Per-student totals/snapshot, reading scoreboards, state/PPD aggregates, exports | **record** | `report_student_record(+_log)`, `report_average_record_by_*`, `report_student_active_by_*` |
| 4 | Catalog `book`, student `mybook` + approval -> book, ebook read/proxy | **library** | `book`, `mybook`, `mybook_chapter` |
| 5 | Chatbot Q&A, tags, suggestions, live chat | **chatbot** | `chat_*` |
| 6 | Articles/news, announcements, sliders, downloads, galleries, menus + content reporting | **content** | `content_*`, `frontend_menu`, `data_dashboard` |
| 7 | GCS upload, signed URLs, `/ebook/proxy` | **pkg/media** (shared lib) | — |
| 8 | Nightly aggregation, async exports, notifications | **worker** | — |
| 9 | SSR/BFF, routing, JWT verify, RBAC+scope guard, legacy reverse-proxy | **edge** | — |

## Boundary rule

Tightly-coupled domain/process lives in **one** service.

- **Within a service:** direct SQL on its own tables (shared `ains` DB).
- **Between services:** REST/JSON or NATS events only — **no cross-service SQL**.
- Cross-domain read data is denormalized locally at write time (e.g.
  `report_student_record` copies school/state/ppd so record reporting never
  calls auth).

## Resolved design decisions (cross-cutting)

1. **Reading totals move to record-svc.** `frontend_user.total_books/total_points`
   is written by the record process; it becomes a **record-owned** table
   (`student_record_total`) so the summary the user sees is served fast from
   record-svc (same process, direct SQL, no auth call). Auth no longer owns
   totals.
2. **Scoring rules move to record-svc.** The points config (currently `ref`
   `BOOK_CAT`/`DIGITAL_CAT`/`JURNAL_CAT`.`other_val` x page brackets) becomes
   record-svc's own config, seeded from those ref rows. No per-insert read of
   auth's `ref`.
3. **`report_student_record` sits in record-svc.** It is the denormalized
   per-student snapshot (student + school name/code/type/ppd/state +
   `total_record_current/past/all`), written on the record write path so record
   reporting and the student/teacher dashboards are served without cross-service
   calls. Same for `report_student_record_log`, `total_student`,
   `report_average_record_by_*`, `report_student_active_by_*`.

## Frontend page -> service mapping (site 20, legacy slugs)

All pages become Go routes + templates on **edge**; **edge** owns SSR/routing and
composes data by calling the owning service (API) or rendering directly for
same-service pages.

**auth** (identity / profile / directory)
`forgot-password`, `auth-failed`, `view-profile-{student,teacher,ppd,jpn,kpm}`,
`update-profile-{student,teacher,ppd,jpn,kpm}`,
`{teacher,ppd,jpn,kpm}-student-list` (student directory, scoped).

**record** (reading records + reading reporting/scoreboards)
- Student: `dashboard`, `student-record-list`, `student-record-details`,
  `new-record-book`, `new-record-digital`, `new-record-article`.
- Teacher: `teacher-dashboard`, `teacher-record-list`, `teacher-record-details`,
  `teacher-record-approved-list`, `teacher-record-pending-list`,
  `teacher-new-record-book`, `teacher-new-record-digital`,
  `teacher-new-record-article`.
- Officer record: `{ppd,jpn,kpm}-record-list`, `{ppd,jpn,kpm}-record-details`.
- Scoreboards: `scoreboard-{student,class,school}`,
  `teacher-scoreboard-{class,school,state,national}`,
  `{ppd,jpn,kpm}-scoreboard-{student,school}`.
- Statistics: `{teacher,ppd,jpn,kpm}-statistics`.
- Reports: `{teacher,ppd,jpn,kpm}-report-list`,
  `*-report-student-record`, `*-report-student-low-record`,
  `*-report-average-record*`, `*-report-student-active-*`,
  `generate-report` (regenerate aggregates; scheduled via worker).

**library** (catalog + mybook + ebook)
`mybook`, `mybook-details`, `mybook-form`,
`teacher-mybook-{list,details,form}`,
`student-mybook-{list,details}` (teacher viewing) / `jpn-mybook-{list,details}`,
`reading-material-book-{list,details}`,
`reading-material-student-creation-{list,details}`, `book-generator`.

**content** (publishing + content reporting)
`home`, `home2`, `article`, `feed`, `reading-center`, `under-maintenance`,
`test`, `reading-material-news-{list,details}` (articles),
`reading-material-multimedia-{list,details}` (video), support/FAQ.

**chatbot**: no page route — embedded widget (JSON API + WebSocket).

**worker**: no page route — scheduled aggregation/exports/notifications.

## Frontend flows (evidence)

- **Student record save** (`ReadingRecordController::actionSaverecord`): pending
  record -> GCS cover -> 3-min/50-per-day limits -> `updatepoints` +
  `updatetotal` + `ReportStudentRecord::updaterecord` (all record-svc).
- **Teacher bulk save** (`actionTeachersaverecord`): one form, `student_list[]`,
  creates one **approved** record per student with the same points/totals/snapshot
  updates (all record-svc). Student picker (`*-student-list`) is auth data via
  API.
- **Approval** (`actionUpdatestatus`): approve/reject (+comment); reject zeroes
  points; recompute totals (record-svc).
- **Student dashboard** (page 124, block 270): reads records + totals
  (`total_books`) + `report_student_record` + user/school; served from record-svc
  snapshot (+ auth profile for photo/name via edge).
- **Teacher dashboard** (page 123, block 266): records + `total_points` + school.
- **Record lists** (129/151/153/154): `reading_record` filtered by student/scope.
