REF-301-03: Quote-to-Entitlement Pipeline
ADR: ADR-301 — Commerce: Quote, Order, Invoice, TaxInvoiceEscopo: 6 etapas do pipeline, automação vs humano em cada etapa, domain events disparados, timestamps para analytics
1. Pipeline Completo (6 etapas)
Etapa 1 Etapa 2 Etapa 3 Etapa 4 Etapa 5 Etapa 6
───────── ───────── ───────── ───────── ───────── ─────────
Deal Quote Aceite WC Order Pagamento Entitlement
HubSpot Criado Portal Criada Confirmado Provisionado
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
HubSpot → Admin revisa Cliente vê Automático Stripe/Inter Auto-provisioning
Plugin sync e envia e aceita (quote→order) webhook por classe2. Detalhamento por Etapa
Etapa 1: Deal HubSpot → Quote
| Aspecto | Detalhe |
|---|---|
| Trigger | Deal criado/atualizado no HubSpot |
| Automação | HubSpot webhook sincroniza deal com middag-account |
| Humano | Discussão inicial de escopo (high-touch para Services) |
| Alternativa | Admin cria quote manualmente na admin UI (sem HubSpot) |
| Resultado | Quote criado com status draft, line items do catálogo |
| Event | middag_quote_created |
| Timestamp | quote_created_at |
Etapa 2: Quote Enviado ao Cliente
| Aspecto | Detalhe |
|---|---|
| Trigger | Admin revisa quote e envia (ou auto-envio configurável) |
| Automação | Email com link do portal enviado ao cliente |
| Humano | Revisão do quote antes de enviar (opcional), precificação custom |
| Resultado | Quote muda para status sent |
| Event | middag_quote_sent |
| Timestamp | quote_sent_at |
Etapa 3: Cliente Aceita no Portal
| Aspecto | Detalhe |
|---|---|
| Trigger | Cliente visualiza quote e clica "Aceitar" |
| Automação | Status viewed registrado ao abrir. Aceite digital registrado. |
| Humano | Nenhum — self-service do cliente |
| Resultado | Quote muda para status accepted |
| Event | middag_quote_accepted |
| Timestamps | quote_viewed_at, quote_accepted_at |
Etapa 4: WC Order Criada
| Aspecto | Detalhe |
|---|---|
| Trigger | Quote aceito dispara criação de order |
| Automação | 100% automático — OrderCreationService cria WC Order |
| Humano | Nenhum |
| Resultado | WC Order criada com line items do quote, roteada para entidade correta |
| Event | woocommerce_new_order (WC nativo) |
| Timestamp | order_created_at |
Etapa 5: Pagamento Confirmado
| Aspecto | Detalhe |
|---|---|
| Trigger | Cliente paga via Stripe (cartão/ACH) ou Banco Inter (Pix/Boleto) |
| Automação | Webhook woocommerce_payment_complete atualiza quote para paid |
| Humano | Nenhum |
| Resultado | Quote → paid, Order → completed |
| Event | middag_quote_paid |
| Timestamp | quote_paid_at, payment_confirmed_at |
Etapa 6: Entitlement Provisionado
| Aspecto | Detalhe |
|---|---|
| Trigger | woocommerce_order_status_completed |
| Automação | EntitlementProvisioningService analisa line items, cria Entitlement(s), provisiona downstream |
| Humano | Nenhum |
| Resultado | Entitlement(s) criado(s) com status active. Quote → fulfilled. |
| Event | middag_entitlement_provisioned |
| Timestamp | entitlement_provisioned_at, quote_fulfilled_at |
3. Resumo Automação vs Humano
| Etapa | Automático | Humano |
|---|---|---|
| 1. Deal → Quote | Sync | Escopo inicial (Services) |
| 2. Quote enviado | Revisão (opcional), pricing custom | |
| 3. Cliente aceita | 100% | — |
| 4. Order criada | 100% | — |
| 5. Pagamento | 100% | — |
| 6. Entitlement | 100% | — |
Etapas 3-6 são zero-touch. Humano envolvido apenas em escopo (Services, enterprise) e revisão de quote (opcional).
4. Domain Events
| Event | Etapa | Consumers Principais |
|---|---|---|
middag_quote_created | 1 | Sync HubSpot |
middag_quote_sent | 2 | Email ao cliente |
middag_quote_accepted | 3 | OrderCreationService, sync HubSpot |
middag_quote_paid | 5 | EntitlementProvisioningService, sync HubSpot |
middag_entitlement_provisioned | 6 | Notificação ao cliente, sync HubSpot |
middag_entitlement_created | 6 | Integrações downstream |
5. Timestamps para Analytics
| Timestamp | Calculado de | Métrica |
|---|---|---|
quote_created_at | Criação do quote | — |
quote_sent_at | Envio ao cliente | Tempo de preparação |
quote_viewed_at | Cliente abre no portal | Tempo de engajamento |
quote_accepted_at | Cliente aceita | Tempo até aceite (sent → accepted) |
order_created_at | Order WC criada | Latência de automação |
payment_confirmed_at | Webhook de pagamento | Tempo até pagamento |
entitlement_provisioned_at | Entitlement criado | Latência de provisioning |
quote_fulfilled_at | Pipeline completo | Tempo total do pipeline |
Métricas derivadas:
- Tempo de conversão:
quote_sent_at→quote_accepted_at - Tempo até pagamento:
quote_accepted_at→payment_confirmed_at - Latência do pipeline:
quote_created_at→entitlement_provisioned_at - Quotes pendentes > 7 dias:
now()-quote_sent_at> 7 dias com statussentouviewed
6. Estado refunded (9o estado)
Além dos 8 estados cobertos no pipeline principal, o quote suporta um 9o estado excepcional:
paid ──────► refunded
(estorno solicitado e processado)| Aspecto | Detalhe |
|---|---|
| Trigger | Admin processa estorno no WC Order |
| Automação | WC Order refunded → Quote status atualizado automaticamente |
| Humano | Admin inicia o estorno |
| Resultado | Quote → refunded, WC Order → refunded |
| Event | middag_quote_refunded |
| Timestamp | quote_refunded_at |
Estado terminal. Sincroniza com HubSpot. Email enviado ao cliente.
7. HubSpot Webhook Handlers
O plugin recebe webhooks do HubSpot no endpoint:
POST /wp-json/middag-account/v1/webhooks/hubspot7.1 Eventos processados
| Evento HubSpot | Ação no plugin |
|---|---|
quote.created | QuoteService::createFromHubSpot() — busca dados completos via HubSpot API, cria CPT, envia email |
quote.updated | QuoteService::updateFromHubSpot() — atualiza campos alterados; se draft → sent se publicado |
quote.deleted | QuoteService::deleteFromHubSpot() — soft delete (status cancelled, não exibido no portal) |
deal.propertyChange | Atualiza quotes associados ao deal quando dealstage ou amount muda |
7.2 Segurança de webhook
| Medida | Detalhe |
|---|---|
| Assinatura HMAC SHA-256 | Validação via X-HubSpot-Signature-v3 e client secret da app |
| Timestamp validation | Rejeita requests com timestamp > 5 minutos (anti-replay) |
| Idempotência | event_id armazenado em cache; eventos já processados são ignorados |
| Rate limiting | Máximo 100 webhooks/minuto por conta HubSpot |
7.3 Fluxo detalhado: quote.created
1. Webhook recebido → valida assinatura e timestamp
2. Identifica conta HubSpot (middag_br ou middag_global) pelo subscription ID
3. Verifica idempotência (event_id já processado?)
4. QuoteService::createFromHubSpot(objectId, account)
4.1. HubSpotService::getQuote() — GET /crm/v3/objects/quotes/{id}?associations=deals,line_items,companies
4.2. Resolve Organization por hubspot_company_id_br ou _global
4.3. Gera quote_number sequencial (formato: QT-{ANO}-{SEQUENCIAL})
4.4. Mapeia campos HubSpot → meta: hs_title→title, hs_total_amount→total, hs_terms→terms, line items→JSON
4.5. Persiste via QuoteRepository::create()
5. NotificationService envia email quote-sent
6. Retorna HTTP 200 ao HubSpot7.4 Tratamento de erros
| Erro | Comportamento | Retry |
|---|---|---|
| Assinatura inválida | HTTP 401, log security warning | Não |
| Timestamp expirado | HTTP 401, log warning | Não |
| Event ID duplicado | HTTP 200 (idempotente, sem processamento) | N/A |
| Organization não encontrada | Cria quote sem org (reconciliação manual), log warning | N/A |
| Erro na API HubSpot | HTTP 500, log error | HubSpot retry automático |
8. Sincronização Bidirecional (WP → HubSpot)
A sincronização WordPress → HubSpot ocorre sempre que o status de um quote muda por ação no portal.
8.1 Eventos sincronizados
| Evento no plugin | Ação no HubSpot |
|---|---|
Quote visualizado (sent → viewed) | Atualiza middag_status = viewed no quote. Timeline event no deal. |
Quote aceito (viewed → accepted) | Atualiza middag_status = accepted. Deal stage → "Contract Sent". |
Pagamento confirmado (accepted → paid) | Atualiza middag_status = paid. Deal stage → "Closed Won". Atualiza deal amount. |
| Quote rejeitado | Atualiza middag_status = rejected. Nota no deal com motivo. Deal → "Closed Lost". |
| Quote expirado | Atualiza middag_status = expired. Timeline event. Não altera deal stage. |
8.2 Propriedades customizadas no HubSpot
| Propriedade | Objeto | Tipo | Valores |
|---|---|---|---|
middag_status | Quote | Enumeration | draft, sent, viewed, accepted, paid, fulfilled, rejected, expired, refunded |
middag_quote_number | Quote | String | Número sequencial gerado pelo plugin |
middag_accepted_at | Quote | DateTime | Data de aceite |
middag_paid_at | Quote | DateTime | Data de pagamento |
middag_payment_method | Quote | Enumeration | pix, boleto_inter, boleto_asaas, stripe_card_br, stripe_card_global |
middag_wp_order_id | Quote | Number | WC Order ID |
8.3 Tolerância a falhas
Sincronização com HubSpot é assíncrona e tolerante a falhas. Uma falha na sincronização nunca bloqueia a operação principal:
1. Operação principal executada (ex: aceitar quote)
2. Sync disparada em background (Action Scheduler)
3. Se falhar: retry com backoff exponencial (1min, 5min, 15min, 1h, 6h)
4. Após 5 tentativas: marca como "sync_failed", notifica admin
5. Dashboard admin mostra quotes com sync pendente/falhado9. WC Order Creation (Etapa 4 detalhada)
Quando o cliente aceita um quote, OrderCreationService::createFromQuote() executa:
9.1 Dados do WC Order
| Campo WC Order | Origem |
|---|---|
billing_email | org_email da Organization |
billing_first_name/last_name | Dados do collaborator que aceitou |
billing_company | org_name |
billing_address_* | Endereço da Organization |
currency | quote_currency |
status | pending (aguardando pagamento) |
9.2 Line items
Cada item JSON do quote é convertido em WC_Order_Item_Product ou WC_Order_Item_Fee. Se o sku corresponde a um produto WC existente, o produto é vinculado. Caso contrário, cria como item avulso (fee).
9.3 Metadados associados
| Meta key | Valor |
|---|---|
_quote_id | ID do quote (post ID) |
_quote_number | Número legível do quote |
_quote_hubspot_id | ID do quote no HubSpot |
_middag_organization | ID da Organization |
_hubspot_account | middag_br ou middag_global |
10. Gateways de Pagamento por Contexto
| Conta HubSpot | Moeda | Gateway | Método | Identificador |
|---|---|---|---|---|
middag_br | BRL | Banco Inter | Pix | pix_inter |
middag_br | BRL | Banco Inter | Boleto | boleto_inter |
middag_br | BRL | Stripe BR | Cartão de crédito | stripe_card_br |
middag_br | BRL | Asaas (fallback) | Boleto | boleto_asaas |
middag_global | USD | Stripe GLOBAL | Cartão de crédito | stripe_card_global |
middag_global | USD | Stripe GLOBAL | ACH / Wire (futuro) | stripe_ach_global |
10.1 Fluxo Pix (Banco Inter)
Cliente seleciona Pix → API Banco Inter gera QR code (validade: 30 min) → Cliente paga via app bancário → Webhook de confirmação → WC Order → processing → Quote → paid → Sync HubSpot.
10.2 Fluxo Boleto (Banco Inter)
Cliente seleciona Boleto → API Banco Inter gera boleto (vencimento: 3 dias úteis) → Cliente paga no banco → Webhook de confirmação (D+1) → WC Order → processing → Quote → paid → Sync HubSpot.
10.3 Fluxo Cartão (Stripe)
Cliente seleciona Cartão → Stripe Checkout ou Stripe Elements → Conta Stripe determinada pela entidade → Pagamento processado → Webhook → WC Order → processing → Quote → paid → Sync HubSpot. Parcelamento disponível apenas para BRL via Stripe BR.
10.4 Reconciliação e fallback
- Pagamento não confirmado em 7 dias (boleto): lembrete por email.
- Pagamento não confirmado em 15 dias (boleto): WC Order cancelado, quote retorna para
viewed(novo aceite possível). - Pix expirado: cliente pode gerar novo QR code pelo portal enquanto o quote não expirou.
11. Email Templates
6 templates de email disparados durante o lifecycle do quote:
| Trigger | Template ID | Assunto | Destinatário |
|---|---|---|---|
| Quote enviado ao cliente | quote-sent | "Novo orçamento #{number} disponível" | org_email |
| Quote aceito pelo cliente | quote-accepted | "Orçamento #{number} aceito" | Equipe de vendas + org_email |
| Quote rejeitado pelo cliente | quote-rejected | "Orçamento #{number} recusado" | Equipe de vendas |
| Quote próximo de expirar | quote-expiring | "Seu orçamento #{number} expira em {days} dias" | org_email |
| Quote expirado | quote-expired | "Orçamento #{number} expirou" | org_email |
| Pagamento confirmado | quote-paid | "Pagamento confirmado — Orçamento #{number}" | org_email + Equipe de vendas |
Lembrete pré-expiração: Cron diário envia quote-expiring quando valid_until - hoje <= 3 dias e status IN (sent, viewed). Máximo 1 lembrete por quote.
12. Cron Job de Expiração
Frequência: diária (01:00 UTC)
Hook: middag_check_expired_quotes
Lógica:
1. SELECT quotes WHERE status IN ('sent', 'viewed') AND valid_until < CURDATE()
2. Para cada quote:
a. Atualizar status para 'expired'
b. Enviar email quote-expired para cliente
c. Sincronizar status com HubSpot
3. Registrar log com quantidade de quotes expirados13. Transições Bloqueadas
| De | Para | Motivo |
|---|---|---|
draft | accepted | Quote precisa ser enviado antes de ser aceito |
expired | accepted | Expirado não pode ser aceito — vendedor deve criar novo quote |
rejected | accepted | Rejeitado não pode ser aceito — vendedor deve criar novo quote |
paid | accepted | Retrocesso de status não permitido |
fulfilled | qualquer | Estado terminal — nenhuma transição possível |
refunded | qualquer | Estado terminal — nenhuma transição possível |