# PRD — Principals (Principal Analysis) Module  ·  **Phase 1 (legacy-derived + DB-verified)**

> **Status of this document:** as of **2026-07-24** this PRD is **derived from the real legacy PHP**
> (the example folder) and **DB-verified** (`SHOW CREATE TABLE` over the whole `principal` family).
> The business rules below trace to a named legacy file or the live schema. What remains OPEN is
> called out explicitly (menu IDs, the `file_uploads` sub-folder name, and the external
> NetSuite/AST integrations — deferred by decision). Wiring proceeds per the §6 roadmap.

---

> ## ⛔ ATURAN ABSOLUT #0 — LEGACY FILES ARE **NEVER** COMMITTED TO GIT
>
> **The legacy Principal-Analysis PHP files must NOT be pushed into git, NO MATTER WHAT.**
>
> - The legacy PHP reference screens live in **the example folder** —
>   `Pages/MenuPrincipals/Just An Example, Do not Link/` — pushed from the user's Mac. They stay
>   **LOCAL ONLY, FOREVER**.
> - **NEVER `git add`** anything under that folder — not `git add .`, not `git add -A`, not any
>   bulk stage. Before **every** commit touching this module, run `git status` and verify **zero**
>   legacy `.php` files are staged.
> - **Files, not knowledge:** summarize a legacy rule in your own words and cite its filename in the
>   README/PRD, but NEVER paste verbatim legacy PHP (whole functions, SQL as-written) into those
>   committed docs — that commits exactly what must stay local-only.
> - **Hard guard — VERIFIED 2026-07-24:** `ColorindoChemtraInertia/.gitignore` carries
>   `/resources/js/Pages/MenuPrincipals/Just An Example, Do not Link/` (`git check-ignore` OK).
>   Do not weaken or remove it.
> - If a legacy file is ever found staged or committed: STOP, tell the user immediately, and wait.

> ## ⚠️ ATURAN ABSOLUT #0b — PARALLEL-SESSION EDIT GUARD
>
> The user runs several Claude sessions on this repo at the same time, on ONE shared git tree + ONE
> dev DB + ONE build bundle. **Before editing any EXISTING file**, check its mtime via Bash
> (`date && stat -c '%y  %n' <files>` or `find app routes database resources/js tests -type f
> -mmin -30`) and **list to the user every target modified < 30 minutes ago** — unless its latest
> change is THIS session's own edit. ≥ 30 min → OK. New files: `ls`/`git status` the exact target
> path immediately before writing. ⚠️ The Principals **proto** pages in `Proto/Principals/` are
> actively editable by the user — re-check before every copy/port. Full rules: `README.md` §1.

---

## 1. Document Metadata

| Field | Value |
|---|---|
| **Title** | PRD — Principals (Principal Analysis) Module — Phase 1 (legacy-derived) |
| **Module** | "Principals" — the **Principal Analysis** report/view. A **read-mostly** module over the `principal` master + its NetSuite linkage, with a per-principal analysis detail (contacts + FAQ + documents + generated CC-system / NetSuite / AST report sections). Legacy menu label / prefix "AST" (`astlistpm.php` list → `astlistpmedit.php` detail). |
| **Status** | **Phase 1 (legacy-derived docs + DB-verify) — 2026-07-24.** Routes/controller/policy/tests/menu/build still TODO (Phase 2+). |
| **Owner** | IT — PT Colorindo Chemtra |
| **Date** | 2026-07-24 |
| **Stack** | Laravel 13.8 / Inertia.js v3 / React 19.2 / Tailwind v4 / MySQL 8.4 |
| **Document Type** | Legacy-derived + DB-verified PRD. Screens/rules summarised from the example-folder PHP; data model confirmed by `SHOW CREATE TABLE`. External-source report sections are **inventoried but deferred** (see §2.3 / §6). |

### Locked decisions
**From 2026-07-24 (Phase 0):**
1. **Structure:** group `Pages/MenuPrincipals/`, one stage subfolder `AnalysisAst/` holding the two proto pages. Render prefix (when wired) `MenuPrincipals/AnalysisAst/<Page>`. Proto filenames kept.
2. **Example folder = gitignored**, the user pushes the legacy PHP there (now present).
3. **Design source = `Proto/Principals/`** — follow it exactly; it is FROZEN (README rule 10).

