feat: connect Tiny through its v3 API with OAuth
All checks were successful
Build and deploy / Validate source (push) Successful in 9s
Build and deploy / Integration suite on a real stack (push) Successful in 2m49s
Build and deploy / Secret scan and release gate (push) Successful in 9s
Build and deploy / Publish images and notify Portainer (push) Has been skipped

Tiny v3 replaces the v2 token adapter. An operator connects Tiny once from
the Kanban; the callback is authorised by a single-use state, because Tiny's
cross-site redirect does not carry the SameSite=Strict operator cookie.
Tokens are kept in provider_tokens, the refresh token rotates under a row
lock, and the worker keeps the connection alive while order creation is off.

Orders find or create the customer's contact by CNPJ, then POST /pedidos
with product ids from TINY_PRODUCT_TEXTIL_FOLHA, _TEXTIL_AVULSA, _UV_FOLHA
and _UV_AVULSA and numeroOrdemCompra DTF-<number>; a retry searches the
customer's recent orders for that number first. The product settings avoid a
_FILE suffix, which the secrets loader reads as a secret file path.

Production passes the application credentials through but keeps
TINY_ADAPTER fake: Tiny has no sandbox, so creating real orders waits for a
supervised test. compose.providers.yaml gives the local API and worker an
internet route for provider testing; the default local stack still has none.

Verified with the full CI integration sequence locally, including the new
tiny_oauth_test against the real database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-24 12:46:09 -03:00
parent c18b9e5b87
commit e3d5558198
17 changed files with 613 additions and 131 deletions

View File

@@ -255,7 +255,15 @@ draws each page and checks where every quadrant of the artwork lands.
`app/mercadopago.py` and `app/tiny.py` follow the providers' public API
documentation and pass their unit suites against a fake transport. They are not
verified integrations until they pass with the client's sandbox accounts.
verified integrations until they pass with the client's accounts.
The default local stack gives the API and worker no route to the internet, so
provider testing adds `compose.providers.yaml`, which does:
```bash
docker compose -f compose.local.yaml -f compose.providers.yaml up -d --wait
```
Put only **test** credentials in `.env`:
```bash
@@ -263,11 +271,38 @@ PAYMENT_ADAPTER=mercadopago
MP_ACCESS_TOKEN=TEST-...
MP_WEBHOOK_SECRET=... # "Assinatura secreta" in the webhook settings
MP_NOTIFICATION_URL=https://<public tunnel>/api/payments/webhook
TINY_ADAPTER=tiny
TINY_TOKEN=...
TINY_TAG=Site DTF # optional marker on created orders
```
### Tiny (API v3)
Tiny uses OAuth2. In the client's Tiny (Construa plan or above, with the
"Gestão de Aplicativos" extension): **Configurações → Geral → Aplicativos →
+ novo aplicativo**, with the redirect URL set to this system's callback. That
gives a client ID and secret:
```bash
TINY_CLIENT_ID=...
TINY_CLIENT_SECRET=...
TINY_REDIRECT_URI=http://localhost:8081/api/operator/tiny/callback # exactly as registered in Tiny
TINY_PRODUCT_TEXTIL_FOLHA=... # Tiny product id for each Site product
TINY_PRODUCT_TEXTIL_AVULSA=...
TINY_PRODUCT_UV_FOLHA=...
TINY_PRODUCT_UV_AVULSA=...
```
With the client ID and secret set, the Kanban header shows **Conectar Tiny**.
Someone with a Tiny login approves access once; the tokens are stored in the
database (the refresh token rotates on every use) and the worker keeps the
connection alive. Only then set `TINY_ADAPTER=tiny`, which starts creating an
order in Tiny for every paid order: find or create the customer's contact by
CNPJ, then `POST /pedidos` with `numeroOrdemCompra = DTF-<order number>`. A
retry searches the customer's recent orders for that number first, so it does
not create a second one. **Tiny has no sandbox**: every test order is real, so
agree the test with the client and cancel the test orders afterwards.
`tests.tiny_oauth_test` checks the connection flow against the real database
with a fake token server; it saves and restores any existing connection.
With Mercado Pago selected, an approved quote shows **Pagar com PIX** on the
Site instead of the local test button. The order is created only by the signed
notification, after the payment is fetched from the Mercado Pago API and its

View File

@@ -237,13 +237,23 @@ From the report already sent. These are dated promises, not backlog.
packaging weight/dimensions per length, subsidy policy.
- `[~]` 1.3 — Idempotent Tiny/Olist order creation with order-number traceability.
Confirm endpoints, tag behaviour and rate limits first.
**Groundwork (2026-09-24):** `app/tiny.py` (API 2.0) maps the approved
snapshot to `pedido.incluir` with `numero_pedido_ecommerce = DTF-<number>`,
searches for that number before creating, and treats Tiny's in-body errors
as failures so the outbox retries. Pickup keeps the existing `forma_envio X`
/ DropStar convention. Selected with `TINY_ADAPTER=tiny`; tested against a
fake transport only. Product codes (`TINY_SKU_<MODE>`), the tag, API version
(2.0 vs 3.0) and rate limits must be confirmed on the client's account.
**Groundwork (2026-09-24), API v3 by decision:** OAuth2 against Tiny's
Keycloak. An operator starts the connection from the Kanban; the callback is
authorised by a single-use state (the operator cookie is SameSite=Strict and
does not survive Tiny's cross-site redirect). Tokens live in
`provider_tokens`; the refresh token rotates under a row lock and the worker
keeps the connection alive. Orders: contact found by CNPJ or created, then
`POST /pedidos` with product ids from `TINY_PRODUCT_TEXTIL_FOLHA` / `_TEXTIL_AVULSA` / `_UV_FOLHA`
/ `_UV_AVULSA` (not `_<MODE>`: a name ending in `_FILE` is read as a secret
file path by `app/core/secrets.py`) and
`numeroOrdemCompra = DTF-<number>`; a retry searches the customer's last
seven days of orders for that number first. Production passes the app
credentials through but keeps `TINY_ADAPTER: fake`. Tested against fake
transports (`tests.test_tiny`) and, for OAuth, the real database
(`tests.tiny_oauth_test`). Tiny has no sandbox: the first real test creates
real orders. Still to confirm on the client's account: plan (Construa+),
product ids, token lifetimes, whether pickup needs a transportador, and
rate limits.
- `[~]` 1.4 — Final print-file generation (see 3.2 and 3.6: production instructions
must survive checkout before an output engine can reproduce the approved job).
**Built (2026-09-24):** each paid item gets a PDF the width of the film and