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

@@ -93,7 +93,7 @@ async function load(){
moreQuotes={pending:board.pending_total>board.quotes.filter(q=>!q.approved).length,
approved:board.approved_total>board.quotes.filter(q=>!!q.approved).length};
$('login').hidden=true;
$('status').textContent='Atualizado às '+new Date().toLocaleTimeString();
$('status').textContent='Atualizado às '+new Date().toLocaleTimeString()+tinyNotice;
render();
}catch(e){$('status').textContent=e.message;$('login').hidden=false;}
}
@@ -108,7 +108,24 @@ async function loadMoreQuotes(kind){
moreQuotes[kind]=page.has_more;
render();
}
// The one-time authorisation of this system in the client's Tiny. Shown only
// when the Tiny application is configured on the server.
function tinyStatus(){
const box=$('tiny');box.replaceChildren();
const tiny=board.tiny||{};
if(!tiny.configured)return;
box.append(node('span',tiny.connected
? 'Tiny conectado'+(tiny.orders_enabled?'':' · envio de pedidos desligado')
: 'Tiny não conectado'));
box.append(action(tiny.connected?'Reconectar Tiny':'Conectar Tiny',async()=>{
const result=await api('/tiny/connect',{});location.href=result.url;
}));
}
const tinyResult=new URLSearchParams(location.search).get('tiny');
const tinyNotice=tinyResult?(tinyResult==='connected'?' · Tiny conectado.':' · Não foi possível conectar o Tiny. Tente de novo.'):'';
if(tinyResult)history.replaceState(null,'',location.pathname);
function render(){
tinyStatus();
paymentIssues();
$('reviews').replaceChildren();
if(board.pending_total || board.approved_total)