Fluxo de Pix
Estrutura completa de Pix da conta ativa: Pix In (cobranças recebidas), Pix Out (envio por chave, copia e cola e dados bancários), consulta de chave no DICT, PIN de aprovação e cadastro de contas bancárias favoritas dos clientes.
- Backend:
fastgivr-api—App\Http\Controllers\Pix\*,App\Services\Pix\*,App\Services\PinValidator - Rotas:
routes/pix.php(prefixo/pix, namepix.), carregado porbootstrap/app.php - Middleware:
auth:api,sanctum·SetActiveAccountMiddleware·InjectAccountIntoRequest - Persistência:
pixes,pix_withdraws,pix_recipients,transactions(ledger) - Adaptador bancário:
Account::getBank()— ambiente sandbox com um único gateway,App\Models\Banks\Teste(mocks determinísticos). Verconfig/banks.php.
Todas as respostas seguem o envelope { code, success, data|<chave>, message? }. Erros de validação: HTTP 422 com { success:false, message, data: { campo: [erros] } }.
1. Mapa de endpoints
| Método | Rota | Ação | Aprovação |
|---|---|---|---|
GET | /pix/charges | lista cobranças Pix (paginado; ?txid= / ?id= p/ item único) | — |
POST | /pix/charges | cria cobrança dinâmica (QR + copia e cola) | — |
GET | /pix/charges/static | Pix estático (copia e cola) da própria conta | — |
POST | /pix/charges/{txid}/simulate-payment | liquida a cobrança (só gateway Teste) — atalho autenticado do webhook | — |
GET | /pix/charges/{txid} | detalhe de uma cobrança | — |
GET | /pix/keys | minhas chaves Pix registradas no DICT | — |
POST | /pix/keys | registra uma chave da conta (cpf|cnpj|email|phone|random) | — |
DELETE | /pix/keys/{id} | remove uma chave da conta do DICT | — |
POST | /pix/keys/lookup | consulta a chave de um terceiro no DICT | — |
POST | /pix/pin/verify | valida o PIN da conta (conta tentativa) | — |
GET | /pix/pin/status | estado do PIN (cadastrado? bloqueado?) | — |
GET | /pix/transfers | histórico de envios (paginado) | — |
POST | /pix/transfers/key/preview | resolve a chave e cria saque AGUARDANDO | — |
POST | /pix/transfers/key | confirma e envia por chave | PIN |
POST | /pix/transfers/copy-paste/preview | decodifica o BR Code e resolve o beneficiário | — |
POST | /pix/transfers/copy-paste | confirma e paga o BR Code | PIN |
POST | /pix/transfers/manual | envia por dados bancários (banco/agência/conta) | PIN |
GET | /pix/transfers/{id} | detalhe de um envio + transação associada | — |
GET | /pix/transfers/{id}/receipt | comprovante do envio (JSON; ?format=pdf → PDF) | — |
GET | /pix/recipients | contas favoritas / destinatários (?favorite=1 ?client_id= ?search=) | — |
POST | /pix/recipients | cadastra uma conta bancária favorita de um cliente | — |
GET | /pix/recipients/frequent | top 10 por número de usos | — |
GET | /pix/recipients/{id} | detalhe | — |
PUT|PATCH | /pix/recipients/{id} | edita | — |
DELETE | /pix/recipients/{id} | remove (soft delete) | — |
O cadastro/alteração do PIN continua em
/access/account/pin.
2. Pix In — cobranças recebidas
Controller Pix\PixInController. Substitui o antigo QuickPay\PixAccount.
2.1 POST /pix/charges — criar cobrança dinâmica
CreatePixChargeRequest:
| Campo | Regra |
|---|---|
amount | required · numeric · min:{accounts.min_pix_in ?? 1} |
webhook | nullable · url — URL do lojista notificada quando o Pix for pago |
name, email, document, phone | nullable — quando os três primeiros vêm juntos, cria/atualiza um Client |
Efeitos: cria Pix (status = 0 PENDING), Charge (status pending, payment_method = PIX) e, opcionalmente, o Client. Retorna txid, value, qrcode (payload EMV) e image (QR Code SVG).
2.2 GET /pix/charges/static — Pix estático da conta
Gera uma vez e persiste em accounts.qrcode o payload EMV estático (App\Services\PixEstatico) com a key_pix da conta. Retorna { payload, image }.
2.3 GET /pix/charges e GET /pix/charges/{txid}
Lista paginada (per_page, page) das cobranças da conta, cada uma com o QR renderizado. ?txid= ou ?id= devolve item único com as transações vinculadas. {txid} devolve o PixChargeResource (status legível via PixStatus).
2.4 Estados do Pix (App\Enums\PixStatus)
| valor | nome | significado |
|---|---|---|
0 | PENDING | aguardando pagamento |
1 | PAID | pago |
2 | PAIDWITHBOLETO | pago via Bolepix |
3 | EXPIRED | expirado |
-2 | DELETED | removido |
2.5 Liquidação do Pix recebido — App\Services\Pix\PixInSettlementService
Um único serviço, idempotente por txid, concentra a liquidação:
Pix→PAID(+paid_amount);- lança
pix-inno ledger (+fee-pix-inseaccounts.tax_pix_in); - marca a
Chargevinculada comopaid; - dispara
PixReceivedNotificationaos usuários da conta (respeita a preferênciapix_credit, nunca lança); - agenda o webhook do lojista (
pix.webhooke/ouaccounts.webhook).
Quem chama o serviço (gateway Teste):
| Gatilho | Descrição |
|---|---|
POST /webhook/teste/pix-in | { txid, amount?, endToEndId?, payer_name? } — público (simula o callback do PSP) |
POST /pix/charges/{txid}/simulate-payment | autenticado — atalho para a tela "Cobrar" |
Chamar duas vezes o mesmo txid não duplica lançamentos (devolve a transação pix-in existente).
3. Chaves Pix (DICT)
3.1 Minhas chaves — GET/POST/DELETE /pix/keys
Chaves que a própria conta registra no DICT para receber Pix. Persistidas em pix_account_keys (App\Models\PixAccountKey), separadas de pix_recipients (chaves de terceiros). Controller Pix\PixKeyController.
O registro/remoção fala com Account::getBank() via createPixKey / deletePixKey, guardados por method_exists — bancos ainda não integrados devolvem 422 "não implementado" (hoje só o gateway Teste cobre 100%).
| Método | Rota | Ação |
|---|---|---|
GET | /pix/keys | lista as chaves da conta ({ keys: [...] }) |
POST | /pix/keys | registra uma chave |
DELETE | /pix/keys/{id} | remove a chave (soft delete + baixa no DICT) |
StorePixAccountKeyRequest:
| Campo | Regra |
|---|---|
type | required · in:cpf,cnpj,email,phone,random |
key | required_unless:type,random · max:100 — formato validado conforme o type; random é gerada pelo PSP |
Regras extras (withValidator): máximo 5 chaves ativas/pendentes por conta, sem chave duplicada e no máximo uma chave cpf/cnpj. O registro cria a linha como pending, chama o PSP e conclui como active (confirmed_at) ou error.
3.2 Consulta de terceiro — POST /pix/keys/lookup
PixKeyLookupRequest: key required, type required · in:cpf,cnpj,email,telefone,aleatorio, amount nullable.
O controller valida o formato da chave conforme o type (CPF 11 dígitos, CNPJ 14, e-mail válido, telefone 11 dígitos, aleatória 32–37 caracteres) e consulta via App\Services\StricService::getPixInfo().
MVP:
StricServicedevolve um titular determinístico derivado da própria chave (mesma entrada → mesma resposta), mantendo o Pix Out por chave e as telas de consulta testáveis no gatewayTeste. O bloco HTTP da integração real (PrismaPay / STRIC) fica comentado como referência.
| Status | Quando |
|---|---|
200 | { status: "success", data: { ...dados do titular no DICT } } |
400 | formato inválido para o tipo informado |
404 | chave não encontrada no DICT |
4. PIN de aprovação
Toda transferência Pix (chave / copia e cola / dados bancários) exige o PIN de 6 dígitos da conta, validado antes da chamada ao banco por App\Services\PinValidator.
| Situação | HTTP | data.reason |
|---|---|---|
| PIN não enviado | 422 | pin_required |
| PIN não cadastrado na conta | 409 | pin_not_configured |
| PIN incorreto | 403 | pin_invalid + attempts_remaining |
| Bloqueado por tentativas | 429 | pin_locked + retry_after_seconds |
- Limite:
MAX_ATTEMPTS = 5tentativas erradas → bloqueio deDECAY_SECONDS = 900s (15 min), contabilizado por conta noRateLimiter(cache). PinExceptionéResponsable— o Laravel renderiza o corpo diretamente, sem passar pelohandleException.POST /pix/pin/verify{ pin }→200 { valid: true }ou o erro acima (também conta tentativa).GET /pix/pin/status→{ configured, locked, retry_after_seconds, attempts_remaining }.
5. Pix Out — envio por chave
Fluxo em 2 passos: preview (resolve o beneficiário e cria o saque AGUARDANDO) → send (PIN + envio).
5.1 POST /pix/transfers/key/preview
PixWithdrawRequest: keypix required·max:150, value nullable·numeric·min:0, message nullable·max:255. withValidator bloqueia saldo insuficiente (balance - tax_pix_out) e duplicidade (mesmo documento+valor AGUARDANDO na última 1 h).
Se já houver um saque AGUARDANDO idêntico na última hora, devolve o existente (não cria outro).
5.2 POST /pix/transfers/key
PixWithdrawalConfirmarRequest: internal_id required·exists:pix_withdraws,id, keypix, value required·min:0.01, pin required·size:6, message nullable·max:140. withValidator reconfirma titularidade, estado AGUARDANDO, ausência de duplicata CONCLUIDO e saldo.
Lançamentos no ledger (App\Services\Pix\PixService::recordOutTransactions):
type_id (slug) | valor |
|---|---|
pix-out | -valor |
fee-pix-out (fallback taxa) | -accounts.tax_pix_out (só se houver) |
Ao final, recordOutTransactions dispara PixSentNotification in-app (preferência pix_debit). O mesmo vale para copia e cola e dados bancários.
5.3 GET /pix/transfers/{id}/receipt — comprovante
Devolve o comprovante normalizado (payer, receiver, value, txid, end_to_end_id, transaction) de um saque CONCLUIDO — 422 caso contrário. ?format=pdf renderiza o PDF (view pdfs.transaction); ?download=0 abre inline. pdf_url no corpo aponta para essa mesma rota.
6. Pix Out — copia e cola (BR Code)
Decodifica o payload EMV®MPM ("Pix Copia e Cola") localmente e paga.
- Decoder:
App\Services\Pix\BrCodeDecoder— TLV EMV, valida o CRC16-CCITT (polinômio0x1021, init0xFFFF) sobre o payload até6304inclusive. - DTO:
App\DTO\Pix\BrCodePayloadDTO—type=static|dynamic(dinâmico = traz URL na tag26/25ou POI method12).
| Tag | Conteúdo |
|---|---|
00 | Payload Format Indicator |
01 | Point of Initiation Method (11 reutilizável, 12 uso único) |
26/00 26/01 26/02 26/25 | GUI (br.gov.bcb.pix) · chave · descrição · URL da cobrança |
52 53 54 58 59 60 | MCC · moeda (986) · valor · país (BR) · nome · cidade |
62/05 | TXID |
63 | CRC16 |
PixCopyPasteRequest: payload required·min:20·max:2000, amountnullable·numeric·min:0.01 (obrigatório quando o BR Code estático não traz valor — checado no controller após o decode), message nullable·max:140, pin nullable·size:6 (exigido no envio).
- Estático: valor vem do payload (tag
54) ou do campoamount; chave da tag26/01. - Dinâmico:
Account::getBank()->getPixCharge(url)busca a cobrança no PSP — mock determinístico no gatewayTeste.
O preview não persiste nada. O send cria o PixWithdraw (com metadata.br_code), lança no ledger e finaliza como CONCLUIDO.
7. Pix Out — dados bancários
POST /pix/transfers/manual
PixBankTransferRequest: bank_code, agency, account, beneficiary_name, beneficiary_document, amount required·min:0.01, message nullable, pin required·size:6.
Resolve o ISPB por BankInstitution::findByIspbOrCode($bank_code), chama makePixTransferBank(PixOutBankRequestDTO), grava o PixWithdraw, lança no ledger (pix-out + fee-pix-out) e cadastra o destinatário.
8. Contas bancárias favoritas dos clientes
pix_recipients = uma conta bancária de um cliente + suas chaves Pix. Controller Pix\PixRecipientController.
favorite— cadastro explícito viaPOST /pix/recipients(vs. só "frequente", cujocountcresce sozinho após cada transferência).pix_keys—jsoncom[{ key, type }]: várias chaves da mesma conta.pix_keyguarda a chave primária (busca / compatibilidade).alias— apelido ("Fornecedor João — Itaú").
POST /pix/recipients (StorePixRecipientRequest)
| Campo | Regra |
|---|---|
client_id | nullable·exists:clients,id — precisa pertencer à conta ativa |
name | required·max:150 |
document | required·cpf_ou_cnpj |
alias | nullable·max:120 |
favorite | sometimes·boolean (default true neste endpoint) |
bank_name bank_code branch account | nullable |
account_type | nullable·in:CORRENTE,POUPANCA,PAGAMENTO,SALARIO |
pix_key | nullable·max:150 |
pix_keys | nullable·array·max:10 — cada item { key (required_with), type? in cpf,cnpj,email,phone,random } |
Regra cruzada: pelo menos uma chave OU dados bancários (bank_code + account). O type ausente é deduzido pelo formato (App\Services\Pix\PixService::guessKeyType).
Identidade do favorito = (account_id, document, conta bancária | chave primária). Re-cadastrar a mesma conta funde novas chaves (não duplica) e restaura um registro soft-deleted.
PUT/PATCH edita (parcial; document imutável); DELETE faz soft delete.
9. Modelos
pixes (App\Models\Pix)
txid, qrcode (payload EMV), value, paid_amount, payment_date, status (PixStatus), webhook, name/email/document (pagador), account_id, metadata. Relação charge, transactions (por txid).
pix_withdraws (App\Models\PixWithdraw)
unique_hash, name, document, bank_code, branch, account_number, account_type, key_pix, value, txid, end_to_end_id, status (WithdrawStatusEnum: aguardando / concluido / erro), scheduled_at, metadata (inclui br_code no fluxo copia e cola), message.
pix_recipients (App\Models\PixRecipient)
account_id, client_id, favorite, alias, pix_key, pix_keys (array), name, document, bank_name, bank_code, branch, account, account_type, count, last_used_at. allKeys() normaliza pix_key + pix_keys.
pix_account_keys (App\Models\PixAccountKey)
Chaves da própria conta no DICT. account_id, type (cpf/cnpj/email/phone/random), key, status (pending/active/error), psp_key_id, metadata, requested_at, confirmed_at, soft delete. Único por (account_id, key).
10. Adaptador bancário (Account::getBank() → App\Models\Banks\Teste)
Sandbox: um único adaptador, com mocks determinísticos.
| Método | Uso |
|---|---|
createPixDinamico(value) | cobrança dinâmica |
getPixTransactionDetails(key) | DICT p/ Pix Out por chave |
makePixTransfer(PixOutRequestDTO) | envio por chave / copia e cola |
makePixTransferBank(PixOutBankRequestDTO) | envio por dados bancários |
getPixCharge(url) | copia e cola dinâmico |
createPixKey(type, key?) / deletePixKey(key) | registrar/remover minha chave |
StricService::getPixInfo cobre o DICT do endpoint /pix/keys/lookup (mock determinístico no MVP).
11. Frontend
fastgivr-internet-bank/src/services/pix.ts — pix.in (create / staticQr / getByTxid / simulatePayment), pix.pin, pix.keys (list / register / remove / lookup), pix.transfers (list / get / receipt / receiptPdfUrl / previews e envios), pix.recipients. Telas em src/pages/pix/* sob o layout AppLayout (rotas /pix, /pix/transferir, /pix/cobrar, /pix/extrato, /pix/chaves).
O interceptor de src/services/api.ts injeta Authorization: Bearer e X-Device-Id; um 401 em request autenticado limpa a sessão e volta para /login?session=expired.