Varejo de instrumentos musicais

Faz Play
PDV que divide uma venda entre 5 formas de pagamento e fecha o caixa conferido por cédula.
- Cliente
- F Salomão Instrumentos Musicais
- Categoria
- PDV / Retail
- Período
- mar 2025 – jul 2026
- Estado
- Em produção (emissão fiscal em homologação)
O problema
Loja de instrumentos musicais com duas unidades, uma delas dentro de shopping. Vender é a parte fácil: o dia vai embora em conferir caixa, declarar faturamento pro shopping, dar nota, controlar o mesmo estoque em dois endereços, fechar comissão e mandar XML pro contador. PDV de prateleira não atende duas contas de adquirente, maquininhas de operadoras diferentes e o relatório de repasse do shopping ao mesmo tempo. E trocar de sistema no meio do movimento não é opção.
O que foi construído
Um único app Flutter que roda como PWA no balcão e como app Android no salão, sobre Firebase (Auth, Firestore, Storage, Crashlytics); iOS, macOS e Windows são alvos configurados, não distribuídos. A venda aceita pagamento dividido entre cinco trilhas heterogêneas na mesma comanda, e a nota sai da tela de detalhe da venda por um backend Node próprio contra a Focus NFe. No repositório, o CNPJ do cliente ainda está roteado para a base de homologação. O caixa fecha com conferência por cédula, valor esperado calculado (abertura + vendas em dinheiro − sangrias) e foto do fechamento no Storage.
Decisões de engenharia
Pagamento dividido entre 5 trilhas na mesma venda
Uma comanda pode ser paga em partes por dinheiro, PIX, duas maquininhas Mercado Pago Point com device IDs distintos e uma maquininha PagBank por Bluetooth. Cada parcela tem aprovação, status e retry independentes: se a terceira trilha falhar, as duas já aprovadas não se perdem. Por trás existem duas contas Mercado Pago com tokens e segredos de webhook separados, e o webhook descobre a qual conta a notificação pertence pelo segredo que validou a assinatura.
lib/principal.dart:352-486 (_processarPagamentos); lib/dialogo_pagamento_screen.dart:284-318 (5 itens do dropdown); pdv-jaguar-backend/controllers/mercadoPagoController.js:10-35 (duas contas), :506 (conta resolvida pelo segredo que validou o HMAC)
Confirmação de pagamento sem depender do webhook
O handler valida HMAC-SHA256 do x-signature contra os segredos das duas contas MP, com timingSafeEqual, antes de aceitar a notificação. Em paralelo, a tela escuta o documento da venda em tempo real e roda polling ativo a cada 3s, com timeout de 120s no PIX e 180s na maquininha. Webhook perdido ou atrasado não trava o caixa.
pdv-jaguar-backend/controllers/mercadoPagoController.js:464-570 (HMAC em :482-486); lib/pagamento_pix_screen.dart:61-111 (polling 3s, timeout 120s); lib/pagamento_maquininha_screen.dart:51,94-96 (timeout 180s)
Ponte Kotlin para o SDK PlugPag da PagBank
Uma camada de Kotlin resolve o que o SDK impõe: instância única de PlugPag por processo com double-checked locking, chamadas bloqueantes em thread própria, permissões de Bluetooth do Android 12+ negociadas em runtime e eventos da maquininha ("insira o cartão", "senha", "processando") streamados pro Dart por EventChannel. No web e no iOS a opção nem chega a aparecer na tela.
android/app/src/main/kotlin/com/example/loja_vendas_com_firebase/PlugPagBridge.kt:38-53,96-98,204 (403 linhas); lib/services/plugpag_service.dart:117-123; android/app/build.gradle:103
Venda e baixa de estoque no mesmo WriteBatch
Cada item carrega o local de estoque, e o WriteBatch que fecha a venda decrementa quantidadeTotal e o campo da loja (quantidadeSjp ou quantidadeFazenda) por FieldValue.increment: venda e baixa acontecem juntas ou nenhuma acontece. Excluir a venda estorna dentro de runTransaction antes de deletar. O caixa exige conferência por cédula em 12 denominações tanto na abertura quanto no fechamento, e o fechamento é recusado se o físico divergir do esperado em mais de R$ 1,00.
lib/principal.dart:617-657; lib/relatorio_bloc.dart:180-222; lib/calculadora_caixa.dart:20-33 (12 denominações); lib/caixa_Screen.dart:342-398 (valor esperado), :637 e :735 (conferência obrigatória), :909-919 (tolerância de R$ 1,00 no fechamento)
Stack
Telas





