# Contract Approval

AI contract review plus a **role-enforced** approval workflow for Twenty Opportunities.

A salesperson creates a **Contract** linked to an Opportunity, attaches the contract PDF, and submits it for review. An AI review reads the PDF and flags risky or inconsistent clauses — notably a **customer-vs-supplier payment-terms mismatch** (e.g. the customer pays monthly while the supplier must be paid in a single upfront installment). Managers/partners (**Gerente/Sócio**) then approve or reject, and **only that role can approve**.

## What it installs

- **Contract** (custom object, related to Opportunity): payment terms (customer/supplier), lifecycle `status`, AI review summary, structured AI findings, `aiPaymentMismatch`, approver, submitter, and timestamps. The contract PDF attaches to the Contract via the native Attachment relation.
- **Renewal tracking** on Contract: `dataInicio` / `dataTermino` (vigência) and `estagioRenovacao` (`ATIVO` → `RENOVACAO_INICIADA` → `RENOVADO` / `ENCERRADO`, default `ATIVO`). See [Renewal tracking](#renewal-tracking).
- **Approval Decision** (custom object, child of Contract): the append-only audit of who decided what, when, and why (`decidedBy`, `decision`, `comment`, `decidedAt`).
- **Gerente/Sócio** role: assignable to users; the record-decision function checks this role before accepting an approval. A declarative field-permission on `Contract.status` is shipped as defense-in-depth.
- **Logic functions**: `ai-review`, `submit-for-review`, and `record-decision` (all route-auth'd), plus a no-op `post-install`.
- **Front-component actions** on a Contract: **Submit for review**, **Approve**, **Reject**.
- **Pending approvals** view: contracts with `status = pending_manager_approval` — the manager's work queue.

## How it works

1. **Submit for review** sets `status = pending_ai_review`, records the submitter, then runs the AI review directly (no workflow required).
2. **AI review** fetches the Contract's most-recent PDF attachment (signed URL), sends it to the Anthropic Messages API as a `document` block with a broad clause-review prompt, writes back `aiReviewSummary` / `aiFindings` / `aiPaymentMismatch`, and advances `status = pending_manager_approval`. Supplier payment terms absent from the contract fall back to the standard — see [Supplier payment default](#supplier-payment-default).
3. **Approve / Reject** calls the route-auth'd `record-decision` function, which resolves the **invoking user** and verifies they hold the Gerente/Sócio role before writing an Approval Decision (`decidedBy` = the actor) and updating the contract. A non-manager attempt is refused with a clear message and nothing is written.

## Setup

1. Install the app.
2. In **Settings → Apps**, set `ANTHROPIC_API_KEY` (required for AI review). Optionally set `LLM_MODEL` (defaults to a multimodal Claude model). Without the key, the AI step degrades gracefully: it writes a "not configured" summary and the contract awaits manual manager review.
3. Assign the **Gerente/Sócio** role to the members who may approve.

## Supplier payment default

When a contract does not state how the supplier is paid, the review applies the Excelium standard: a **single payment (à vista) 30 days after signature** (`src/constants/payment-terms.ts`).

The *format* matters as much as the term. Paying the supplier in one shot is exactly what creates cash-flow exposure when we bill the customer in installments — money goes out at once while it comes in over months. That comparison is the point of the whole check.

The default is applied in two places, deliberately split:

- **In the prompt** — the model is told the standard so it can still return a `match`/`mismatch` verdict instead of `unknown`, plus an explicit rule: customer paying in installments/recurring + supplier paid in a single installment ⇒ `mismatch`, with a `vermelho` finding flagged for manual review. It is also told *not* to invent a clause — when the contract is silent it must return `supplierPaymentTerms: null`.
- **In code** — `paymentTermsSupplier` is then filled from that null with the default. Manual input is never overwritten, and an extracted clause always wins over the default.

The stored value carries its own label — `Parcela única (à vista) em 30 dias após a assinatura do contrato (padrão — não explícito no contrato)` — and the AI-review Note states the same. This is the point of the design: a presumption must never be readable as a contractual term. To change the standard, edit `DEFAULT_SUPPLIER_PAYMENT_TERMS_DAYS` / `DEFAULT_SUPPLIER_PAYMENT_TERMS_SHORT`; the prompt, the stored value and the Note all derive from them.

### Mismatch backstop

The prompt is what actually reads the contract, so it is the primary mechanism. On top of it, `ai-review-contract` runs a narrow guard (`paysInInstallments`, a pt-br text heuristic): when the review returns **`unknown`**, the supplier side is the **applied default** (so it is known to be a single payment), and the customer terms read as installments/recurring, the verdict is escalated to `MISMATCH`.

It only ever escalates a non-verdict — an explicit `MATCH` or `MISMATCH` from the review is never overridden — and it stands down when supplier terms were filled by hand, since those may themselves be an installment plan. A false positive here costs one extra manager glance; a false negative costs cash.

## Renewal tracking

`dataInicio`, `dataTermino` and `estagioRenovacao` support renewal follow-up: `dataTermino` is the current term's expiry and is what any renewal alert should key off; `estagioRenovacao` is where the contract sits in the renewal cycle. Both are nullable — a contract with no `dataTermino` is invisible to every renewal report, so backfilling existing contracts is a prerequisite, not an optimisation.

**These three fields were first created directly in the production workspace** (via the metadata API, under the workspace's own application) and only afterwards declared here. Their `universalIdentifier`s in `src/constants/universal-identifiers.ts` are therefore **pinned to the live UUIDs** rather than freshly generated — `scripts/generate-universal-identifiers.mjs` must not be allowed to overwrite them.

Consequence for the first deploy that ships these declarations: the live fields belong to a *different* `applicationId` than the Contract object, so they are outside this app's metadata diff. The deploy will try to **create** them rather than adopt them, and creation collides with the existing fields on the `(object, name)` uniqueness constraint. Before that deploy, either confirm the migration adopts fields by `universalIdentifier`, or delete the three workspace-owned fields first and let the app recreate them. Deleting them also invalidates any dashboard widget that references their field-metadata IDs — those widgets must be repointed afterwards.

## Graceful degradation

- **No LLM key** → AI review writes a "not configured" summary and leaves the status for manual review.
- **No downloadable PDF** → the AI review falls back to the structured payment-terms fields.

## Development

```bash
yarn install
yarn twenty dev:build
yarn typecheck
yarn lint
yarn test        # unit tests (offline, fixtures/mocks)
```

Regenerate all universal identifiers:

```bash
node scripts/generate-universal-identifiers.mjs
```

## Notes / out of scope

- Live-instance validation (real PDF → LLM round trip, front-component→route end-user auth, install-time workflow seeding) is deferred; unit tests use mocks.
- Email notifications require a connected account and are not install-seedable; the pending-approvals view is the notification surface.
