# Contract: Produto — Marca SO link + Classe rules (Story 1)

Endpoints: `sistema/new/includes/ajax/produto/{create,update,read}.php`, new `sistema/new/includes/ajax/produto/so_brand.php`
Callers: `sistema/new/pages/produto/cadastro.php`

## `so_brand.php` (new)

**Purpose**: populate the "Marca SO" select in Cadastro > Produto from live SO5 data (FR-002).

**Request**: none beyond the standard session/auth (this list isn't scoped by anything else — SO5's own marca catalog is the whole list).

**Response**: `[{codigo, nome}]` — proxies the SO5 outbound call described in `so5-integration.md`. On SO5 unreachable: `502` with a clear message; the produto form shows an inline error on the Marca SO select and lets the rest of the produto form still be saved (FR-002 doesn't require the link to complete the produto save — a produto can exist with `so_brand IS NULL`, per FR-003 edge case).

## `create.php` / `update.php` (changed)

**Request — new field**: `so_brand` (string|null) — the `codigo` selected from `so_brand.php`'s list. Written to the new `Produto新.so_brand` column (data-model.md).

**No new validation on `classe[]`**: `classe[]` (existing field, already present per John RH's earlier addition) is written exactly as before — a produto_marca can be linked to any subset of the three classe-eligible values (`Plataforma`/`Serviço Padrão`/`VHP`), including all three, since this is configuring the set of **available options**, not a selection. The mutual-exclusivity/dependency rule (FR-004) is enforced at contract time instead — see `contrato-crud.md`'s `classe-{produto}` row. (Corrected this session — an earlier pass of this implementation added the FR-004 check here by mistake; reverted.)

**Server-side handling**: unchanged transaction shape, `ProdutoClasse新` rows written the same way as today (loop + insert), plus the new `so_brand` column write.

## `read.php` (changed)

**Response — new field**: `so_brand` (the stored SO5 code, or `null`).

## Migration (one-off script under `sistema/new/migrations/`, FR-038/FR-039)

Scope: `Produto新` rows where `grupo=1` and `categoria IN (1, 2)` (the 12 classe-eligible produtos today — research.md §6). For each:

1. **Classe** (FR-038): if the produto has none of the three (`Plataforma`/`Serviço Padrão`/`VHP`) linked, insert `VHP`(3) as a default baseline available option (an unclassified produto otherwise has zero selectable Classe options in a contract). Produtos already linked to any combination of the three, including all three at once, are left as-is — that's not a conflict at the produto-configuration level (corrected this session; an earlier pass wrongly deleted the `VHP` link from 5 produtos also linked to `Plataforma`, reverted via `migrations/sql/produto_classe_vhp_correction.sql`). Idempotent: re-running only ever inserts a still-missing default — a second run finds nothing left to do (research.md §6, FR-041).
2. **Marca SO** (FR-039): for every produto in scope with `so_brand IS NULL`, `UPDATE Produto新 SET so_brand = id`. Guarded by the `IS NULL` check, so re-running is a no-op on already-migrated rows (idempotent per FR-041).
3. ~~**Classe rename** (FR-005): `UPDATE Classe新 SET nome='Plataforma + Serviço Padrão' WHERE id=2 AND grupo=1` — idempotent (setting the same value twice is a no-op).~~ **Reverted post-launch**: `migrations/sql/produto_classe_nome_reversao.sql` (`UPDATE Classe新 SET nome='Serviço Padrão' WHERE id=2 AND grupo=1`, same id+grupo targeting, idempotent) restores the original name. FR-005 is withdrawn.

Logged via the same `Logs新` mechanism (`objeto="Produto"`, one row per produto touched) so the migration's effect is auditable (CNF-08).

## Acceptance mapping

FR-002–FR-005, FR-016, FR-038, FR-039, FR-041 ↔ this contract.
