---
description: "Task list template for feature implementation"
---

# Tasks: Contracts Module (Contratos)

**Input**: Design documents from `/specs/003-contratos-module/`

**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/, quickstart.md (all present)

**Tests**: Not included — no automated test suite exists in this codebase (Constitution Principle VIII) and the spec does not request one. `quickstart.md` is the manual verification plan (Polish phase runs it).

**Organization**: Tasks are grouped by user story (spec.md's Story 1–7, priority P1–P7) so each can be implemented and verified independently, in priority order. This is a completion/correction pass over an already-substantial existing implementation (see `research.md` §1) — most tasks modify existing files rather than create new ones.

**Revision note**: This list was corrected twice this session. First, after a `/speckit-analyze` pass plus user clarification found: (a) "Gestão" is not a new `Perfil新` tier — it's an existing `AreaComercial新` row reached via a Padrão-profile user's `Cargo新.area`, so the originally-planned `Perfil新` insert task is removed entirely and replaced with a small access-check helper; (b) three requirements (FR-014, FR-015 server-side, FR-026 on update.php, FR-028 on update.php modes, FR-040) had no task coverage and now do; (c) `create.php`/`update.php`'s error-leaking catch blocks are fixed using the same response shape already established on `fix/general-backlog` (research.md §12), to avoid diverging patterns at merge time. Second, two earlier decisions were explicitly reversed by the user and `spec.md` was updated to match: (d) Renovar/Ampliar/Reduzir keep the existing `duplicate()`-row/deactivate-old-row design — the "convert to in-place UPDATE" work is no longer part of this feature; (e) Cancelar's `motivo` stays required — the "make it optional" work is no longer part of this feature. Both are now correctly reflected in `spec.md` (FR-031, FR-036, User Stories 4/6/7) as well as here.

## Format: `[ID] [P?] [Story] Description`

- **[P]**: Can run in parallel (different files, no dependency on an incomplete task)
- **[Story]**: Which user story this task belongs to (US1–US7)
- Every task states its exact file path

## Path Conventions

All paths are under `sistema/new/` (existing single monolithic project, pages/ajax/js triad — no new top-level structure, per plan.md's Structure Decision).

---

## Phase 1: Setup

**Purpose**: New, self-contained infrastructure with no dependency on schema changes — safe to build first.

- [X] T001 [P] Create `sistema/new/includes/funcoes/so5.php` with `so5_get_marcas()`, `so5_get_segmentos(string $marcaCodigo)`, `so5_get_produtos_insumo(string $marcaCodigo)` — raw `curl_init()` calls (matching `includes/apis/get_cnpj.php`'s existing pattern, no new dependency), base URL/token read via `getenv()` only (Constitution Principle VI), each function returns `null` (not `[]`) on any non-2xx/timeout/malformed-JSON response and `error_log()`s the failure before returning (Constitution Principle VII, CNF-18). Per `contracts/so5-integration.md`.
- [X] T002 [P] Add `usuario_pode_gerir_contratos($sql)` to `sistema/new/includes/funcoes/funcoes.php` (existing shared file, already included via `ajaxheader.php` — no new include wiring): returns true if `$_SESSION['perfil'] == 2` (Administrador), or if `$_SESSION['perfil'] == 1` (Padrão) and the logged-in user's área resolves to Gestão via `Usuarios.cargo → Cargo新.id → Cargo新.area → AreaComercial新.id == 3` (a small JOIN query using `$_SESSION['user_id']`). No schema change — `AreaComercial新` id 3 "Gestão" already exists. Per research.md §8.

---

## Phase 2: Foundational (Blocking Prerequisites)

**Purpose**: Schema change User Story 1 (and transitively User Story 3) depends on. Must complete before Phase 3.

**⚠️ CRITICAL**: US1 cannot write `so_brand`, and US3's `segmento.php`/`insumo.php` cannot look up a produto's SO5 link, until T003 lands.

- [X] T003 Create `sistema/new/migrations/sql/produto_so_brand_column.sql`: `ALTER TABLE Produto新 ADD COLUMN so_brand VARCHAR(64) NULL`, guarded via an `INFORMATION_SCHEMA.COLUMNS` existence check + `PREPARE`/`EXECUTE` so re-running is a no-op (idempotent, per FR-041/CNF-10 — MySQL has no native `ADD COLUMN IF NOT EXISTS`). Plain `.sql` file, run directly against the DB (`mysql ... < file.sql`) — schema/table changes now live under `migrations/sql/` as SQL files, not PHP scripts (convention changed this session; existing PHP scripts under `migrations/` remain data-backfill scripts, a different concern). Run against the local dev DB, verified via `DESCRIBE` and a second run confirming the no-op path.

**Checkpoint**: Schema ready — Phase 3 (US1) can begin.

---

## Phase 3: User Story 1 - Configure produto_marca for the Contracts module (Priority: P1)

**Goal**: An Admin can link a produto_marca to its SO5 marca and assign it a valid Classe (VHP / Plataforma / Serviço Padrão), in Cadastro > Produto.

**Independent Test**: Open a produto in Cadastro > Produto, select a "Marca SO" value, select a non-conflicting Classe, save, reopen — both persisted. Try an invalid Classe combination — rejected.

### Implementation for User Story 1

- [X] T004 [P] [US1] `sistema/new/includes/ajax/produto/create.php`: accept new `so_brand` field and write it to `Produto新.so_brand`. **No Classe combination-rule validation here** — corrected mid-session after live testing: a produto_marca can be linked to any subset of the three Classe values as available options; the mutual-exclusivity rule (FR-004) is enforced at contract time instead, moved to T016/T017 (`contrato/create.php`/`update.php`). Per `contracts/produto-classe-so-brand.md`.
- [X] T005 [P] [US1] `sistema/new/includes/ajax/produto/update.php`: same `so_brand` write as T004, no Classe validation here either (same correction).
- [X] T006 [P] [US1] `sistema/new/includes/ajax/produto/read.php`: return `so_brand` in the response.
- [X] T007 [US1] Create `sistema/new/includes/ajax/produto/so_brand.php`: calls `so5_get_marcas()` (T001), returns `[{id, nome}]`; `502` with a clear message if the call returns `null` (SO5 unreachable). Depends on T001.
- [X] T008 [US1] `sistema/new/pages/produto/cadastro.php`: add the "Marca SO" select field; `sistema/new/includes/js/produto.js`: wire it via `initChoicesSelect()`/`choiceslist.so_brand` — the explicit-call pattern this file already uses for Categoria (not the declarative `.choices-select-element` scan), so the existing generic edit-handler branch picks up `so_brand` automatically. Depends on T006, T007.
- [X] T009 [US1] Create `sistema/new/migrations/sql/produto_classe_so_brand_backfill.sql` covering, for `Produto新` where `grupo=1 AND categoria IN (1,2)`: (a) Classe default — where none of the three are linked, insert `VHP`(3) as default; (b) `so_brand` backfill — `UPDATE Produto新 SET so_brand = id WHERE so_brand IS NULL` (scoped to the same produto set); (c) `UPDATE Classe新 SET nome='Plataforma + Serviço Padrão' WHERE id=2 AND grupo=1`. Idempotent (a temp table captures touched produtos before mutation; a second run finds nothing to touch or log). Logs one `Logs新` row per produto touched (`objeto="Produto"`, `responsavel=1` matching the existing Admin-user convention already used by every pre-existing `Logs新` row). Plain `.sql` file per the Phase 2 convention (schema/data changes as SQL, run directly against the DB). Per `contracts/produto-classe-so-brand.md`'s Migration section, research.md §6. Depends on T003. Run against the local dev DB and verified: live data was 5 conflicts (not research.md's stale "5 all-three + 1 two-only = 6")/7 unclassified/12 in scope — research.md corrected to match; post-run state confirmed correct for all 12 produtos, rename confirmed, 12 `Logs新` rows confirmed, second run confirmed idempotent (still 12 rows, no changes). **Correction (mid-session, after T004/T005's mistake was caught)**: this migration originally also deleted the `VHP` link from the 5 conflicting produtos, based on the same wrong "produto-level exclusivity" assumption — reverted via `migrations/sql/produto_classe_vhp_correction.sql` (re-adds `VHP` to those 5 produtos, verified against the pre-migration state, idempotent, 5 `Logs新` rows).
- [X] T009a [US1] **Post-launch correction**: the (c) rename from T009 was reverted per user request — `sistema/new/migrations/sql/produto_classe_nome_reversao.sql` (`UPDATE Classe新 SET nome='Serviço Padrão' WHERE id=2 AND grupo=1`, same idempotent temp-table + `Logs新` pattern) restores the classification's display name to "Serviço Padrão". FR-005 is withdrawn (spec.md/research.md updated). FR-004's dependency-on-Plataforma rule is untouched — it's an id-based check in `contrato/create.php`/`contrato/update.php`, never tied to this label. Run against the local dev DB via `docker exec crm-mysql-crm-1`, verified (before: "Plataforma + Serviço Padrão"; after: "Serviço Padrão"; one `Logs新` row; second run confirmed idempotent, no duplicate row).

**Checkpoint**: Story 1 fully functional and independently testable — Cadastro > Produto now drives real SO5 links and valid Classe values.

---

## Phase 4: User Story 2 - View and access-control the contracts list (Priority: P2)

**Goal**: Admin/Gestão see the contracts list with the required fields and action buttons; every other profile is blocked, including by direct URL.

**Independent Test**: Non-elevated user hits the contracts screen/URL — denied. Admin/Gestão sees the full list with all action buttons and no "Interações de sucesso" button.

### Implementation for User Story 2

- [X] T010 [US2] `sistema/new/includes/ajax/empresa/load.php`: gate the `case 'contratos':` dispatch branch (the only place `pages/contratos/contratos.php` is ever loaded — a tab of the empresa detail view, not a standalone route) with `usuario_pode_gerir_contratos()`, including `pages/error/403.php` (pre-existing) instead when denied. `sistema/new/pages/contratos/contratos.php`: removed the "Interações de sucesso" button.
- [X] T011 [US2] `sistema/new/includes/ajax/contrato/read.php`: added the same `usuario_pode_gerir_contratos()` gate (403 JSON — this ajax entry point needs its own gate regardless of T010's page-level block); added the missing tenant check (`JOIN Empresa新 ON Empresa新.id = Contrato新.empresa` + `Empresa新.grupo = ?` using `$_SESSION['grupo']`) to both list-mode and detail-mode queries — research.md §4. Verified: 0 of 23 existing contracts have an orphaned `empresa` reference, so the new (non-LEFT) `JOIN` drops nothing legitimate.
- [X] T012 [US2] `sistema/new/includes/ajax/contrato/download.php`: added the same `usuario_pode_gerir_contratos()` gate and the same tenant check before streaming the `arquivo` BLOB (had neither — research.md §4).

**Checkpoint**: Story 2 fully functional and independently testable — list, access control, and tenant isolation all correct, even before Story 3's wizard is complete.

---

## Phase 5: User Story 3 - Register a new contract (Priority: P3)

**Goal**: Admin/Gestão can create a complete new contract — SO5-backed segmento/insumo per produto_marca, dates, payment, signature — in one flow.

**Independent Test**: Start a new contract, select a produto_marca, complete its section (SO5-backed segmento/insumo, classe, tipo, entregáveis), fill dates/payment/signature, save; reopen and confirm every value persisted.

**Depends on**: US1 (needs real `so_brand` links + valid Classe data to have anything meaningful to select) and US2 (the screen this launches from).

### Implementation for User Story 3

- [X] T013 [P] [US3] Rewrote `sistema/new/includes/ajax/contrato/segmento.php`: replaced the hardcoded array with a lookup of the requested produto's `so_brand` (returns `[]` if `NULL`) then `so5_get_segmentos($marcaCodigo)`; `502` with `{error:"so5_unavailable"}` if that call returns `null`. Live-verified (produto 1 → 8 real segmentos).
- [X] T014 [P] [US3] Rewrote `sistema/new/includes/ajax/contrato/insumo.php`: same pattern via `so5_get_produtos_insumo($marcaCodigo)`. This forced a JS-side breaking change since insumo.php is no longer one universal catalog: `getInsumoCatalog()` in `contratos.js` became per-produto (`insumoCatalog`/`insumoCatalogPromise` keyed by produto id, request body includes `produto`), covered under T019.
- [X] T015 [US3] `sistema/new/includes/ajax/contrato/formproduto.php`: added `data-param="produto" data-produto="<?= $id ?>"` to the segmento select (needed once segmento.php became produto-scoped); added FR-040 legacy-flag computation (`$so5Status`: `legacySegmentos`/`legacyInsumos`/`unavailable`) comparing stored ids against the produto's live SO5 lists, passed to the fragment's inline script for T019's `renderSo5Status()` to render.
- [X] T016 [US3] `sistema/new/includes/ajax/contrato/create.php`: added `usuario_pode_gerir_contratos()` gate; added `validateArquivoUpload()` (PDF-only/≤10MB, mirrors `interacoes/create.php`) as one small reusable function — kept separate from persistence so a future S3-backed upload only changes the persist step, not this check; added `validateInsumoOneSegmento()` (FR-015) called per produto; added demais≠focal check (FR-026); catch block now `error_log($e)` + the exact `fix/general-backlog` safe shape. **Scoping decision**: FR-029a (SO5-down blocks a produto's section) is enforced client-side only (matching how `contracts/so5-integration.md` frames it) — no server-side SO5 re-check at save time, since a produto saved with an incomplete matrix from a transient SO5 outage is the same accepted state as an unlinked `so_brand` (FR-003). Flagged for review. **Correction added after live testing**: `validateClasseCombination()` (FR-004) moved here from `produto/create.php` (T004) — it's a contract-time selection rule, not a produto-configuration rule; called per produto on `classe-{produto}`.
- [X] T017 [US3] `sistema/new/includes/ajax/contrato/update.php`: added the gate at the top of the file (covers every mode). Added the same `validateArquivoUpload()`/`validateInsumoOneSegmento()`/demais≠focal guards to `editar` and `editaradmin` specifically (per contract scope — `renovar`/`ampliar`/`reduzir` get theirs in T022/T027/T028). Same catch-block fix as T016. Same `validateClasseCombination()` correction as T016, added to `editaradmin` (the only mode here where `classe-{produto}` is re-submitted/editable — `editar` locks it).
- [X] T018 [US3] `sistema/new/includes/js/contratos.js` + `formproduto.php`: added `updateProdutoStatus()` (gray/yellow/green dot based on Classe+Tipo required fields plus any optional field touched), wired to classe/tipo/pacotes/entregaveis changes and to `syncInsumosUI()` (covers segmento/insumo changes transitively); produto-groups now render with the `close` class and `i-caret-right` by default (collapsed).
- [X] T019 [US3] `sistema/new/includes/js/contratos.js`: `getInsumoCatalog(produto)` made per-produto (T014's breaking change); added `produtoSo5Errors` tracking + `markProdutoSo5Error()`/`clearProdutoSo5Error()`, disabling `.form-submit-button` and blocking `submitContrato()` while any produto has an error; added `renderSo5Status()` rendering T015's legacy/unavailable flags inline. **Note**: the segmento `<select>`'s own failure path still goes through the generic `.choices-select-element` loader's existing fallback (a "Sem dados" placeholder choice, not a bespoke message) rather than a new bespoke handler — reusing shared infra instead of forking it; the insumo-fetch failure path (which fires on every segmento addition) is the one with full bespoke `so5_unavailable` handling.
- [X] T020 [US3] `sistema/new/includes/js/contratos.js`: added a `change` handler on `#contrato-input-arquivo` checking type/size client-side (same rule as `validateArquivoUpload()`), clearing the input and showing an inline error on violation.
- [X] T021 [US3] `sistema/new/includes/js/contratos.js`: added the Pacote → insumo cascade on `.contrato-input-produto-pacotes` change, reading each selected pacote's `customProperties.insumos` (already provided by `choicesModules.pacotes`/`pacote.php`, unchanged) and marking them under `segmentoChoices.getValue(true)[0]`, tracking the previous mark set per produto to clear before re-applying (removed the stale "follow-up task" comment).
- [X] T021a [US3] Client-side FR-004 Classe combination check added to `contratos.js` (`validateClasseCombination()`, mirroring the PHP one), wired into the live classe-checkbox change handler (inline error) and into `validateClasseGroups()` (submit-time gate) — plus fixed a pre-existing bug where `submitContrato()` called `validateClasseGroups()` without checking its return value, so even the pre-existing "select at least one classe" rule never actually blocked submission (matches the file's own stale `TODO` comment, now removed). `updateProdutoStatus()`'s "complete" (green) state also now requires a *valid* combination, not just any Classe checked.

**Checkpoint**: Story 3 fully functional and independently testable — a complete new contract can be created, saved, viewed, and edited end-to-end.

---

## Phase 6: User Story 4 - Renew a contract (Priority: P4)

**Goal**: Renovar carries the contract's produto_insumo/dates/payment/signature forward as a new active record, deactivating the previous one — the existing `duplicate()`-based mechanism, unchanged.

**Independent Test**: Renovar an existing contract, change an insumo mark and a payment field, save — a new `Contrato新` row is active with the renewed data, the previous row is deactivated (`ativo=0`), and a history entry is recorded.

**Depends on**: US3 (a contract must already exist).

### Implementation for User Story 4

- [X] T022 [US4] `sistema/new/includes/ajax/contrato/update.php`, `renovar` mode: **found and fixed a critical pre-existing bug while implementing this task** — the mode unconditionally deleted+reinserted `ContratoProdutoSegmento新`/`Classe新`/`Tipo新`/`Pacote新`/`Entregavel新` from `$_POST`, but those fields are locked/disabled client-side for this mode (`CONTRATO_ALLOWED_FIELDS_RENOVAR`), so they were never submitted — binding `null` into `classe`/`tipo`'s `NOT NULL` columns, throwing and rolling back every renewal attempt (confirmed: Renovar failed 100% of the time before this fix). Fixed by no longer touching those five satellite tables at all — `duplicate()` already copies them correctly, and they're not editable in this mode; only `ContratoProdutoInsumoSegmento新` is still delete+reinsert'd per submitted produto (FR-033). Also found & fixed a second bug in the same case: only `inicio`/`vencimento`/`prazo` were ever applied to the new row — every other editable field (`nf`, `prazopagamento`, `pagamento`, `focal`, `demais`, `assinado`, `regularizado`, `arquivo`, etc.) was silently discarded despite being unlocked client-side (duplicate() copied the *old* values through unchanged). Added a comprehensive `UPDATE` applying all of them, matching the pattern already used for Ampliar/Reduzir. Added guards: one-segmento-per-insumo (FR-015) per produto, demais≠focal (FR-026), PDF/size via `validateArquivoUpload()` (FR-028, with existing-file-preserved-if-none-reuploaded, matching `editaradmin`'s pattern). Added `diffContratoFields()` (new shared helper) and a structured `Logs新` diff row supplementing the existing `"Renovado de {id}"` lineage string. **Live-verified end-to-end** against the real dev DB with two real contracts (one bare, one with real Classe/Tipo data): new row correctly created with submitted fields applied, old row deactivated, Classe/Tipo/Segmento preserved unmodified on all 5 produtos (only the explicitly-submitted produto's insumo marks changed), both `Logs新` rows present with a correct diff. A hand-counted `bind_param` type-string length mismatch was caught and fixed via a live 500 error during this verification (mysqli's own `ArgumentCountError`), then re-verified programmatically before re-testing.
- [X] T023 [US4] `sistema/new/includes/ajax/contrato/read.php`: added a `historico` key to the detail-mode response reading `Logs新 WHERE objeto='Contrato' AND alvo=? ORDER BY data`, `json_decode()`ing `observacao` per row (falls back to the raw string for older/lineage rows that aren't JSON). Live-verified: correctly returns both the plain lineage string and the T022 structured diff (decoded to a real object) for the same contract's history.
- [X] T024 [US4] Renovar form itself was already correctly wired (`fillContratoFields(contrato, CONTRATO_ALLOWED_FIELDS_RENOVAR)` already locks the right fields client-side — T022's bug was entirely server-side). Added the history view: `sistema/new/pages/contratos/contratos.php` gets a new `#contrato-historico-modal`; `sistema/new/includes/js/contratos.js` gets a "Ver histórico" button per contract card and a `.contrato-historico` click handler rendering T023's `historico` array (tipo/data header, diff rendered as a `field: "old" → "new"` list, plain text for lineage/pre-feature rows). Minimal CSS added for the list.

**Checkpoint**: Story 4 fully functional and independently testable.

---

## Phase 7: User Story 5 - Extend a contract (Priority: P5)

**Goal**: Estender updates only início/vencimento/prazo-de-pagamento fields in place.

**Independent Test**: Estender an existing contract, change vencimento, save — only those fields changed, same `id`, history entry recorded.

**Depends on**: US3.

### Implementation for User Story 5

- [X] T025 [US5] `sistema/new/includes/ajax/contrato/update.php`, `estender` mode: widened the in-place `UPDATE` to also set `prazopagamento`/`prazopagamentoobservacao`/`pagamento` alongside início/vencimento/prazo (previously only the 3 date fields were ever applied, matching the old narrower `CONTRATO_ALLOWED_FIELDS_ESTENDER`). Added `$oldRow`/`$newRow` fetch + `diffContratoFields()` + a second `logging("Estender", ...)` call carrying the JSON diff, mirroring T022's pattern exactly (keeps the original empty-observação lineage row too). Live-verified against contract id=32 in the dev DB via a disposable test session: POSTed changed prazopagamento/prazopagamentoobservacao/pagamento, confirmed the row updated and both `Logs新` rows were written with the correct diff content, confirmed `read.php`'s `historico` (T023) decodes it correctly alongside older plain-text rows, then reverted the row back to its original values via the same endpoint.
- [X] T026 [US5] `sistema/new/includes/js/contratos.js`: `CONTRATO_ALLOWED_FIELDS_ESTENDER` widened from `['inicio', 'vencimento', 'prazo']` to also include `'prazopagamento'` (prefix also covers `prazopagamentoobservacao`) and `'pagamento'` — same prefixes already used by `CONTRATO_ALLOWED_FIELDS_EDITAR`. No HTML changes needed: page navigation isn't mode-restricted, so the shared modal's page 3 (Pagamento) was already reachable from Estender's page 2 opening point — those fields were just disabled by `lockFormFields()` before this change.

**Checkpoint**: Story 5 fully functional and independently testable.

---

## Phase 8: User Story 6 - Expand or reduce a contract (Priority: P6)

**Goal**: Ampliar/Reduzir carry the full contract (except Kick Off) forward as a new active record, deactivating the previous one — the existing `duplicate()`-based mechanism and growth-direction guard, unchanged.

**Independent Test**: Ampliar/Reduzir an existing contract with a valid direction change — a new `Contrato新` row is active with the updated values, the previous row is deactivated, and a history entry is recorded; an invalid-direction change is rejected.

**Depends on**: US3.

### Implementation for User Story 6

- [X] T027 [US6] `sistema/new/includes/ajax/contrato/update.php`, `ampliar` mode: kept the existing `duplicate()`-based mechanism and `getContratoTotals()` growth guard exactly as-is (unchanged). While reading the code before implementing, found two more pre-existing bugs of the same shape already fixed in Renovar/Estender: (1) `ContratoDemais新` was never touched by this mode and `duplicate()` doesn't copy it either, so "Demais usuários" was silently wiped on every Ampliar despite being unlocked in the form; (2) `ordem`/`inclusao`/`ampliacao`/`observacao` are unlocked in the form (`CONTRATO_ALLOWED_FIELDS_FULL`) but were never included in the `UPDATE`, so edits to them were silently discarded (duplicate()'s stale copy stuck instead). Fixed both alongside the planned work: added `validateClasseCombination()` + `validateInsumoOneSegmento()` per produto, demais≠focal and PDF/size guards (mirroring `editaradmin`'s exact pattern), widened the `UPDATE` to include the 4 missing fields, added `ContratoDemais新` insert from `$_POST['demais']`, and added `$oldRow`/`$newRow` + `diffContratoFields()` + a second `logging()` call (`tipo="Ampliar"`, `alvo`=new row id). Live-verified against contract id=32 in the dev DB via a disposable test session: confirmed `ContratoDemais新` now populates, `ordem`/`inclusao`/`ampliacao`/`observacao` apply and appear in the `Logs新` diff, growth guard still passes/blocks correctly, and a forced Plataforma+VHP combination on one produto is correctly rejected (`validateClasseCombination()` wired and functional, not just called).
- [X] T028 [US6] `sistema/new/includes/ajax/contrato/update.php`, `reduzir` mode: same fixes as T027 (guards, `ContratoDemais新`, `ordem`/`inclusao`/`ampliacao`/`observacao`, diff logging), guard direction unchanged ("must not grow"), `tipo="Reduzir"`. Live-verified against the contract produced by the T027 test: `ContratoDemais新` correctly reflects an explicitly-submitted empty list, `ordem`/`inclusao`/`ampliacao`/`observacao` changes applied and diffed correctly, shrink guard passed with a reduced produto valor.
- [X] T029 [US6] `sistema/new/includes/js/contratos.js` + `sistema/new/pages/contratos/contratos.php`: no changes needed — `CONTRATO_ALLOWED_FIELDS_FULL` (shared with Editar Admin) already unlocks every field these modes need, including `demais`, `ordem`, `inclusao`, `ampliacao`, `observacao`; the backend just wasn't applying all of what the form already submitted.

**Checkpoint**: Story 6 fully functional and independently testable.

---

## Phase 9: User Story 7 - Cancel a contract (Priority: P7)

**Goal**: Cancelar requires a justification and changes only the contract's status — the existing required-`motivo` validation, unchanged.

**Independent Test**: Try to cancel a contract without a justification — rejected. Cancel with a justification — accepted, only status (and the justification) changed, history entry recorded.

**Depends on**: US3.

### Implementation for User Story 7

- [X] T030 [US7] `sistema/new/includes/ajax/contrato/cancel.php`: added `usuario_pode_gerir_contratos()` gate (matching `update.php`'s placement/pattern — this file previously had no gate at all, unlike every other `contrato/*.php` ajax entry point). Kept the required-`motivo` check untouched. Added `$oldRow`/`$newRow` + `diffContratoFields()` + a second `logging("Cancelar", ...)` call with the JSON diff, supplementing the existing plain-text `motivo` log entry. `diffContratoFields()` was moved from `update.php` (file-local) to the shared `sistema/new/includes/funcoes/funcoes.php` (included by every ajax file via `ajaxheader.php`) since `cancel.php` needed it too and doesn't include `update.php`. Live-verified against a contract in the dev DB via a disposable test session: `ativo` flipped to 0, `motivocancelamento`/`datacancelamento` set, both the lineage-text and JSON-diff `Logs新` rows written correctly.

**Checkpoint**: Story 7 fully functional and independently testable. All 7 user stories now complete.

---

## Phase 10: Polish & Cross-Cutting Concerns

**Purpose**: End-to-end verification across all stories.

- [X] T031 [P] Ran the backend-testable portion of `quickstart.md`'s validation plan against the local Docker stack via disposable authenticated test sessions (no browser-automation tool available in this environment, so the purely visual items below are NOT covered and still need a manual click-through):
  - **Backend access enforcement**: confirmed 403 from `read.php`, `download.php`, `update.php`, `cancel.php` for a non-elevated same-tenant user. While testing this, found `formproduto.php` had **no access gate at all** (the only `contrato/*.php` file missing it) — any authenticated user, any role, any tenant, could pull full segmento/pacote/insumo/classe/tipo/entregavel data for any contract by guessing a `contrato`+`produto` id pair. Fixed: added the `usuario_pode_gerir_contratos()` gate, and tenant-scoped the `ContratoProduto新` lookup (joined through `Contrato新`→`Empresa新.grupo`) since `contrato` is caller-supplied; added an explicit 403 when that join finds nothing. Verified both the non-elevated and the cross-tenant-elevated attack paths are now blocked (403), and both legitimate flows (same-tenant detail load, and the no-`contrato` new-contract-form load) still return 200.
  - **Tenant isolation**: confirmed `read.php`/`download.php`/`formproduto.php` all refuse a `grupo=1` contract's data to a `grupo≠1` elevated user. Found and fixed a second bug in the process: `read.php`'s `historico` block (added in T023) fetched by raw `$id` unconditionally whenever `$resultproduto` was non-empty, regardless of whether the tenant-filtered main query actually returned a row — a cross-tenant request got back an empty contract but a **full leaked history array** (`$contratos[0]['historico']` fabricating a row out of nothing). Fixed by gating the whole historico block on `!empty($contratos)`. Re-verified: cross-tenant request now returns `[]` cleanly; legitimate same-tenant request still returns full data including `historico`.
  - **Legacy contract data (FR-040)**: loaded `formproduto.php` for a genuinely pre-feature contract (id 28, seeded before the SO5 cutover) — page renders without error, and all 4 of its stored segmento ids are correctly reported in `so5Status.legacySegmentos` (flagged, not dropped or crashed).
  - **SO5 credentials**: confirmed `so5.php` reads `SO5_URL`/`INTERNAL_API_SECRET` via `getenv()` only (no literals), and `contratos.js` never references SO5 directly — all calls go through same-origin `includes/ajax/contrato/*.php` proxies.
  - **NOT covered here (needs a manual browser walkthrough)**: Stories 1–7's UI-level steps in `quickstart.md` — status dot colors, Choices.js dropdown population, "Finalizar" button gating, PDF upload rejection messages, section collapse/expand, page 1/2/3 navigation, and the SO5-down inline-error UI (FR-029a). The underlying server-side logic for all of these was exercised and verified during each phase's own live-testing this session, but the visual/interactive behavior itself has not been clicked through in a real browser.
- [X] T032 Spot-checked the `Logs新` JSON diff format across all 9 diff rows produced by this session's live tests (2 Renovar, 2 Estender, 2 Ampliar, 1 Reduzir, 1 Cancelar) via a direct DB query — all consistently `{field: {old, new}}`, no malformed rows. Confirmed the history-rendering code (`contratos.js`'s `.contrato-historico` handler) is fully generic — it never branches on `tipo`, only on whether `observacao` decoded to an object (renders a diff list) or stayed a string (renders as text, or nothing if empty) — so all five action types and legacy pre-feature rows (sampled real rows: empty-string `Estender`/`Editar`, plain lineage strings like `"Renovado de 71"`) render through the same one code path with no special-casing needed.

---

## Dependencies & Execution Order

### Phase Dependencies

- **Setup (Phase 1)**: No dependencies — T001 and T002 can start immediately, in parallel.
- **Foundational (Phase 2)**: No dependency on Phase 1's content (different files), but conventionally run after — BLOCKS User Story 1 (T003) and, transitively, User Story 3's `segmento.php`/`insumo.php` (T013/T014 also need T003).
- **User Story 1 (Phase 3)**: Depends on Phase 2 (T003 for `so_brand`, T007 for T001).
- **User Story 2 (Phase 4)**: Depends on T002 (the access helper), otherwise independent of US1 — only touches `contratos.php`/`read.php`/`download.php`'s access/tenant logic. Can run in parallel with US1.
- **User Story 3 (Phase 5)**: Depends on US1 (real `so_brand`/Classe data) and US2 (the screen) per spec.md's stated story dependencies, and on T001/T002/T003 directly (T013/T014/T016/T017).
- **User Stories 4–7 (Phases 6–9)**: Each depends on US3 (a contract must exist to act on) but are independent of each other — can proceed in any order or in parallel once US3 is done. (The earlier note about `duplicate()` removal ordering no longer applies — `duplicate()` is kept, not removed, per this session's reversal.)
- **Polish (Phase 10)**: Depends on all 7 stories being complete.

### Parallel Opportunities

- T001 and T002 (Setup) touch disjoint files and can run fully in parallel; T003 (Foundational) touches yet another file and can be worked alongside them, though US1/US3 tasks that write/read `so_brand` must wait for T003 to land.
- Within US1: T004, T005, T006 touch three different files and can run in parallel; T007 (new file) can run in parallel with them; T008 depends on T006+T007.
- US2 (Phase 4) can be worked in parallel with US1 (Phase 3) — no shared files (both depend only on earlier, already-parallel Setup/Foundational tasks).
- Within US3: T013 and T014 touch different files and can run in parallel; T015–T021 have sequential dependencies on them (formproduto.php/JS need the real endpoints working first).
- US4, US5, US6, US7 all modify `update.php`'s distinct `case` blocks (and `cancel.php` for US7) — not truly parallel-safe as simultaneous edits to the same file by different people, but independent in logic and can be done in any order once US3 lands.

---

## Implementation Strategy

### MVP First

1. Phase 1 (Setup) + Phase 2 (Foundational).
2. Phase 3 (US1) — produto_marca can be linked/classified.
3. Phase 4 (US2) — the list screen is safe and correct.
4. Phase 5 (US3) — **STOP and VALIDATE**: a complete contract can be created end-to-end. This is the real MVP; US1+US2+US3 together are the smallest slice that delivers standalone value (a working, correctly-scoped registration flow).

### Incremental Delivery

Add US4 (Renovar) → US5 (Estender) → US6 (Ampliar/Reduzir) → US7 (Cancelar), each independently testable via `quickstart.md`'s corresponding section, each deployable on its own once US3 is live.
