7. Relatório de Execução do Batch

A API oferece um endpoint dedicado para monitorar o progresso e resultado de operações em lote.


7.1 Endpoint de Relatório

Definição

URL: GET /erp-data/batch-job-report/{jobId}

Autenticação: X-API-Key obrigatória

Resposta: JSON com status de processamento

Exemplo Requisição:

curl --request GET \
  --url https://api.retailportal.com/erp-data/batch-job-report/hangfire-job-1234567890 \
  --header 'X-API-Key: ERP-RETAILPORTAL-2024-SECURE-KEY-ABC123'

7.2 Campo "state" — Estados Possíveis

O campo state indica o status atual do processamento:

Estado Significado isComplete Ação
Enqueued Aguardando processamento na fila false Continue consultando
Processing Processando ativamente false Continue consultando
Finished Concluído (sucesso ou parcial) true Analise resultados
Failed Falha total do servidor true Aguarde e reenvie

7.2.1 Estado: Enqueued

Significado: O job está na fila do Hangfire, aguardando processamento.

Exemplo:

{
  "jobId": "hangfire-job-1234567890",
  "entityType": "ErpProduct",
  "state": "Enqueued",
  "isComplete": false,
  "createdAt": "2024-12-06T10:30:00Z",
  "startedAt": null,
  "completedAt": null,
  "duration": null,
  "totalItems": 100,
  "successCount": 0,
  "failureCount": 0,
  "retryAttempt": 0,
  "hasPartialData": false,
  "errorMessage": null,
  "exceptionDetails": null
}

Interpretação: - Nenhum processamento começou ainda - Não há contadores de sucesso/falha (ambos 0) - startedAt é null (não iniciou)

O que fazer: - Aguarde 5-10 segundos - Consulte novamente - Sistemas com pouca carga: geralmente passa rápido - Sistemas com muita carga: pode ficar neste estado por minutos


7.2.2 Estado: Processing

Significado: O sistema está processando ativamente os itens.

Exemplo:

{
  "jobId": "hangfire-job-1234567890",
  "entityType": "ErpProduct",
  "state": "Processing",
  "isComplete": false,
  "createdAt": "2024-12-06T10:30:00Z",
  "startedAt": "2024-12-06T10:30:02Z",
  "completedAt": null,
  "duration": null,
  "totalItems": 100,
  "successCount": 47,
  "failureCount": 0,
  "retryAttempt": 0,
  "hasPartialData": false,
  "errorMessage": null,
  "exceptionDetails": null
}

Interpretação: - Processamento iniciou em 10:30:02 - 47 itens já foram processados com sucesso - 53 itens ainda estão sendo processados - Progresso: 47%

O que fazer: - Continue consultando a cada 5-10 segundos - Você pode dar feedback de progresso ao usuário - Não reenvie ainda

Cálculo de ETA (Tempo Estimado):

Tempo decorrido: 10:30:02 a 10:30:15 = 13 segundos
Itens processados: 47
Taxa: 47 / 13 = 3.6 itens/segundo
Itens faltantes: 100 - 47 = 53
ETA: 53 / 3.6 = ~15 segundos
Conclusão esperada: ~10:30:30

7.2.3 Estado: Finished

Significado: Processamento concluído. Pode ser sucesso total, parcial ou falha de validação.

Caso A: Sucesso Total

{
  "jobId": "hangfire-job-1234567890",
  "entityType": "ErpProduct",
  "state": "Finished",
  "isComplete": true,
  "createdAt": "2024-12-06T10:30:00Z",
  "startedAt": "2024-12-06T10:30:02Z",
  "completedAt": "2024-12-06T10:35:15Z",
  "duration": "00:05:13",
  "totalItems": 100,
  "successCount": 100,
  "failureCount": 0,
  "retryAttempt": 0,
  "hasPartialData": false,
  "errorMessage": null,
  "exceptionDetails": null
}

Interpretação: - successCount: 100 — Todos os itens foram salvos ✓ - failureCount: 0 — Nenhum erro - hasPartialData: false — 100% dos dados processados - duration: "00:05:13" — Levou 5 minutos e 13 segundos

Resultado:SUCESSO TOTAL — Nada a fazer


Caso B: Sucesso Parcial (Alguns Itens Falharam)

