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
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:
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:
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
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.
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.
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.
Veja também