> ## Documentation Index
> Fetch the complete documentation index at: https://developers.pontua.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Sincronizar frequência com ERP/folha externa

> Cron noturno que puxa frequências do dia anterior do Pontua e alimenta seu sistema de folha. Padrão idempotente com cursor incremental.

Caso de uso clássico: sua empresa usa folha de pagamento externa
(Domínio Sistemas, Sage, ContabilQuasar, ERP próprio) e precisa que
a frequência do Pontua reflita lá diariamente para fechamento mensal.

Este guia mostra o padrão **cron noturno + cursor incremental** que
roda à 1h da manhã, puxa frequências de `D-1` e popula seu ERP.

## Estratégia

```
1. Cron 01:00 BRT
2. Calcular data ontem (D-1)
3. Verificar se D-1 está em fechamento concluído (opcional mas recomendado)
4. Paginar por GET /frequency/daily?dataInicio=D-1&dataFim=D-1
5. Para cada linha, upsertar no ERP
6. Salvar cursor (timestamp da última sync) para auditoria
```

## Implementação Node.js

```javascript theme={null}
async function syncDiarioFrequencia(token) {
  const TOKEN = token || process.env.PONTUA_API_TOKEN
  const ontem = new Date(Date.now() - 86400000).toISOString().slice(0, 10)
  const headers = { Authorization: `Bearer ${TOKEN}` }

  console.log(`Sync frequência de ${ontem}`)

  // 1. Validação opcional: só sync se fechamento aberto/concluído
  // (algumas empresas preferem aguardar fechamento; outras puxam preview)

  // 2. Paginar todas as frequências do dia
  let pagina = 0
  let totalLido = 0
  let totalUpsertado = 0
  let totalErros = 0

  while (true) {
    const params = new URLSearchParams({
      dataInicio: ontem,
      dataFim: ontem,
      pagina: String(pagina),
      limite: '100',
    })

    const resp = await fetch(
      `https://api.pontua.com.br/frequency/daily?${params}`,
      { headers },
    )

    if (!resp.ok) {
      throw new Error(`Falha ao listar frequências: ${resp.status}`)
    }

    const { resultados, totalRegistros } = await resp.json()
    totalLido += resultados.length

    // 3. Upsert cada linha no ERP
    for (const freq of resultados) {
      try {
        await meuErp.upsertFrequencia({
          colaboradorCpf: freq.colaboradorCpf,  // ou ID conforme ERP
          data: freq.data,
          horasTrabalhadasMinutos: parseHHMMToMin(freq.totais.horasTrabalhadas),
          horasExtrasMinutos: parseHHMMToMin(freq.totais.horasExtras),
          status: freq.status,
        })
        totalUpsertado++
      } catch (e) {
        console.error(`Erro upsert ${freq.colaboradorId} ${freq.data}:`, e.message)
        totalErros++
      }
    }

    // 4. Próxima página?
    if ((pagina + 1) * 100 >= totalRegistros) break
    pagina++
  }

  // 5. Log/cursor
  console.log(`Sync ${ontem} concluído: ${totalLido} lidos, ${totalUpsertado} upsertados, ${totalErros} erros`)
  await storage.set('pontua.ultimo_sync_frequencia', new Date().toISOString())

  return { lido: totalLido, upsertado: totalUpsertado, erros: totalErros }
}

function parseHHMMToMin(hhmm) {
  const [h, m] = hhmm.split(':').map(Number)
  return h * 60 + m
}

// Setup cron (ex: node-cron)
import cron from 'node-cron'
cron.schedule('0 1 * * *', () => syncDiarioFrequencia().catch(console.error), {
  timezone: 'America/Sao_Paulo',
})
```

## Tratamento de retry

Se o cron falhar (rede, timeout), use **backoff exponencial** + **idempotência**:

```javascript theme={null}
async function syncComRetry(maxAttempts = 5) {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await syncDiarioFrequencia()
    } catch (e) {
      if (attempt === maxAttempts - 1) throw e
      const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000
      console.warn(`Tentativa ${attempt + 1} falhou, aguardando ${delay}ms`)
      await new Promise((r) => setTimeout(r, delay))
    }
  }
}
```

A idempotência vem do **upsert no ERP**: rodar o cron 2x no mesmo dia
não cria duplicatas (`colaboradorCpf` + `data` é a chave natural).

## Catch-up de períodos esquecidos

Cron pode ter ficado quebrado por dias. Para alcançar atrasados:

```javascript theme={null}
async function catchUpFrequencia(token, diasAtras = 7) {
  const hoje = new Date()

  for (let i = 1; i <= diasAtras; i++) {
    const data = new Date(hoje.getTime() - i * 86400000)
      .toISOString().slice(0, 10)

    console.log(`Catch-up ${data}`)
    await syncDiaEspecifico(token, data)
  }
}
```

## Recomendação de horário

| Hora           | Vantagem                                            |
| -------------- | --------------------------------------------------- |
| **01:00 BRT**  | Madrugada, pouca pressão na API. Recomendado.       |
| 06:00 BRT      | Antes do expediente — frequência já consolidada     |
| Hora comercial | Não recomendado — competindo com tráfego app/portal |

Se você atende **múltiplas UNs** (contador), distribua os jobs em
janelas de 5-10 min para não saturar.

## Gotchas

<Warning title="Frequência de período aberto pode mudar">
  Se `D-1` ainda está em período de fechamento aberto, a frequência
  pode mudar com aprovações de ajuste posteriores. Para snapshot
  imutável, use `GET /relatorio/<tipo>` após o fechamento concluir —
  ver [Relatórios](/referencia/relatorios).
</Warning>

<Warning title="Status FALTA pode virar ATESTADO retroativamente">
  Colaborador faltou ontem, sync trouxe `status: FALTA`, mas hoje o
  RH aprovou atestado retroativo. Próximo sync vai mudar o status —
  seu ERP precisa **aceitar update do mesmo registro**, não criar duplicata.
</Warning>

<Warning title="Cuidado com fuso horário">
  `data` no response é date pura (`yyyy-MM-dd`) sem timezone. Já
  representa BRT. Não converta para UTC ao salvar no seu DB se ele
  trata como timestamp — pode resultar em "dia anterior" depending no fuso.
</Warning>

## Veja também

* [Frequência](/referencia/frequencia) — schema completo dos endpoints
* [Webhooks (em breve)](/conceitos/webhooks) — alternativa ao polling cron
* [Paginação](/conceitos/paginacao) — padrão de iteração em listas grandes
* [Idempotência](/conceitos/idempotencia) — evitar duplicatas em retry
