Importante: todo o projeto usa apenas JavaScript nos
<script>dos componentes Svelte. Os exemplos abaixo seguem esse padrão (nada de TypeScript ou arquivos.ts).
Este guia explica como o frontend Svelte do Tooeasy deve se integrar aos novos
endpoints PHP de orderbook e pagamentos. O objetivo é aproveitar os scripts
já existentes no projeto (<script> dos componentes Svelte) sem criar novos
bundles JavaScript externos.
VITE_API_URL configurada apontando para a API PHP (ex.: http://localhost:8000).authToken e companyId já disponíveis em $lib/utils/stores.src/routes/trading/+page.svelte: filtros de estado/commodity e tabela do
book (reaproveitaremos a lógica atual).src/routes/cpr/+page.svelte: modal Pix (pode ser reutilizado para o fluxo
de compra de ordens).const token = get(authToken);
const headers = {
'content-type': 'application/json',
Authorization: `Bearer ${token}`
};
Todas as chamadas descritas abaixo devem ser feitas dentro do <script> dos
componentes Svelte. Use fetch nativo e atualize as let reativas existentes.
No topo do script do +page.svelte (trading) vale manter um helper para tratar
as respostas envelopadas:
async function parseResponse(res) {
const raw = await res.text();
return raw ? JSON.parse(raw) : null;
}
POST /orderbook/filterOnde usar: sempre que selectedState ou selectedCommodity mudar.
{
"state": "SP",
"commodity_type": "SOJA"
}
{
"status": "ok",
"msg": "[100] Request ok.",
"code": "S_ORDERBOOK_FILTER",
"data": {
"state": "SP",
"commodity_type": "SOJA",
"orders": [
{
"orderbook_id": 321,
"orderbook_amount": "1000",
"token_external_id": "TOKEN_ABC123",
"status_id": 0,
"token_commodities_value": 150000,
"token_commodities_amount": 1000,
"chain_id": 1,
"wallet_id": 10
}
]
}
}
400 – E_VALIDATE: campos ausentes ou vazios.500 – E_DATABASE: falha ao consultar o book.
async function fetchOrderbook(state, commodityId) {
const payload = {
state,
commodity_type: commodityPayloadValue(commodityId)
};
orderbookLoading = true;
orderbookError = '';
try {
const res = await fetch(`${apiUrl}/orderbook/filter`, {
method: 'POST',
headers,
body: JSON.stringify(payload)
});
const body = await parseResponse(res);
if (!res.ok || body?.status !== 'ok') {
throw new Error(body?.msg ?? 'Falha ao carregar ordens.');
}
const orders = body?.data?.orders ?? [];
ordensVenda = orders.map(mapOrderResponse);
orderbookEmptyMessage = orders.length ? '' : 'Nenhuma ordem encontrada.';
} catch (err) {
orderbookError = err?.message ?? 'Falha ao carregar ordens.';
resetOrderbook();
} finally {
orderbookLoading = false;
}
}
mapOrderResponse deve converter token_commodities_value para valor em R$
e orderbook_amount para quantidade.
POST /token/orderbookOnde usar: após emitir uma CPR e desejar listar o token no book (ex.: botão "Publicar no orderbook" dentro da tela de emissão).
{
"cpr_id": 42,
"value": 150000.75,
"state": "SP",
"commodity_type": "SOJA",
"token_external_id": "TOKEN_ABC123"
}
{
"status": "ok",
"msg": "[100] Request ok.",
"code": "S_ORDERBOOK_CREATED",
"data": {
"message": "Token atualizado e ordem registrada com sucesso",
"token_id": 99,
"orderbook_id": 321
}
}
400 – E_VALIDATE: validação falhou (cpr_id, campos vazios etc.).404 – E_TOKEN_NOT_FOUND: token inexistente para a CPR.409 – E_TOKEN_MISMATCH: token informado não bate com o cadastro da CPR.500 – E_ORDERBOOK: erro interno ao atualizar token/criar ordem.
async function createOrderbookEntry(formData) {
const payload = {
cpr_id: formData.cpr_id,
value: Number(formData.value),
state: formData.state?.toUpperCase(),
commodity_type: formData.commodity,
token_external_id: formData.token_external_id
};
const res = await fetch(`${apiUrl}/token/orderbook`, {
method: 'POST',
headers,
body: JSON.stringify(payload)
});
const body = await parseResponse(res);
if (!res.ok || body?.status !== 'ok') {
throw new Error(body?.msg ?? 'Falha ao registrar ordem.');
}
return body?.data; // contém token_id e orderbook_id
}
POST /orderbook/paymentOnde usar: quando o usuário clicar em uma ordem para comprar.
{
"orderbook_id": 321
}
{
"status": "ok",
"msg": "[100] Request ok.",
"code": "S_ORDERBOOK_PAYMENT",
"data": {
"orderbook_id": 321,
"payment_id": 555,
"payment_code": "000201...",
"payment_external_id": "PAY_a1b2c3d4",
"token_external_id": "TOKEN_ABC123"
}
}
400 – E_VALIDATE: ID inválido.404 – E_NOT_FOUND: orderbook inexistente.409 – E_ORDERBOOK_STATUS: ordem não está STATUS_OPEN.422 – E_TOKEN_VALUE: valor calculado ≤ 0.500 – E_DATABASE ou E_PAYMENT: falha ao iniciar pagamento.
async function startOrderPayment(orderbookId) {
const res = await fetch(`${apiUrl}/orderbook/payment`, {
method: 'POST',
headers,
body: JSON.stringify({ orderbook_id: orderbookId })
});
const body = await parseResponse(res);
if (!res.ok || body?.status !== 'ok') {
throw new Error(body?.msg ?? 'Falha ao iniciar pagamento.');
}
const data = body?.data;
if (browser && data?.payment_external_id && data?.token_external_id) {
const storageKey = `tooeasy_orderbook_payment_${orderbookId}`;
try {
// substitui qualquer estado anterior (não damos refresh na página)
localStorage.setItem(
storageKey,
JSON.stringify({
orderbook_id: orderbookId,
payment_id: data.payment_id,
payment_code: data.payment_code,
payment_external_id: data.payment_external_id,
token_external_id: data.token_external_id,
startedAt: Date.now()
})
);
} catch (err) {
console.warn('Não foi possível persistir estado do pagamento do orderbook:', err);
}
}
return data; // payment_id, payment_code, payment_external_id...
}
Por quê? O endpoint
/orderbook/transferprecisa receberpayment_external_idetoken_external_id. Salvar esses campos nolocalStoragegarante que o QR code continue disponível sem precisar dar refresh. Ao final do fluxo (pagamento confirmado e token transferido), remova essa entrada dolocalStoragepara evitar dados obsoletos.
Ao confirmar o pagamento (resposta de sucesso do /orderbook/transfer), limpe a chave
do localStorage correspondente para impedir que o modal reabra com dados antigos:
function clearOrderbookPaymentState(orderbookId) {
if (!browser) return;
const storageKey = `tooeasy_orderbook_payment_${orderbookId}`;
localStorage.removeItem(storageKey);
}
Com esses dados reutilize o modal Pix já presente em src/routes/cpr/+page.svelte:
payment_code para gerar o QR via qr-code-styling.localStorage (ex.: tooeasy_orderbook_payment_<orderbookId>)
para restaurar o modal se a página recarregar.POST /orderbook/transferChamado após o usuário confirmar que o pagamento foi compensado (Woovi envia o
webhook e o usuário clica em "Transferir"). Importante: o webhook apenas
atualiza o banco de dados — cabe ao frontend continuar consultando esse endpoint
para descobrir quando o status mudou. Enquanto o backend não retornar
success, mantenha um polling a cada 10 segundos, por até 30 minutos,
mostrando o status atual para o usuário.
{
"external_id": "PAY_a1b2c3d4",
"token_external_id": "TOKEN_ABC123"
}
{
"status": "ok",
"msg": "[100] Request ok.",
"code": "S_TOKEN_TRANSFERRED",
"data": {
"orderbook_id": 321,
"token_external_id": "TOKEN_ABC123",
"destination_address": "0x8AC9...",
"transfer_output": "Transfer success",
"transfer_error": ""
}
}
400 – E_VALIDATE: body inválido.403 – E_FORBIDDEN: ordem de outra empresa.404 – E_NOT_FOUND ou E_WALLET_NOT_FOUND.409 – E_PAYMENT_PENDING / E_PAYMENT_STATUS: pagamento ainda pendente ou em status inválido.500 – E_DATABASE ou E_TRANSFER: falha interna.
async function transferToken({ external_id, token_external_id }) {
const res = await fetch(`${apiUrl}/orderbook/transfer`, {
method: 'POST',
headers,
body: JSON.stringify({ external_id, token_external_id })
});
const body = await parseResponse(res);
if (!res.ok || body?.status !== 'ok') {
throw new Error(body?.msg ?? 'Falha ao transferir token.');
}
return body?.data; // dados da transferência
}
const ORDERBOOK_TRANSFER_POLL_INTERVAL_MS = 10_000;
const ORDERBOOK_TRANSFER_TIMEOUT_MS = 30 * 60 * 1000;
async function pollTransferUntilDone(params) {
const startedAt = Date.now();
while (Date.now() - startedAt < ORDERBOOK_TRANSFER_TIMEOUT_MS) {
try {
const data = await transferToken(params);
return data; // sucesso
} catch (err) {
if (err.message?.includes('pendente')) {
await new Promise((resolve) => setTimeout(resolve, ORDERBOOK_TRANSFER_POLL_INTERVAL_MS));
continue;
}
throw err; // erro fatal
}
}
throw new Error('Tempo limite para confirmação do pagamento expirou.');
}
Tratar códigos especificados no backend:
E_PAYMENT_PENDING: mostrar banner "Pagamento ainda pendente" e permitir
tentar novamente.E_FORBIDDEN, E_NOT_FOUND, E_TRANSFER: mensagens orientando contato
com suporte.POST /b3/payment/confirmFluxo já implementado na emissão de CPR. Apenas certifique-se de:
Enviar payment_id (e opcionalmente b3_access_token). Payload esperado:
{
"payment_id": 555,
"b3_access_token": "Bearer ..."
}
Resposta de sucesso:
{
"status": "ok",
"msg": "[100] Request ok.",
"code": "S_CPR_SENT",
"data": {
"message": "CPR enviada e token criado com sucesso",
"payment_id": 555,
"b3_response": { "status": "OK" },
"token_id": 99,
"token_external_id": "TOKEN_ABC123",
"tx_hash": "0x..."
}
}
Enviar { payment_id } e, se existir, b3_access_token.
Continuar usando o modal Pix + polling existente.
localStorage dentro de if (browser) para guardar dados de pagamentos
pendentes e restaurá-los em onMount.let paymentModalVisible, let paymentCode, let paymentCountdownMs, etc.,
continuam vivos dentro do <script> Svelte.onDestroy (clearInterval, clearTimeout)./token/orderbook ao finalizar uma CPR./orderbook/filter (já integrado na tela trading)./orderbook/payment → abre modal Pix./orderbook/transfer após confirmação do usuário./b3/payment/confirm continua igual ao fluxo atual.Toda a lógica reside nos <script> dos componentes Svelte, reutilizando as
stores e helpers existentes. Não é necessário (nem desejado) criar arquivos JS
separados: mantenha o padrão do projeto, com as funções acima declaradas no
script da página e conectadas ao template através de bindings e eventos.