{
  "jobId": "hangfire-job-1234567891",
  "entityType": "ErpProduct",
  "state": "Finished",
  "isComplete": true,
  "createdAt": "2024-12-06T10:40:00Z",
  "startedAt": "2024-12-06T10:40:02Z",
  "completedAt": "2024-12-06T10:45:30Z",
  "duration": "00:05:28",
  "totalItems": 100,
  "successCount": 95,
  "failureCount": 5,
  "retryAttempt": 0,
  "hasPartialData": true,
  "errorMessage": "5 items failed validation during processing",
  "exceptionDetails": null
}

Interpretação: - successCount: 95 — 95 itens foram salvos ✓ - failureCount: 5 — 5 itens falharam ✗ - hasPartialData: true — Dados processados parcialmente - errorMessage — Resumo do problema

Resultado: ⚠️ SUCESSO PARCIAL — Investigar os 5 itens falhados

O que fazer: 1. Consulte o histórico de integração para identificar quais itens falharam 2. Revise os dados desses itens (validação, formato, etc.) 3. Corrija e reenvie apenas esses 5 itens 4. Pode usar um novo batch com apenas os itens com erro


Caso C: Falha de Validação (Pré-processamento)

{
  "jobId": "hangfire-job-1234567892",
  "entityType": "ErpProduct",
  "state": "Finished",
  "isComplete": true,
  "createdAt": "2024-12-06T11:00:00Z",
  "startedAt": null,
  "completedAt": "2024-12-06T11:00:00Z",
  "duration": null,
  "totalItems": 100,
  "successCount": 0,
  "failureCount": 100,
  "retryAttempt": 0,
  "hasPartialData": false,
  "errorMessage": "Batch validation failed: Item at index 15 has invalid data",
  "exceptionDetails": null
}

Interpretação: - startedAt: null — Falha aconteceu antes do processamento - successCount: 0 — Nenhum item foi salvo - errorMessage — Especifica qual item tem problema - Pode ser erro de validação ou dados duplicados

Resultado:FALHA DE VALIDAÇÃO — Dados precisam de correção

O que fazer: 1. Leia a mensagem de erro 2. Corrija o item indicado (índice 15) 3. Reenvie o batch


7.2.4 Estado: Failed

Significado: Falha total no servidor, NÃO é erro de dados.

{
  "jobId": "hangfire-job-1234567893",
  "entityType": "ErpProduct",
  "state": "Failed",
  "isComplete": true,
  "createdAt": "2024-12-06T12:00:00Z",
  "startedAt": "2024-12-06T12:00:02Z",
  "completedAt": "2024-12-06T12:00:45Z",
  "duration": "00:00:43",
  "totalItems": 100,
  "successCount": 0,
  "failureCount": 100,
  "retryAttempt": 5,
  "hasPartialData": false,
  "errorMessage": "Database connection pool exhausted",
  "exceptionDetails": "PostgreSQL: Connection timeout after 5 retry attempts. Max pool size: 25"
}

Interpretação: - state: "Failed" — Falha do servidor - successCount: 0 — Nenhum dado foi salvo - retryAttempt: 5 — Sistema tentou automaticamente 5 vezes - errorMessage — Problema técnico do servidor - exceptionDetails — Stack trace técnico

Resultado:FALHA DO SISTEMA — Não é erro de dados

O que fazer: 1. Não é erro de dados — Seus dados estão corretos 2. É um problema do servidor Portal Retail 3. Aguarde 5-10 minutos 4. Reenvie a mesma requisição (o sistema tentará novamente) 5. Se persistir, contacte o suporte


7.3 Propriedade "isComplete" — Trabalho Finalizado?

A propriedade isComplete facilita validação booleana:

isComplete = true  →  state é "Finished" ou "Failed"
isComplete = false →  state é "Enqueued" ou "Processing"

Uso prático:

response = get_batch_status(job_id)

if not response['isComplete']:
    # Job ainda está processando
    esperar(5)
    tentar_novamente()
else:
    # Job terminou (pode ser sucesso ou falha)
    if response['state'] == 'Finished':
        analisar_resultados()
    elif response['state'] == 'Failed':
        reenviar_apos_esperar()

7.4 Propriedade "hasPartialData" — Dados Parciais

hasPartialData indica se ALGUNS itens falharam:

hasPartialData: true  →  Alguns itens falharam, outros sucesso
hasPartialData: false →  Ou todos sucedem, ou todos falham

Exemplo: - 100 itens, 95 sucesso, 5 falha → hasPartialData: true - 100 itens, 100 sucesso, 0 falha → hasPartialData: false - 100 itens, 0 sucesso, 100 falha → hasPartialData: false


