Pular para conteúdo

Erros de Integração

Erros comuns ao integrar com a API Saipos.

Erro 401 - Unauthorized

Sintoma

{
  "statusCode": 401,
  "message": "Unauthorized"
}

Causas

  1. Credenciais Saipos inválidas
  2. Token expirado
  3. Headers incorretos

Solução

-- Verificar credenciais do restaurante
SELECT 
  id,
  name,
  "codeStoreSaipos",
  "saiposIdPartner",
  "saiposSecret"
FROM "Restaurant"
WHERE id = 'restaurant_id';

-- Atualizar credenciais
UPDATE "Restaurant"
SET 
  "saiposIdPartner" = 'novo_id_partner',
  "saiposSecret" = 'novo_secret'
WHERE id = 'restaurant_id';

-- Invalidar cache de token
DELETE FROM "TokenCache" WHERE provider = 'SAIPOS';

Erro 901/902 - Credenciais Inválidas

Sintoma

{
  "errorCode": 901,
  "message": "Invalid partner credentials"
}

ou

{
  "errorCode": 902,
  "message": "Secret key invalid"
}

Solução

O sistema já invalida o cache automaticamente quando recebe 901/902:

if (isAuth) {
  await this.prisma.tokenCache.delete({ where: { provider: 'SAIPOS' } });
}

Ação manual: Atualizar credenciais no banco

UPDATE "Restaurant"
SET 
  "saiposIdPartner" = 'novo_id',
  "saiposSecret" = 'novo_secret'
WHERE "codeStoreSaipos" = 'LOJA_001';

Erro 400 - Bad Request

order_id já existe

Sintoma:

{
  "errorCode": 400,
  "message": "order_id já existe no sistema"
}

Causa: Em testes, o mesmo transactionKey está sendo reutilizado.

Solução: Use isTestTransaction: true no evento de teste:

{
  "transactionKey": "test-1234",
  "isTestTransaction": true,
  ...
}

O sistema adiciona timestamp automaticamente:

if (isTest) {
  const shortTs = Math.floor(Date.now() / 1000);
  order_id = `${baseOrderId}-${shortTs}`;
}

Campo obrigatório faltando

Sintoma:

{
  "errorCode": 400,
  "message": "Campo 'cod_store' é obrigatório"
}

Solução: Verificar mapeamento do restaurante

SELECT 
  "codeStoreNayax",
  "codeStoreSaipos"
FROM "Restaurant"
WHERE "codeStoreNayax" = 'LOJA001';

-- Se null, configurar
UPDATE "Restaurant"
SET "codeStoreSaipos" = 'LOJA_SAIPOS_001'
WHERE "codeStoreNayax" = 'LOJA001';

Formato inválido

Sintoma:

{
  "errorCode": 400,
  "message": "total_amount deve ser um número"
}

Solução: Verificar evento no banco e mapper

SELECT "totalAmount" FROM "NayaxEvent"
WHERE "transactionKey" = 'event_key';

Erro 422 - Unprocessable Entity

integration_code não encontrado

Sintoma:

{
  "errorCode": 422,
  "message": "integration_code 'PROD001' não encontrado"
}

Causa: Produto não cadastrado no Saipos

Soluções:

  1. Cadastrar produto no Saipos (recomendado)
  2. Mapear para produto existente
  3. Usar produto genérico

cod_store inválido

Sintoma:

{
  "errorCode": 422,
  "message": "cod_store 'LOJA_001' não encontrado"
}

Solução: Verificar código correto no Saipos

-- Ver código atual
SELECT "codeStoreSaipos" FROM "Restaurant"
WHERE "codeStoreNayax" = 'LOJA001';

-- Atualizar
UPDATE "Restaurant"
SET "codeStoreSaipos" = 'codigo_correto_saipos'
WHERE "codeStoreNayax" = 'LOJA001';

payment_types inválido

Sintoma:

{
  "errorCode": 422,
  "message": "Forma de pagamento 'INVALID' não permitida"
}

Solução: Verificar mapeamento de tenderType

// Verificar TENDER_MAP em nayax-to-saipos.mapper.ts
const TENDER_MAP = {
  1: { code: 'DIN', type: 'OFFLINE' },
  50: { code: 'PARTNER_PAYMENT', type: 'ONLINE', complement: 'pix' },
  // ...
};

Erro 500 - Internal Server Error

Sintoma

{
  "errorCode": 500,
  "message": "Internal server error"
}

Solução

