README_ORDERBOOK.md 12 KB

Integração do Orderbook no Frontend (Svelte)

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.

1. Pré-requisitos

  • VITE_API_URL configurada apontando para a API PHP (ex.: http://localhost:8000).
  • Stores authToken e companyId já disponíveis em $lib/utils/stores.
  • Telas existentes:
    • 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).

2. Convenções de requisições

Cabeçalhos padrão

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.

Helper genérico

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;
}

3. Endpoint: POST /orderbook/filter

Onde usar: sempre que selectedState ou selectedCommodity mudar.

Payload esperado

{
  "state": "SP",
  "commodity_type": "SOJA"
}

Resposta de sucesso

{
  "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
      }
    ]
  }
}

Erros comuns

  • 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.

4. Endpoint: POST /token/orderbook

Onde 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).

Payload esperado

{
  "cpr_id": 42,
  "value": 150000.75,
  "state": "SP",
  "commodity_type": "SOJA",
  "token_external_id": "TOKEN_ABC123"
}

Resposta de sucesso

{
  "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
  }
}

Erros principais

  • 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
    }
    

5. Endpoint: POST /orderbook/payment

Onde usar: quando o usuário clicar em uma ordem para comprar.

Payload esperado

{
  "orderbook_id": 321
}

Resposta de sucesso

{
  "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"
  }
}

Erros principais

  • 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/transfer precisa receber payment_external_id e token_external_id. Salvar esses campos no localStorage garante que o QR code continue disponível sem precisar dar refresh. Ao final do fluxo (pagamento confirmado e token transferido), remova essa entrada do localStorage para 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.
  • Persistir estado no localStorage (ex.: tooeasy_orderbook_payment_<orderbookId>) para restaurar o modal se a página recarregar.

6. Endpoint: POST /orderbook/transfer

Chamado 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.

Payload esperado

{
  "external_id": "PAY_a1b2c3d4",
  "token_external_id": "TOKEN_ABC123"
}

Resposta de sucesso

{
  "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": ""
  }
}

Erros principais

  • 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.

7. Endpoint: POST /b3/payment/confirm

Fluxo 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.

8. Persistência local e reatividade

  • Use 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.
  • Sempre limpe timers em onDestroy (clearInterval, clearTimeout).

9. Fluxo completo

  1. Criar ordem: /token/orderbook ao finalizar uma CPR.
  2. Listar ordens: /orderbook/filter (já integrado na tela trading).
  3. Iniciar pagamento: /orderbook/payment → abre modal Pix.
  4. Concluir transferência: /orderbook/transfer após confirmação do usuário.
  5. Registrar CPR: /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.