# Contract: Contrato CRUD + actions (Stories 2, 3, 4, 5, 6, 7)

Endpoints: `sistema/new/includes/ajax/contrato/{create,update,cancel,read,download}.php`
Callers: `sistema/new/includes/js/contratos.js`, `sistema/new/pages/contratos/contratos.php`

## Access control (all endpoints in this file)

Every endpoint calls `usuario_pode_gerir_contratos($sql)` (new helper, `includes/funcoes/funcoes.php`): true if `$_SESSION['perfil'] == 2` (Administrador), or if `$_SESSION['perfil'] == 1` (Padrão) and the logged-in user's `Cargo新.area` resolves to `AreaComercial新` id 3 ("Gestão") — research.md §8. A request that fails this check gets `HTTP 403` and no data, matching the existing `die(header(...))` shape `ajaxheader.php` already uses for the 401 case. This is a backend check independent of any menu/UI hiding (FR-001).

## `create.php` (Story 3)

**Request** (unchanged shape from today, additive fields only):

| Field | Type | Status |
|---|---|---|
| `empresa`, `inicio`, `kickoff`, `vencimento`, `moeda`, `ptax`, `periodicidade`, `divisao`, `reajustemes`, `reajustetipo`, `nf`, `focal`, `demais[]`, `produtos[]`, `valor-*` | existing | unchanged |
| `segmento-{produto}[]` | existing key, new source | now validated against a live SO5 lookup per produto instead of the mock array |
| `insumo-{produto}-{segmento}[]` | existing key, new source | same — SO5-sourced; still enforced one-segmento-per-insumo (FR-015) |
| `pacote-{produto}[]` | existing | unchanged (still `pacote.php`'s manually-curated data, research.md §5 does not touch this) |
| `classe-{produto}` | existing | must be one of the produto's linked `ProdutoClasse新` rows (already the case); **new**: reject (`400`) if it contains both `Plataforma`(1) and `VHP`(3), or `Serviço Padrão`(2) without `Plataforma`(1) — FR-004, enforced here (contract-time selection), not in `produto/create.php`/`update.php` (produto-time configuration of available options) |
| `tipo-{produto}` | existing | unchanged (`Brasil`/`Estados Unidos`) |
| `entregavel-{produto}[]` | existing | unchanged (still fixed/mocked, per spec Assumptions) |
| `arquivo` | file | **new validation**: must be `application/pdf`, ≤10MB (research.md §7); non-PDF or oversize → `422` with an explanatory message (FR-028) |

**Server-side handling**: unchanged transactional insert shape (`Contrato新` + all `ContratoProduto新` satellites + `ContratoDemais新` + `ContratoValores新`), plus: reject if `demais[]` contains `focal` (FR-026); reject if the same insumo id appears under more than one segmento in the posted matrix for the same produto (FR-015); reject if SO5 is unreachable for any selected produto_marca's segmento/insumo lookup (FR-029a — `422`, error scoped to that produto so the client can highlight the specific section); reject the file per the new PDF/size check above. **These same three guards (demais≠focal, one-segmento-per-insumo, PDF/size) apply identically to every `update.php` mode below that accepts the corresponding fields — they are not create-only.**

**Response**: unchanged (`{id}` of the created `Contrato新` row).

## `update.php` (Stories 3, 4, 5, 6)

Dispatches on `mode`. All six modes stay in one transaction, matching today.

| Mode | Fields accepted | Behavior change |
|---|---|---|
| `editar` | nf/pagamento/focal/demais/assinado/regularizado/arquivo | none (already in-place) |
| `editaradmin` | everything `create.php` accepts | none (already in-place); PDF/size check added same as create |
| `estender` | inicio, vencimento, prazo, prazopagamento, prazopagamentoobservacao, pagamento | none (already in-place) — **field list tightened to exactly FR-034**; today's code already restricts to inicio/vencimento/prazo, this feature adds prazopagamento/prazopagamentoobservacao/pagamento per the spec's field list and drops anything else |
| `renovar` | produto_insumo per produto_marca (existing segmentos only, not addable/removable — FR-033), inicio, vencimento, all payment fields, all signature fields (incl. PDF/size check) | **no mechanism change** (reverted this session, research.md §2): still calls `duplicate()` — `INSERT`s a new `Contrato新` row copying/overwriting the old one, deactivates the old row (`ativo=0`). Segmento selects are rendered read-only for this mode. Gains the one-segmento-per-insumo (FR-015) guard and a structured `Logs新` diff (see below) that it didn't have before. |
| `ampliar` | every field `editaradmin` accepts, except `kickoff` | **no mechanism change** (reverted this session, research.md §2): still calls `duplicate()`. `getContratoTotals()` guard is unchanged — still compares old-row vs newly-duplicated-row, must not shrink. Gains the same new guards/diff as `renovar`. |
| `reduzir` | same field set as `ampliar` | same as `ampliar`; guard direction unchanged (must not grow) |

**All six modes**, after applying their update: write one `Logs新` row with `objeto='Contrato'`, `alvo` = the affected `Contrato新.id` (the **new** id for `renovar`/`ampliar`/`reduzir`, since those create a new row), `tipo` = the existing per-mode string, `observacao` = JSON diff of changed fields relative to the prior record (research.md §9), `responsavel=$uid`. This supplements — it does not replace — the existing "Renovado de {id}"-style lineage string already written by `renovar`/`ampliar`/`reduzir` today.

## `cancel.php` (Story 7)

**Request**: `id`, `motivo` (**required**, unchanged — reverted this session, research.md §3). No other field accepted (FR-036).

**Server-side**: `UPDATE Contrato新 SET ativo=0, motivocancelamento=?, datacancelamento=CURDATE() WHERE id=?` — no change to the existing required-`motivo` validation. Writes a `Logs新` entry (`tipo="Cancelar"`, `observacao` including the recorded `motivo`).

## `read.php` (Story 2)

**List mode** (no `mode`/`id` param — contracts list for the screen):

- **New tenant check** (research.md §4): `JOIN Empresa新 ON Empresa新.id = Contrato新.empresa AND Empresa新.grupo = ?` using `$_SESSION['grupo']`.
- Response fields unchanged in kind (id="Número do Contrato", ativo, inicio, vencimento, produtos, total/media, assinado, regularizado, has-arquivo flag) — already satisfies FR-006's field list.

**Detail mode** (`mode`+`id`): same new tenant check; response already flattens per-produto satellite data (`segmento-{id}`, `pacotes-{id}`, etc.) — now sourced from the SO5-integration-backed columns instead of mock ids, no shape change. For a contract predating the SO5 cutover, `formproduto.php` (not `read.php` itself) is where a stored segmento/insumo id that no longer resolves against the produto's live SO5 list gets flagged as legacy/unrecognized rather than silently dropped or erroring (FR-040, research.md §11).

**New**: a `historico` sub-response (or a separate `historico.php`, whichever keeps `read.php`'s existing size manageable) reading `Logs新 WHERE objeto='Contrato' AND alvo=? ORDER BY data`, parsing `observacao` as JSON when possible (older rows before this change fall back to raw text) — backs FR-032/CF-29's "view history" requirement (Stories 4–7).

## `download.php`

**New tenant check** (research.md §4): same `Empresa新.grupo` join before streaming the BLOB — currently has none at all. **New access check**: same `usuario_pode_gerir_contratos()` gate as every other endpoint in this file (today it has no profile check either).

## Acceptance mapping

FR-001, FR-006–FR-009, FR-019–FR-037, FR-041 ↔ this contract. CNF-04 (backend-enforced access) ↔ the Access control section above.
