Skip to content

REF-901-02: Stripe Dual-Account

ADR: ADR-901 — IntegrationsEscopo: StripeAccountResolver, webhook routing, 5 eventos tratados, HMAC-SHA256, customer IDs por entidade, product channels block


1. StripeAccountResolver

Resolve qual conta Stripe usar com base no CompanyContext:

Evento de pagamento / criação de customer

    ├── CompanyContext = middag_br → Stripe BR (BRL)
    └── CompanyContext = middag_global → Stripe US (USD)

CompanyContext determinado por:
    1. Atribuição explícita no Quote/Order (override admin)
    2. Product channels block
    3. Organization.billing_entity
    4. Auto-detect por tax_id / preferred_currency (fallback)

2. Webhook Routing

Duas URLs de webhook, uma por conta:

URLContaWebhook Secret
/wp-json/middag-account/v1/webhooks/stripe/brStripe BRSecret BR
/wp-json/middag-account/v1/webhooks/stripe/llcStripe LLCSecret LLC

Cada endpoint usa seu próprio webhook secret para validação HMAC.


3. 5 Eventos Tratados

Evento StripeAção no Plugin
invoice.paidInvoice sincronizado. Quote → paid (se vinculado).
invoice.payment_failedPaymentRecoveryPolicy consultada. Entitlement → suspended (se aplicável).
subscription.updatedEntitlement atualizado (tier, período).
subscription.deletedEntitlement → cancelled.
charge.refundedInvoice marcado como refunded. Notificação ao admin.

4. Validação HMAC-SHA256

AspectoDetalhe
AlgoritmoHMAC-SHA256 (Stripe-Signature header)
Tolerância300 segundos (5 minutos)
ImplementaçãoStripe\Webhook::constructEvent($payload, $sig, $secret, 300)
FalhaHTTP 401 — webhook rejeitado, log de erro
Secret separadoCada conta Stripe tem seu próprio webhook signing secret

Regra de segurança: Rejeitar SEMPRE sem signature válida. Sem exceções.


5. Customer IDs por Entidade

Organization armazena IDs separados:

CampoContaCriado Quando
stripe_customer_id_brStripe BRPrimeira compra roteada para BR
stripe_customer_id_globalStripe LLCPrimeira compra roteada para LLC

Lazy creation: Customer criado na conta correspondente apenas na primeira transação.

Organization pode ter AMBOS os IDs (raro: cliente que compra da BR e da LLC).


6. Product Channels Block

Entity routing is determined by the channels block in each product YAML. The sync.stripe.entity field was removed in favor of channels.

yaml
channels:
  stripe_br:
    enabled: true      # Sincroniza com Stripe BR (BRL)
  stripe_global:
    enabled: true      # Sincroniza com Stripe US (USD)
Channel ConfigSignificado
channels.stripe_br.enabledSincroniza apenas com Stripe BR (BRL)
channels.stripe_global.enabledSincroniza apenas com Stripe US (USD)

7. Idempotência

RegraImplementação
Webhook duplicadoVerificar stripe_event_id — skip se já processado
Invoice duplicadoVerificar stripe_invoice_id na entidade Invoice
Subscription update duplicadoComparar updated_at — skip se mais antigo
LogTodo evento processado registrado com event_id

8. Resolução 3-Tier — Lógica Detalhada

O StripeAccountResolver aplica três estratégias em cascata. Se a primeira resolve, as seguintes não executam.

8.1 Tier 1: Atribuição Explícita (Quote/Order override)

Administrador ou sistema atribui billing_entity diretamente no Quote ou Order. Header HTTP X-Middag-Company também é aceito como override explícito em requests da API.

Valores válidos: middag_br, middag_global. Valor inválido retorna HTTP 400.

8.2 Tier 2: Product Channels Block

Quando tier 1 não resolve, o sistema consulta o channels block do produto:

Categoria de ProdutoChannel HabilitadoConta Resolvida
Consultoria/Desenvolvimentostripe_brStripe BR (BRL)
Suporte Técnico Dedicadostripe_brStripe BR (BRL)
Plugins WordPressstripe_globalStripe LLC (USD)
Plataforma SaaSstripe_globalStripe LLC (USD)