**From 2026-07-24 (Phase 1 fleshing-out — user rulings):**
4. **External sources DEFERRED.** The **NetSuite** sections (×5), the **AST** sections (×2) and the **Export-Ratio Excel** query a live NetSuite REST API and a **separate AST/PostgreSQL ERP** that do **not** exist in this Laravel stack. They are **NOT ported now** — rendered as disabled "coming soon" stubs (exactly the proto's current Generate stubs) and tracked in a GitHub issue. Nothing external is faked. Only **local-MySQL** sections are wired now.
5. **Scope = BOTH menus.** Port **two** list screens as two `rolemenu` leaves: (a) **Principal Analysis** — per-user scoped to the caller's head-division principals (`userprincipal.IsHeadDiv=1`), the legacy `astlistpm.php`; and (b) **All Principal Analysis** — every principal, the legacy `astlistallpm.php`. Same design, same Detail target.
6. **Red-row rule aligned to legacy.** A row is "not linked to NetSuite" (**red**) **iff `PrincipalNSCode` AND `PrincipalNSBrand` are both empty**; a soft-deleted row is a **separate** state (legacy pink; our design keeps the "Deleted" badge). The frozen proto (which reddened on `nsCode` alone) is corrected to this on wiring.

---

## 2. Overview, Screens & Report Inventory (legacy-derived)

**Principal Analysis** lets a PM/head-division user look up a **principal** (the brand owner Colorindo
represents) and open an **analysis detail**: the principal header, its contact persons, an FAQ
knowledge base, uploaded documents, and **generated reports** rolling up this principal's activity
across the CC system (Sample Import / Sample Order / Quotation / LWR / Visit) plus **NetSuite** and
the legacy **AST ERP** (both external). It is **read-mostly** — the only writes are contact & FAQ
CRUD and (legacy) business-card / document uploads. The principal **master** itself is edited in a
*different* menu (`listprincipallogistic.php`), confirmed by the dead `btn-update`/`btn-delete`
handlers in `astlistpmedit.php` that redirect there.

### 2.1 Search — two screens (legacy `astlistpm.php` + `astlistallpm.php`)

Both render the same grid via a shared list partial (`astlistpmview.php` / `astlistallpmview.php`):

- **Columns:** `No` · `View` (→ Detail) · **PrincipalCode** · **PrincipalNSCode** · **PrincipalNSBrand** · **PrincipalName** · **PrintName** (= `SalesPrincipalName`). Order by `PrincipalName ASC`.
- **Search fields (legacy = 5 separate `LIKE '%..%'` inputs):** Code, Netsuite Code, Netsuite Brand, Name, Print Name, plus an **Isdeleted** checkbox. *(The frozen proto collapses the 5 into one box that matches any of the five — house list-grammar; keep it.)*
- **Soft-delete toggle:** off → `WHERE IsDeleted = 0`; on → `IsDeleted IN (0,1)`.
- **Row colour (verified `astlistpmview.php`):** `IsDeleted=1` → **pink**; else `PrincipalNSCode=='' AND PrincipalNSBrand==''` → **red** ("not linked to NetSuite"); else normal. Footer note: *"Principal will be red if it's not linked to Netsuite."*
- **Pagination:** legacy server-side, items-per-page 10/20/50/100 (session-persisted). *(Proto paginates client-side since it loads all rows for the menu.)*
- **The difference between the two screens = the scope join only:**
  - **Principal Analysis** (`astlistpm`): `principal i INNER JOIN (SELECT PrincipalID FROM userprincipal WHERE IsHeadDiv=1 AND isdeleted=0 AND UserID = <current user>) up ON up.PrincipalID = i.ID`. Only principals the caller heads. **No admin bypass** — a user with no `userprincipal` rows (e.g. `administrator`) sees an empty list.
  - **All Principal Analysis** (`astlistallpm`): `FROM principal i` with **no** `userprincipal` join — every principal.

### 2.2 Detail — "View Principal - Analysis" (legacy `astlistpmedit.php?ID=`)

Header (read-only unless noted), from `principal` by ID: **ID** · **AST Code** (`PrincipalCode`) · **Netsuite Code** (`PrincipalNSCode`) · **Netsuite Brand** (`PrincipalNSBrand`) · **Name** (`PrincipalName`) · **Print Name** (`SalesPrincipalName`). Then, in order:

1. **Analysis Period + Export Ratio Excel** — period is legacy **multi-checkbox** (3M / 6M / 1Y / Last Year / Custom `StartDate`≥2020-01-01 & `EndDate`); Export posts to `exportratioforecast.php` keyed on `PrincipalNSBrand` + the checked periods. **DEFERRED** (NetSuite-dependent — §2.3). *(Proto shows single-select pills; period selection is moot until export is un-deferred.)*
2. **Address (Contacts)** — legacy iframe `listprincipalcp1.php`; data = `principalcp` (see §5 write rules).
3. **FAQ** — legacy iframe `faqperprincipal.php`; data = `principalfaq`.
4. **CC System Reports** (local MySQL — **portable**): Sample Order Import, Sample Order, Quotation, LWR, Visit Report — §2.3.
5. **NetSuite Integration** (external — **deferred**): Princ-Analysis, Import, Booking, Import-vs-SO, Stock Barang.
6. **AST Integration** (external Postgres — **deferred**): Princ-Analysis, Import.
7. **Documents** ("Details Princ-DocumentDownload") — `documentupload ⋈ documentcategory`; **already built** in Laravel (`PrincipalDocumentController` / `PrincipalDocumentDownloadController`, `documents/principal/*`). The Detail lists the principal's `IsDeleted=0` documents and links the existing **gated** download.

### 2.3 Report inventory (source of truth for §6 Phase 4/6)

| # | Detail section | Legacy file | Source / linkage to principal | Period? | Port |
|---|---|---|---|---|---|
| CC1 | Detail Sample Order Import | `listsoprincipal.php` | `samplerequestimportdetail ⋈ samplerequestimport` where `sr.PrincipalID = ID` (+ users/satuan/delivery/urgency/from), `IsDeleted=0` | none (all-time) | ✅ local |
| CC2 | Details Sample Order | `listsocompany.php` | `sampleorderdetail ⋈ sampleorder` filtered by product's `barang.PrincipalID = ID` (+ company/status/satuan/…), `IsDeleted=0` | none | ✅ local |
| CC3 | Details Quotation | `listquotationcompany.php` | per-company pivot of `quotation ⋈ quotationdetails` status counts (statuses 1–6), resolved to the principal | none | ✅ local |
| CC4 | LWR | `listlwrcompany.php` | `labworkrequestdetails ⋈ labworkrequest ⋈ barang` where `barang.PrincipalID = ID` (+ status/users/type), `IsDeleted=0` | none | ✅ local |
| CC5 | Visit Report | `listvisitreporthistoryview.php` | per-company `visitplan` status pivot + `visitplan` detail (status/division/users/company) | none | ✅ local |
| NS1 | Details Princ-Analysis | `listnssjanalysispm.php` | NetSuite Restlet API (`getmastervendor`/`getprincipal`) keyed on NSCode/NSBrand | endDate | ⛔ deferred |
| NS2 | Details Import | `nslistimportpm.php` | NetSuite Restlet API keyed on NSCode/NSBrand | endDate | ⛔ deferred |
| NS3 | Details Booking | `listnsbookingperprincipal.php` | NetSuite Restlet API (`getbookingorder`) keyed on NSCode/NSBrand | — | ⛔ deferred |
| NS4 | Import Vs SO-Analysis | `listimportvsso.php` | NetSuite (`getprincipal`/`getbarang`) + local SO, keyed on Vendor/Manufacturer, Start/End date | Start/End | ⛔ deferred |
| NS5 | Stock Barang | `listnetsuitestockbarangframe.php` (+ `getdatastocknetsuite.php`) | NetSuite stock by NSBrand/NSCode; only shown if NSCode **or** NSBrand set | — | ⛔ deferred |
| AST1 | Details Princ-Analysis | `listsjanalysispm.php` | **AST/PostgreSQL ERP** (`vf04_fak2 ⋈ vf04_fak …`, `date_part`) keyed on `ASTCode`=`PrincipalCode` | EndDate | ⛔ deferred |
| AST2 | Details Import | `astlistimportpm.php` | **AST/PostgreSQL ERP** (`vf06_0012 ⋈ vf06_001 …`) keyed on `merk_code IN (ASTCode)` | — | ⛔ deferred |
| EXP | Export Ratio Excel | `exportratioforecast.php` (1005 ln) | NetSuite Restlet + PHPExcel; ratio of import vs SO over the checked periods | 3M/6M/1Y/LY/Custom | ⛔ deferred |
| — | Documents | `listdocumentpmdownload.php` | `documentupload ⋈ principal ⋈ documentcategory`, `IsDeleted=0` | — | ✅ **built** |

> **"All-principal" report variants** (`astlistimportallpm.php`, `astlistimportall.php`, `nslistimportall.php`, `listnsbookingallsales.php`) belong to a separate legacy menu ("List Principal Import - All") and are all NetSuite/AST-sourced → **deferred** with the rest.
>
> The exact **display column list** of each CC report (CC1–CC5) is finalised at Phase-4 wiring by
> reading the cited file in full; §2.3 records the verified source table, principal linkage and
> filters (no fabricated columns).

---

## 3. Scope

### 3.1 Done
- **Phase 0 (2026-07-24):** skeleton PRD + README + `NEXT SESSION PROMPT.md`; gitignored example folder; files-only proto copy in `AnalysisAst/`.
- **Phase 1 (2026-07-24):** legacy read end-to-end; `principal` family DB-verified; this PRD + README rewritten from the real rules; scope/red-row/external-source decisions locked.

### 3.2 In scope to wire (local MySQL only)
Search (both menus, scoped + all) · Detail read (header + contacts + FAQ + documents) · CC-System reports CC1–CC5 · contact & FAQ write actions (with history + card upload via the `file_uploads` convention) · menu/rolemenu grants · policy (rolemenu gate) · Pest · sidebar.

### 3.3 Out of scope / deferred
NetSuite sections (NS1–NS5), AST sections (AST1–AST2), the Export-Ratio Excel, and the "All-principal import/booking" reports — **blocked on external integrations** (a NetSuite API client + credentials; a reachable AST Postgres connection). Rendered as disabled stubs; tracked as a GitHub issue. The **principal master edit** stays in its own legacy menu (`listprincipallogistic.php`), not this module.

---

## 4. Data Model — **VERIFIED `SHOW CREATE TABLE` 2026-07-24**

All principal-family tables carry a real `AUTO_INCREMENT` PK `ID` (no missing-auto_increment trap),
`ENGINE=InnoDB`, and **dev-enforced RESTRICT foreign keys** (prod may not — [[dev-db-fk-drift]]).
Charset `latin1` except `principalcp`/`principalcpassignment`/`userprincipalgroupdivision` (table
`utf8mb4`, per-column mixed). Models already exist in `app/Models/`.

| Table | Model | PK / auto-inc | Soft-delete col | Key columns (verified) | FKs (RESTRICT) |
|---|---|---|---|---|---|
| `principal` | `Principal` ✓ scoped | `ID` ✓ | **`IsDeleted`** | `PrincipalCode`(100) `PrincipalNSCode`(100) `PrincipalNSBrand`(100) `PrincipalName`(200) `SalesPrincipalName`(500) | none |
| `nsprincipal` | `NsPrincipal` (no scope) | `ID` ✓ | **`isDeleted`** (lowercase!) | `PrincipalNSCode`(100) `PrincipalNSName`(200) | none |
| `principalcp` | `PrincipalCp` ✓ scoped | `ID` ✓ | **`IsDeleted`** | `Tanggal`(dt,NOT NULL) `PrincipalID` `AddressType` `IsVisit` `PrincipalCP*` (name/salut/dept/pos/addr/kec/prov/pos/tel/hp/email) `Preference`/`Restriction`(utf8mb4) `CardUpload*` (`CardUploadContent` longblob) | `PrincipalID→principal`, `AddressType→addresstype` |
| `principalcpassignment` | `PrincipalCpAssignment` ✓ | `ID` ✓ | `IsDeleted` | history mirror + `UsersID` `Status`(20) `PrincipalCPID` | `UsersID→users`, `PrincipalCPID→principalcp`, `PrincipalID→principal`, `AddressType→addresstype` |
| `principalfaq` | `PrincipalFaq` (no scope/softDelete) | `ID` ✓ | **`isDeleted`** (lowercase!) | `Tanggal`(dt,NOT NULL) `PrincipalID` `Question`(255) `Answer`(255) | `PrincipalID→principal` |
| `principalfaqhistory` | `PrincipalFaqHistory` | `ID` ✓ | **`IsDeleted`** (PascalCase — split-cased pair!) | `Status`(50) `PrincipalFAQID` `PrincipalID` `Question` `Answer` | `PrincipalFAQID→principalfaq`, `PrincipalID→principal` |
| `userprincipal` | `UserPrincipal` ✓ scoped | `IsDeleted` | `ID` ✓ | `UserID` `PrincipalID` **`IsHeadDiv`** `IsMM` | `UserID→users`, `PrincipalID→principal` |
| `userprincipalgroupdivision` | `UserPrincipalGroupDivision` | `ID` ✓ | `IsDeleted` | `UserPrincipalID` `GroupDivID` | `UserPrincipalID→userprincipal`, `GroupDivID→groupdivision` |
| `documentupload` | (via `PrincipalDocument*`) | `ID` ✓ | `IsDeleted` | `IsShow` `PrincipalID` `DocumentCategoryID` `name`(200) `type`(30) `size` `content`(longblob) `ExpiryDate` | `PrincipalID→principal`, `DocumentCategoryID→documentcategory` |
| `documentcategory` | — | `ID` ✓ | `IsDeleted` | `DocumentCategoryName`(100) `IsSalesView` | none |

**Traps confirmed / to respect when wiring:**
- **Two soft-delete spellings:** `nsprincipal.isDeleted` + `principalfaq.isDeleted` are **lowercase**; the FAQ *history* table is PascalCase `IsDeleted` — the FAQ pair is split-cased. A model touching `principalfaq` needs a lowercase-aware scope (or an explicit `where('isDeleted',0)`), like `NsPrincipal` which today has **no** scope at all.
- **`Tanggal` is `NOT NULL`** on `principalcp` and `principalfaq` — the legacy FAQ insert omits it (relies on non-strict MySQL). On port, **set `Tanggal` explicitly**.
- **`AddressType` FK → `addresstype` (RESTRICT):** legacy leaves it NULL (column nullable, commented out). Keep NULL unless the port adds an Address-Type picker.
- **Contact insert order (FK-safe):** insert `principalcp` first → use its new `ID` as `principalcpassignment.PrincipalCPID`; `UsersID` must be a real user.
- **BLOB → disk:** card (`principalcp.CardUpload*`) and document (`documentupload.content`) bytes are legacy `longblob`. New uploads follow the **`file_uploads`** convention (private disk, store-after-commit, gated download). `menu_name` for the card upload is **OPEN** (propose `principalcp`); documents already use `principaldocument`/`internaldocument`.
- **latin1 columns can't hold 4-byte chars** — no emoji in names/addresses (`Preference`/`Restriction`/`Kecamatan`/`Provinsi`/`KodePos` are utf8mb4 and can).

---

## 5. Users & Permissions (resolved)

- **Two menu leaves**, each gated by `rolemenu` (sidebar-visible == 403-gated, identical grant condition — CLAUDE.md "Role/Menu Authorization"):
  - **Principal Analysis** — list **row-scoped** in the controller to the caller's head-division principals: `principal.ID IN (SELECT PrincipalID FROM userprincipal WHERE IsHeadDiv=1 AND IsDeleted=0 AND UserID = auth()->id())`. Scope is applied in the **query** (not just the Policy), per the security-default rule for user-owned listings.
  - **All Principal Analysis** — **unscoped** list of every principal, gated only by the menu grant.
- **Detail + reports + writes** are reachable from either menu; the Detail itself is **not** re-scoped in legacy (any principal ID opens if you have the menu), but for the scoped menu we should confirm the caller heads that principal before rendering (recommended hardening — **confirm with user**).
- **Documents** reuse the existing `PrincipalDocument*` gated routes/policy — no new download path.
- **No admin bypass** exists in legacy; that is why the scoped menu can be empty for `administrator`. The "All" menu is the admin/PM-wide view.
- **Menu IDs / `LinkLaravel` / role grants / display labels are OPEN** — proposed in §7.

---

## 6. Roadmap

| Phase | Content | Status |
|---|---|---|
| **0 ✓** | Docs skeleton + files-only proto copy + example placeholder | ✅ 2026-07-24 |
| **1 ✓** | Legacy read + DB-verify + rewrite PRD/README from real rules; lock scope/red-row/external decisions | ✅ 2026-07-24 |
| **2** | **Wire Search (both menus):** routes + `PrincipalAnalysisController` (scoped + all list, red-row per §2.1, `PrincipalName` order) + Policy (rolemenu gate) + menu/rolemenu rows + Pest + sidebar entries + build | ◻ next |
| **3** | **Wire Detail read:** header + contacts (`principalcp`) + FAQ (`principalfaq`) + documents (existing gated download) rows; External sections rendered as disabled "coming soon" stubs | ◻ |
| **4 ✓** | **CC-System reports CC1–CC5** — real local-MySQL queries per §2.3 (all-time, no period filter), in `CcReportService`, served by `PrincipalAnalysisController@report` and fetched per section via `useHttp`. Principal id is a BOUND param (legacy interpolated it). Pest: every statement validated against the live schema. | ✅ 2026-07-24 |
| **5 ✓** | **Write actions:** contact CRUD + card upload (`file_uploads`, `menu_name` = **`principalcp`**) + history (`principalcpassignment`); FAQ CRUD + history (`principalfaqhistory`), `Tanggal` set + `isDeleted` lowercase; FormRequest-only + **menu-aware gate** (`GatesPrincipalAnalysis`: All→any, scoped→headed only) + toast/inline errors; real **Activity Summary** counts (`PrincipalActivityService`). `PrincipalCpController`/`PrincipalFaqController`, frozen dialogs re-wired. **40 Pest green + Playwright-verified.** | ✅ 2026-07-27 |
| **6 ◐** | **External integrations — WIRED, awaiting credentials (2026-07-24).** The earlier "no NetSuite client in this stack" reading was WRONG: `app/Services/NetSuite/NetSuiteRestletClient` is a port of the legacy `netsuitegeneratejson.php`. NS1–NS5 now call `NetSuitePrincipalClient` (saved searches 143/146/156/167/171/190/278/302, ids read from that toolkit) and Export-Ratio streams through the shared ExcelJS worker; AST1–AST2 run the legacy Postgres SQL over a new `ast` connection. **Blocked only on config:** the 8 `NETSUITE_*` env vars, and `pdo_pgsql` + `AST_*` for AST. Until then each section renders an honest "not connected" notice — never invented figures. Field→column mapping for NS is derived from the live payload and should be pinned to the legacy headers once a real response is observable. | ◐ config-blocked |
| **6b ✓** | **NetSuite transport moved off the RESTlet (2026-07-31, issue #208).** `NetSuitePrincipalClient` and `NetSuiteItemClient` now read the **`netsuite_2`** DB connection (`ccins-function`, one table per legacy saved-search function) instead of the OAuth RESTlet. Method names, the 4-arg signature and the saved-search ids (kept in docblocks) are unchanged. **Consequence for this module: the `NETSUITE_*` credentials no longer gate anything** — NS1–NS5 are reachable now. NS4 (`ns-ratio`) needed a new `contains` operator on the transport, which previously **threw**; without it that section 500s. NS1–NS5 still render empty because all eight backing tables hold **0 rows** — a DATA gap, not a code gap, and the notice now distinguishes "not connected" from "returned no rows". AST1–AST2 are untouched and still `pdo_pgsql`/`AST_*`-blocked. | ✅ 2026-07-31 |
| **7** | Recheck + docs + follow-up GitHub issues | ◻ |

Per CLAUDE.md: file GitHub issues for remaining follow-ups at the end of each phase (including the Phase-6 external-integration blocker at the end of Phase 2).

---

## 7. Open Questions — resolved / residual

**Resolved this pass:**
1. **Schema** — DB-verified (§4). ✅
2. **Red-row** — deleted→pink; else red iff `PrincipalNSCode` **and** `PrincipalNSBrand` both empty (`astlistpmview.php`). ✅ (§2.1)
3. **Report queries** — inventoried (§2.3); CC1–CC5 local & all-time; NS/AST/Export external. ✅
4. **NetSuite/AST** — ~~reached via an external **NetSuite Restlet REST API**~~ **SUPERSEDED 2026-07-31:** NetSuite is now read from the local `netsuite_2` DB connection (`ccins-function`), so NS1–NS5 need no credentials at all; they are empty only because those tables have no rows yet. **AST is unchanged** — still a separate AST/PostgreSQL ERP that is not in this stack, still deferred. ✅
5. **Write rules** — contacts & FAQ each write a history row (Insert/Update/Delete); card upload type∈{jpg/jpeg/png/gif/bmp/pdf/tiff}, <1 MB. ✅ (§5, §4 traps)
6. **Access scope** — `userprincipal.IsHeadDiv=1` gates the scoped menu; "All" menu is unscoped. ✅ (§5)
7. **Naming** — proposed below; needs a nod.

**Residual (answer before/at the phase that needs it):**
- **Menu labels & `LinkLaravel`** (Phase 2). Proposed: routes `principal-analysis` (scoped) + `principal-analysis/all` (unscoped); labels "Principal Analysis" + "All Principal Analysis"; sidebar group **"Principals"**; controller `PrincipalAnalysisController`; policy `PrincipalAnalysisPolicy`. **Which role(s)** get the grants?
- ~~**`file_uploads` `menu_name` for the contact business card** (Phase 5).~~ **RESOLVED 2026-07-27:** `principalcp` (`card{id}_{YmdHis}_{n}.{ext}`; `CardUploadContent` empty). Registry row added to CLAUDE.md.
- ~~**Scoped-Detail hardening** (§5).~~ **RESOLVED 2026-07-27 (user):** the scoped "Principal Analysis" grant opens/edits **only headed** principals (`userprincipal.IsHeadDiv=1`); "All Principal Analysis" opens **any**. Enforced in `GatesPrincipalAnalysis` for show/report/export-ratio + all contact/FAQ writes (stricter than legacy's any-ID-opens).
- **External integrations** (Phase 6): is a NetSuite API client / AST Postgres connection ever going to be available in this stack, or are these sections permanently out?

---

## 8. References
- **Legacy (example folder, local-only):** Search `Principal Analysis - AST/astlistpm.php` (+ list `General Menu/astlistpmview.php`); All `All Principal Analysis - AST/astlistallpm.php` (+ `astlistallpmview.php`); Detail `Principal Analysis - AST/astlistpmedit.php`; contacts `General Menu/{listprincipalcp1,principalCPAddressAction,listprincipalcphistorypopup}.php`; FAQ `General Menu/{faqperprincipal,principalfaqaction,principalfaqpopup}.php`; documents `General Menu/listdocumentpmdownload.php`; reports per §2.3.
- **Design source (frozen):** `Pages/Proto/Principals/{PrincipalAnalysisAstSearch,PrincipalAnalysisAstDetail}.jsx`; proto routes `proto.principals.analysis-ast[.detail]` (`PrototypeController@principalAnalysisAst[Detail]`).
- **Pattern siblings:** `Pages/MenuGeneralAffairs/`, `Pages/MenuComplaintAndReturns/`; Role/Menu + `file_uploads` + ExcelJS foundations in `CLAUDE.md`.
- **Existing Laravel:** models `app/Models/{Principal,NsPrincipal,PrincipalCp,PrincipalCpAssignment,PrincipalFaq,PrincipalFaqHistory,UserPrincipal,UserPrincipalGroupDivision}.php`; documents `app/Http/Controllers/PrincipalDocument*Controller.php` + `documents/principal/*` routes.
- **Memories:** `ask-first-protocol`, `php-via-docker-cci`, `frontend-build`, `dev-db-fk-drift`, `never-read-opt-lampp`, `parallel-session-broad-commits`, `playwright-screenshots`.
