feat(plataforma): CronJobs do financeiro (mensalidades, notas, conciliacao)

This commit is contained in:
deploy
2026-09-21 11:33:17 +00:00
parent 2c834ed505
commit 2014c3789e
@@ -0,0 +1,214 @@
# =============================================================================
# Os tres processos do Financeiro que ninguem disparava.
#
# Conferido em 2026-09-18: `kubectl get cronjob -A` tinha UM cronjob por tenant,
# o `pg-backup`, e mais nada. Gerar mensalidade, emitir nota e conciliar
# pagamento sao endpoints POST que existem, tem teste e nunca foram chamados
# por ninguem que nao fosse uma pessoa clicando.
#
# Consequencia pratica antes disto: **nenhuma cobranca nascia sozinha** — nem a
# mensalidade do aluno, nem a assinatura da propria Athletic Map. A regra de
# "vence na data de entrada" (V008, no ar desde hoje) existia sem nada que a
# executasse.
#
# ── COMO ELES SE AUTENTICAM ─────────────────────────────────────────────────
#
# Pelo client `operacao` (client_credentials), o mesmo que o
# `scripts/teste_cobranca_asaas.sh` usa. Ele e confidencial, nao representa
# pessoa nenhuma, e tem APENAS `atm_financeiro` — nao `atm_admin`.
#
# O segredo vem do Secret `operacao-credentials` e nunca passa por `argv`: o
# corpo do POST do token vai por STDIN (`-d @-`), e o header `Authorization`
# entra por um arquivo de configuracao do curl lido do STDIN (`-K -`). Sem isso
# um `ps` dentro do container mostraria o bearer.
#
# **Este arquivo depende do Secret `operacao-credentials` existir.** Ele e criado
# selado por `scripts/gerar_credenciais_plataforma.sh operacao`. Sem ele os pods
# ficam em CreateContainerConfigError — que e barulhento, e nao silencioso.
#
# ── POR QUE CADA CADENCIA ───────────────────────────────────────────────────
#
# Nao sao numeros redondos escolhidos por gosto:
#
# mensalidades dia 1, 06:00 A competencia e mensal. Rodar no dia 1 da o mes
# inteiro de folga para corrigir o que falhar, e
# 06:00 poe a falha no horario comercial de quem
# vai olhar.
# notas de hora em hora Nota pendente e dinheiro que ja entrou
# esperando documento. Uma hora e o atraso maximo
# aceitavel; mais frequente nao acelera nada,
# porque o gatilho e o pagamento confirmado.
# conciliacao a cada 6 horas CADA cobranca em aberto custa uma chamada ao
# provedor, e a cota e de 25.000 por 12 horas. A
# cada 6h com limite de 100 gasta no maximo 400
# chamadas por dia — 1,6% da cota.
#
# `concurrencyPolicy: Forbid` nos tres: uma execucao lenta nao pode ganhar uma
# segunda em paralelo disputando o mesmo provedor.
# =============================================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: financeiro-cron
namespace: plataforma-prod
data:
# O script e o mesmo para os tres; muda o `ALVO`. Fica num ConfigMap, e nao
# embutido em cada CronJob, porque tres copias divergem — e a que diverge e
# sempre a que ninguem lembrou de corrigir.
disparar.sh: |
#!/bin/sh
set -eu
: "${KEYCLOAK_URL:?falta KEYCLOAK_URL}"
: "${ALVO:?falta ALVO}"
: "${CLIENT_SECRET:?falta CLIENT_SECRET}"
echo "[$(date -u +%FT%TZ)] alvo: ${ALVO}"
# 1. Token. O segredo vai por STDIN: em `argv` um `ps` dentro deste
# container o veria.
RESP="$(printf 'grant_type=client_credentials&client_id=operacao&client_secret=%s' \
"${CLIENT_SECRET}" \
| curl -sS --fail-with-body -X POST "${KEYCLOAK_URL}" \
-H 'Content-Type: application/x-www-form-urlencoded' -d @-)"
TOKEN="$(printf '%s' "${RESP}" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')"
if [ -z "${TOKEN}" ]; then
# Nao imprime RESP: em erro de client_credentials o corpo pode ecoar
# parte do que foi enviado.
echo "ERRO: nao consegui token pelo client 'operacao'." >&2
echo " Confira o Secret operacao-credentials e se o client existe no realm." >&2
exit 1
fi
# 2. A chamada. O bearer entra por arquivo de configuracao lido do STDIN,
# pelo mesmo motivo do segredo acima.
# `--fail-with-body` faz o curl sair != 0 em >=400 E ainda imprimir o
# corpo: sem o corpo, um 403 de plano e indistinguivel de um 403 de papel.
printf 'header = "Authorization: Bearer %s"\n' "${TOKEN}" \
| curl -sS --fail-with-body -K - -X POST "${ALVO}" -o /tmp/saida.json -w '%{http_code}\n'
echo "resposta:"
cat /tmp/saida.json
echo
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: financeiro-mensalidades
namespace: plataforma-prod
spec:
schedule: "0 6 1 * *"
timeZone: "America/Sao_Paulo"
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
concurrencyPolicy: Forbid
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 900
template:
spec:
restartPolicy: OnFailure
containers:
- name: disparar
image: docker.io/curlimages/curl:8.9.1
imagePullPolicy: Never
# A competencia e resolvida AQUI, e nao no servidor: o endpoint
# exige `YYYY-MM` explicito de proposito, para reprocessar um mes
# antigo ser um ato deliberado e nao um efeito do relogio.
command: ["/bin/sh", "-c"]
args:
- |
export ALVO="http://servico-financeiro:8083/api/financeiro/matriculas/gerar-mensalidades?competencia=$(date +%Y-%m)"
exec /bin/sh /script/disparar.sh
env:
- { name: KEYCLOAK_URL, value: "http://keycloak:8080/realms/athleticmap/protocol/openid-connect/token" }
- name: CLIENT_SECRET
valueFrom:
secretKeyRef: { name: operacao-credentials, key: client-secret }
volumeMounts:
- { name: script, mountPath: /script }
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 64Mi }
volumes:
- name: script
configMap: { name: financeiro-cron, defaultMode: 0555 }
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: financeiro-notas
namespace: plataforma-prod
spec:
schedule: "15 * * * *"
timeZone: "America/Sao_Paulo"
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
concurrencyPolicy: Forbid
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 600
template:
spec:
restartPolicy: OnFailure
containers:
- name: disparar
image: docker.io/curlimages/curl:8.9.1
imagePullPolicy: Never
command: ["/bin/sh", "/script/disparar.sh"]
env:
- { name: KEYCLOAK_URL, value: "http://keycloak:8080/realms/athleticmap/protocol/openid-connect/token" }
- { name: ALVO, value: "http://servico-financeiro:8083/api/financeiro/notas/processar?limite=50" }
- name: CLIENT_SECRET
valueFrom:
secretKeyRef: { name: operacao-credentials, key: client-secret }
volumeMounts:
- { name: script, mountPath: /script }
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 64Mi }
volumes:
- name: script
configMap: { name: financeiro-cron, defaultMode: 0555 }
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: financeiro-conciliacao
namespace: plataforma-prod
spec:
schedule: "30 */6 * * *"
timeZone: "America/Sao_Paulo"
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
concurrencyPolicy: Forbid
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 900
template:
spec:
restartPolicy: OnFailure
containers:
- name: disparar
image: docker.io/curlimages/curl:8.9.1
imagePullPolicy: Never
command: ["/bin/sh", "/script/disparar.sh"]
env:
- { name: KEYCLOAK_URL, value: "http://keycloak:8080/realms/athleticmap/protocol/openid-connect/token" }
# limiteCobrancas=100: cada uma custa uma chamada ao provedor.
- { name: ALVO, value: "http://servico-financeiro:8083/api/financeiro/conciliacao?limiteEventos=100&limiteCobrancas=100" }
- name: CLIENT_SECRET
valueFrom:
secretKeyRef: { name: operacao-credentials, key: client-secret }
volumeMounts:
- { name: script, mountPath: /script }
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 64Mi }
volumes:
- name: script
configMap: { name: financeiro-cron, defaultMode: 0555 }