# Implementation Plan: Contracts Module (Contratos)

**Branch**: `003-contratos-module` | **Date**: 2026-08-25 | **Spec**: [spec.md](./spec.md)

**Input**: Feature specification from `/specs/003-contratos-module/spec.md`

## Summary

This is a completion/correction pass over an already-substantially-built Contratos module, not a greenfield build. `sistema/new/pages/contratos/` already has a working 4-page wizard, and `includes/ajax/contrato/*` already implements most of create/update/cancel/read — research (`research.md`) found the real gaps are: (1) `segmento`/`insumo` are hardcoded mock arrays that need a real, newly-built outbound SO5 integration (no such outbound-call precedent exists anywhere in this codebase today); (2) Renovar/Ampliar/Reduzir duplicate the `Contrato新` row rather than updating it in place — an earlier draft of this plan proposed changing that to match the spec's original wording, but that decision was reverted this session: the existing duplicate-row/deactivate-old-row design is kept as-is (no code change), and `spec.md` (FR-031, FR-033/FR-035, User Stories 4/6) was updated to describe that design instead; (3) Cancelar requires a justification field — an earlier draft proposed dropping that requirement to match the spec's original wording, also reverted this session: the requirement stays (no code change), and `spec.md` (FR-036, User Story 7) was updated to describe that instead; (4) `read.php`/`download.php` have no tenant (`grupo`) check at all, a Constitution Principle I gap this feature's own heavy edits to those files must close; (5) "Gestão" is **not** a new permission tier — it's an existing `AreaComercial新` row (id 3) reached via a Padrão-profile user's `Cargo新.area`; access is `perfil==2` OR (`perfil==1` AND área Gestão), via one new shared helper — no schema change; (6) Produto's Classe infrastructure (`Classe新`/`ProdutoClasse新`/`CategoriaClasse新`) already exists and already holds the right 3 commercial values — this feature adds combination-rule validation, a rename, and a migration, it does not build a new classification system; (7) a new `Produto新.so_brand` column carries the SO5 link; (8) contract history reuses the existing generic `Logs新` audit table with a structured JSON diff (computed old-record-vs-new-record for Renovar/Ampliar/Reduzir, since those create a new row), rather than a new table; (9) a Pacote Padrão → insumo pre-marking cascade (FR-014) and a legacy-data display path for pre-migration contracts' segmento/insumo (FR-040) were both missing from the original plan and are added here; (10) `create.php`/`update.php`'s catch-block stack-trace leaks are fixed using the same response shape the in-flight `fix/general-backlog` branch already established for this exact problem, to avoid diverging patterns at merge time.

## Technical Context

**Language/Version**: PHP 8.0 (server), vanilla JavaScript + jQuery (client, no bundler/transpiler) — matches every file already in `sistema/new`.

