Banco de Dados — Histórico de Correções
Registro único de duas rodadas de trabalho no schema de fastgivr-api: o diagnóstico e implementação do onboarding (Etapa 1) e uma auditoria completa do banco (tabelas, colunas, índices, constraints e enums de status). Substitui três documentos anteriores (diagnóstico, implementação do onboarding e auditoria), que tratavam do mesmo assunto em estágios sucessivos.
Convenções atuais do schema (SoftDeletes, metadata JSON, activity_logs, critério enum-vs-tabela-de-lookup, estratégia de FK) estão documentadas em Apresentação — §2.4 Modelo de dados, não repetidas aqui. Este documento é só o histórico do que foi encontrado e corrigido.
Status: todos os itens abaixo foram implementados (migrations + models/services). ⚠️ Nada foi executado contra um banco real — não há PHP nem MySQL disponíveis no ambiente onde este trabalho foi feito. Antes de rodar em qualquer ambiente com dado de verdade, ver Antes de aplicar em produção.
1. Onboarding (Etapa 1)
Fluxo multi-etapas em /access/register/* — contrato completo em Fluxo de Cadastro. O que foi corrigido:
| Item | Problema | Resolução |
|---|---|---|
| Endereço em 3 formatos | registers.address vs company_data.endereco vs accounts.metadata.address — complemento/bairro se perdiam na promoção da conta | AccountProvisioningService normaliza PF e PJ para um formato canônico e grava via Account::address() (relação polimórfica já existente, nunca usada) |
| KYC não persistia na conta | birth_date, rg, mother_name, is_pep, pep_details, legal_representative só existiam em registers — somem se o registro for arquivado | Migration 2026_09_05_000002_add_kyc_fields_to_accounts_table.php promove esses campos para accounts na aprovação |
| Sem registro de quem aprovou | Aprovação/rejeição do backoffice não gravava o agente responsável | registers.reviewed_by (migration 2026_09_05_000003_...); preenchido por Backoffice\RegistrationController::approve/reject |
| Onboarding nunca chamava o parceiro de KYC | Contrato documentado na collection Postman, nunca integrado | App\Interfaces\PartnerOnboardingInterface (21 operações) + duas implementações: PartnerOnboardingSandbox (mock local, modo padrão) e PartnerOnboardingHttpClient (parceiro real, via PARTNER_ONBOARDING_MODE=http) |
registers.data (JSON genérico) | Legado sem leitor — o fluxo multi-etapas já usa colunas dedicadas | Removido do $fillable; coluna mantida no schema só por compatibilidade com linhas antigas |
users.dt_birth nunca preenchido | Diagnóstico original recomendava remover — correção: a coluna é usada por Backoffice\UserController para qualquer User, não só onboarding | Mantida e sincronizada automaticamente a partir de registers.birth_date (PF) na aprovação |
Migrations: 2026_09_05_000001 (endereço: complement/district em addresses), 000002 (KYC em accounts), 000003 (reviewed_by + campos do parceiro em registers), 2026_09_06_000009 (tabelas do parceiro simulado: onboarding_partner_clients, onboarding_partner_verifications).
Em aberto (decisão de produto, não de código)
RegisterStatus— os casos legados (PENDING,DELETED,BLOCKED,CANCELLED,EMAIL_NOT_VERIFIED,INVALID_ADDRESS,INVALID_IMAGE) foram marcados como não usados pelo código atual, mas não removidos — falta confirmar num banco real que nenhuma linha de produção ainda usa esses valores (remover um case usado quebra o cast do Eloquent).PartnerOnboardingService— o payloadPOST /clientsdo parceiro real esperacbo(ocupação) einvoicing(faturamento), que não são coletados em nenhuma etapa atual do onboarding. O sandbox aceitanull; o parceiro real pode exigir — se exigir, adicionar a pergunta apersonal-data/company-data.accounts.metadata.address— contas aprovadas antes desta correção continuam com o endereço só emmetadata, sem backfill retroativo (migração de dado, não de schema — fora deste escopo).
2. Auditoria completa do banco
Varredura de todo o schema (tabelas, relacionamentos, colunas, índices, constraints, enums de status) — achados confirmados por grep de uso real antes de qualquer decisão (não são suposições). Inclui uma autoauditoria: 2 itens (marcados 🔁) são inconsistências introduzidas nas próprias tabelas criadas durante este trabalho.
| # | Problema | Resolução |
|---|---|---|
| 01 | BoletoStatus::Deleted (-2) duplicava DELETED (-1) — zero usos do case duplicado | Case removido; migration normaliza qualquer boletos.status = -2 remanescente para -1 antes da remoção |
| 02 | StatusPixWithdrawEnum — enum inteiro morto (zero usos, com um typo nunca notado) | Arquivo removido |
| 03 | WithdrawStatusEnum misturava 3 vocabulários (PT-BR/EN/numérico legado) em 12 cases — só 3 eram gravados de fato | Reduzido a AWAITING/COMPLETED/ERROR; migration normaliza os 9 valores legados para o equivalente antes da limpeza |
| 04 | 5 representações de "status de pagamento" sem critério documentado de qual usar onde | Documentado em Apresentação §2.4 — "Vocabulário de status por tabela" |
| 05 | activity_logs.channel = 'security' — valor inventado fora do vocabulário da tabela (system|audit|access|suspicious) 🔁 | Trocado para 'audit' (código + backfill) |
| 06 | receivables (+ model Receivable, + charges.receivable_id) — zero uso confirmado, 100% morta | Tabela, coluna e relações removidas |
| 07 | accounts.expiration_time — sem nenhum uso funcional fora do $fillable | Coluna removida |
| 08 | accounts.keypix duplicava key_pix | Coluna removida |
| 09 | bank_institutions.institution_name duplicava full_name | Coluna removida (ReferenceDataSeeder também parou de escrevê-la) |
| 10 | Account::$fillable citava 9 colunas fantasma (webhook, information, street, city, state, zip_code, number, path_logo, webhook_events) + deleted_at | $fillable alinhado ao schema real; accounts.webhook_url criada para o caso genuinamente em uso. Achados no caminho: AccountResource/AccountDetailResource e dois jobs liam essas mesmas colunas fantasma — corrigidos junto (ver nota abaixo) |
| 11 | charges.tax_paid gravado por 2 serviços mas a coluna não existia — descartado silenciosamente | Coluna criada; bug de precedência de operador na mesma linha ($taxFee ?? 0 + $tax nunca somava) corrigido junto |
| 12 | accounts.token_expires era string guardando uma data | TIMESTAMP + cast datetime |
| 13 | Colunas monetárias com 3 precisões diferentes (decimal(10,2)/(11,2)/(12,2)) para o mesmo conceito | boletos.value/paid_amount/amount_to_pay padronizados em decimal(12,2) |
| 14 | clients sem UNIQUE(account_id, document) — proteção só na aplicação | Dedupe (soft delete + metadata.merged_into_client_id) + constraint; call sites migrados para Client::updateOrCreateEvenIfTrashed() |
| 15 | pix_recipients sem constraint real para a identidade usada em updateOrCreate (pix_key OU bank_code+account) | Coluna match_key computada automaticamente (PixRecipient::booted()) + UNIQUE(account_id, document, match_key), com o mesmo dedupe do item 14 |
| 16 | accounts.document sem UNIQUE no schema — só validado na aplicação | UNIQUE(document) — sem dedupe automático: a migration aborta se achar duplicata (conta carrega saldo/transações, mesclar sozinha é arriscado demais) |
| 17 | clients.birthdate (sem underscore) — única exceção à convenção snake_case do resto do schema | Renomeada para birth_date; contrato de API preservado (birthdate continua aceito/devolvido, mapeado internamente) |
| 18 | Duas estratégias de FK (com/sem constrained()) sem critério documentado | Documentado em Apresentação §2.4 — "Estratégia de chave estrangeira" |
| 19 | Duas estratégias para status — enum PHP vs. tabela de lookup — sem critério documentado | Documentado em Apresentação §2.4 — "Enum PHP vs. tabela de lookup" |
| 20 | pix_recurrence_runs, onboarding_partner_clients, onboarding_partner_verifications sem softDeletes() 🔁 | deleted_at adicionado às 3; nenhuma tinha ->delete() em uso, mudança puramente aditiva |
Migrations: 2026_09_06_000010 a 2026_09_06_000023, uma por item (ordem sequencial, ver database/migrations/).
Padrões técnicos que valem a pena reaproveitar
updateOrCreateEvenIfTrashed()(app/Models/Client.php) — sempre que uma coluna ganha umaUNIQUEe o modelo usaSoftDeletes, umupdateOrCreate()comum colide com uma linha já soft-deletada (MySQL não exclui linhas trashed de um índice único). O helper busca comwithTrashed()e restaura em vez de tentar inserir. Mesmo padrão aplicado apix_recipientsviawithTrashed()->updateOrCreate()+restore().- Coluna computada para identidade com match variável (
PixRecipient.match_key) — quando a "mesma entidade" pode ser identificada por mais de uma combinação de campos (aqui:pix_keyOUbank_code+account) e o banco não tem índice único parcial/condicional (MySQL não tem), normalizar num único campo calculado via hook do model (saving) e colocar a constraint nele, em vez de tentar duas constraints paralelas. - Abortar em vez de mesclar automaticamente (
accounts.document, item 16) — dedupe automático com soft delete é seguro para tabelas "satélite" (clients,pix_recipients), mas não para uma tabela que carrega saldo/transações/usuários. Nesse caso a migration lança uma exceção clara listando o dado duplicado, e exige resolução manual antes de rodar de novo. - Compatibilidade de contrato ao renomear uma coluna (
clients.birthdate, item 17) — renomear no banco não obriga quebrar a API: a chave de request/response pode continuar igual, só mudando o atributo interno do model. Vale sempre que houver qualquer front-end consumindo a chave antiga.
Como unificar registros duplicados (metodologia, reaproveitável em qualquer tabela nova)
- Identificar o grupo de duplicatas (
GROUP BYnas colunas-chave). - Escolher o "sobrevivente" — o mais recente com mais campos preenchidos, não simplesmente o mais antigo.
- Repontar toda FK que aponte para os registros descartados, para o sobrevivente.
- Soft-delete (nunca hard-delete) os descartados, com
metadata.merged_into_*_idapontando pro sobrevivente — mantém rastro de auditoria. - Só depois disso, aplicar a constraint
UNIQUE.
Confirmado como correto (não é achado)
webhooks,webhook_notificationseactivity_logsnão são redundantes apesar do nome parecido:webhooksé o log bruto de callbacks recebidos do PSP;webhook_notificationsrastreia as tentativas de entrega de saída ao lojista (attempts,last_attempt_at);activity_logsé o log interno unificado de auditoria/sistema.boletos(emissão) vs.boleto_withdraws(pagamento de boleto de terceiro) também não são redundantes — domínios diferentes (emitir vs. pagar).
Antes de aplicar em produção (checklist)
Nada neste documento foi executado contra um banco real — rode php artisan migrate primeiro num ambiente de staging com uma cópia do dado de produção, e confira:
- [ ] Itens 06/07 (remoção):
SELECT COUNT(*) FROM receivableseSELECT COUNT(*) FROM accounts WHERE expiration_time <> 99999— se> 0, decidir o que fazer com esse dado antes domigrate. - [ ] Itens 14/15 (dedupe automático): confira o resultado do dedupe —
SELECT COUNT(*) FROM clients WHERE deleted_at IS NOT NULL AND metadata->>'$.merged_into_client_id' IS NOT NULL(análogo parapix_recipients). - [ ] Item 16 (
accounts.document): a migration aborta com umaRuntimeExceptionse encontrar duplicata — leia a mensagem de erro e resolva manualmente antes de rodar de novo. - [ ] Item 13:
ALTER...MODIFYemboletospode bloquear/reescrever a tabela conforme o volume — rodar em janela de manutenção se a tabela for grande.