Erro temporário do Saipos. O job será retentado automaticamente com backoff:

  • Tentativa 1: ~30s
  • Tentativa 2: ~2min
  • Tentativa 3: ~5min
  • Tentativa 4: ~15min
  • Tentativa 5: ~30min
  • Tentativa 6+: ~60min

Se persistir: Contatar suporte Saipos

Erro 503 - Service Unavailable

Sintoma

{
  "statusCode": 503,
  "message": "Service temporarily unavailable"
}

Solução

API Saipos fora do ar temporariamente. Sistema retenta automaticamente.

Verificar: https://status.saipos.com (se existir)

Timeout

Sintoma

Error: timeout of 30000ms exceeded

Causas

  1. API Saipos lenta
  2. Rede lenta
  3. Payload muito grande

Solução

// Aumentar timeout (em src/webhooks/saipos/saipos.client.ts)
const response = await axios.post(url, body, {
  timeout: 60000 // 60 segundos
});

Network Error

Sintoma

Error: Network Error
Error: connect ECONNREFUSED
Error: getaddrinfo ENOTFOUND

Soluções

  1. Verificar conectividade:

    ping order-api.saipos.com
    curl -I https://order-api.saipos.com
    

  2. Verificar DNS:

    nslookup order-api.saipos.com
    

  3. Verificar firewall:

    # Permitir saída HTTPS
    sudo ufw allow out 443
    

  4. Verificar proxy (se usar):

    HTTP_PROXY=http://proxy:8080
    HTTPS_PROXY=http://proxy:8080
    

SSL Error

Sintoma

Error: unable to verify the first certificate
Error: self signed certificate in certificate chain

Solução (apenas development)

// NÃO USE EM PRODUÇÃO
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

Em produção: Instalar certificados corretos

Erros de Validação de Totais

Soma não fecha

Sintoma: Job vai para DLQ com erro de validação

Debug:

SELECT 
  "transactionKey",
  "totalAmount",
  (SELECT SUM(amount) FROM "Payment" WHERE "nayaxEventId" = "transactionKey") as payments_sum,
  "totalAmount" - (SELECT SUM(amount) FROM "Payment" WHERE "nayaxEventId" = "transactionKey") as diff
FROM "NayaxEvent"
WHERE "transactionKey" = 'event_key';

Solução: O mapper já ajusta automaticamente, mas se persistir:

// Verificar to2() function no mapper
function to2(n: number) {
  return Math.round((n + Number.EPSILON) * 100) / 100;
}

Debug de Erro Específico

Ver Erro Completo

SELECT 
  id,
  "eventKey",
  attempts,
  status,
  "lastError"
FROM "OutboxJob"
WHERE id = 'job_abc123';

Parsear JSON do Erro

SELECT 
  id,
  "eventKey",
  "lastError"::json->>'msg' as message,
  ("lastError"::json->>'status')::int as http_status,
  "lastError"::json->'data' as response_data
FROM "OutboxJob"
WHERE status IN ('FAILED', 'DLQ')
ORDER BY "createdAt" DESC
LIMIT 10;

Reprocessar após Correção

Depois de corrigir o problema:

-- Resetar job específico
UPDATE "OutboxJob"
SET 
  status = 'PENDING',
  "nextRunAt" = NOW(),
  attempts = 0,
  "lastError" = NULL
WHERE id = 'job_abc123';

-- Reprocessar todos DLQ de uma loja
UPDATE "OutboxJob" o
SET 
  status = 'PENDING',
  "nextRunAt" = NOW(),
  attempts = 0,
  "lastError" = NULL
FROM "NayaxEvent" e
WHERE o."eventKey" = e."transactionKey"
  AND e."storeCode" = 'LOJA001'
  AND o.status = 'DLQ';

Estatísticas de Erros

-- Erros mais comuns
SELECT 
  "lastError"::json->>'status' as http_status,
  COUNT(*) as occurrences
FROM "OutboxJob"
WHERE status = 'DLQ'
  AND "lastError" IS NOT NULL
GROUP BY "lastError"::json->>'status'
ORDER BY occurrences DESC;

-- Taxa de erro por loja
SELECT 
  e."storeCode",
  COUNT(*) FILTER (WHERE o.status = 'SENT') as success,
  COUNT(*) FILTER (WHERE o.status = 'DLQ') as failed,
  ROUND(
    COUNT(*) FILTER (WHERE o.status = 'DLQ')::numeric / 
    COUNT(*)::numeric * 100, 
    2
  ) as error_rate
FROM "OutboxJob" o
JOIN "NayaxEvent" e ON o."eventKey" = e."transactionKey"
GROUP BY e."storeCode"
ORDER BY error_rate DESC;

Próximos Passos