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:
| URL | Conta | Webhook Secret |
|---|---|---|
/wp-json/middag-account/v1/webhooks/stripe/br | Stripe BR | Secret BR |
/wp-json/middag-account/v1/webhooks/stripe/llc | Stripe LLC | Secret LLC |
Cada endpoint usa seu próprio webhook secret para validação HMAC.
3. 5 Eventos Tratados
| Evento Stripe | Ação no Plugin |
|---|---|
invoice.paid | Invoice sincronizado. Quote → paid (se vinculado). |
invoice.payment_failed | PaymentRecoveryPolicy consultada. Entitlement → suspended (se aplicável). |
subscription.updated | Entitlement atualizado (tier, período). |
subscription.deleted | Entitlement → cancelled. |
charge.refunded | Invoice marcado como refunded. Notificação ao admin. |
4. Validação HMAC-SHA256
| Aspecto | Detalhe |
|---|---|
| Algoritmo | HMAC-SHA256 (Stripe-Signature header) |
| Tolerância | 300 segundos (5 minutos) |
| Implementação | Stripe\Webhook::constructEvent($payload, $sig, $secret, 300) |
| Falha | HTTP 401 — webhook rejeitado, log de erro |
| Secret separado | Cada 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:
| Campo | Conta | Criado Quando |
|---|---|---|
stripe_customer_id_br | Stripe BR | Primeira compra roteada para BR |
stripe_customer_id_global | Stripe LLC | Primeira 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.
channels:
stripe_br:
enabled: true # Sincroniza com Stripe BR (BRL)
stripe_global:
enabled: true # Sincroniza com Stripe US (USD)| Channel Config | Significado |
|---|---|
channels.stripe_br.enabled | Sincroniza apenas com Stripe BR (BRL) |
channels.stripe_global.enabled | Sincroniza apenas com Stripe US (USD) |
7. Idempotência
| Regra | Implementação |
|---|---|
| Webhook duplicado | Verificar stripe_event_id — skip se já processado |
| Invoice duplicado | Verificar stripe_invoice_id na entidade Invoice |
| Subscription update duplicado | Comparar updated_at — skip se mais antigo |
| Log | Todo 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 Produto | Channel Habilitado | Conta Resolvida |
|---|---|---|
| Consultoria/Desenvolvimento | stripe_br | Stripe BR (BRL) |
| Suporte Técnico Dedicado | stripe_br | Stripe BR (BRL) |
| Plugins WordPress | stripe_global | Stripe LLC (USD) |
| Plataforma SaaS | stripe_global | Stripe 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_brpreenchido: resolve paramiddag_br - Organization com apenas
stripe_customer_id_globalpreenchido: resolve paramiddag_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 Container | Env Var |
|---|---|
stripe.middag_br.secret_key | STRIPE_BR_SECRET_KEY |
stripe.middag_br.publishable_key | STRIPE_BR_PUBLISHABLE_KEY |
stripe.middag_br.webhook_secret | STRIPE_BR_WEBHOOK_SECRET |
stripe.middag_global.secret_key | STRIPE_LLC_SECRET_KEY |
stripe.middag_global.publishable_key | STRIPE_LLC_PUBLISHABLE_KEY |
stripe.middag_global.webhook_secret | STRIPE_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 Stripe | Handler | Ação |
|---|---|---|
checkout.session.completed | OrderService | Cria Order a partir de checkout Stripe |
payment_intent.succeeded | OrderService | Marca Order como pago |
payment_intent.payment_failed | OrderService | Registra falha de pagamento |
customer.subscription.created | SubscriptionHooks | Cria/atualiza subscription local |
charge.dispute.created | AlertService | Notifica 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:
| Entidade | Campo | Exemplo |
|---|---|---|
| Order | billing_entity | middag_br |
| Invoice | billing_entity | middag_global |
| License | billing_entity | middag_global |
| Contract | billing_entity | middag_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):
- Adicionar env vars
STRIPE_EU_SECRET_KEY,STRIPE_EU_PUBLISHABLE_KEY,STRIPE_EU_WEBHOOK_SECRET - Registrar parâmetros no DI Container
- Adicionar
middag_euna whitelist do StripeAccountResolver - Registrar rota
/wp-json/middag-account/v1/webhooks/stripe/eu - Adicionar campo
stripe_customer_id_euna Organization - 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.