From 2014c3789e2c4169f16bd8b1c2f5b937e0ae9227 Mon Sep 17 00:00:00 2001 From: deploy Date: Mon, 21 Sep 2026 11:33:17 +0000 Subject: [PATCH] feat(plataforma): CronJobs do financeiro (mensalidades, notas, conciliacao) --- .../plataforma/55-cronjobs-financeiro.yaml | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 tenants/plataforma/55-cronjobs-financeiro.yaml diff --git a/tenants/plataforma/55-cronjobs-financeiro.yaml b/tenants/plataforma/55-cronjobs-financeiro.yaml new file mode 100644 index 0000000..6f05a25 --- /dev/null +++ b/tenants/plataforma/55-cronjobs-financeiro.yaml @@ -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 }