ShesBlej
Kosova'da satılık/kiralık daire, araç ve ikinci el eşya ilanları. Ücretsiz ilan ver, satıcıyla doğrudan iletişime geç.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Plataforma global para venda de produtos digitais, cursos, comunidades, assinaturas e mentorias.
A Kabrun recebe o pagamento do comprador, registra a venda no seu próprio ledger, cobra a taxa da plataforma e repassa o saldo ao produtor quando o prazo de liberação vence. Ou seja: é uma plataforma de pagamentos/marketplace, não uma loja com checkout integrado.
Mercados da Fase 1: Brasil, União Europeia e Estados Unidos (BRL, EUR, USD).
Cada camada tem uma responsabilidade única, e elas nunca se misturam:
| Camada | Responsabilidade |
|---|---|
| Keycloak | Identidade: login, senha, recuperação, verificação de e-mail, sessão, MFA |
| Kabrun | Autorização: quem pertence a qual organização e o que pode fazer |
| PostgreSQL | Fonte de verdade de commerce e finanças |
| MongoDB | Dados flexíveis: apresentação do checkout, eventos, payloads |
| Redis | Cache, locks, rate limit e (futuramente) filas |
| Payment provider | Movimentação real do dinheiro (Stripe) |
| Ledger | Fonte de verdade financeira da Kabrun |
| Balance | Projeção do ledger (cache), nunca fonte de verdade |
| Order | Intenção comercial |
| Payment | Movimentação vinda do comprador |
| Payout | Movimentação indo para o produtor |
O fluxo de uma requisição:
Server Component / Server Action / Route Handler
↓ (Zod valida a entrada, requireUser/requireOrganization autoriza)
Service ← regra de negócio, um domínio por service
↓
Repository ← interface; a implementação Prisma/Mongo fica isolada
↓
PostgreSQL / MongoDB ← operações financeiras sempre em transação
Serviços nunca importam PrismaClient, MongoClient ou o SDK da Stripe. Eles recebem
repositories e interfaces de provider (inversão de dependência), o que também torna os testes
determinísticos. A montagem fica em src/server/services.ts e
src/server/container.ts.
jose)@prisma/adapter-pg)MockPaymentProvider para desenvolvimentoPostgreSQL é a fonte de verdade de tudo que é relacional e financeiro:
User, Organization, OrganizationMember, Product, Offer, Checkout, Customer, Order,
OrderItem, Payment, Transaction, Subscription, Refund, Dispute, LedgerJournal,
LedgerEntry, Balance, Reserve, Payout, PayoutDestination, MerchantAccount,
SettlementPolicy, PlatformFeePolicy, Affiliate, Coupon, Course, Enrollment,
ApiKey, AuditLog, WebhookEvent, OutboxEvent, IdempotencyKey.
MongoDB guarda o que é flexível, volumoso ou muda de forma com frequência:
checkout_configs (versionado), analytics_events, webhook_payloads, provider_snapshots,
activity_events, risk_events.
Regra que não se quebra: dado financeiro nunca tem o MongoDB como única fonte de verdade.
O preço oficial vem sempre da Offer no PostgreSQL; o Mongo só configura a apresentação.
Realm kabrun, client kabrun-web (confidencial, PKCE S256). A Kabrun não armazena senhas:
a tabela User guarda keycloakUserId (único), e-mail, nome e preferências.
GET /api/auth/login inicia o fluxo (state + nonce + PKCE em cookie assinado de 10 min)GET /api/auth/callback valida state/nonce, troca o code, verifica o ID token via JWKS e cria a sessãoPOST /api/auth/logout limpa a sessão e encerra a sessão no Keycloak (RP-initiated logout)A sessão é um JWT HS256 em cookie httpOnly, SameSite=Lax (e Secure em produção).
O src/proxy.ts faz apenas a checagem otimista (existe sessão válida?);
a autorização real acontece no servidor, em cada página e action.
Toda entidade de negócio carrega organizationId, e todo repositório filtra por ele.
Helpers: getCurrentUser(), requireUser(), requireOrganization(), requireRole(),
requirePermission(). O organizationId que chega do cliente (cookie, formulário, URL) nunca é
confiado: o vínculo é sempre reconferido no banco.
Papéis: OWNER, ADMIN, FINANCE, SUPPORT, MARKETING, VIEWER (mapeados para permissões em
permissions.ts), mais o papel global PLATFORM_ADMIN
para a equipe Kabrun (/admin).
A marca é roxa em ~70% da interface; o dourado fica reservado a 10–15% (CTA principal, o raio, selos premium), para a Kabrun parecer uma fintech moderna e não uma marca de joias.
| Token | Valor | Uso |
|---|---|---|
| Roxo principal | #4B168C | marca, botões primários, ícones de destaque |
| Roxo escuro | #220A45 | sidebar, gradiente de fundo, modo escuro |
| Roxo vibrante | #7C3AED | gráficos, estado ativo, foco e links |
| Dourado principal | #D9A52A | CTAs decisivos e selos |
| Dourado claro | #F2C75C | gradiente e hover do dourado |
| Cinza muito claro | #F6F5F8 | fundo do dashboard |
| Cinza médio | #8B8793 | texto secundário |
| Preto azulado | #17121F | texto principal |
Gradientes: .bg-brand-gradient (#220A45 → #7C3AED) e .bg-gold-gradient (#B98012 → #F2C75C),
definidos em globals.css junto com os tokens de sombra (shadow-soft,
shadow-card, shadow-lift) e os utilitários bg-aurora, bg-dot-grid e text-brand-gradient.
Tipografia: Plus Jakarta Sans nos títulos e números (font-heading), Inter no texto de
interface. Valores monetários usam a classe .tabular para alinhar as colunas.
Arte oficial em public/brand: kabrun-icon.png (transparente, serve em qualquer
fundo), kabrun-logo.png (lockup completo) e kabrun-wordmark-light.png (versão knockout usada na
sidebar escura). O favicon sai de src/app/icon.png.
Gráficos seguem uma paleta validada para daltonismo (roxo #7C3AED como série principal, dourado
#B98012 como segunda série), com linha de 2px, grade recessiva, tooltip com crosshair e nenhum
número impresso sobre cada ponto.
Nunca usamos float. Todo valor é inteiro (bigint) em unidades menores + moeda:
money(1090, "BRL") // R$ 10,90
money(2999, "USD") // US$ 29,99
Helpers em src/lib/money: formatMoney, addMoney, subtractMoney,
calculatePercentage (em basis points), allocateProportionally (rateio sem perder centavos),
parseDecimalToMinorUnits, convertMinorUnits. Operar entre moedas diferentes lança
CurrencyMismatchError — conversão precisa ser explícita.
Comprador → Checkout Kabrun → PaymentProvider → PaymentIntent
↓
pagamento confirmado (webhook)
↓
Kabrun Ledger → Saldo pendente do produtor → Settlement → Saldo disponível
↓
Payout → conta bancária do produtor
A regra mais importante: o frontend nunca confirma pagamento. Um Payment só vira PAID
depois de um evento verificado do provedor. O retorno do navegador serve só para mostrar status.
A interface PaymentProvider isola o domínio do SDK:
OrderService → PaymentService → PaymentProvider → StripePaymentProvider
└ MockPaymentProvider (dev/testes)
PAYMENT_PROVIDER=stripe|mock decide qual usar. O mock é determinístico, não move dinheiro e
emite webhooks assinados — o fluxo de confirmação é idêntico ao de produção.
Stripe: StripePaymentProvider + StripeConnectService (contas conectadas, onboarding hospedado).
Cada produtor tem um MerchantAccount (entidade genérica, não "StripeAccount") cujo status interno
(PENDING, REQUIRES_ACTION, UNDER_REVIEW, ACTIVE, RESTRICTED, DISABLED) é derivado de
charges_enabled, payouts_enabled, details_submitted e dos requirements — nunca de um booleano só.
Partida dobrada desde o primeiro dia. Um LedgerJournal agrupa LedgerEntry e só é aceito se
débitos == créditos. Entries são imutáveis: o repositório não expõe update nem delete, e
correções são novos lançamentos.
Contas: PLATFORM_CASH, PRODUCER_PENDING, PRODUCER_AVAILABLE, PLATFORM_REVENUE,
PROCESSOR_FEES, REFUNDS, CHARGEBACKS, RESERVES, TAXES, PAYOUTS.
Venda de R$ 100,00 com R$ 4,00 de taxa de processamento e R$ 10,00 de taxa Kabrun:
| Conta | Direção | Valor |
|---|---|---|
PLATFORM_CASH | DEBIT | 10000 |
PROCESSOR_FEES | CREDIT | 400 |
PLATFORM_REVENUE | CREDIT | 1000 |
PRODUCER_PENDING | CREDIT | 8600 |
Toda movimentação passa pelo LedgerService, com idempotencyKey por evento financeiro
(payment:{id}:captured, payment:{id}:settled, refund:{id}, payout:{id}:paid…). Repetir a
mesma chave é no-op — é isso que impede um webhook duplicado de criar dinheiro.
Balance (pendente / disponível / reservado por organização e moeda) é projeção recalculada a
partir do ledger dentro da mesma transação do lançamento. Nunca fazemos balance += x, e o saldo
não é a soma das Orders (isso ignoraria taxas, reembolsos e chargebacks).
Depois da venda confirmada, o valor do produtor entra como pendente. A liberação segue a
SettlementPolicy (D+2, D+7, D+14, D+30…), resolvida por organização → país → padrão global.
Nada é hardcoded: as políticas são dados (veja prisma/seed.ts).
Ter saldo disponível na Stripe não libera nada: o settlement interno da Kabrun é independente.
npm run settlement:run # libera o que já venceu
npm run settlement:simulate # finge que a janela passou (desenvolvimento)
Produtor → valida MerchantAccount (ACTIVE + payoutsEnabled)
→ valida saldo disponível (lock da linha de Balance + leitura do ledger)
→ valida mínimo por moeda
→ cria Payout + lançamento (AVAILABLE → PAYOUTS)
→ provider (idempotency key payout:{id})
→ webhook → PAID (PAYOUTS → PLATFORM_CASH)
O que a plataforma não permite: saque acima do disponível, saque usando saldo pendente ou reservado, saque duplicado por retry, saque para conta não verificada, e marcar como pago sem confirmação do provedor. Falha ou cancelamento gera lançamento compensatório que devolve o valor ao saldo disponível.
REFUNDS). Nunca é
possível reembolsar mais do que foi pago, nem duas vezes com a mesma chave.RESERVES. Ganhou, volta para o produtor;
perdeu, sai do caixa da plataforma (CHARGEBACKS) e o pagamento vira CHARGEBACK.Endpoint (POST /api/webhooks/stripe, POST /api/webhooks/mock) faz apenas:
provider + providerEventId); 4. enfileira; 5. responde 2xx.O processamento pesado roda no worker, com backoff e retry. Há três camadas de proteção contra
duplicidade: unicidade do evento, claim atômico no processamento e idempotencyKey do journal.
Mudanças de estado importantes gravam um OutboxEvent na mesma transação (transactional
outbox), e o worker publica os eventos de domínio depois — sem o risco de "banco atualizou, evento
não saiu".
Pré-requisitos: Node 22+, Docker (Docker Desktop, OrbStack ou Colima).
cp .env.example .env
Gere os dois segredos locais (SESSION_SECRET e DATA_HASH_SECRET):
openssl rand -base64 48
Suba a infraestrutura (PostgreSQL, MongoDB, Redis, Keycloak + banco próprio, Mailpit):
npm run infra:up
Instale, prepare os bancos e rode:
yarn install && yarn db:deploy && yarn db:seed && yarn mongo:setup && yarn dev
O gerenciador é o Yarn, porque é o que a Vercel usa para construir a produção — o
yarn.locké o lockfile do projeto e o CI instala com--frozen-lockfile. Rodarnpm installaqui reescreve oyarn.lockem formato npm, o que muda silenciosamente a árvore que vai ao ar. Para executar um script isolado,npm run <script>continua funcionando; é só a instalação que precisa ser pelo Yarn.
Confira se tudo está no ar:
npm run check:infra
Aplicação: http://localhost:3100 · Keycloak: http://localhost:8080 (admin/admin) · Caixa de e-mail: http://localhost:8025
O Mailpit recebe o que o Keycloak envia — recuperação de senha e verificação de e-mail —, que é
um caminho separado do EMAIL_PROVIDER da aplicação. Os e-mails da própria Kabrun (compra
confirmada, saque pago) continuam indo para EMAIL_PREVIEW_DIR como arquivo.
Como verifyEmail está ligado, uma conta criada pelo cadastro só entra depois de clicar no link
que chega no Mailpit. Os links valem 12 horas (verificação) e 30 minutos (recuperação de senha) —
o padrão do Keycloak é 5 minutos para os dois, curto demais para quem só abre o e-mail mais tarde.
Os usuários de desenvolvimento do realm já nascem verificados.
Usuários de desenvolvimento do realm: [email protected] e [email protected] (este com o papel
platform_admin), senha kabrun-dev-123.
O worker de background (retry de webhooks, outbox e settlement) roda separado:
npm run worker
MongoDB local roda sem autenticação, preso a
127.0.0.1(omongoshdo container falha em alguns kernels virtualizados, o que quebra o bootstrap do usuário). Em staging/produção o Mongo precisa subir com autenticação.
Todas em .env.example, validadas com Zod no boot (src/lib/env.ts).
Destaques: DATABASE_URL, MONGODB_URI, REDIS_URL, KEYCLOAK_*, APP_URL, SESSION_SECRET,
DATA_HASH_SECRET, PAYMENT_PROVIDER, STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
STRIPE_WEBHOOK_SECRET, STRIPE_CONNECT_CLIENT_ID, STORAGE_PROVIDER, EMAIL_PROVIDER, QUEUE_DRIVER.
Segredos nunca são commitados. A chave secreta da Stripe é exclusivamente server-side, e o app se
recusa a subir com chave sk_live_ fora de produção.
Com APP_ENV=production o boot também é recusado se a configuração ainda aponta para os
substitutos de desenvolvimento, porque todos eles parecem saudáveis em runtime e perdem dados em
silêncio: PAYMENT_PROVIDER=mock, EMAIL_PROVIDER=console|preview, STORAGE_PROVIDER=local
(disco do container), APP_URL sem https, ou os segredos ainda iguais aos do .env.example.
npm run db:migrate # cria migration em desenvolvimento
npm run db:deploy # aplica migrations (CI/produção)
npm run db:seed # políticas de settlement e de taxa
npm run db:demo # dados de demonstração: catálogo + 30 dias de vendas (apenas dev)
npm run db:studio # Prisma Studio
npm run mongo:setup # cria collections e índices
Redis é usado hoje para rate limit do checkout; filas e locks distribuídos entram com o BullMQ.
Use sempre chaves de teste. Preencha STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY e
STRIPE_WEBHOOK_SECRET no .env, e mude PAYMENT_PROVIDER=stripe.
Encaminhe os webhooks com a Stripe CLI (não dependa de webhook configurado no dashboard para local):
stripe listen --forward-to localhost:3100/api/webhooks/stripe
O STRIPE_WEBHOOK_SECRET é o whsec_… que o comando imprime. Para eventos de contas conectadas,
acrescente --events account.updated,payout.paid,payout.failed ou use stripe listen --latest.
npm test # 68 testes
npm run verify # lint + typecheck + testes
Os testes de integração usam o banco hypei_test (criado pelo Docker Compose) e rodam contra
PostgreSQL de verdade — é assim que validamos transações, locks e restrições de unicidade.
Cobertura atual: money, cálculo de taxas, ledger (balanceamento, imutabilidade, idempotência), fluxo financeiro completo, projeção do balance, settlement, payouts (duplicidade, saldo insuficiente, concorrência, conta não verificada, falha e estorno), reembolsos (total, parcial, duplicado, acima do pago), disputas (reserva, ganha, perdida), idempotência de webhook (mesmo evento duas vezes ⇒ um único lançamento), assinatura inválida, eventos fora de ordem, isolamento de tenant, autorização, checkout e o provider Stripe (PaymentIntent, assinatura, fee real, mapeamentos).
src/
app/ rotas (App Router): landing, (app) dashboard, checkout público,
members, admin, onboarding, api/{auth,webhooks,health}
components/ ui/ (shadcn), layout/ (sidebar, topbar), shared/, brand/
modules/ um diretório por domínio — auth, organizations, products, offers,
checkout, customers, orders, payments, transactions, ledger, balances,
settlements, payouts, refunds, disputes, subscriptions, merchant-accounts,
members, analytics, webhooks, risk, compliance, taxes, fees, audit, integrations
(cada um com service, repository, schemas)
lib/ money/, errors/, env, logger/, clock, actions/, security/, idempotency/,
events/ (domain event, event bus, outbox), database/{postgres,mongo,redis},
providers/{payment,email,storage,queue}, stripe/, ui/
server/ repositories.ts, unit-of-work.ts, services.ts, container.ts, actions/
workers/ worker de background
prisma/ schema, migrations, seed
docker/ realm do Keycloak, init do Postgres
docs/architecture ADRs
tests/ unit/, integration/, support/
Funciona de ponta a ponta: login no Keycloak → onboarding → produto → oferta → checkout → compra no checkout público → webhook assinado → pagamento PAID → ledger → saldo pendente → settlement → saldo disponível → saque → payout pago, com o ledger fechando em cada etapa.
Também já implementados: assinaturas recorrentes (webhooks customer.subscription.* e invoice.*),
afiliados, cupons, editor da área de membros com upload, storage S3/R2/MinIO e as telas do /admin.
Imposto calcula e registra na venda, com o produtor como vendedor e a alíquota embutida no preço (ADR 008). O produtor configura o próprio regime em Configurações, e o tributo aparece no detalhe de cada venda — ao produtor, não ao comprador, por decisão registrada no ADR. Falta a emissão de NFS-e, que é onde a informação ao consumidor deve entrar, e os motores de UE (VAT/OSS) e EUA (sales tax).
Ainda não implementado (fases seguintes): AML — monitoramento e triagem; o KYC/KYB é delegado ao
Stripe Connect, que registra as pendências em Verificação —, API pública e os drivers de fila
BullMQ/Redis Streams (QUEUE_DRIVER aceita só memory).
Decisões arquiteturais estão registradas em docs/architecture.
A Fase 1 roda de ponta a ponta em desenvolvimento, mas estes pontos ainda separam isso de um ambiente de produção. Não são dívidas de código soltas: cada um tem consequência direta.
Impedem o funcionamento
npm run db:deploy. Na Vercel o
caminho usual é o comando de build, com um porém sério: preview deploys rodariam migrations
contra o banco que estiver em DATABASE_URL. Precisa ser resolvido com cuidado.QUEUE_DRIVER=memory tem implementação. Em serverless isso é pior do
que parece: trabalho agendado depois da resposta pode nunca rodar, porque a função é congelada.
Hoje o cron cobre o estrago — retryDue reclama eventos RECEIVED presos —, então nada se
perde, mas há latência de até um ciclo. A correção limpa é after() do next/server na rota
de webhook.Worker — resolvido na Vercel
O worker roda por cron, não por processo longo: vercel.json agenda
GET /api/cron/worker a cada minuto, e a rota chama a mesma runWorkerTick que o
npm run worker usa localmente. O agendamento por minuto exige plano Pro — no Hobby o cron
é diário, e settlement, outbox e retry de webhook ficariam com um dia de atraso.
A rota falha fechada: sem CRON_SECRET ela recusa tudo, e por isso o boot em produção também
é recusado sem esse segredo — senão o worker nunca rodaria e o saldo ficaria preso em pendente,
sem erro visível.
Identidade
EMAIL_PROVIDER da aplicação, e sem ela "esqueci minha senha" vira um beco sem saída: o
formulário aceita e o e-mail nunca chega. O npm run check:infra falha se o realm estiver sem
SMTP.verifyEmail está ligado, o que torna o SMTP acima obrigatório: realm de produção sem
remetente real não trava só a recuperação de senha, trava todo cadastro novo. O
npm run check:infra existe justamente para pegar isso antes.otpPolicyType ausente, nenhuma required action): o usuário não
consegue nem optar.start-dev e os redirectUris só aceitam localhost.
Produção exige realm próprio, kc start com hostname e TLS, e backup do banco de identidades.Infra e segurança
nosniff, Referrer-Policy e Permissions-Policy já
são enviados em todas as respostas (next.config.ts).images.remotePatterns libera https://** (next.config.ts), o que torna o
domínio um proxy de imagem aberto.Produto e regulatório
stripe-payment-provider.ts): a
conta do produtor precisa liquidar na moeda da venda. Aparece rápido com BR, UE e EUA juntos.