API de Administração¶
Endpoints exclusivos para usuários com user_type='admin' para gerenciar usuários, subscriptions, webhook logs e estatísticas.
👥 Gestão de Usuários (STI)¶
A API utiliza Single Table Inheritance (STI) para usuários. Todos são armazenados na collection users, diferenciados por user_type:
| Endpoint | user_type | Descrição |
|---|---|---|
GET /api/v1/users |
admin |
Lista apenas administradores |
GET /api/v1/customers |
customer |
Lista apenas clientes |
GET /api/v1/users - Listar Admins¶
Buscar usuários administradores (apenas user_type='admin').
Headers:
Query Parameters:
| Parâmetro | Tipo | Descrição |
|---|---|---|
query |
str |
Query MongoDB em JSON |
current_page |
int |
Página atual (default: 0) |
qty_docs_page |
int |
Itens por página (default: 10) |
Response (200 OK):
{
"docs": [
{
"_id": "507f1f77bcf86cd799439011",
"username": "admin_john",
"email": "john@admin.com",
"user_type": "admin",
"name": "John Doe",
"avatar_id": null,
"enable": true,
"created_at": "2026-01-01T00:00:00Z"
}
],
"links": [
{"link_type": "GET", "rel": "self", "href": "..."},
{"link_type": "GET", "rel": "get document", "href": ".../{id}"},
{"link_type": "DELETE", "rel": "delete document", "href": ".../{id}"},
{"link_type": "POST", "rel": "insert document", "href": "..."},
{"link_type": "PUT", "rel": "update document", "href": ".../{id}"}
],
"msg": "ok",
"pagination": {
"current_page": 0,
"qty_docs_page": 10,
"qty_of_pages": 1,
"qty_total_docs": 1
}
}
POST /api/v1/users - Criar Admin¶
Criar novo usuário administrador.
Request:
[{
"username": "new_admin",
"email": "admin@example.com",
"password": "SecurePassword123!",
"name": "New Admin",
"user_type": "admin"
}]
PUT /api/v1/users - Atualizar Admin¶
DELETE /api/v1/users - Deletar Admin¶
🔑 Alterar Senha (Admin)¶
Use o endpoint dedicado POST /api/v1/auth/change-password para alterar a senha.
Este endpoint funciona para Admin e Customer. Não requer o hash da senha armazenado no banco — apenas a senha atual e a nova senha.
Endpoint antigo descontinuado
O padrão anterior de enviar PUT /api/v1/my-user com o objeto password contendo hash, password e new_password está descontinuado. Use POST /api/v1/auth/change-password conforme documentado abaixo.
Fluxo de Alteração de Senha¶
sequenceDiagram
participant Frontend
participant API
participant MongoDB
Frontend->>API: POST /auth/change-password
Note over Frontend: Authorization: Bearer <admin_token>
API->>MongoDB: Buscar admin pelo token
API->>API: Verificar senha atual (Argon2)
alt Senha atual correta
API->>API: Hash nova senha (Argon2)
API->>MongoDB: Atualizar password hash
API-->>Frontend: 200 OK
else Senha atual incorreta
API-->>Frontend: 400 Bad Request
end
Request¶
POST /api/v1/auth/change-password
Authorization: Bearer eyJhbGci...
Content-Type: application/json
{
"current_password": "senha_atual_123",
"new_password": "nova_senha_segura_456"
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
current_password |
string |
Sim | Senha atual em texto plano |
new_password |
string |
Sim | Nova senha (mínimo 8 caracteres) |
Response (200 OK)¶
Errors¶
400 - Senha Atual Incorreta¶
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Current password is incorrect",
"field": "password"
}
}
400 - Nova Senha Muito Curta¶
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Password must be at least 8 characters",
"field": "new_password"
}
}
Exemplo Flutter/Dart¶
import 'dart:convert';
import 'package:http/http.dart' as http;
class AdminPasswordService {
final String baseUrl;
final String token;
AdminPasswordService({required this.baseUrl, required this.token});
Future<void> changePassword({
required String currentPassword,
required String newPassword,
}) async {
final response = await http.post(
Uri.parse('$baseUrl/api/v1/auth/change-password'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode({
'current_password': currentPassword,
'new_password': newPassword,
}),
);
if (response.statusCode != 200) {
final error = jsonDecode(response.body);
throw Exception(
error['error']?['message'] ?? 'Falha ao alterar senha',
);
}
}
}
Para mais detalhes sobre autenticação e fluxo de reset de senha obrigatório (usuários migrados), consulte API de Autenticação.
✏️ PUT /api/v1/my-user — Alterar Dados Pessoais (Admin)¶
Atualizar dados pessoais do administrador autenticado.
Todos os campos são opcionais. Envie apenas os campos que deseja alterar.
Request¶
PUT /api/v1/my-user
Authorization: Bearer eyJhbGci...
Content-Type: application/json
[{
"name": "John Doe Updated",
"email": "john.new@admin.com"
}]
Campos Editáveis¶
| Campo | Tipo | Validação | Descrição |
|---|---|---|---|
username |
string |
3-50 chars, único | Nome de usuário |
email |
string |
Email válido, único | |
name |
string |
3-200 chars | Nome de exibição |
avatar_id |
ObjectId |
ID válido do GridFS | Avatar |
Response (200 OK)¶
{
"docs": [{
"_id": "507f1f77bcf86cd799439011",
"username": "admin_john",
"email": "john.new@admin.com",
"name": "John Doe Updated",
"updated_at": "2026-01-26T15:45:00Z"
}],
"info": {},
"links": [
{"link_type": "GET", "rel": "self", "href": "..."},
{"link_type": "GET", "rel": "get document", "href": ".../{id}"},
{"link_type": "DELETE", "rel": "delete document", "href": ".../{id}"},
{"link_type": "POST", "rel": "insert document", "href": "..."},
{"link_type": "PUT", "rel": "update document", "href": ".../{id}"}
],
"msg": "ok",
"pagination": {
"current_page": 0,
"qty_docs_page": 1,
"qty_of_pages": 1,
"qty_total_docs": 1
}
}
Errors¶
409 - Username Já Existe¶
409 - Email Já Existe¶
Exemplo Flutter/Dart¶
import 'dart:convert';
import 'package:http/http.dart' as http;
class AdminProfileService {
final String baseUrl;
final String token;
AdminProfileService({required this.baseUrl, required this.token});
Future<Map<String, dynamic>> updateProfile({
String? name,
String? email,
String? username,
String? avatarId,
}) async {
final payload = <String, dynamic>{};
if (name != null) payload['name'] = name;
if (email != null) payload['email'] = email;
if (username != null) payload['username'] = username;
if (avatarId != null) payload['avatar_id'] = avatarId;
final response = await http.put(
Uri.parse('$baseUrl/api/v1/my-user'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode([payload]),
);
if (response.statusCode != 200) {
final error = jsonDecode(response.body);
throw Exception(
error['error']?['message'] ?? 'Falha ao atualizar perfil',
);
}
final data = jsonDecode(response.body);
return data['docs'][0] as Map<String, dynamic>;
}
}
// Uso:
// final service = AdminProfileService(
// baseUrl: 'https://fdplay-api.infraifd.com',
// token: token,
// );
// final updatedAdmin = await service.updateProfile(
// name: 'John Doe Updated',
// email: 'john.new@admin.com',
// );
🔐 Autenticação Admin¶
Todos os endpoints nesta seção exigem:
- Token JWT válido -
Authorization: Bearer <token> - user_type='admin' - Verificado automaticamente
Resposta 403 para não-admins:
Gestao de Assinaturas — Rotas por Gateway¶
O sistema possui rotas admin separadas para cada gateway de pagamento. O frontend deve exibir menus distintos:
| Menu | Prefixo | Gateway |
|---|---|---|
| Stripe | /api/v1/admin/stripe/subscriptions |
Subscriptions com gateway='stripe' |
| Asaas | /api/v1/admin/asaas/subscriptions |
Subscriptions com gateway='asaas' |
Rotas Genericas vs Gateway-Specific
As rotas genericas em /api/v1/subscriptions continuam funcionando para listagem e edicao manual. As rotas gateway-specific em /admin/stripe/ e /admin/asaas/ sao recomendadas para operacoes que interagem com a API do gateway (cancelar, estornar, consultar pagamentos).
Admin Stripe¶
Prefixo: /api/v1/admin/stripe
| Metodo | Endpoint | Descricao |
|---|---|---|
| GET | /admin/stripe/subscriptions |
Listar subscriptions Stripe |
| GET | /admin/stripe/subscriptions/stats |
Estatisticas Stripe |
| POST | /admin/stripe/subscriptions/{id}/cancel |
Cancelar via Stripe API |
| GET | /admin/stripe/subscriptions/{id}/payments |
Historico de pagamentos (charges reais) |
| POST | /admin/stripe/subscriptions/{id}/refund |
Estornar pagamento Stripe |
| GET | /admin/stripe/subscriptions/{id}/refunds |
Historico de estornos |
GET /admin/stripe/subscriptions¶
Lista subscriptions com gateway='stripe'.
Mesmos query parameters (query, current_page, qty_docs_page, sort).
GET /admin/stripe/subscriptions/stats¶
Estatisticas agregadas de subscriptions Stripe.
Response (200 OK):
{
"gateway": "stripe",
"total_subscriptions": 270,
"active": 230,
"suspended": 15,
"cancelled": 20,
"pending": 5,
"expired": 0,
"monthly_recurring_revenue": 685700,
"average_subscription_value": 2981
}
POST /admin/stripe/subscriptions/{id}/cancel¶
Cancelar subscription via Stripe API (cancelamento imediato, diferente do cancelamento do customer que e no fim do periodo).
POST /api/v1/admin/stripe/subscriptions/507f1f77bcf86cd799439011/cancel
Authorization: Bearer <admin_token>
Processamento:
- Valida que
gateway == 'stripe'(rejeita outros gateways com 422) - Chama
StripeClient.cancel_subscription(stripe_subscription_id, at_period_end=False) - Atualiza
status='cancelled'no MongoDB - Limpa
current_subscription_iddo customer
Response (200 OK):
{
"message": "Stripe subscription cancelled by admin",
"subscription_id": "507f1f77bcf86cd799439011",
"customer_id": "507f1f77bcf86cd799439012",
"gateway": "stripe",
"stripe_subscription_id": "sub_1RChxWG6Qr6aGiCI",
"cancelled_at": "2026-03-01T15:00:00Z",
"gateway_response": {"status": "canceled"}
}
GET /admin/stripe/subscriptions/{id}/payments¶
Historico de pagamentos reais via Stripe API (charges do customer).
GET /api/v1/admin/stripe/subscriptions/507f1f77bcf86cd799439011/payments
Authorization: Bearer <admin_token>
Response (200 OK):
{
"subscription_id": "507f1f77bcf86cd799439011",
"gateway": "stripe",
"stripe_subscription_id": "sub_1RChxWG6Qr6aGiCI",
"stripe_customer_id": "cus_RU4xxxxxxx",
"payments": [
{
"id": "ch_3RChxWG6Qr6aGiCI",
"payment_intent": "pi_3RChxWG6Qr6aGiCI",
"status": "succeeded",
"amount": 2990,
"amount_refunded": 0,
"refunded": false,
"currency": "brl",
"created": 1709312400,
"description": "Subscription creation"
}
]
}
Dados reais
Os gateways retornam dados reais: Stripe via Charge.list(customer=cus_xxx), com fallback para webhook_logs.
POST /admin/stripe/subscriptions/{id}/refund¶
Estornar pagamento Stripe.
POST /api/v1/admin/stripe/subscriptions/507f1f77bcf86cd799439011/refund
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"reason": "Customer requested refund"
}
| Campo | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
payment_intent_id |
string |
Nao | Stripe PaymentIntent ID (pi_xxx). Se omitido, auto-detecta o charge mais recente nao estornado |
reason |
string |
Sim | Motivo do reembolso (5-500 chars) |
Auto-deteccao: Se payment_intent_id nao for fornecido, o sistema:
- Busca
stripe_customer_iddo customer - Lista os 10 charges mais recentes via
Charge.list() - Seleciona o primeiro charge com
status='succeeded'erefunded=false - Usa o
payment_intentdesse charge
Response (200 OK):
{
"message": "Stripe refund processed successfully",
"subscription_id": "507f1f77bcf86cd799439011",
"gateway": "stripe",
"payment_intent_id": "pi_3RChxWG6Qr6aGiCI",
"refund_amount": 2990,
"gateway_response": {"id": "re_xxx", "status": "succeeded"},
"reason": "Customer requested refund",
"refunded_at": "2026-03-01T15:00:00Z",
"admin": "admin_john"
}
GET /admin/stripe/subscriptions/{id}/refunds¶
Historico completo de estornos e chargebacks de uma subscription Stripe.
GET /api/v1/admin/stripe/subscriptions/507f1f77bcf86cd799439011/refunds
Authorization: Bearer <admin_token>
Response (200 OK):
{
"subscription_id": "507f1f77bcf86cd799439011",
"gateway": "stripe",
"total_refunded": 2990,
"has_chargeback": false,
"chargeback_count": 0,
"refunds": [
{
"event_id": "REFUND-507f1f77bcf86cd799439011-20260301150000",
"type": "ADMIN.REFUND.STRIPE_REFUND",
"payment_intent_id": "pi_3RChxWG6Qr6aGiCI",
"reason": "Customer requested refund",
"admin": "admin_john",
"processed_at": "2026-03-01T15:00:00Z"
}
],
"chargebacks": []
}
Admin Asaas¶
Prefixo: /api/v1/admin/asaas
| Metodo | Endpoint | Descricao |
|---|---|---|
| GET | /admin/asaas/subscriptions |
Listar subscriptions Asaas |
| GET | /admin/asaas/subscriptions/stats |
Estatisticas Asaas |
| POST | /admin/asaas/subscriptions/{id}/cancel |
Cancelar via Asaas API |
| GET | /admin/asaas/subscriptions/{id}/payments |
Historico de pagamentos (pagamentos reais) |
| POST | /admin/asaas/subscriptions/{id}/refund |
Estornar pagamento Asaas |
| GET | /admin/asaas/subscriptions/{id}/refunds |
Historico de estornos |
GET /admin/asaas/subscriptions¶
Lista subscriptions com gateway='asaas'.
Mesmos query parameters (query, current_page, qty_docs_page, sort).
GET /admin/asaas/subscriptions/stats¶
Estatisticas agregadas de subscriptions Asaas.
Response (200 OK):
{
"gateway": "asaas",
"total_subscriptions": 150,
"active": 120,
"suspended": 10,
"cancelled": 15,
"pending": 5,
"expired": 0,
"monthly_recurring_revenue": 358800,
"average_subscription_value": 2990
}
POST /admin/asaas/subscriptions/{id}/cancel¶
Cancelar subscription via Asaas API (cancelamento imediato).
POST /api/v1/admin/asaas/subscriptions/507f1f77bcf86cd799439011/cancel
Authorization: Bearer <admin_token>
Processamento:
- Valida que
gateway == 'asaas'(rejeita outros gateways com 422) - Chama
AsaasClient.cancel_subscription(asaas_subscription_id) - Atualiza
status='cancelled'no MongoDB - Limpa
current_subscription_iddo customer
Response (200 OK):
{
"message": "Asaas subscription cancelled by admin",
"subscription_id": "507f1f77bcf86cd799439011",
"customer_id": "507f1f77bcf86cd799439012",
"gateway": "asaas",
"asaas_subscription_id": "sub_abc123",
"cancelled_at": "2026-03-01T15:00:00Z",
"gateway_response": {"deleted": true}
}
GET /admin/asaas/subscriptions/{id}/payments¶
Historico de pagamentos reais via Asaas API (pagamentos da subscription).
GET /api/v1/admin/asaas/subscriptions/507f1f77bcf86cd799439011/payments
Authorization: Bearer <admin_token>
Response (200 OK):
{
"subscription_id": "507f1f77bcf86cd799439011",
"gateway": "asaas",
"asaas_subscription_id": "sub_abc123",
"asaas_customer_id": "cus_000012345678",
"payments": [
{
"id": "pay_abc123",
"status": "RECEIVED",
"billing_type": "CREDIT_CARD",
"value": 29.90,
"net_value": 27.41,
"due_date": "2026-03-01",
"payment_date": "2026-03-01",
"description": "Assinatura mensal",
"refunded": false,
"refunded_value": 0
}
]
}
POST /admin/asaas/subscriptions/{id}/refund¶
Estornar pagamento Asaas.
POST /api/v1/admin/asaas/subscriptions/507f1f77bcf86cd799439011/refund
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"reason": "Customer requested refund"
}
| Campo | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
payment_id |
string |
Nao | Asaas Payment ID (pay_xxx). Se omitido, auto-detecta o pagamento mais recente nao estornado |
value |
float |
Nao | Valor a estornar em reais (None = reembolso total) |
reason |
string |
Sim | Motivo do reembolso (5-500 chars) |
Auto-deteccao: Se payment_id nao for fornecido, o sistema:
- Busca
asaas_subscription_idda subscription - Lista os 10 pagamentos mais recentes via
AsaasClient.list_subscription_payments() - Seleciona o primeiro pagamento com
status='RECEIVED'ou'CONFIRMED'e semrefundedValue - Usa o
iddesse pagamento
Response (200 OK):
{
"message": "Asaas refund processed successfully",
"subscription_id": "507f1f77bcf86cd799439011",
"gateway": "asaas",
"payment_id": "pay_abc123",
"refund_amount_centavos": 2990,
"gateway_response": {"status": "REFUNDED"},
"reason": "Customer requested refund",
"refunded_at": "2026-03-01T15:00:00Z",
"admin": "admin_john"
}
GET /admin/asaas/subscriptions/{id}/refunds¶
Historico completo de estornos e chargebacks de uma subscription Asaas.
GET /api/v1/admin/asaas/subscriptions/507f1f77bcf86cd799439011/refunds
Authorization: Bearer <admin_token>
Response (200 OK):
{
"subscription_id": "507f1f77bcf86cd799439011",
"gateway": "asaas",
"total_refunded": 2990,
"has_chargeback": false,
"chargeback_count": 0,
"refunds": [
{
"event_id": "REFUND-507f1f77bcf86cd799439011-20260301150000",
"type": "ADMIN.REFUND.ASAAS_REFUND",
"payment_id": "pay_abc123",
"reason": "Customer requested refund",
"admin": "admin_john",
"processed_at": "2026-03-01T15:00:00Z"
}
],
"chargebacks": []
}
Rotas Genericas (Todos os Gateways)¶
As rotas abaixo continuam funcionando para todos os gateways (Stripe e Asaas):
| Metodo | Endpoint | Descricao |
|---|---|---|
| GET | /subscriptions |
Listar todas as subscriptions |
| PUT | /subscriptions |
Editar subscription manualmente |
| GET | /subscriptions/stats |
Estatisticas globais |
| GET | /subscriptions/{id}/payments |
Historico de pagamentos (generico) |
| POST | /subscriptions/{id}/cancel |
Cancelar (generico — despacha por gateway) |
| POST | /subscriptions/{id}/refund |
Estornar (generico — despacha por gateway) |
| GET | /subscriptions/{id}/refunds |
Historico de estornos |
| POST | /subscriptions/{id}/chargeback |
Gerenciar chargeback |
Recomendacao
Para cancelamento e estorno, prefira usar as rotas gateway-specific (/admin/stripe/... ou /admin/asaas/...). Elas validam o gateway antes de operar e evitam operacoes acidentais no gateway errado.
Chargeback / Disputa¶
POST /api/v1/subscriptions/{id}/chargeback — Gerenciar Chargeback¶
Suspender ou resolver manualmente um chargeback/disputa em uma subscription (funciona para ambos os gateways).
Headers:
Path Parameters:
| Parametro | Tipo | Descricao |
|---|---|---|
id |
OID |
ID da subscription (MongoDB) |
Request Body:
{
"action": "suspend",
"reason": "Chargeback recebido do emissor do cartao",
"dispute_id": "DIS_ABC123"
}
| Campo | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
action |
string |
Sim | "suspend" ou "resolve" |
reason |
string |
Sim | Motivo (5-500 caracteres) |
dispute_id |
string |
Nao | ID da disputa no gateway (referencia) |
Acoes:
| Acao | Efeito |
|---|---|
suspend |
Suspende subscription, marca has_chargeback=True, incrementa chargeback_count, bloqueia acesso do customer |
resolve |
Marca has_chargeback=False (NAO reativa automaticamente) |
Resolve nao reativa automaticamente
A acao resolve apenas limpa o flag has_chargeback. Para reativar a subscription, use PUT /api/v1/subscriptions para alterar o status manualmente.
Fluxo Completo: Chargeback¶
sequenceDiagram
participant Gateway as Stripe/Asaas
participant Webhook
participant DB
participant Admin
participant API
Gateway->>Webhook: PAYMENT.CHARGEBACK / charge.dispute.created
Webhook->>DB: Suspender subscription
Webhook->>DB: Bloquear customer
Note over DB: status=suspended<br/>has_chargeback=true<br/>current_subscription_id=null
Admin->>API: GET /admin/{gateway}/subscriptions/{id}/refunds
API-->>Admin: Historico de chargebacks
alt Disputa vencida pelo merchant
Admin->>API: POST /subscriptions/{id}/chargeback<br/>action=resolve
API->>DB: has_chargeback=false
Admin->>API: PUT /subscriptions<br/>status=active
API->>DB: Reativar subscription
else Disputa perdida
Admin->>API: POST /admin/{gateway}/subscriptions/{id}/cancel
API->>DB: Cancelar subscription
end
📝 Logs de Webhooks¶
GET /api/v1/webhook-logs - Listar Logs¶
Buscar logs de webhooks recebidos dos gateways.
Headers:
Query Parameters:
| Parâmetro | Tipo | Descrição |
|---|---|---|
event_type |
str |
Filtrar por tipo: SUBSCRIPTION.UPDATED, PAYMENT.PAID, etc. |
signature_valid |
bool |
Filtrar por validade de assinatura |
processed |
bool |
Filtrar por status de processamento |
start_date |
str (ISO) |
Data de início (received_at >= start_date) |
end_date |
str (ISO) |
Data de fim (received_at <= end_date) |
current_page |
int |
Página atual (default: 0) |
qty_docs_page |
int |
Itens por página (default: 50) |
Exemplos:
# Buscar todos os logs
GET /api/v1/webhook-logs
# Buscar logs de SUBSCRIPTION.UPDATED
GET /api/v1/webhook-logs?event_type=SUBSCRIPTION.UPDATED
# Buscar logs com assinatura inválida (possível ataque)
GET /api/v1/webhook-logs?signature_valid=false
# Buscar logs não processados (falhas)
GET /api/v1/webhook-logs?processed=false
# Buscar logs de hoje
GET /api/v1/webhook-logs?start_date=2026-01-08T00:00:00Z&end_date=2026-01-08T23:59:59Z
# Buscar logs de uma subscription específica
GET /api/v1/webhook-logs?query={"payload.data.id":"SUBS_XYZ789"}
Response (200 OK):
{
"logs": [
{
"_id": "507f1f77bcf86cd799439011",
"event_id": "EVT_ABC123DEF456",
"event_type": "SUBSCRIPTION.UPDATED",
"payload": {
"id": "EVT_ABC123DEF456",
"type": "SUBSCRIPTION.UPDATED",
"created_at": "2026-01-08T14:30:00Z",
"data": {
"id": "SUBS_XYZ789",
"status": "ACTIVE"
}
},
"signature": "sha256=abc123...",
"signature_valid": true,
"processed": true,
"result": {
"status": "processed",
"subscription_id": "507f1f77bcf86cd799439012",
"new_status": "active"
},
"received_at": "2026-01-08T14:30:00.123Z",
"processed_at": "2026-01-08T14:30:00.456Z"
}
],
"total": 1,
"current_page": 0,
"total_pages": 1
}
GET /api/v1/webhook-logs/stats - Estatísticas de Webhooks¶
Obter métricas de performance e confiabilidade dos webhooks.
Headers:
Query Parameters (Opcional):
| Parâmetro | Tipo | Descrição |
|---|---|---|
start_date |
str (ISO) |
Data de início para cálculo |
end_date |
str (ISO) |
Data de fim para cálculo |
Response (200 OK):
{
"total_received": 5420,
"total_processed": 5385,
"total_failed": 35,
"success_rate": 0.9935,
"invalid_signatures": 2,
"by_event_type": {
"SUBSCRIPTION.UPDATED": 3200,
"PAYMENT.PAID": 2100,
"SUBSCRIPTION.CANCELLED": 85,
"PAYMENT.FAILED": 35
},
"avg_processing_time_ms": 245,
"last_received_at": "2026-01-08T15:00:00Z"
}
Campos:
| Campo | Descrição |
|---|---|
total_received |
Total de webhooks recebidos |
total_processed |
Webhooks processados com sucesso |
total_failed |
Webhooks com falha de processamento |
success_rate |
Taxa de sucesso (0.9935 = 99.35%) |
invalid_signatures |
Webhooks com assinatura HMAC inválida |
by_event_type |
Contagem por tipo de evento |
avg_processing_time_ms |
Tempo médio de processamento (ms) |
last_received_at |
Timestamp do último webhook recebido |
Fluxo Frontend: Dashboard Admin¶
O frontend admin deve ter menus separados por gateway para gerenciar subscriptions:
Menu Admin
├── Asaas
│ ├── Subscriptions (GET /admin/asaas/subscriptions)
│ ├── Stats (GET /admin/asaas/subscriptions/stats)
│ └── Acoes: Cancel, Refund
└── Stripe
├── Subscriptions (GET /admin/stripe/subscriptions)
├── Stats (GET /admin/stripe/subscriptions/stats)
└── Acoes: Cancel, Refund, Payment History
Exemplo Flutter/Dart (gateway-aware):
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
/// Service generico para admin de subscriptions por gateway
class AdminSubscriptionService {
final String baseUrl;
final String token;
final String gateway; // 'asaas' ou 'stripe'
AdminSubscriptionService({
required this.baseUrl,
required this.token,
required this.gateway,
});
String get _prefix => '$baseUrl/api/v1/admin/$gateway';
Map<String, String> get _headers => {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
};
/// Listar subscriptions do gateway
Future<Map<String, dynamic>> listSubscriptions({int page = 0}) async {
final response = await http.get(
Uri.parse('$_prefix/subscriptions?current_page=$page'),
headers: _headers,
);
return jsonDecode(response.body);
}
/// Estatisticas do gateway
Future<Map<String, dynamic>> getStats() async {
final response = await http.get(
Uri.parse('$_prefix/subscriptions/stats'),
headers: _headers,
);
return jsonDecode(response.body);
}
/// Cancelar subscription
Future<Map<String, dynamic>> cancel(String subscriptionId) async {
final response = await http.post(
Uri.parse('$_prefix/subscriptions/$subscriptionId/cancel'),
headers: _headers,
);
return jsonDecode(response.body);
}
/// Historico de pagamentos
Future<Map<String, dynamic>> getPayments(String subscriptionId) async {
final response = await http.get(
Uri.parse('$_prefix/subscriptions/$subscriptionId/payments'),
headers: _headers,
);
return jsonDecode(response.body);
}
/// Estornar pagamento
Future<Map<String, dynamic>> refund(
String subscriptionId, {
required String reason,
String? paymentIntentId, // Stripe only
String? paymentId, // Asaas only
}) async {
final body = <String, dynamic>{'reason': reason};
if (gateway == 'stripe' && paymentIntentId != null) {
body['payment_intent_id'] = paymentIntentId;
}
if (gateway == 'asaas' && paymentId != null) {
body['payment_id'] = paymentId;
}
final response = await http.post(
Uri.parse('$_prefix/subscriptions/$subscriptionId/refund'),
headers: _headers,
body: jsonEncode(body),
);
return jsonDecode(response.body);
}
}
// Uso:
// final asaasAdmin = AdminSubscriptionService(
// baseUrl: 'https://fdplay-api.infraifd.com',
// token: adminToken,
// gateway: 'asaas',
// );
// final stripeAdmin = AdminSubscriptionService(
// baseUrl: 'https://fdplay-api.infraifd.com',
// token: adminToken,
// gateway: 'stripe',
// );
// final stats = await stripeAdmin.getStats();
🔍 Troubleshooting¶
Assinatura Ativa Mas Cliente Não Consegue Acessar Vídeos¶
Diagnóstico:
-
Verificar
current_subscription_iddo customer: -
Verificar status da subscription:
-
Verificar logs de webhooks:
Solução:
Se current_subscription_id estiver null mas subscription estiver ativa:
Webhook Não Processado (processed=false)¶
Diagnóstico:
Causas Comuns:
- Subscription não encontrada:
Solução: Verificar se subscription foi criada antes do webhook. Se não, criar manualmente.
- Assinatura inválida:
Solução: Verificar webhook_secret em .secrets/credentials.toml.
🖼️ Avatar do Admin¶
O campo avatar_id armazena uma referência ao GridFS para a imagem de perfil do admin.
Estrutura do Campo¶
| Campo | Tipo | Descrição |
|---|---|---|
avatar_id |
ObjectId \| null |
ID do arquivo no GridFS |
Upload e Atualização¶
O fluxo é idêntico ao do Customer:
- Upload da imagem via
POST /api/v1/archive-records - Atualizar admin com o
file_idretornado
PUT /api/v1/users
Content-Type: application/json
Authorization: Bearer <admin_token>
[{
"_id": "<admin_id>",
"avatar_id": "507f1f77bcf86cd799439012"
}]
Buscar Avatar¶
Ou sem header (para Image.network, <img>, etc.):
⚙️ Configuração da Plataforma¶
O admin pode alterar configurações da plataforma em runtime. Atualmente suporta a definição do gateway de pagamento padrão.
GET /api/v1/config/payment-gateway — Consultar Gateway Padrão¶
Endpoint público (sem autenticação). O frontend consulta para saber qual gateway apresentar como opção principal.
Response (200 OK):
Ambos Gateways Disponíveis
A resposta indica apenas a preferência do admin. Asaas e Stripe continuam igualmente disponíveis. O frontend pode usar essa informação para destacar o gateway preferido ou decidir qual fluxo exibir primeiro.
PUT /api/v1/admin/config/payment-gateway — Definir Gateway Padrão¶
Headers:
Request:
| Campo | Tipo | Valores | Descrição |
|---|---|---|---|
gateway |
string |
"asaas" | "stripe" |
Gateway que o frontend deve usar como padrão |
Response (200 OK):
{
"message": "Default payment gateway updated",
"default_payment_gateway": "stripe",
"updated_by": "admin_user",
"updated_at": "2026-03-02T12:00:00Z"
}
Erros:
| Status | Quando |
|---|---|
| 401 | Token ausente ou inválido |
| 403 | Usuário não é admin |
| 422 | Valor de gateway inválido (não é "asaas" nem "stripe") |
Exemplo Flutter (Admin)¶
// Alterar gateway padrão para Stripe
final resp = await http.put(
Uri.parse('$baseUrl/api/v1/admin/config/payment-gateway'),
headers: {'Authorization': 'Bearer $adminToken', 'Content-Type': 'application/json'},
body: jsonEncode({'gateway': 'stripe'}),
);
Gestao de Clientes (Admin)¶
Endpoints para o admin gerenciar clientes diretamente — verificar email, suspender/reativar conta, estender ou ativar assinaturas manualmente.
Prefixo: /api/v1/admin/customers e /api/v1/admin/subscriptions
| Metodo | Endpoint | Descricao |
|---|---|---|
| POST | /admin/customers/{id}/verify-email |
Verificar email (bypass do codigo) |
| POST | /admin/customers/{id}/toggle-active |
Suspender/reativar conta |
| POST | /admin/subscriptions/{id}/extend |
Estender prazo da assinatura |
| POST | /admin/subscriptions/{customer_id}/activate |
Criar assinatura manual (cortesia) |
POST /admin/customers/{id}/verify-email — Verificar Email¶
Marca o email do customer como verificado sem precisar do codigo de verificacao.
POST /api/v1/admin/customers/507f1f77bcf86cd799439011/verify-email
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"verify": true
}
| Campo | Tipo | Default | Descricao |
|---|---|---|---|
verify |
bool |
true |
true = verificar, false = desverificar |
Response (200 OK):
{
"docs": [{
"_id": "507f1f77bcf86cd799439011",
"username": "joao_silva",
"email": "joao@example.com",
"email_verified": true,
"email_verified_at": "2026-03-26T20:00:00Z"
}],
"info": {
"message": "Email verified by admin",
"customer_id": "507f1f77bcf86cd799439011",
"email_verified": true,
"admin": "root"
}
}
Efeitos colaterais:
- Limpa
email_verification_code_hash,email_verification_expires_ateemail_verification_attempts - Registra audit log
ADMIN.CUSTOMER.VERIFY_EMAIL
POST /admin/customers/{id}/toggle-active — Suspender/Reativar¶
Suspende ou reativa a conta de um customer. Suspender bloqueia o acesso a videos.
POST /api/v1/admin/customers/507f1f77bcf86cd799439011/toggle-active
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"active": false,
"reason": "Chargeback recorrente — conta suspensa"
}
| Campo | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
active |
bool |
Sim | true = reativar, false = suspender |
reason |
string |
Sim | Motivo (5-500 chars, audit trail) |
Comportamento:
| Acao | Efeito no Customer | Efeito na Subscription |
|---|---|---|
active: false |
current_subscription_id = null |
status = 'suspended' |
active: true |
Restaura current_subscription_id |
status = 'active' (se existir sub suspensa) |
Response (200 OK):
{
"docs": [{
"_id": "507f1f77bcf86cd799439011",
"username": "joao_silva",
"current_subscription_id": null
}],
"info": {
"message": "Customer suspended by admin",
"customer_id": "507f1f77bcf86cd799439011",
"active": false,
"reason": "Chargeback recorrente — conta suspensa",
"admin": "root"
}
}
POST /admin/subscriptions/{id}/extend — Estender Prazo¶
Estende a data de expiracao de uma assinatura. Aceita dias relativos ou data absoluta.
POST /api/v1/admin/subscriptions/507f1f77bcf86cd799439011/extend
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"days": 30,
"reason": "Cortesia por problema tecnico"
}
| Campo | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
days |
int |
Um dos dois | Dias para estender (1-365, relativo ao expires_at atual) |
expires_at |
string (ISO 8601) |
Um dos dois | Nova data absoluta de expiracao |
reason |
string |
Sim | Motivo (5-500 chars, audit trail) |
Reativacao automatica
Se a subscription estiver com status expired ou suspended, o extend reativa automaticamente para active e restaura o current_subscription_id do customer.
Response (200 OK):
{
"docs": [{
"_id": "507f1f77bcf86cd799439011",
"status": "active",
"expires_at": "2026-04-25T20:00:00Z",
"customer_id": "507f1f77bcf86cd799439012"
}],
"info": {
"message": "Subscription extended by admin",
"subscription_id": "507f1f77bcf86cd799439011",
"new_expires_at": "2026-04-25T20:00:00Z",
"reason": "Cortesia por problema tecnico",
"admin": "root"
}
}
POST /admin/subscriptions/{customer_id}/activate — Ativar Manualmente¶
Cria uma nova subscription sem passar pelo gateway de pagamento. Uso: cortesia, suporte, migracoes.
POST /api/v1/admin/subscriptions/507f1f77bcf86cd799439011/activate
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"plan_id": "plan-basic",
"days": 30,
"reason": "Cortesia suporte — migracao de plataforma"
}
| Campo | Tipo | Default | Obrigatorio | Descricao |
|---|---|---|---|---|
plan_id |
string |
- | Sim | Slug do plano (ex: "plan-basic") |
days |
int |
30 |
Nao | Duracao em dias (1-365) |
reason |
string |
- | Sim | Motivo (5-500 chars, audit trail) |
Comportamento:
- Valida que o customer e o plan existem
- Cancela subscription ativa anterior (se existir)
- Cria nova subscription com
status='active' - Vincula ao customer via
current_subscription_id - Registra audit log
ADMIN.SUBSCRIPTION.ACTIVATE
Response (200 OK):
{
"docs": [{
"_id": "682f1a99bcf86cd799439099",
"customer_id": "507f1f77bcf86cd799439011",
"plan_id": "plan-basic",
"status": "active",
"gateway": "asaas",
"payment_method": "pix",
"started_at": "2026-03-26T20:00:00Z",
"expires_at": "2026-04-25T20:00:00Z"
}],
"info": {
"message": "Subscription activated manually by admin",
"subscription_id": "682f1a99bcf86cd799439099",
"customer_id": "507f1f77bcf86cd799439011",
"plan_id": "plan-basic",
"expires_at": "2026-04-25T20:00:00Z",
"reason": "Cortesia suporte — migracao de plataforma",
"admin": "root"
}
}
GET /admin/subscriptions/insights — Análise interpretada (admin)¶
Camada de análise interpretada sobre os dados do dashboard. Usar para cards de saúde, badges, alertas priorizados e recomendações de ação. Para tabelas e drill-down de números brutos, usar /admin/subscriptions/dashboard.
Request¶
Response (200 OK)¶
{
"health": {
"score": 0.89,
"label": "good",
"active_healthy": 1514,
"active_at_risk_pix_stuck": 134
},
"conversion": {
"rate": 0.58,
"label": "warning",
"customers_without_subscription": 1271,
"customers_signup_incomplete": 1437,
"customers_email_unverified": 1233
},
"financial_health": {
"label": "healthy",
"mrr_cents": 3279520,
"arr_cents": 39354240,
"refund_rate": 0.0093,
"chargeback_rate": 0,
"failure_rate": 0.0712,
"ticket_uniformity": "all_basic"
},
"gateway_distribution": {
"dominant": "asaas",
"stripe_share": 0.13,
"asaas_share": 0.87
},
"recurrence_distribution": {
"label": "high_one_time_risk",
"one_time_share": 0.67,
"one_time_count": 1314,
"recurring_count": 649
},
"top_alerts": [
{
"code": "PIX_STUCK",
"severity": "critical",
"title": "134 PIX pendentes ha mais de 24h",
"description": "96% do pending PIX esta estagnado. Webhook PIX pode estar falhando.",
"metric": 134,
"potential_recovery_cents": 266660,
"action_hint": "Investigar DEBT-037 (webhook PIX)"
}
],
"opportunities": [
{
"code": "MIGRATE_PIX_TO_CARD",
"title": "Migrar 1314 PIX one-time para cartao recorrente",
"rationale": "PIX nao renova automaticamente.",
"potential_mrr_increase_cents": 0,
"potential_arr_increase_cents": 31394640
}
],
"data_quality": {
"label": "good",
"current_orphan_active": 0,
"current_unknown_gateway_active": 0
},
"generated_at": "2026-04-26T20:30:00Z"
}
Labels e regras de severidade¶
health.label — saúde geral
| Label | Score |
|---|---|
good |
≥ 0.85 |
warning |
0.70–0.84 |
critical |
< 0.70 |
Score =
(active − pending_pix_over_24h) / (active + pending). Mede quantas das subscriptions "ativas+pendentes" não estão estagnadas.
conversion.label
| Label | Rate |
|---|---|
good |
≥ 0.80 |
warning |
0.50–0.79 |
critical |
< 0.50 |
financial_health.label
| Label | Regra |
|---|---|
healthy |
chargeback_count == 0 AND refund_rate < 2% |
attention |
chargeback_count <= 5 AND refund_rate < 5% |
unhealthy |
qualquer outro caso |
recurrence_distribution.label
| Label | Regra |
|---|---|
mostly_recurring |
one_time_share < 0.20 |
balanced |
0.20 ≤ one_time_share ≤ 0.50 |
high_one_time_risk |
one_time_share > 0.50 |
data_quality.label — pós-DEBT-051 cleanup
| Label | Regra |
|---|---|
good |
0 órfãs ativas + 0 unknown_gateway ativos |
attention |
até 10 docs anômalos ativos |
unhealthy |
> 10 docs anômalos ativos |
Top alerts — códigos suportados¶
code |
Quando aparece | Severidade calculada |
|---|---|---|
PIX_STUCK |
pending_pix_over_24h > 0 |
critical se ≥50% do pending; high se ≥20%; medium caso contrário |
INCOMPLETE_SIGNUP |
customers_signup_incomplete > 0 |
high se ≥30% da base; medium se ≥10%; low |
PIX_CHURN_RISK |
one_time_share > 0.50 |
high se ≥0.70; medium caso contrário |
LOW_CONVERSION |
customers_without_subscription > 0 |
high se ≥40% da base; medium se ≥20%; low |
CHARGEBACK_DETECTED |
chargeback_count > 0 |
sempre critical |
Lista ordenada por severidade DESC. Máximo 5 alerts retornados. Front-end pode exibir todos ou filtrar por severidade.
Opportunities — códigos suportados¶
code |
Cálculo do impacto |
|---|---|
RECOVER_PIX_STUCK |
potential_mrr_increase_cents = pending_pix_over_24h × ticket_médio |
MIGRATE_PIX_TO_CARD |
potential_arr_increase_cents = one_time_count × ticket_médio × 12 |
COMPLETE_SIGNUP_CAMPAIGN |
sem estimativa direta — listada quando signup_incomplete > 0 |
Ticket médio =
mrr_cents / active_count(ou média deplans.amountse MRR=0).
Errors¶
- 401 — Token ausente/inválido
- 403 — Usuário não é admin
Performance¶
- Mesmas 12 aggregations do dashboard (paralelas via
asyncio.gather) - Camada de interpretação é puro Python pós-processamento — overhead < 5ms
- Sem cache — tipico < 500ms
Quando usar /insights vs /dashboard¶
| Use case | Endpoint |
|---|---|
| Cards de KPI com label de saúde | /insights |
| Alertas priorizados | /insights |
| Recomendações de ação com impacto $ | /insights |
| Tabelas drill-down de números | /dashboard |
| Gráficos com breakdown completo | /dashboard |
| BI/data export | /dashboard |
Exemplo Flutter/Dart¶
class InsightsTopAlert {
final String code;
final String severity; // 'critical' | 'high' | 'medium' | 'low'
final String title;
final int metric;
final int potentialRecoveryCents;
final String actionHint;
InsightsTopAlert.fromJson(Map<String, dynamic> j)
: code = j['code'],
severity = j['severity'],
title = j['title'],
metric = j['metric'],
potentialRecoveryCents = j['potential_recovery_cents'] ?? 0,
actionHint = j['action_hint'] ?? '';
Color get color => switch (severity) {
'critical' => Colors.red,
'high' => Colors.orange,
'medium' => Colors.amber,
_ => Colors.blue,
};
}
Future<Map<String, dynamic>> fetchInsights(
String baseUrl,
String adminToken,
) async {
final resp = await http.get(
Uri.parse('$baseUrl/api/v1/admin/subscriptions/insights'),
headers: {'Authorization': 'Bearer $adminToken'},
);
if (resp.statusCode != 200) throw Exception('Failed: ${resp.statusCode}');
return jsonDecode(resp.body);
}
GET /admin/subscriptions/dashboard — Dashboard de assinaturas (admin)¶
Visao 360° de assinaturas e clientes para tomada de decisao do admin. Nao quebra o legado GET /subscriptions/stats.
Request¶
Response (200 OK)¶
{
"totals": {
"subscriptions": 1959,
"customers": 3051,
"customers_with_subscription": 1779,
"customers_without_subscription": 1272,
"customers_email_unverified": 412,
"customers_signup_incomplete": 587
},
"by_status": {
"active": 1674,
"pending": 152,
"suspended": 3,
"cancelled": 123,
"expired": 7
},
"by_gateway": {
"stripe": {
"total": 845,
"active": 720,
"pending": 60,
"suspended": 1,
"cancelled": 60,
"expired": 4,
"mrr_cents": 2152800
},
"asaas": {
"total": 1114,
"active": 954,
"pending": 92,
"suspended": 2,
"cancelled": 63,
"expired": 3,
"mrr_cents": 2851860
}
},
"by_payment_method": {
"credit_card": 1502,
"pix": 380,
"boleto": 77
},
"by_recurrence": {
"recurring": 1579,
"one_time": 380
},
"by_status_x_gateway": {
"stripe": { "active": 720, "pending": 60, "suspended": 1, "cancelled": 60, "expired": 4 },
"asaas": { "active": 954, "pending": 92, "suspended": 2, "cancelled": 63, "expired": 3 }
},
"financial": {
"mrr_cents": 5004660,
"arr_cents": 60055920,
"average_subscription_value_cents": 2989,
"total_refunded_cents": 145000,
"chargeback_count": 12,
"with_payment_failures": 38
},
"alerts": {
"subscriptions_with_chargeback": 12,
"suspended_with_3plus_failures": 3,
"pending_pix_over_24h": 47
},
"generated_at": "2026-04-26T19:30:00Z"
}
Definicao dos campos¶
totals — visao macro
| Campo | Significado |
|---|---|
subscriptions |
Total de docs em subscriptions |
customers |
Total de users.user_type='customer' |
customers_with_subscription |
Clientes com pelo menos uma subscription (calculado: customers − without) |
customers_without_subscription |
Clientes que nunca tiveram assinatura (via $lookup — preciso, ignora subs órfãs) |
customers_email_unverified |
Clientes com email_verified ≠ true |
customers_signup_incomplete |
Clientes com email_verified=false OU tax_id vazio OU phone vazio (OR logico) |
by_status — distribuicao de subscriptions por estado de pagamento
| Status | Significado |
|---|---|
active |
Pagando regularmente |
pending |
Aguardando pagamento (PIX nao confirmado, cartao em validacao) |
suspended |
Suspensa por falhas (≥3) ou chargeback |
cancelled |
Cancelada (admin ou cliente) |
expired |
PIX/anual expirou sem renovacao |
by_gateway — split Stripe vs Asaas
Cada gateway tem total, contagem por status e mrr_cents (apenas active de planos MONTH).
by_payment_method — distribuicao de metodos
| Metodo | Comportamento |
|---|---|
credit_card |
Cobrança recorrente automatica |
pix |
One-time (precisa renovar manual) |
boleto |
Recorrente (Asaas) |
by_recurrence — agrupa por modelo de cobranca
| Tipo | Composicao |
|---|---|
recurring |
credit_card + boleto (renova automaticamente) |
one_time |
pix (sem renovacao automatica — risco de churn passivo) |
by_status_x_gateway — cross-tab para identificar onde concentra cada estado.
financial — KPIs em centavos
| Campo | Calculo |
|---|---|
mrr_cents |
Soma de plan.amount para subscriptions active em planos mensais |
arr_cents |
mrr_cents × 12 |
average_subscription_value_cents |
mrr / count(active) |
total_refunded_cents |
Soma de subscriptions.total_refunded |
chargeback_count |
Count de has_chargeback=true |
with_payment_failures |
Count de payment_failures > 0 |
alerts — sinais operacionais que exigem atencao
| Alerta | Quando dispara | Acao sugerida |
|---|---|---|
subscriptions_with_chargeback |
has_chargeback=true |
Suspender conta + revisar pagamento |
suspended_with_3plus_failures |
status=suspended AND payment_failures >= 3 |
Contato manual de recuperacao |
pending_pix_over_24h |
status=pending AND payment_method=pix AND started_at < now − 24h |
Lembrete ou cancelar para liberar capacidade |
subscriptions_with_unknown_gateway |
gateway NOT IN ('stripe', 'asaas') — inclui null, vazio, legados (pagseguro) |
Migrar gateway via script ou cancelar — ver DEBT-051 |
orphaned_subscriptions |
customer_id sem doc correspondente em users (cliente deletado ou admin) |
Cancelar — KPIs active/pending podem estar inflados — ver DEBT-051 |
Casos de uso¶
| Caso | Como interpretar |
|---|---|
| Quanto da base nao converte? | totals.customers_without_subscription / totals.customers |
| Onde concentra inadimplencia? | by_gateway.<gw>.suspended + by_gateway.<gw>.cancelled por gateway |
| Risco de churn passivo PIX | by_recurrence.one_time + alerts.pending_pix_over_24h |
| Saude do funil | customers_signup_incomplete alto = friccao no signup |
| Receita projetada | financial.arr_cents / 100 |
Errors¶
- 401 — Token ausente/invalido
- 403 — Usuario nao e admin
Performance¶
- 10 aggregations rodam em paralelo via
asyncio.gather - Tipico < 500ms para bases ate ~50k subscriptions
- Sem paginacao — todas metricas em uma resposta
Notas para o frontend¶
Gateways inesperados em by_gateway
O contrato declara stripe e asaas no by_gateway e by_status_x_gateway. Na pratica, o backend pode encontrar documentos legados com gateway=null ou valores antigos (ex.: pagseguro). Esses nao sao retornados no dashboard atual — a soma de by_status_x_gateway pode ser menor que by_status. Diferenca = subscriptions com gateway anomalo.
Acao no frontend: comparar sum(by_status.values()) com sum(by_status_x_gateway[gw].values()). Se divergir, exibir aviso ao admin.
Valores possiveis de payment_method
Schema declara credit_card | pix | boleto, mas em producao apenas credit_card e pix aparecem. boleto pode ser zero por longos periodos — tratar como sempre presente no contrato (nao quebrar UI).
Risco de churn passivo PIX
Se by_recurrence.one_time / totals.subscriptions > 0.4, a base esta exposta a churn passivo (PIX nao renova automatico). Cruzar com alerts.pending_pix_over_24h para identificar volume estagnado.
Exemplo Flutter/Dart¶
import 'dart:convert';
import 'package:http/http.dart' as http;
class SubscriptionsDashboard {
final Map<String, dynamic> totals;
final Map<String, dynamic> byStatus;
final Map<String, dynamic> byGateway;
final Map<String, dynamic> byPaymentMethod;
final Map<String, dynamic> byRecurrence;
final Map<String, dynamic> financial;
final Map<String, dynamic> alerts;
final DateTime generatedAt;
SubscriptionsDashboard.fromJson(Map<String, dynamic> json)
: totals = json['totals'],
byStatus = json['by_status'],
byGateway = Map<String, dynamic>.from(json['by_gateway']),
byPaymentMethod = json['by_payment_method'],
byRecurrence = json['by_recurrence'],
financial = json['financial'],
alerts = json['alerts'],
generatedAt = DateTime.parse(json['generated_at']);
// KPIs derivados
double get conversionRate =>
totals['customers_with_subscription'] / totals['customers'];
double get oneTimeShare =>
byRecurrence['one_time'] / totals['subscriptions'];
bool get hasGatewayAnomalies {
final statusTotal = (byStatus as Map<String, dynamic>)
.values
.fold<int>(0, (a, b) => a + (b as int));
final crossTotal = byGateway.values
.map((g) => g as Map<String, dynamic>)
.fold<int>(0, (a, g) => a + (g['total'] as int));
return statusTotal != crossTotal;
}
int get unaccountedSubscriptions {
if (!hasGatewayAnomalies) return 0;
final statusTotal = (byStatus as Map<String, dynamic>)
.values
.fold<int>(0, (a, b) => a + (b as int));
final crossTotal = byGateway.values
.map((g) => g as Map<String, dynamic>)
.fold<int>(0, (a, g) => a + (g['total'] as int));
return statusTotal - crossTotal;
}
}
Future<SubscriptionsDashboard> fetchSubscriptionsDashboard(
String baseUrl,
String adminToken,
) async {
final resp = await http.get(
Uri.parse('$baseUrl/api/v1/admin/subscriptions/dashboard'),
headers: {'Authorization': 'Bearer $adminToken'},
);
if (resp.statusCode != 200) {
throw Exception('Failed to load dashboard: ${resp.statusCode}');
}
return SubscriptionsDashboard.fromJson(jsonDecode(resp.body));
}
Polling vs cache¶
- O endpoint nao tem cache no backend — cada request recalcula.
- Recomendado polling a cada 30-60 segundos em telas de admin (cuidado: cada request faz 10 aggregations).
- Para tela de overview (KPIs) usar este endpoint. Para drill-down por cliente, usar
/admin/customers/exportou/subscriptionspaginado.
GET /admin/customers/export — Exportar clientes segmentados¶
Exporta clientes filtrados por status de assinatura para campanhas de retencao, recuperacao de inadimplentes e analytics de churn.
Autenticacao: admin via header Authorization: Bearer <token> ou query ?token=<jwt> (para download direto pelo browser).
Request¶
GET /api/v1/admin/customers/export?segment=without_subscription&format=xlsx
Authorization: Bearer <admin_token>
Query Params¶
| Param | Tipo | Obrigatorio | Default | Valores |
|---|---|---|---|---|
segment |
string |
✅ | — | active, pending, suspended, cancelled, expired, without_subscription, all |
format |
string |
❌ | xlsx |
xlsx (default, formatado), csv (UTF-8 BOM), json (aninhado) |
gateway |
string |
❌ | — | stripe, asaas (filtra pela assinatura mais recente) |
start |
string |
❌ | — | Data inicial (YYYY-MM-DD) — aplica em signup_date |
end |
string |
❌ | — | Data final (YYYY-MM-DD) — aplica em signup_date |
token |
string |
❌ | — | JWT alternativo ao header (uso por browser download) |
Definicao dos segmentos¶
| Segment | Regra |
|---|---|
active |
Cliente com alguma subscription status = active (ADR garante 1 por vez) |
pending |
Cliente com alguma subscription status = pending |
suspended |
Cliente com alguma subscription status = suspended |
cancelled |
Cliente cuja subscription mais recente (sort por created_at DESC) tem status = cancelled |
expired |
Cliente cuja subscription mais recente tem status = expired |
without_subscription |
Cliente que nunca teve assinatura (sem doc em subscriptions) |
all |
Todos os clientes (user_type = 'customer') |
Colunas do export¶
| Coluna | Tipo | Origem |
|---|---|---|
customer_id |
string (OID) |
users._id |
full_name |
string |
users.full_name |
email |
string |
users.email |
tax_id |
string (CPF) |
users.tax_id |
phone |
string |
users.phone |
signup_date |
datetime (UTC) |
users.created_at |
subscription_status |
string ou vazio |
subscriptions.status (mais recente) |
subscription_gateway |
string ou vazio |
subscriptions.gateway — apenas stripe ou asaas |
plan_name |
string ou vazio |
plans.name (lookup por subscriptions.plan_id) |
last_payment_date |
datetime (UTC) ou vazio |
subscriptions.last_payment_at |
total_revenue_cents |
int (centavos) |
LTV aproximado (ver fórmula abaixo) — exibido no Excel como R$ #,##0.00 |
chargeback_detected |
bool |
subscriptions.has_chargeback |
total_revenue_cents— fórmula: Para cada subscription do customer, somaplan.amount × ciclos, ondeciclos = ceil((last_payment_at − started_at).days / 30) + 1. Aproximação para LTV — não substitui relatório financeiro do gateway.
Formatos (ADR-059)¶
| Formato | Comportamento | Limite |
|---|---|---|
xlsx (default) |
Workbook formatado: título, metadados, freeze A4, autofilter, zebra, números nativos (R$ #,##0.00, dd/mm/yyyy hh:mm) |
50.000 linhas/aba — acima retorna 422 |
csv |
UTF-8 BOM () para Excel pt-BR. Datas ISO 8601, valores em reais com 2 casas |
50.000 (single-aba neste endpoint) |
json |
Aninhado: {meta: {...}, sheets: {Customers: [rows]}} |
50.000 |
Response¶
- 200 OK —
Content-Disposition: attachment; filename="customers_{segment}_{YYYYMMDD_HHMMSS}.{ext}" - 400 — Formato de data inválido ou XLSX acima do limite
- 401 — Token ausente/inválido
- 403 — Usuário não é admin
Casos de uso¶
| Caso | Request |
|---|---|
| Marketing — campanha de conversão (free → paid) | ?segment=without_subscription&format=xlsx |
| Operações — recuperação de inadimplentes | ?segment=suspended&gateway=asaas&format=xlsx |
| Análise de churn por janela | ?segment=cancelled&start=2026-01-01&end=2026-03-31&format=xlsx |
| Dashboard externo (BI) | ?segment=all&format=json |
| Pipe BI (UTF-8 BOM) | ?segment=all&format=csv |
Exemplo Flutter/Dart¶
Future<void> downloadCustomersExport({
required String segment,
String format = 'xlsx',
String? gateway,
}) async {
final params = {
'segment': segment,
'format': format,
'token': adminToken, // download direto via browser/webview
if (gateway != null) 'gateway': gateway,
};
final uri = Uri.parse('$baseUrl/api/v1/admin/customers/export')
.replace(queryParameters: params);
// Em desktop/web: abrir no browser para download nativo
// Em mobile: usar dio ou http com responseType bytes e salvar com path_provider
await launchUrl(uri);
}
Exemplo Flutter/Dart — Admin Customer Service¶
import 'dart:convert';
import 'package:http/http.dart' as http;
class AdminCustomerService {
final String baseUrl;
final String token;
AdminCustomerService({required this.baseUrl, required this.token});
Map<String, String> get _headers => {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
};
/// Verificar email do customer (bypass do codigo)
Future<Map<String, dynamic>> verifyEmail(String customerId, {bool verify = true}) async {
final response = await http.post(
Uri.parse('$baseUrl/api/v1/admin/customers/$customerId/verify-email'),
headers: _headers,
body: jsonEncode({'verify': verify}),
);
return jsonDecode(response.body);
}
/// Suspender ou reativar conta
Future<Map<String, dynamic>> toggleActive(
String customerId, {
required bool active,
required String reason,
}) async {
final response = await http.post(
Uri.parse('$baseUrl/api/v1/admin/customers/$customerId/toggle-active'),
headers: _headers,
body: jsonEncode({'active': active, 'reason': reason}),
);
return jsonDecode(response.body);
}
/// Estender prazo da assinatura
Future<Map<String, dynamic>> extendSubscription(
String subscriptionId, {
int? days,
String? expiresAt,
required String reason,
}) async {
final body = <String, dynamic>{'reason': reason};
if (days != null) body['days'] = days;
if (expiresAt != null) body['expires_at'] = expiresAt;
final response = await http.post(
Uri.parse('$baseUrl/api/v1/admin/subscriptions/$subscriptionId/extend'),
headers: _headers,
body: jsonEncode(body),
);
return jsonDecode(response.body);
}
/// Ativar assinatura manualmente (cortesia)
Future<Map<String, dynamic>> activateSubscription(
String customerId, {
required String planId,
int days = 30,
required String reason,
}) async {
final response = await http.post(
Uri.parse('$baseUrl/api/v1/admin/subscriptions/$customerId/activate'),
headers: _headers,
body: jsonEncode({
'plan_id': planId,
'days': days,
'reason': reason,
}),
);
return jsonDecode(response.body);
}
}
// Uso:
// final adminService = AdminCustomerService(
// baseUrl: 'https://fdplay-api.infraifd.com',
// token: adminToken,
// );
//
// // Verificar email
// await adminService.verifyEmail('customer_id_here');
//
// // Suspender conta
// await adminService.toggleActive(
// 'customer_id_here',
// active: false,
// reason: 'Chargeback recorrente',
// );
//
// // Estender assinatura por 30 dias
// await adminService.extendSubscription(
// 'subscription_id_here',
// days: 30,
// reason: 'Cortesia por problema tecnico',
// );
//
// // Ativar assinatura manual (cortesia)
// await adminService.activateSubscription(
// 'customer_id_here',
// planId: 'plan-basic',
// days: 60,
// reason: 'Acordo comercial',
// );
Fluxo: Troubleshooting com Novos Endpoints¶
flowchart TD
A[Cliente nao consegue acessar] --> B{Email verificado?}
B -->|Nao| C["POST /admin/customers/{id}/verify-email"]
B -->|Sim| D{Tem subscription ativa?}
D -->|Nao, expirou| E["POST /admin/subscriptions/{id}/extend<br/>days: 30"]
D -->|Nao, nunca teve| F["POST /admin/subscriptions/{customer_id}/activate<br/>plan_id: plan-basic, days: 30"]
D -->|Sim, mas suspensa| G["POST /admin/customers/{id}/toggle-active<br/>active: true"]
D -->|Sim, ativa| H[Verificar current_subscription_id]
Auditoria¶
Todas as acoes admin sao registradas em webhook_logs com event_type prefixado ADMIN.:
| Event Type | Acao |
|---|---|
ADMIN.CUSTOMER.VERIFY_EMAIL |
Email verificado/desverificado |
ADMIN.CUSTOMER.SUSPEND |
Conta suspensa |
ADMIN.CUSTOMER.REACTIVATE |
Conta reativada |
ADMIN.SUBSCRIPTION.EXTEND |
Prazo estendido |
ADMIN.SUBSCRIPTION.ACTIVATE |
Assinatura criada manualmente |
Consultar via:
QR Code — Geração de Imagens¶
Endpoints para gerar imagens PNG de QR code a partir de texto ou de um arquivo do GridFS.
Autenticação: Bearer token (JWT) — suporta também ?token=<jwt> para uso direto em Image.network ou launchUrl.
GET /qrcode/{text}¶
Gera QR code a partir de qualquer texto (URL, ID, string arbitrária).
Ou via URL direta:
Response: StreamingResponse PNG (Content-Disposition: attachment; filename="qrcode_<uuid>.png")
Parâmetros:
| Param | Tipo | Local | Descrição |
|---|---|---|---|
text |
string |
path | Texto a ser codificado no QR |
Exemplo Flutter:
// Usando Image.network com token no query param
Widget buildQrCode(String text, String token) {
return Image.network(
'$baseUrl/api/v1/qrcode/${Uri.encodeComponent(text)}?token=$token',
);
}
// Download para salvar em disco
Future<File> downloadQrCode(String text, String token) async {
final response = await Dio().get(
'$baseUrl/api/v1/qrcode/${Uri.encodeComponent(text)}',
options: Options(
headers: {'Authorization': 'Bearer $token'},
responseType: ResponseType.bytes,
),
);
final dir = await getApplicationDocumentsDirectory();
final file = File('${dir.path}/qrcode.png');
await file.writeAsBytes(response.data as List<int>);
return file;
}
GET /qrcode/{file_id}/{is_public}¶
Gera QR code para acesso a um arquivo do GridFS.
Ou via URL direta:
Parâmetros:
| Param | Tipo | Local | Descrição |
|---|---|---|---|
file_id |
string |
path | ObjectId do arquivo no GridFS |
is_public |
bool |
path | true = QR aponta para endpoint público (Fernet-encrypted); false = endpoint autenticado |
Comportamento por is_public:
is_public |
URL gerada no QR | Quem pode acessar |
|---|---|---|
true |
/archive-records-public/{fernet_encrypted_id} |
Qualquer pessoa (sem auth) |
false |
/archive-records/{file_id} |
Apenas usuários autenticados |
Response: StreamingResponse PNG com header Content-Qrcode: <url_gerada> para debug.
Proximos Passos¶
- Exportacao de relatorios - CSV/Excel de assinaturas, pagamentos e reembolsos
- Notificacoes em tempo real - WebSocket para admins (nova assinatura, cancelamento, chargeback)
- Analise de cohorts - Retention por mes de signup
- Previsao de receita - MRR projetado baseado em historico (deduzindo reembolsos)
- Automacao de retry - Reprocessar webhooks com falha automaticamente
- Dashboard de chargebacks - Visao consolidada de disputas e seus status
- Alertas de chargeback - Notificacao automatica via email/Slack quando chargeback detectado