Produto sem channels block com pelo menos um Stripe channel habilitado gera erro.

8.3 Tier 3: Organization Billing Entity

Quando nem override nem produto determinam a conta, o resolver verifica os Stripe Customer IDs da Organization:

  • Organization com apenas stripe_customer_id_br preenchido: resolve para middag_br
  • Organization com apenas stripe_customer_id_global preenchido: resolve para middag_global
  • Organization com ambos preenchidos: erro — requer atribuição explícita (tier 1)
  • Organization sem nenhum: erro — sem conta Stripe configurada

8.4 Tier 4 (Fallback): Auto-detect

Se nenhum tier anterior resolve, fallback por tax_id (CNPJ = BR) ou preferred_currency (BRL = BR, USD = LLC). Usado apenas em cenários de criação de customer novo.


9. StripeClient Multi-Account

O Middag\Account\Integration\Stripe\StripeClient mantém um cache de instâncias \Stripe\StripeClient por company ID:

StripeClient
├── forCompany('middag_br')     → \Stripe\StripeClient (cached, sk_live BR)
├── forCompany('middag_global') → \Stripe\StripeClient (cached, sk_live LLC)
├── getPublishableKey('middag_br') → pk_live BR
└── getConfiguredCompanies()    → ['middag_br', 'middag_global']

Credenciais carregadas do DI Container:

Parâmetro ContainerEnv Var
stripe.middag_br.secret_keySTRIPE_BR_SECRET_KEY
stripe.middag_br.publishable_keySTRIPE_BR_PUBLISHABLE_KEY
stripe.middag_br.webhook_secretSTRIPE_BR_WEBHOOK_SECRET
stripe.middag_global.secret_keySTRIPE_LLC_SECRET_KEY
stripe.middag_global.publishable_keySTRIPE_LLC_PUBLISHABLE_KEY
stripe.middag_global.webhook_secretSTRIPE_LLC_WEBHOOK_SECRET

10. Eventos Completos de Webhook

Além dos 5 eventos primários (seção 3), o Stripe pode enviar eventos adicionais que o plugin deve tratar:

Evento StripeHandlerAção
checkout.session.completedOrderServiceCria Order a partir de checkout Stripe
payment_intent.succeededOrderServiceMarca Order como pago
payment_intent.payment_failedOrderServiceRegistra falha de pagamento
customer.subscription.createdSubscriptionHooksCria/atualiza subscription local
charge.dispute.createdAlertServiceNotifica admin, registra disputa

Todos os handlers recebem o companyId (derivado do endpoint path /br ou /llc) e persistem transações com tag billing_entity correspondente.


11. Tagging de Transações

Todo registro financeiro carrega o identificador da empresa:

EntidadeCampoExemplo
Orderbilling_entitymiddag_br
Invoicebilling_entitymiddag_global
Licensebilling_entitymiddag_global
Contractbilling_entitymiddag_br

API v1 suporta filtro ?billing_entity=middag_br em endpoints de listagem (orders, invoices, contracts).


12. Escalabilidade

Para adicionar nova entidade (ex: middag_eu):

  1. Adicionar env vars STRIPE_EU_SECRET_KEY, STRIPE_EU_PUBLISHABLE_KEY, STRIPE_EU_WEBHOOK_SECRET
  2. Registrar parâmetros no DI Container
  3. Adicionar middag_eu na whitelist do StripeAccountResolver
  4. Registrar rota /wp-json/middag-account/v1/webhooks/stripe/eu
  5. Adicionar campo stripe_customer_id_eu na Organization
  6. Configurar webhook no Stripe Dashboard apontando para novo endpoint

Componentes genéricos (StripeClient::forCompany(), webhook handlers, API filters) funcionam sem alteração.


13. Bloqueador #4

Este era bloqueador #4 do scope v5.0: "Arquitetura dual-account especificada mas resolver não construído." Este REF documenta a arquitetura. Implementação necessária antes do v5.0-beta.