Garu

2026-05-05

v0.8.0 — Recorrência B2B2C: códigos de falha, vencimento de cartão e portal por produto

scheduled-chargesrecorrentescartaowebhooksprodutosb2b2capisdkmcp

A v0.8.0 fecha as pontas do ciclo de vida de cobranças recorrentes que ficaram em aberto na v0.7.0: agora cada falha vem com um código identificável, cartões prestes a vencer avisam o seller antes de quebrar a cobrança, e plataformas B2B2C (SaaS que vende para sellers que vendem para clientes finais) podem customizar a página de pagamento por produto, não só por seller.

O que mudou

Códigos de falha normalizados

Toda transação ou ciclo recorrente que falha agora grava um GaruFailureCode estável, independente do gateway por baixo:

  • insufficient_funds, card_declined, card_expired, card_canceled
  • processing_error, issuer_unavailable, fraud_suspected, invalid_cvv
  • do_not_honor_repeated, unknown

O código é exposto em três campos: failureCode (enum normalizado), failureReason (mensagem em português) e gatewayFailureCode (código bruto do Celcoin, p.ex. ABECS 51/54/91). Você roteia em cima do enum, e ainda tem o código original para auditoria. Disponível no payload de transaction.payment.failed e em scheduled_charge.cycle_failed.

Eventos de transação separados

Antes, qualquer transação que não terminasse em paid virava transaction.failed. Agora a Garu separa por intenção:

  • transaction.payment.failed — tentativa de cobrança recusada (com failureCode).
  • transaction.canceled — cancelamento explícito (pelo seller, por contrato, ou pela operadora).
  • transaction.chargeback — chargeback registrado pelo emissor.

Quem precisa rotear cancelamento, falha técnica e chargeback para fluxos diferentes não precisa mais inferir pelo motivo.

Vencimento de cartão automático

Cron diário às 09:00 BRT acompanha o ciclo de vida do cartão tokenizado:

  • D-30 / D-14 / D-7: webhook payment_method.expiring_soon por estágio (sem disparar duas vezes o mesmo estágio).
  • Dia do vencimento: o cartão é flippado de activeexpired atomicamente, e sai payment_method.expired.
  • No próximo ciclo recorrente: se o cartão já está expired, o ciclo é marcado failed com failureCode = 'card_expired' antes de tentar — não desperdiça uma chamada ao gateway.

Isso resolve o cenário de SaaS com base recorrente grande: agora dá pra avisar o cliente antes da cobrança quebrar.

Limpar cartão sem deixar retry pendente

DELETE /api/scheduled-charges/:id/payment-method agora cancela qualquer retry em voo do ciclo atual: o cron de retry verifica que a série não tem mais cartão e zera o nextRetryAt em vez de tentar de novo. Race entre "trocar cartão" e "tentar de novo" fechada.

Portal customizado por produto (B2B2C)

4 endpoints novos sob /api/products/:id/portal-config:

  • GET — retorna a config atual (ou null se o produto cai no fallback do seller).
  • POST / PATCH — upsert com merge: só os campos enviados são gravados; os omitidos preservam o valor anterior. Mande null num campo para herdar do seller.
  • DELETE — limpa a config inteira; o produto volta a usar a config do seller.

Campos customizáveis: businessName, logoUrl, primaryColor, e a suíte completa de políticas do portal (allowCancelSubscription, requireCancelReason, cancelAtPeriodEndOnly, mensagens de boas-vindas / sucesso / cancelamento, e flags de e-mail).

Use case principal: plataformas que modelam professores / coaches / prestadores como Products sob um único Seller — a página de pagamento e o portal /minha-area podem ter logo, cor e nome de cada profissional, sem fragmentar a contabilidade do seller.

Gatilho manual de webhook (infraestrutura)

A v0.8.0 adiciona o serviço dispatchManualTestEvent com whitelist de eventos, payload de exemplo por tipo e merge raso de overrides. O endpoint HTTP que expõe esse serviço chega na v0.8.1 (POST /api/webhook-endpoints/:id/trigger) — o serviço por si só não é chamado por nada em v0.8.0.

SDK + MCP

@garuhq/node@0.7.0 traz os novos tipos e o recurso de portal por produto:

const cfg = await garu.products.portalConfig.set(57, {
  businessName: 'Coach Maria — Corrida & Trilha',
  primaryColor: '#257264',
  logoUrl: 'https://cdn.exemplo.com/coaches/maria.png',
});

Tipos exportados: GaruFailureCode, FailurePayload, PaymentMethodExpiringPayload, PaymentMethodExpiredPayload, ProductPortalConfig, SetProductPortalConfigParams.

MCP server (@garuhq/mcp@0.7.0) ganhou 3 ferramentas novas: get_product_portal_config, set_product_portal_config, clear_product_portal_config. Agentes podem customizar o portal por produto direto pela ferramenta.

Migração

Aditivo. Nada quebra. O schema novo (colunas de failure_code/failure_reason/gateway_failure_code em transaction e scheduled_charge_cycle, tabela scheduled_charge_cycle_attempt, timestamps de expiring_*_notified_at / expired_at em payment_method, índice parcial em scheduled_charge.external_reference) é todo retrocompatível. Webhooks antigos (transaction.failed) continuam disparando para quem ainda assinou o legacy — adicione os novos eventos quando estiver pronto.

Próximos passos

  • Per-attempt log endpoint (GET /api/scheduled-charges/:id/attempts) — granularidade de auditoria por tentativa.
  • SDK Flutter — paridade completa com @garuhq/node para apps mobile.
  • Cookbook de integração B2B2C com receitas de Atletia + similares.