7.5 Campo "duration" — Tempo de Processamento

Formato: HH:mm:ss (horas, minutos, segundos)

Exemplos: - "duration": "00:05:13" — 5 minutos e 13 segundos - "duration": "00:00:45" — 45 segundos - "duration": "01:23:47" — 1 hora, 23 minutos, 47 segundos - "duration": null — Job ainda não processou ou validação falhou


7.6 Histórico de Tentativas — "retryAttempt"

O Hangfire reprocessa jobs que falharam automaticamente:

retryAttempt Significado
0 Primeira tentativa (ou sucesso na primeira)
1 Job falhou, Hangfire retentou 1x
2 Job falhou, Hangfire retentou 2x
... ...
5 Job falhou 5 vezes (limite máximo)

Politica de Retry: - Máximo: 5 tentativas - Intervalo: Exponencial (aumenta a cada falha) - Tipo: Automático (não você, o Hangfire)

Se retryAttempt: 5: - Sistema tentou 5 vezes e falhou - Está fora do pool de retry - Você deve reenviar manualmente após aguardar


7.7 Fluxo de Polling Recomendado

PASSO 1: Enviar batch
POST /product/batch → 202 + jobId
jobId: "hangfire-1234567890"

PASSO 2: Consultar status (LOOP)
GET /batch-job-report/{jobId}

INTERVALO: 5-10 segundos
TIMEOUT: 5 minutos máximo
CONDIÇÃO PARADA: isComplete = true

PASSO 3: Analisar resultado
Se successCount = totalItems → ✓ Sucesso
Se failureCount > 0 → Investigar erros
Se state = "Failed" → Aguardar 5 min e reenviar

FLUXO COMPLETO:
─────────────────
00:00 → POST batch
00:05 → GET report → Enqueued
00:10 → GET report → Processing (25%)
00:15 → GET report → Processing (50%)
00:20 → GET report → Processing (75%)
00:25 → GET report → Finished (100%)
00:26 → Análise de resultado

7.8 Implementação em Código

Python com Polling

import requests
import time
import json

def aguardar_batch(job_id, timeout_segundos=300):
    """
    Aguarda conclusão de um batch job com polling

    Args:
        job_id: Identificador do job (recebido em 202 Accepted)
        timeout_segundos: Tempo máximo de espera (padrão: 5 minutos)

    Returns:
        Dicionário com resultado do batch
    """
    api_key = "SUA-CHAVE-API"
    headers = {'X-API-Key': api_key}
    url = f"https://api.retailportal.com/erp-data/batch-job-report/{job_id}"

    inicio = time.time()

    while True:
        # Verificar timeout
        tempo_decorrido = time.time() - inicio
        if tempo_decorrido > timeout_segundos:
            print(f"❌ Timeout: Job não completou em {timeout_segundos} segundos")
            return None

        # Consultar status
        response = requests.get(url, headers=headers)

        if response.status_code != 200:
            print(f"❌ Erro ao consultar status: {response.status_code}")
            return None

        data = response.json()

        # Mostrar progresso
        if data['state'] == 'Processing':
            progresso = (data['successCount'] / data['totalItems']) * 100
            print(f"⏳ Processando... {data['successCount']}/{data['totalItems']} ({progresso:.0f}%)")

        # Verificar conclusão
        if data['isComplete']:
            print(f"✓ Job concluído: {data['state']}")
            return data

        # Aguardar antes de próxima consulta
        print("Consultando novamente em 5 segundos...")
        time.sleep(5)

def criar_batch_e_aguardar(produtos):
    """Cria batch e aguarda conclusão"""
    api_key = "SUA-CHAVE-API"
    headers = {
        'X-API-Key': api_key,
        'Content-Type': 'application/json'
    }

    # PASSO 1: Enviar batch
    print("Enviando batch...")
    response = requests.post(
        'https://api.retailportal.com/erp-data/product/batch',
        headers=headers,
        json={'products': produtos}
    )

    if response.status_code != 202:
        print(f"❌ Erro ao enviar batch: {response.status_code}")
        print(response.json())
        return None

    batch_response = response.json()
    job_id = batch_response['jobId']
    print(f"✓ Batch enfileirado: {job_id}")

    # PASSO 2: Aguardar conclusão
    print("\nAguardando processamento...")
    resultado = aguardar_batch(job_id)

    # PASSO 3: Analisar resultado
    if resultado:
        print("\n=== RESULTADO FINAL ===")
        print(f"Total: {resultado['totalItems']}")
        print(f"Sucesso: {resultado['successCount']}")
        print(f"Erro: {resultado['failureCount']}")
        print(f"Duração: {resultado['duration']}")

        if resultado['failureCount'] > 0:
            print(f"\n⚠️ {resultado['failureCount']} itens falharam")
            if resultado['errorMessage']:
                print(f"Erro: {resultado['errorMessage']}")

    return resultado

