# Quickstart: Contracts Module (Contratos)

No automated test suite exists in this codebase (Constitution Principle VIII) — this is the actual verification plan. Run these against the local Docker stack after implementation, in order, since later scenarios build on earlier ones (e.g. Story 4 needs a contract Story 3 already created).

## Prerequisites

- `docker compose up -d` (containers `crm-mysql-crm-1`, `crm-php-crm-1`, `crm-phpmyadmin-crm-1` running).
- Log into `sistema/new` as a user with `Usuarios.grupo = 1`.
- Two test user accounts ready for the access-control checks: one Administrador (`perfil = 2`), and one Padrão (`perfil = 1`) whose `Cargo新` resolves to área "Gestão" (`AreaComercial新` id 3) — e.g. assign a cargo like "Diretor Financeiro" (`Cargo新.area = 3` in the seeded data) via the existing Usuario/Cargo management screens. Also have a third, non-elevated user (Padrão profile, cargo in a different área) to confirm access is correctly denied.
- At least one `Produto新` in `grupo=1, categoria IN (1,2)` linked to a `so_brand` value the local/stubbed SO5 responds to (or SO5 mocked/stubbed for local dev if the real SO5 endpoints aren't reachable from this environment yet).

## Story 1 — Marca SO + Classe on Produto

1. Open Cadastro > Produto, edit an existing categoria-1/2 produto (e.g. `GlobalFert Agro`). Confirm the "Marca SO" select lists live SO5 marcas, not a hardcoded list.
2. Select a marca, save, reopen the produto — the same link is still shown (`so_brand` persisted).
3. Select both `Plataforma` and `VHP` as classe on the same produto and save — accepted (this field configures the produto's *available* options; the mutual-exclusivity rule doesn't apply here, only at contract time - see Story 3 step 5).
4. Run the migration script (research.md §6 / `contracts/produto-classe-so-brand.md`) against a copy of the seeded data; confirm afterward: every categoria-1/2 produto has exactly a valid classe combination and a non-null `so_brand`; re-run the script a second time and confirm no duplicate rows / no changed row count (idempotent).

## Story 2 — Contracts list + access control

1. Log in as the Padrão (non-elevated) user, try to open the contracts screen from the menu and via a direct URL — both denied.
2. Log in as Admin/Gestão, open the contracts screen — listing shows Número (id), Valor médio/total, produtos, início/vencimento, PDF link for every existing contract in `grupo=1`.
3. Confirm no "Interações de sucesso" button/tab appears anywhere on the screen.
4. Confirm every row has Editar/Visualizar/Renovar/Estender/Ampliar-Reduzir/Cancelar buttons, and that starting a second new contract works without any "one contract only" block.

## Story 3 — Register a new contract

1. Click "+ Novo Contrato", select 2 produto_marca — confirm two collapsed sections appear, each with a gray (not-started) status dot.
2. Expand one section: segmento select is populated from SO5 for that produto's `so_brand` (not the old mock list). Select a segmento.
3. Open "Configurar Insumos": rows = SO5 produto_insumo for that marca, columns = selected segmentos. Mark an insumo under one segmento, then mark the same insumo under a second segmento — confirm the first mark clears (one-segmento-only rule).
4. Select a Pacote Padrão — confirm its insumos pre-mark under the first segmento column; change the pacote — confirm the marks reset to the new pacote's set.
5. Pick a Classe — confirm only options the produto_marca was configured with (Story 1) are offered; try selecting `Plataforma` and `VHP` together — rejected; try `Serviço Padrão` without `Plataforma` — rejected.
6. Complete both sections — confirm each status dot goes gray → yellow → green as fields fill in.
7. Advance to Datas: enter a Vencimento before Início — rejected with an inline error, both while typing and on submit; enter a valid range — confirm "prazo de contratação" auto-computed and read-only.
8. Advance to Pagamento: fill Uniforme split, confirm USD/total/média auto-calc; switch to Personalizado, confirm per-month fields and totals recompute; confirm NF-issuance options read "Período" (not "Bimestre") with 1–30 / 1–22 ranges; select "Personalizado" under reajuste "Meses, conforme" — confirm no extra field appears; pick a Ponto focal — confirm they disappear from "Demais usuários".
9. Advance to Assinatura: answer "Não" to Contrato assinado — confirm "Regularizado?" appears; answer "Sim" to either — confirm the PDF upload appears; try uploading a non-PDF and a >10MB PDF — both rejected with a clear message; upload a valid PDF ≤10MB — accepted.
10. Save — confirm the contract appears in the list immediately with the entered values, and that `Contrato新.id` in the DB matches what's shown as "Número do Contrato".
11. Open it via "Visualizar Contrato" — read-only view with correct data; click "Editar Contrato" — form becomes editable with data intact; change a field, save — same `id` is updated.
12. **SO5-down check** (FR-029a): with SO5 stubbed to fail, start a new contract and expand a produto_marca section — confirm a scoped inline error appears on that section and "Finalizar" is blocked, while dates/payment/signature and any other produto_marca section remain usable.

## Story 4 — Renovar

1. Note the `Contrato新.id` of the contract from Story 3. Click "Renovar".
2. Confirm the form shows only: produto_insumo per produto_marca (segmentos shown read-only, not addable/removable), início, vencimento, all payment fields, all signature fields.
3. Change an insumo mark within an existing segmento and a payment field, save.
4. **Lineage check** (duplicate-row design, kept as-is this session): query `Contrato新` — confirm a **new** row now exists with the renewed data (`ativo=1`) and the previous row is deactivated (`ativo=0`); the contract's displayed "Número do Contrato" now points to the new row's id.
5. Open the contract's history — confirm a "Renovar" entry with date, user, and the specific fields that changed relative to the previous record.

## Story 5 — Estender

1. Click "Estender" on the same contract. Confirm the form shows only início, vencimento, prazo de pagamento, prazo de pagamento observação, pagamento via.
2. Change vencimento, save. Confirm only those fields changed and `id` is unchanged; confirm the contract's Valor Total/Valor Médio Mensal on the list recompute for the new term (same per-period rate, produtos untouched).
3. History shows an "Estender" entry with the changed field(s).

## Story 6 — Ampliar/Reduzir

1. Click "Ampliar/Reduzir". Confirm every field is present except Kick Off.
2. Try submitting a change that would *reduce* total value/classes via "Ampliar" — rejected. Try the equivalent *increase* via "Reduzir" — rejected.
3. Submit a valid increase via Ampliar (or decrease via Reduzir), save — confirm (duplicate-row design, kept as-is this session) a **new** `Contrato新` row is active with the updated values, the previous row is deactivated, and a history entry is recorded with the changed fields.

## Story 7 — Cancelar

1. Click "Cancelar" on a contract without entering a justification — confirm the submission is rejected (required field, kept as-is this session).
2. Click "Cancelar" again, this time with a justification — confirm only `ativo`/status (and the justification) changes — no other field is touched.
3. History shows a "Cancelar" entry with date, user, and the justification provided.

## Regression checks (tie to Constitution / non-functional gates)

- **Tenant isolation**: as a `grupo≠1` user (or by temporarily editing session for a test), confirm the contracts list, detail read, and PDF download all refuse a `grupo=1` contract's data/id.
- **Backend access enforcement**: call `includes/ajax/contrato/read.php` and `download.php` directly (e.g. via curl with a non-elevated session cookie) — confirm `403`, not just a hidden UI button.
- **Legacy contract data** (FR-040): open a contract created before the SO5 cutover (e.g. one of the 23 existing `grupo=1` contracts seeded before this feature) — it opens without error, and any stored segmento/insumo id the current SO5 data doesn't recognize is shown flagged as legacy/unrecognized rather than blank or crashing the page.
- **SO5 credentials**: confirm `includes/funcoes/so5.php` reads its base URL/token via `getenv()`, not a literal in source (Principle VI); confirm the browser network tab never shows a direct call to SO5's domain (calls are server-side only).
