# Contract: SO5 outbound integration (Stories 1, 3)

Endpoints changed: `sistema/new/includes/ajax/contrato/{segmento,insumo}.php`, `sistema/new/includes/ajax/produto/so_brand.php` (see `produto-classe-so-brand.md`)
New helper: `sistema/new/includes/funcoes/so5.php`

This is the CRM-side half of the integration. The SO5 endpoints themselves live in SO5's own codebase (`Documents/SO5/SO5-Back-End`, outside this repo, `routes/api.php`) — built as part of the same delivery per the spec's Assumptions. Everything below was confirmed by reading that source and calling the live local SO5 instance (`so5-back-end-web-1`, `SO5_URL=http://localhost:8080`) during implementation, not guessed.

## SO5 endpoints (confirmed live)

All three are POST-only, under `SO5_URL`:

- `api/brands` — no request body needed. Response: `{"message": "...", "brands": [{"id", "slug", "name", "acronym", "description", "primaryColor", "secondaryColor", "website"}], "execution_time": "..."}`.
- `api/segments` — optional `{"brand": <id or name-slug>}` JSON body, filters server-side. Response: `{"message": "...", "segments": [{"id", "name", "description", "brands": [{"id","slug","name"}]}], "execution_time": "..."}`.
- `api/products` — same shape/filtering as `api/segments`, under a `products` key.

**Auth**: `X-Internal-Secret` header, checked against `INTERNAL_API_SECRET` (already defined in `sistema/funcoes/.env` — reused, not a new SO5-specific secret).

**Request body must be raw JSON** (`Content-Type: application/json`) — `routes/api.php` reads `json_decode(file_get_contents('php://input'))`, not `$_POST`. A form-urlencoded body silently fails to populate the `brand` filter and the endpoint falls through to its unfiltered branch (confirmed live: identical response regardless of `brand` value until the body was switched to JSON).

An unresolvable `brand` value (no matching id/slug) returns an empty result, not an error or the unfiltered list.

## Outbound calls (new — no existing precedent in this codebase, research.md §5)

`includes/funcoes/so5.php` provides:

- `so5_get_marcas(): ?array` — used by `produto/so_brand.php`. Calls `api/brands`, unwraps the `brands` envelope, returns `[{id, nome}]`.
- `so5_get_segmentos($marcaCodigo): ?array` — used by `contrato/segmento.php`. Calls `api/segments` with `{"brand": $marcaCodigo}`, unwraps `segments`, returns `[{id, nome}]` scoped to that marca.
- `so5_get_produtos_insumo($marcaCodigo): ?array` — used by `contrato/insumo.php`. Same pattern against `api/products`.

Each function builds the request with `curl_init()`/`curl_setopt_array()` (matching this codebase's only existing outbound-call precedent, `includes/apis/get_cnpj.php`, for the curl style — POST + JSON body, not form fields, to match SO5's actual expectation; no new dependency), reads `SO5_URL`/`INTERNAL_API_SECRET` via `getenv()` (Constitution Principle VI — no hardcoded secret), and on any non-2xx response, curl error, timeout, malformed JSON, or a response missing the expected envelope key, returns `null` (not an empty array — callers must be able to distinguish "SO5 said zero results" from "SO5 didn't answer"). SO5's `id`/`name` fields are normalized to `{id, nome}` before returning, so downstream code keeps the same shape the old mock array already returned.

## `contrato/segmento.php` (rewritten from a hardcoded array)

**Request**: `produto` (the `Produto新.id`).

**Server-side**: look up `Produto新.so_brand` for that id. If `NULL`, return `[]` (empty result, not an error — FR-012/CF-09 edge case, produto not yet linked). Otherwise call `so5_get_segmentos($marcaCodigo)`.

**Response**:
- Success: `[{id, nome}]` (same shape the mock array already returns, so `formproduto.php`'s existing rendering keeps working unchanged) — `id` is SO5's integer brand/segment/product id (confirmed live, research.md §5).
- SO5 unreachable: `502` with `{error: "so5_unavailable"}`. The contract form's produto_marca section shows the scoped inline error from FR-029a and cannot be marked complete until a retry succeeds.

## `contrato/insumo.php` (rewritten from a hardcoded array)

Same request/response shape as `segmento.php`, scoped additionally to the segmentos already selected for that produto_marca in the current form session (matches "Configurar Insumos" — table rows = SO5 produto_insumo for the marca, columns = selected segmentos, per FR-013).

## Failure-mode summary (ties to spec Edge Cases + FR-029a)

| Situation | Behavior |
|---|---|
| Viewing an already-saved contract, SO5 down | Contract still opens; the segmento/insumo display for the affected produto_marca shows a clear inline error instead of the SO5 name, rest of the contract renders normally (existing edge case, unchanged by this feature) |
| Creating/editing a contract, SO5 down | That produto_marca section cannot be completed/marked green; "Finalizar" is blocked for the contract until SO5 responds; other sections (dates, payment, signature, other produto_marca) stay usable (FR-029a, confirmed via `/speckit-clarify`) |
| Produto_marca has no `so_brand` link at all | Empty segmento/insumo list, no error (distinct from "SO5 unreachable" — this is a data-completeness state, not a failure) |

## Acceptance mapping

FR-012, FR-013, FR-029a ↔ this contract. CNF-06 (no client-exposed SO5 credentials) ↔ these calls are entirely server-side (`ajax/*.php`), the browser never talks to SO5 directly. CNF-18 (SO5 failures logged) ↔ `so5.php` logs the failure via `error_log()` before returning `null`, consistent with Constitution Principle VII (no `var_dump()`/`display_errors` in production, log server-side instead).