# Usar
produtos = [
    {
        "erpId": 12345,
        "ncmCode": "84733090",
        "status": "Active",
        "description": "Dipirona 500mg"
    },
    # ... mais produtos
]

resultado = criar_batch_e_aguardar(produtos)

Bash com cURL

#!/bin/bash

API_KEY="SUA-CHAVE-API"
URL_BATCH="https://api.retailportal.com/erp-data/product/batch"
URL_REPORT="https://api.retailportal.com/erp-data/batch-job-report"

# Função para enviar batch
enviar_batch() {
    local json_file=$1

    curl -s -X POST \
        -H "Content-Type: application/json" \
        -H "X-API-Key: $API_KEY" \
        -d @"$json_file" \
        "$URL_BATCH"
}

# Função para consultar status
consultar_status() {
    local job_id=$1

    curl -s -X GET \
        -H "X-API-Key: $API_KEY" \
        "$URL_REPORT/$job_id"
}

# Função para aguardar conclusão
aguardar_conclusao() {
    local job_id=$1
    local timeout=300
    local inicio=$(date +%s)

    while true; do
        agora=$(date +%s)
        decorrido=$((agora - inicio))

        if [ $decorrido -gt $timeout ]; then
            echo "❌ Timeout após $timeout segundos"
            return 1
        fi

        status=$(consultar_status "$job_id")
        is_complete=$(echo "$status" | jq '.isComplete')
        state=$(echo "$status" | jq -r '.state')

        echo "Estado: $state"

        if [ "$is_complete" = "true" ]; then
            echo "✓ Job concluído"
            echo "$status" | jq '.'
            return 0
        fi

        echo "Aguardando 5 segundos..."
        sleep 5
    done
}

# Executar
echo "Enviando batch..."
resposta=$(enviar_batch "produtos.json")
job_id=$(echo "$resposta" | jq -r '.jobId')

echo "Job ID: $job_id"
echo "Aguardando conclusão..."

aguardar_conclusao "$job_id"

7.9 Logging Agregado de Batch

A API implementa logging condicional para reduzir ruído e evitar duplicação:

O que é registrado no histórico de integração (_Erp_IntegrationHistory): - ✅ UM log por batch contendo: - Total de itens processados - Número de sucessos - Número de falhas - Resumo de erros - Duração total - Status final (sucesso/falha) - ❌ NENHUM log para itens individuais do batch - Mesmo para itens que falharam (evita duplicação) - Mesmo para itens que tiveram sucesso

Razão: - Um batch de 1.000 itens geraria 1.000+ logs individuais - Estratégia condicional reduz para 1 log agregado - Mantém rastreabilidade sem criar ruído excessivo

Exemplo de Relatório:

Batch Job (jobId: hangfire-123):
- Data: 2024-12-06 10:30:00
- Tipo: ErpProduct
- Operação: CreateBatch
- Status: Finished
- Total: 100 itens
- Sucessos: 95
- Falhas: 5
- Duração: 5 minutos 13 segundos
- Histórico: ✓ UM registro contendo tudo acima

Se você precisa de detalhes de itens individuais: - Consulte o endpoint /batch-job-report/{jobId} que retorna progresso em tempo real - Implemente logging no lado cliente que correlacione jobId com items originais - Mantenha mapeamento local de qual item foi enviado em qual batch


7.10 Checklist de Relatório

  • [ ] Consultar status com jobId correto?
  • [ ] Verificar campo state (Enqueued, Processing, Finished, Failed)?
  • [ ] Verificar isComplete (false = ainda processando)?
  • [ ] Se isComplete: true: Analisar successCount vs failureCount?
  • [ ] Se failureCount > 0: Consultar errorMessage?
  • [ ] Se state: "Failed": Aguardar e reenviar após 5 minutos?
  • [ ] Se hasPartialData: true: Implementar logging cliente para rastrear itens?
  • [ ] Verificar duration para otimizar tamanho de batch?
  • [ ] Entender que histórico mostra UM log agregado por batch (não items individuais)?

Próxima Seção

8. Fluxo de Reenvio em Caso de Erro