**Primary Dependencies**: mysqli (prepared statements only), Choices.js (existing searchable-select selects, `.choices-select-element`), no new client-side dependency. Server-side outbound HTTP to SO5 via raw `curl_init()` (research.md §5 — no HTTP client library exists in this codebase to reuse; introducing one, e.g. Guzzle, would require a `composer.json` this project doesn't have and is out of proportion to two new outbound calls).

**Storage**: MySQL (`yebcrm_sistema`), mysqli only, no ORM. Exactly one new column (`Produto新.so_brand`) — no new tables, no new lookup rows ("Gestão" already exists in `AreaComercial新`; data-model.md).

**Testing**: No automated test suite exists in this codebase (Constitution Principle VIII). `quickstart.md` is the actual manual verification plan.

**Target Platform**: Apache + PHP 8 under Docker Compose (server); evergreen browsers rendering server-rendered PHP + jQuery (client). Local dev DB confirmed reachable at `crm-mysql-crm-1` for this planning pass.

**Project Type**: Single monolithic web application, pages/ajax/js triad (Constitution Principle V) — `sistema/new/pages/contratos/`, `sistema/new/pages/produto/`, `sistema/new/includes/ajax/{contrato,produto}/`, `sistema/new/includes/js/contratos.js`. No new frontend/backend split.

**Performance Goals**: None newly introduced beyond the spec's own non-functional proposal baseline (CNF-01–03, explicitly marked "adjust before treating as a formal gate" in spec.md); this plan does not add any hard perf requirement.

**Constraints**: Tenant isolation via `Contrato新.empresa → Empresa新.grupo` (Principle I — currently missing in `read.php`/`download.php`, closed by this plan since both files are heavily touched anyway); parameterized queries only (Principle II); auth gate on every touched/new ajax entry point (Principle III, already present via `ajaxheader.php`) plus a new `usuario_pode_gerir_contratos()` access check (FR-001 — Admin, or Padrão + área Gestão); no new sibling/fork files (Principle IV); all new functionality lands inside the existing triad (Principle V); SO5 credentials via `getenv()` only (Principle VI, CNF-06); production errors fail safe — SO5 failures `error_log()`'d server-side, never `var_dump()`'d to the client (Principle VII, CNF-18), and `create.php`/`update.php`'s own catch blocks are brought in line with the same principle using the response shape already established on `fix/general-backlog`; Estender/Cancelar are same-row `UPDATE`s; Renovar/Ampliar/Reduzir keep their existing create-new-row/deactivate-old-row design (reverted this session) — no hard `DELETE` of a `Contrato新` row anywhere either way (Principle X).

**Scale/Scope**: Internal CRM, effectively single-tenant in practice today (`grupo=1` is the only tenant with `Contrato新`/classe-eligible `Produto新` rows — confirmed against the live DB), tens of concurrent internal Admin/Gestão users. Touches ~15 existing files across 8 user stories, adds 2 new files (`includes/funcoes/so5.php`, `includes/ajax/produto/so_brand.php`) plus migration scripts; no new pages.

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

| Principle | Check | Status |
|---|---|---|
| I. Tenant isolation (`grupo`) | `read.php` and `download.php` currently have **no** tenant check (`read.php` assigns `$grupo` but never uses it; `download.php` has no check of any kind) — a real, pre-existing gap. Both files are already being heavily modified by this feature (SO5-sourced fields, new access gate, PDF validation), so the `Empresa新.grupo` join is added as part of this same change, not deferred. Every other touched/new query (produto's `so_brand`, `Classe新`/`ProdutoClasse新` writes, `Logs新` writes) already goes through `grupo`-scoped parent lookups matching existing patterns. | PASS (gap found and closed by this plan, not silently left) |
| II. Parameterized queries only | All new/modified SQL (so_brand column, Perfil新 insert, Classe新 rename, migration script, history diff writes) uses mysqli `prepare()`/`bind_param()`, matching every file touched. The new SO5 outbound calls are HTTP, not SQL — no injection surface there. | PASS |
| III. Auth gate on every entry point | Every touched/new ajax file already starts with `ajaxheader.php` (`sec_session_start()` + `login_check()`); the new `so_brand.php` file follows the identical pattern. Additionally, every contracts-module entry point gets the new `usuario_pode_gerir_contratos()` check (FR-001) — stricter than the baseline, not a relaxation. | PASS |
| IV. No new forks; warn before touching an old one | No `-dev`/`-old`/`-bak`/`v2` files created. `readdetailed.php` (an existing exploratory/debug script with a hardcoded `id=38` and an unreachable function) is **not** touched or extended by this feature — flagged here for reviewer visibility as a pre-existing dead/broken file this plan does not attempt to fix or remove, since it's out of this feature's scope. | PASS |
| V. Pages/Ajax/JS triad | Every touched or new file lives inside `pages/contratos/`, `pages/produto/`, `includes/ajax/{contrato,produto}/`, or `includes/js/contratos.js`; the one new non-ajax file (`includes/funcoes/so5.php`) matches the existing sibling pattern (`includes/funcoes/funcoes.php`, `get_info.php`, etc.). No legacy root-level monolith file is touched. | PASS |
| VI. No secrets in source | New `includes/funcoes/so5.php` reads SO5's base URL/credentials via `getenv()` only, matching how existing secrets are sourced elsewhere in this codebase (Constitution baseline) — no hardcoded token. | PASS |
| VII. Errors fail safe in production | SO5 call failures are caught in `so5.php` and logged via `error_log()`, returning `null` to the caller — never `var_dump()`'d or surfaced with `display_errors`. `create.php`/`update.php`'s own catch blocks currently `echo $e->getMessage()` + `print_r($e->getTrace())` on failure — a pre-existing violation, but one this feature's own new code sits directly inside (T016, T017, T022, T025, T027, T028 all add logic within that same try/catch scope), so per the Constitution's "contributors fix anti-patterns in code they touch" workflow rule this is fixed as part of this feature, not left in place. Fixed using the exact response shape (`error_log($e)`; `{"success":false,"message":"Erro inesperado. Tente novamente.","data":null}`) the `fix/general-backlog` branch already established for this same problem (research.md §12), so the two branches converge instead of producing conflicting "safe" error formats at merge time. | PASS |
| VIII. Manual verification standard | `quickstart.md` (Phase 1) is the full manual verification plan for all 8 user stories plus tenant/access regression checks, since no automated suite exists. | PASS |
| IX. Plataforma API auth | Not applicable — unrelated subsystem (external Next.js "Plataforma" portal), not touched by this feature. | N/A |
| X. No hard deletes for main objects | `Contrato新` itself is never hard-deleted by any path in this feature. Cancelar sets `ativo=0` on the same row. Renovar/Ampliar/Reduzir keep their existing design (reverted this session, research.md §2): each creates a new `Contrato新` row and deactivates the previous one (`ativo=0`) — the previous record is deactivated, never hard-deleted, so this already satisfies the deactivate-not-delete spirit of this principle without any code change. Satellite-row wipe-and-reinsert on `ContratoProduto新` and its children (existing pattern for `create`/`editaradmin`; `renovar`/`ampliar`/`reduzir` insert fresh satellite rows against the new row, unchanged) is the Constitution-recognized related-row pattern, not a main-object delete — flagged here per the Constitution's own instruction that every such instance is a warn-and-discuss case, not an automatic pass. | PASS (flagged, not blocked) |

No unjustified violations. Complexity Tracking table below is empty as a result.

**Post-Phase-1 re-check**: `data-model.md` (new `Produto新.so_brand` column, no new tables, no new lookup rows) and `contracts/` (three files) were reviewed against the table above after design — nothing new surfaces. The tenant-isolation fix to `read.php`/`download.php` and the `usuario_pode_gerir_contratos()` access gate are still accurately reflected; Renovar/Ampliar/Reduzir's row-duplication design and Cancelar's required `motivo` are both kept as-is (reverted this session) and `spec.md` was updated to match. Gate still PASSes.

## Project Structure

### Documentation (this feature)

```text
specs/003-contratos-module/
├── plan.md              # This file (/speckit-plan command output)
├── research.md          # Phase 0 output (/speckit-plan command)
├── data-model.md        # Phase 1 output (/speckit-plan command)
├── quickstart.md        # Phase 1 output (/speckit-plan command)
├── contracts/           # Phase 1 output (/speckit-plan command)
│   ├── contrato-crud.md
│   ├── produto-classe-so-brand.md
│   └── so5-integration.md
└── tasks.md             # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan)
```

### Source Code (repository root)

Single existing monolithic project (`sistema/new`), pages/ajax/js triad (Constitution Principle V). This feature modifies an already-substantial existing Contratos/Produto implementation in place, plus a small number of new files consistent with existing directory conventions — no new pages, no new top-level modules:

```text
sistema/new/
├── pages/
│   ├── contratos/
│   │   └── contratos.php              # Story 2: add access gate; Story 3: new produto_marca session fields (segmento/insumo now SO5-backed, classe rules, status indicator); Story 4-7: renovar/estender/ampliar/reduzir/cancelar form field sets
│   └── produto/
│       └── cadastro.php               # Story 1: "Marca SO" select field
├── includes/
│   ├── ajax/
│   │   ├── contrato/
│   │   │   ├── create.php             # Story 3: PDF/size validation, SO5-unreachable handling (FR-029a), demais-excludes-focal guard, one-segmento-per-insumo guard, safe catch-block response
│   │   │   ├── update.php             # Stories 3-6: renovar/ampliar/reduzir keep duplicate()/deactivate-old-row unchanged (reverted this session, research.md §2); structured Logs新 diff added on every mode; same guards/PDF-check as create.php on the modes that accept those fields; safe catch-block response
│   │   │   ├── cancel.php             # Story 7: access gate added; motivo stays required, unchanged (reverted this session, research.md §3); structured Logs新 diff
│   │   │   ├── read.php               # Story 2: tenant (grupo) check added; new history sub-response
│   │   │   ├── download.php           # Story 2: tenant check + access gate added (currently has neither)
│   │   │   ├── segmento.php           # Story 3: rewritten from hardcoded array → so5_get_segmentos()
│   │   │   ├── insumo.php             # Story 3: rewritten from hardcoded array → so5_get_produtos_insumo()
│   │   │   ├── formproduto.php        # Story 3: render Marca SO-derived segmento/insumo instead of mock; flag legacy (pre-cutover) segmento/insumo ids the current SO5 data doesn't recognize (FR-040, research.md §11); classe options already correct (existing ProdutoClasse新 query)
│   │   │   └── (divisao.php, periodicidade.php, tipo.php, entregavel.php, pacote.php, form.php, formvalores.php — unchanged, already correct per research.md §1)
│   │   └── produto/
│   │       ├── create.php             # Story 1: so_brand write only - no classe combination-rule validation here (that rule applies at contract time, see contrato/create.php|update.php above, research.md §6)
│   │       ├── update.php             # Story 1: same, so_brand write only
│   │       ├── read.php               # Story 1: return so_brand
│   │       └── so_brand.php           # NEW — Story 1: SO5 marcas list for the "Marca SO" select
│   ├── funcoes/
│   │   ├── so5.php                    # NEW — outbound SO5 HTTP calls (research.md §5); no existing helper to extend
│   │   └── funcoes.php                # add usuario_pode_gerir_contratos() — Admin, or Padrão + área Gestão via Cargo新.area/AreaComercial新 (research.md §8)
│   └── js/
│       └── contratos.js               # Stories 2-7: access-gate-aware UI, SO5-backed segmento/insumo rendering, per-section status indicator, Pacote Padrão → insumo pre-marking cascade (FR-014, research.md §10), renovar/estender/ampliar/reduzir/cancelar field-set wiring, drop required on motivo cancelamento
└── migrations/
    ├── sql/produto_so_brand_column.sql  # NEW — add `Produto新.so_brand` column (schema changes now live here as plain .sql, convention adopted this session)
    └── (new, Story 1) Classe/Marca SO backfill for existing categoria-1/2 produtos, PHP script (data backfill with business logic + Logs新 writes — not a pure schema change, stays under the existing PHP-script convention; contracts/produto-classe-so-brand.md)
```

**Structure Decision**: No new pages, no new top-level ajax modules. Every change is a modification to a file that already exists inside the established `pages/contratos/`, `pages/produto/`, `includes/ajax/{contrato,produto}/`, or `includes/js/contratos.js`, plus exactly two new files that match existing sibling conventions (`includes/funcoes/so5.php` alongside `includes/funcoes/funcoes.php`; `includes/ajax/produto/so_brand.php` alongside its existing `produto/*.php` siblings) and two new one-off migrations under `migrations/`: the `so_brand` schema change as a plain `.sql` file in the (pre-existing, previously-empty) `migrations/sql/` folder — the new convention adopted this session for column/table changes, run directly against the DB, idempotent via an `INFORMATION_SCHEMA` check + `PREPARE`/`EXECUTE` since MySQL has no native `ADD COLUMN IF NOT EXISTS` — and the Classe/so_brand data backfill as a manual, web-triggered PHP script matching `sistema/new/migrations/`'s existing convention for backfills with business logic (no migrations-tracking framework either way).

## Complexity Tracking

*No entries — no unjustified Constitution Check violations. The two flagged-not-blocked items (Principle X's satellite wipe-and-reinsert; Principle VII's pre-existing `create.php`/`update.php` catch-block `echo`/`print_r`) are existing patterns this feature either already matches the Constitution's own accepted-pattern carve-out for, or does not expand — neither required a complexity justification.*