O papel da CG Mixart
Único desenvolvedor do projeto: arquitetura e implementação do app Flutter (6 plataformas alvo, 5 configuradas no Firebase), do backend Node/Express no Render, das integrações com Mercado Pago (Point, PIX, Checkout Pro), com a PagBank via ponte nativa Kotlin e com a Focus NFe, além da modelagem do Firestore, geração de PDF e cupom térmico, e do deploy no Firebase Hosting.
Ressalvas e notas técnicas, verificadas no códigoabrir
Cliente e escopo: a operação tem duas unidades, "Centro Musical SJP" (dentro de shopping) e "Faz Play Fazenda". O app trata as duas com estoque e caixa separados. O host de produção e os dois backends no Render respondem 200; o emitente configurado é CNPJ real com IE e responsável técnico exigidos pela SEFAZ-PR (lib/services/nfe_service.dart:18-42). O projeto começou em março de 2025 e a última alteração é de julho de 2026. CORRIGIDO nesta revisão (roteamento entre as duas contas de adquirente): a versão anterior da ficha afirmava que "o roteamento entre as duas contas é decidido pelo prefixo do device ID". Isso não é recurso, é bug. Os dois device IDs reais estão em lib/principal.dart:84 (azul, da SJP) e :88 (amarela), e ambos começam com "NEWLAND"; o terceiro, da Fazenda, é placeholder (:85-86). Tanto o app (lib/pagamento_maquininha_screen.dart:288) quanto o backend (controllers/mercadoPagoController.js:105) fazem `deviceId.startsWith("NEWLAND") ? "amarela" : "sjp"`. Ou seja, a maquininha azul da SJP também cai nas credenciais da conta amarela. O que de fato funciona é a resolução no webhook, por qual dos dois segredos validou o HMAC (mercadoPagoController.js:506). O prefixo precisa virar uma tabela device ID → conta. Ainda em pagamentos: `_deviceIdFazenda` é o literal 'ID_DA_MAQUININHA_FAZENDA_SE_HOUVER_OUTRA' (lib/principal.dart:85-86). A trilha Point da unidade Fazenda não está configurada; as 5 formas de pagamento existem de verdade no balcão da SJP. Emissão fiscal: a métrica conta as opções que existem na tela da venda, não notas autorizadas em produção. O `.env.example` do backend documenta `FOCUS_NFE_BASE_URL_HOMO` como "Homologação (fsalomão durante testes)" e explicita que "o backend escolhe homo se o CNPJ do payload for 54887255000147", que é exatamente o CNPJ do emitente configurado no app. Os dois serviços trazem `BASES_POR_CNPJ = { "54887255000147": FOCUS_NFE_BASE_URL_HOMO }` com o comentário que marca a F Salomão como "homologação durante testes" (services/focusNfeService.js:29-33, services/focusNfceService.js:32-36), a base default cai em homologacao.focusnfe.com.br (focusNfeService.js:26) e o mock é `NFE_MOCK !== "false"` (focusNfeService.js:20, focusNfceService.js:23), ou seja, ligado se a env faltar. O `.env.example` traz `NFE_MOCK=false`, mas quem manda é o Render. Em contrapartida, nfe_service.dart:18-19 traz comentário afirmando que os dados do emitente foram "conferidos contra NF-e modelo 55 já autorizada em produção (ref FSL-..., 14/05/2026)". É indício, não prova, e `ambienteHomologacao = false` (:15) é só rótulo de UI. Anunciar emissão fiscal como recurso de produção exige o env do Render, não o repositório; por isso ficou fora da tagline. CORRIGIDO nesta revisão: desktop saiu da solução. Existem seis diretórios de plataforma, mas build/ só tem artefato android e web; macOS reusa o appId de iOS e Windows reusa o appId de web no firebase.json; e firebase_options.dart lança UnsupportedError para Linux. Desktop é alvo configurado, não app distribuído. CORRIGIDO nesta revisão: contradição interna sobre pdf/printing. A ficha anterior dizia que esses pacotes "não constam da stack publicada" enquanto os listava na camada "Documentos e hospedagem". Estão listados de propósito: o cupom térmico 80 mm é real e usado (lib/relatorio.dart:571, lib/relatorio_shopping.dart:300, lib/venda_do_dia_screen.dart:408). O que está errado é a declaração: pdf, printing e path_provider estão sob `dev_dependencies` (pubspec.yaml:64, 67-69), o que faz o build de release funcionar por sorte, não por projeto. Mover para `dependencies`. Já `provider` é importado em lib/main.dart:15 e lib/configuracoes.dart:2 sem estar declarado em lugar nenhum do pubspec (entra transitivo). Por isso não entra na stack. Regras de segurança do Firestore: não existem versionadas. `lib/firestore.rules` está no repo com 0 byte e o `firebase.json` não declara seção `firestore`: nada de regras é deployado por este projeto, e a proteção do banco vive só no console, fora de controle de versão e de review. É o item que eu resolveria primeiro. App Check não está ativado. Não há `firebase_app_check` no pubspec.yaml nem no lock; o único arquivo que tocava no assunto (lib/bacaptela_caixa.dart) está inteiro comentado, órfão e com chave reCAPTCHA hardcoded. Combinado com o item acima, o acesso ao Firestore hoje se apoia só em Auth. Guarda de estado final no webhook: existe apenas na cópia em Cloud Functions, que está fora de uso. O `functions/index.js` passa os bindings mpTokenSJP, mpSecretSJP, mpTokenAmarela e mpSecretAmarela ao setGlobalOptions (:40) sem nunca declará-los (só MERCADO_PAGO_ACCESS_TOKEN e MERCADO_PAGO_WEBHOOK_SECRET são criados, :22-23), então o módulo estoura ReferenceError na carga. O webhook que atende produção é o do Render e faz `saleDocRef.update()` direto (mercadoPagoController.js:562-563), sem transaction e sem guarda: um webhook "rejected" atrasado pode sobrescrever uma venda já concluída. É a próxima correção da fila. A duplicação Functions/Render é dívida assumida, e existem dois hosts Render distintos para o mesmo backend, ambos no ar: meu-backend-mp (lib/mercado_pago_service.dart:22) e pdv-jaguar-backend (lib/services/nfe_service.dart:11). Os dois precisam ser unificados. O serviço de análise por IA aponta pra um túnel de desenvolvimento (https://heavy-places-bet.loca.lt em lib/services/ai_analysis_service.dart:7): o chat que devolve gráficos em fl_chart está em validação, não em produção. O README ainda é o boilerplate do Flutter. Módulos verificados que não couberam nos destaques: ordem de serviço e orçamento, comissão por meta com vales e bonificações, chat interno com FCM, importação de produtos por XML de NF-e do fornecedor e por Excel, scanner de código de barras, relatório de repasse pro shopping em PDF, cupom térmico 80 mm, perfis admin/gerente/vendedor. O front web recebeu tuning pra iPhone: inicializações em paralelo antes do primeiro frame, cache do Firestore com teto de 40 MB (main.dart:46), CanvasKit local em vez de CDN (web/index.html:48, firebase.json:60) e overlay de loading removido no evento flutter-first-frame (web/index.html:51).