Loja de Ingressos — Guia de Integracao Frontend¶
Visao Geral¶
Customers compram ingressos para eventos diretamente pela plataforma. Nao precisa de assinatura — qualquer customer autenticado pode comprar.
Alem do fluxo autenticado abaixo, existe um segundo caminho — compra sem cadastro
(checkout) — para quem so quer o ingresso, sem passar por POST /customers/signup.
Ver secao Compra sem login (checkout).
Fluxo de Compra¶
┌─────────────────────────────────────────────────────────────┐
│ FRONTEND (Flutter) │
└─────────────┬───────────────────────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ 1. Listar eventos a venda │
│ GET /store/events │
│ (publico, sem token) │
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ 2. Selecionar evento │
│ GET /store/events/{id} │
│ → available_tickets: 47 │
│ → ticket_value: 50.00 │
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ 3. Escolher quantidade │
│ e metodo de pagamento │
│ (cartao ou PIX) │
└─────────┬─────────┬─────────┘
│ │
CARTAO│ │PIX
▼ ▼
┌─────────────┐ ┌──────────────┐
│ POST /store │ │ POST /store │
│ /purchase/ │ │ /purchase/ │
│ asaas/card │ │ asaas/pix │
│ │ │ │
│ ou │ │ ou │
│ stripe/card │ │ stripe/pix │
└──────┬──────┘ └──────┬───────┘
│ │
▼ ▼
┌─────────────┐ ┌──────────────┐
│ Cartao: │ │ PIX: │
│ confirmado │ │ aguardando │
│ sincronamte │ │ pagamento │
└──────┬──────┘ └──────┬───────┘
│ │
▼ ▼
┌─────────────────────────────────┐
│ 3. Tickets criados sincronamte │
│ Cartao OK → status: available │
│ PIX → status: pending │
│ (webhook PIX move para │
│ available apos confirmacao — │
│ DEBT-037 em aberto) │
└─────────────┬───────────────────┘
│
▼
┌─────────────────────────────┐
│ 5. Customer consulta │
│ GET /me/ticket-orders │
│ GET /me/tickets/{id}/qr │
│ → QR code para entrada │
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ 6. No evento (admin) │
│ POST /tickets/consume-qr │
│ → Escaneia QR │
│ → Ticket: consumed │
└─────────────────────────────┘
Endpoints¶
Loja (publico)¶
Listar eventos a venda¶
Publico — nao envie Authorization (ADR-0085).
Cada evento traz image_url: URL pronta e publica da imagem (file_id cifrado em Fernet).
Use ela — image_id cru so funciona com archive_token de sessao, que o visitante nao tem.
image_url e null quando o evento nao tem imagem. E a vitrine do checkout: sem ela,
quem nao tem conta nao descobre o event_id para comprar. Retorna eventos com
is_sale_box_office=true, is_active=true e que ainda nao terminaram (event_end_date,
com fallback para event_date + 3h — ADR-0086); so dado de evento a venda, nada de cliente.
Response:
{
"events": [
{
"_id": "69c1dadce9c9cdd70fe6b58c",
"title": "A Escolha de Ficar - Goiania",
"description": "Pre-estreia exclusiva",
"event_type": "movie_premiere",
"event_date": "2026-04-06T20:00:00Z",
"location": "Cinema Central - Sala 3",
"ticket_value": 50.00,
"image_id": "69c1db...",
"capacity": 100,
"tickets_issued": 53,
"available_tickets": 47,
"is_sale_box_office": true
}
]
}
| Campo | Descricao |
|---|---|
ticket_value |
Preco unitario em reais (50.00 = R$50,00) |
available_tickets |
Ingressos restantes (capacity - tickets_issued). null = ilimitado |
is_sale_box_office |
Sempre true nesta rota (filtrado) |
Detalhe do evento¶
Mesmo formato, evento unico. Publico, como a listagem.
Compra (autenticado)¶
Asaas Cartao¶
POST /api/v1/store/purchase/asaas/card
Authorization: Bearer <customer_token>
Content-Type: application/json
{
"event_id": "69c1dadce9c9cdd70fe6b58c",
"quantity": 2,
"credit_card": {
"holderName": "Joao da Silva",
"number": "5162306219378829",
"expiryMonth": "05",
"expiryYear": "2028",
"ccv": "318"
},
"credit_card_holder_info": {
"name": "Joao da Silva",
"email": "joao@example.com",
"cpfCnpj": "12345678909",
"postalCode": "74000100",
"addressNumber": "123",
"phone": "+5562999999999"
}
}
Asaas PIX¶
POST /api/v1/store/purchase/asaas/pix
Authorization: Bearer <customer_token>
Content-Type: application/json
{
"event_id": "69c1dadce9c9cdd70fe6b58c",
"quantity": 2
}
Stripe Cartao¶
POST /api/v1/store/purchase/stripe/card
Authorization: Bearer <customer_token>
Content-Type: application/json
{
"event_id": "69c1dadce9c9cdd70fe6b58c",
"quantity": 2,
"payment_method_id": "pm_1234567890abcdef"
}
Stripe PIX¶
POST /api/v1/store/purchase/stripe/pix
Authorization: Bearer <customer_token>
Content-Type: application/json
{
"event_id": "69c1dadce9c9cdd70fe6b58c",
"quantity": 2
}
Response de compra¶
Cartao (Asaas — sucesso imediato):
{
"order": {
"_id": "69cb5a...",
"customer_id": "69c192...",
"event_id": "69c1da...",
"quantity": 2,
"unit_price": 50.00,
"total": 100.00,
"gateway": "asaas",
"payment_method": "credit_card",
"gateway_payment_id": "pay_abc123",
"payment_status": "CONFIRMED",
"status": "pending",
"ticket_ids": ["69cb5b...", "69cb5c..."]
},
"payment": {
"id": "pay_abc123",
"status": "CONFIRMED",
"billing_type": "CREDIT_CARD"
}
}
PIX (Asaas — QR code):
{
"order": {
"_id": "69cb5a...",
"customer_id": "69c192...",
"event_id": "69c1da...",
"quantity": 2,
"unit_price": 50.00,
"total": 100.00,
"gateway": "asaas",
"payment_method": "pix",
"gateway_payment_id": "pay_xyz789",
"payment_status": "PENDING",
"status": "pending",
"ticket_ids": ["69cb5b...", "69cb5c..."]
},
"payment": {
"id": "pay_xyz789",
"status": "PENDING",
"billing_type": "PIX"
},
"pix": {
"payload": "00020126580014br.gov.bcb.pix...",
"encoded_image": "<base64 PNG>",
"expiration_date": "2026-04-01T15:30:00Z"
}
}
Errors de compra¶
| HTTP | Cenario |
|---|---|
| 404 | Evento nao encontrado |
| 400 | Evento nao esta a venda (is_sale_box_office=false) |
| 400 | Evento ja passou (event_date < now) |
| 409 | Esgotado — capacidade insuficiente |
| 400 | Dados de cartao invalidos / transacao recusada |
Compra sem login (checkout)¶
Nao existe mais "conta de convidado" (ADR-0090). Se o e-mail informado nao tem cadastro, a compra cria uma conta real de customer e envia por e-mail um codigo de 6 digitos para o comprador definir a propria senha (
POST /auth/reset-password). Nenhuma senha trafega por e-mail. E-mail ja cadastrado e reaproveitado como esta, sem sobrescrever nada.CPF ja cadastrado em outra conta →
409.tax_ide unico; a compra e recusada com mensagem pedindo que a pessoa entre com a conta dela. Nao criamos duplicata nem sequestramos a conta existente.
Caminho paralelo ao acima para quem nao quer criar conta — sem POST /customers/signup,
sem login, sem Authorization header. O front decide qual caminho usar (ex.: um toggle
"comprar sem cadastro" na tela de checkout); os dois convivem e nao se excluem.
┌─────────────────────────────────────────────────────────────┐
│ FRONTEND (Flutter) │
└─────────────┬───────────────────────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ 1. Listar/selecionar evento│
│ GET /store/events │ (ainda exige token — ver nota)
│ GET /store/events/{id} │
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ 2. Formulario do comprador │
│ nome, email, CPF/CNPJ, │
│ telefone (opcional) │
└─────────┬─────────┬─────────┘
│ │
CARTAO│ │PIX
▼ ▼
┌─────────────────────┐ ┌──────────────────────┐
│ POST /store/purchase/│ │ POST /store/purchase/│
│ checkout/asaas/card │ │ checkout/asaas/pix │
│ ou checkout/stripe/… │ │ ou checkout/stripe/… │
│ (sem Authorization) │ │ (sem Authorization) │
└──────────┬───────────┘ └──────────┬────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌───────────────────────┐
│ Confirmado na hora: │ │ Fica pendente: │
│ resposta ja traz │ │ resposta so tem o QR │
│ `tickets` com o QR │ │ de PAGAMENTO (pix) │
│ de cada ingresso │ │ — sem QR de ingresso │
└──────────┬────────────┘ └──────────┬────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌───────────────────────┐
│ Exibir QR na tela │ │ Aguardar confirmacao │
│ (ja veio na resposta) │ │ do pagamento (webhook)│
│ + email de confirma- │ │ → email de confirma- │
│ cao tambem enviado │ │ cao com o QR chega │
└───────────────────────┘ │ sozinho depois │
└───────────────────────┘
Sem polling no checkout. No fluxo autenticado, PIX pendente e resolvido com
GET /me/ticket-orders/{id}(login). Guest nao tem login, entao nao existe um endpoint publico equivalente hoje — o unico jeito de o comprador saber que o PIX confirmou e o email que chega quando o webhook processa o pagamento. Avise o usuario disso na UI ("verifique seu email em alguns minutos").
Endpoints de checkout¶
Mesmos 4 gateways/metodos da compra autenticada, sob /checkout/, sem Authorization.
O corpo e igual ao da rota autenticada correspondente mais o campo buyer:
{
"full_name": "Maria Convidada",
"email": "maria@example.com",
"tax_id": "12345678909",
"phone": "+5562999999999"
}
tax_id aceita CPF (11 digitos) ou CNPJ (14 digitos), so numeros. phone e opcional.
Asaas Cartao (checkout)¶
POST /api/v1/store/purchase/checkout/asaas/card
Content-Type: application/json
{
"event_id": "69c1dadce9c9cdd70fe6b58c",
"quantity": 2,
"credit_card": {
"holderName": "Maria Convidada",
"number": "5162306219378829",
"expiryMonth": "05",
"expiryYear": "2028",
"ccv": "318"
},
"credit_card_holder_info": {
"name": "Maria Convidada",
"email": "maria@example.com",
"cpfCnpj": "12345678909",
"postalCode": "74000100",
"addressNumber": "123",
"phone": "+5562999999999"
},
"buyer": {
"full_name": "Maria Convidada",
"email": "maria@example.com",
"tax_id": "12345678909",
"phone": "+5562999999999"
}
}
Asaas PIX (checkout)¶
POST /api/v1/store/purchase/checkout/asaas/pix
Content-Type: application/json
{
"event_id": "69c1dadce9c9cdd70fe6b58c",
"quantity": 2,
"buyer": {
"full_name": "Maria Convidada",
"email": "maria@example.com",
"tax_id": "12345678909",
"phone": "+5562999999999"
}
}
Stripe Cartao (checkout)¶
POST /api/v1/store/purchase/checkout/stripe/card
Content-Type: application/json
{
"event_id": "69c1dadce9c9cdd70fe6b58c",
"quantity": 2,
"payment_method_id": "pm_1234567890abcdef",
"buyer": {
"full_name": "Maria Convidada",
"email": "maria@example.com",
"tax_id": "12345678909",
"phone": "+5562999999999"
}
}
Stripe PIX (checkout)¶
POST /api/v1/store/purchase/checkout/stripe/pix
Content-Type: application/json
{
"event_id": "69c1dadce9c9cdd70fe6b58c",
"quantity": 2,
"buyer": {
"full_name": "Maria Convidada",
"email": "maria@example.com",
"tax_id": "12345678909",
"phone": "+5562999999999"
}
}
Response da compra no checkout¶
Cartao — confirmado na hora, traz o QR de cada ingresso:
{
"order": {
"_id": "69cb5a...",
"customer_id": "69c199...",
"event_id": "69c1da...",
"quantity": 2,
"unit_price": 50.00,
"total": 100.00,
"gateway": "asaas",
"payment_method": "credit_card",
"gateway_payment_id": "pay_abc123",
"payment_status": "CONFIRMED",
"status": "pending",
"without_login": true,
"ticket_ids": ["69cb5b...", "69cb5c..."]
},
"payment": {
"id": "pay_abc123",
"status": "CONFIRMED",
"billing_type": "CREDIT_CARD"
},
"tickets": [
{ "ticket_id": "69cb5b...", "qr_data_uri": "data:image/png;base64,iVBORw0KGgo..." },
{ "ticket_id": "69cb5c...", "qr_data_uri": "data:image/png;base64,iVBORw0KGgo..." }
]
}
tickets[].qr_data_uri ja e uma imagem PNG pronta (data:image/png;base64,...) — basta
colocar direto num Image.memory/<img src=...>, nao precisa de biblioteca de QR no
client (diferente do fluxo autenticado, que devolve so o qr_payload cru).
tickets vem null quando o pagamento nao confirmou na hora (raro em cartao — cai no
mesmo caso do PIX abaixo: so chega por email).
PIX — fica pendente, sem QR de ingresso na resposta (so o QR de pagamento):
{
"order": {
"_id": "69cb5a...",
"customer_id": "69c199...",
"event_id": "69c1da...",
"quantity": 2,
"unit_price": 50.00,
"total": 100.00,
"gateway": "asaas",
"payment_method": "pix",
"gateway_payment_id": "pay_xyz789",
"payment_status": "PENDING",
"status": "pending",
"without_login": true,
"ticket_ids": ["69cb5b...", "69cb5c..."]
},
"payment": {
"id": "pay_xyz789",
"status": "PENDING",
"billing_type": "PIX"
},
"pix": {
"payload": "00020126580014br.gov.bcb.pix...",
"encoded_image": "<base64 PNG>",
"expiration_date": "2026-04-01T15:30:00Z"
}
}
pix.encoded_image aqui e o QR do pagamento (para o comprador escanear e pagar) —
nao confundir com o QR do ingresso, que so existe depois que o pagamento confirma e
chega por email.
Errors do checkout¶
Mesma tabela da compra autenticada (404/400/409), mais:
| HTTP | Cenario |
|---|---|
| 422 | buyer ausente ou tax_id/email invalidos |
Comportamento por tras (o que o front nao chama diretamente)¶
- O comprador vira uma conta
userscomum nos bastidores (sem senha utilizavel, sem login) — isso e interno, o front nunca recebe token nem precisa fazer nada com ela. - Comprar de novo com o mesmo email reaproveita essa conta (nao duplica, nao perde
historico). Se a pessoa decidir criar conta de verdade depois com o mesmo email
(
POST /customers/signup) com o mesmo e-mail, o ingresso ja comprado aparece automaticamente nessa conta. - Sem sessao nao ha
GET /me/ticket-ordersnemGET /me/tickets/{id}/qr— essas rotas exigem login. O email de confirmacao (com o QR de cada ingresso) e a unica forma de recuperar o ingresso pos-compra.
Minhas compras (autenticado)¶
Listar pedidos¶
Response:
{
"orders": [
{
"_id": "69cb5a...",
"customer_id": "69c192...",
"event_id": "69c1da...",
"quantity": 2,
"unit_price": 50.00,
"total": 100.00,
"gateway": "asaas",
"payment_method": "credit_card",
"gateway_payment_id": "pay_abc123",
"payment_status": "CONFIRMED",
"status": "pending",
"ticket_ids": ["69cb5b...", "69cb5c..."],
"created_at": "2026-04-01T14:30:00Z"
}
]
}
Campos relevantes:
| Campo | Descricao |
|---|---|
total |
Valor total em reais (100.00 = R$100,00) |
status |
Status interno do pedido (atualmente sempre pending — ver nota abaixo) |
payment_status |
Status real do gateway (CONFIRMED, RECEIVED, PENDING, succeeded, etc.) |
payment_method |
credit_card ou pix |
gateway_payment_id |
ID do pagamento no gateway (para rastreamento) |
Nota: O campo
statusdo TicketOrder atualmente permanece comopendingpois os webhooks ainda nao atualizamticket_orders(ver DEBT-037). Usepayment_statuspara determinar se o pagamento foi confirmado. Status confirmados:CONFIRMED,RECEIVED(Asaas),succeeded(Stripe).
Detalhe do pedido¶
QR Code do ingresso¶
Response:
{
"ticket_id": "69cb5b...",
"qr_payload": "gAAAAABh...",
"title": "A Escolha de Ficar - Goiania",
"status": "available",
"event_date": "2026-04-06T20:00:00Z"
}
O frontend renderiza qr_payload como QR code (usar biblioteca client-side como qr_flutter).
Regra: o endpoint so emite QR para tickets com
status='available'. Ticketspending(PIX aguardando confirmacao),consumedouexpiredretornam400 VALIDATION_ERROR. Para PIX, faca polling emGET /me/ticket-orders/{order_id}(campopayment_status) ateCONFIRMED/RECEIVED/succeededantes de tentar emitir o QR.
Consumo de ingresso (admin)¶
Validar QR sem consumir¶
POST /api/v1/tickets/validate-qr
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"qr_payload": "gAAAAABh..."
}
Response:
{
"valid": true,
"ticket_id": "69cb5b...",
"title": "A Escolha de Ficar",
"status": "available",
"customer_name": "Joao da Silva",
"event_title": "A Escolha de Ficar - Goiania"
}
Validar + consumir¶
POST /api/v1/tickets/consume-qr
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"qr_payload": "gAAAAABh..."
}
Response:
{
"consumed": true,
"ticket_id": "69cb5b...",
"title": "A Escolha de Ficar",
"consumed_at": "2026-04-06T20:15:00Z"
}
| HTTP | Cenario |
|---|---|
| 200 | Consumido com sucesso |
| 400 | QR invalido ou expirado |
| 422 | Ticket ja consumido ou expirado |
| 404 | Ticket nao encontrado |
Controle de capacidade¶
O backend garante atomicamente que a capacidade do evento nao e excedida:
Capacidade: 100
Emitidos: 98
Compra: 3 ingressos
→ 98 + 3 = 101 > 100
→ Rollback automatico
→ HTTP 409
Isso vale para TODOS os caminhos de criacao de ingresso:
- Compra na loja
- Promo code no signup
- Promo code no redeem
- Admin criacao manual
- Admin bulk via promo codes
Formato da resposta 409¶
O detail varia por caminho:
| Caminho | Exemplo de detail |
|---|---|
| Compra na loja | "Event sold out: only 2 tickets remaining." |
| Promo code (redeem/signup) | "Event at capacity: 2 slots remaining, 5 requested." |
| Admin manual | "Event at capacity: 2 slots remaining, 5 requested." |
// Tratar 409 no frontend
if (resp.statusCode == 409) {
final detail = jsonDecode(resp.body)['detail'] as String;
// Exibir detail diretamente ao usuario
showDialog(context, title: 'Sold out', message: detail);
}
Flutter/Dart¶
Listar eventos a venda¶
Future<List<Map<String, dynamic>>> getStoreEvents(String token) async {
final resp = await http.get(
Uri.parse('$baseUrl/api/v1/store/events'),
headers: {'Authorization': 'Bearer $token'},
);
final data = jsonDecode(resp.body);
return List<Map<String, dynamic>>.from(data['docs']);
Comprar ingresso (Asaas PIX)¶
Future<Map<String, dynamic>> purchaseTicketPix({
required String token,
required String eventId,
int quantity = 1,
}) async {
final resp = await http.post(
Uri.parse('$baseUrl/api/v1/store/purchase/asaas/pix'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode({
'event_id': eventId,
'quantity': quantity,
}),
);
if (resp.statusCode == 201) return jsonDecode(resp.body);
if (resp.statusCode == 409) throw Exception(jsonDecode(resp.body)['detail'] ?? 'Sold out');
throw Exception(jsonDecode(resp.body)['detail'] ?? 'Error');
}
Comprar ingresso (Asaas Cartao)¶
Future<Map<String, dynamic>> purchaseTicketCard({
required String token,
required String eventId,
required Map<String, String> creditCard,
required Map<String, String> holderInfo,
int quantity = 1,
}) async {
final resp = await http.post(
Uri.parse('$baseUrl/api/v1/store/purchase/asaas/card'),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
body: jsonEncode({
'event_id': eventId,
'quantity': quantity,
'credit_card': creditCard,
'credit_card_holder_info': holderInfo,
}),
);
if (resp.statusCode == 201) return jsonDecode(resp.body);
throw Exception(jsonDecode(resp.body)['detail'] ?? 'Error');
}
Listar meus pedidos¶
Future<List<Map<String, dynamic>>> getMyTicketOrders(
String token,
) async {
final resp = await http.get(
Uri.parse('$baseUrl/api/v1/me/ticket-orders'),
headers: {'Authorization': 'Bearer $token'},
);
final data = jsonDecode(resp.body);
return List<Map<String, dynamic>>.from(data['orders']);
}
Obter QR code do ingresso¶
Future<Map<String, dynamic>> getTicketQr(
String token,
String ticketId,
) async {
final resp = await http.get(
Uri.parse('$baseUrl/api/v1/me/tickets/$ticketId/qr'),
headers: {'Authorization': 'Bearer $token'},
);
return jsonDecode(resp.body);
// Renderizar qr_payload com qr_flutter
}
Comprar ingresso sem login (checkout, Asaas PIX)¶
Future<Map<String, dynamic>> purchaseGuestTicketPix({
required String eventId,
required Map<String, String> buyer, // full_name, email, tax_id, phone
int quantity = 1,
}) async {
final resp = await http.post(
Uri.parse('$baseUrl/api/v1/store/purchase/checkout/asaas/pix'),
headers: {'Content-Type': 'application/json'}, // sem Authorization
body: jsonEncode({
'event_id': eventId,
'quantity': quantity,
'buyer': buyer,
}),
);
if (resp.statusCode == 201) return jsonDecode(resp.body);
if (resp.statusCode == 409) throw Exception(jsonDecode(resp.body)['detail'] ?? 'Sold out');
throw Exception(jsonDecode(resp.body)['detail'] ?? 'Error');
// PIX: sem polling possivel (sem login) — avisar "confirmacao chega por email".
}
Comprar ingresso sem login (checkout, Asaas Cartao) — QR ja vem pronto¶
Future<void> purchaseGuestTicketCard({
required String eventId,
required Map<String, String> creditCard,
required Map<String, String> holderInfo,
required Map<String, String> buyer,
int quantity = 1,
}) async {
final resp = await http.post(
Uri.parse('$baseUrl/api/v1/store/purchase/checkout/asaas/card'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'event_id': eventId,
'quantity': quantity,
'credit_card': creditCard,
'credit_card_holder_info': holderInfo,
'buyer': buyer,
}),
);
if (resp.statusCode != 201) {
throw Exception(jsonDecode(resp.body)['detail'] ?? 'Error');
}
final data = jsonDecode(resp.body);
final tickets = data['tickets'] as List<dynamic>?;
if (tickets == null) {
// Nao confirmou na hora — so chega por email, igual ao caso PIX.
return;
}
for (final t in tickets) {
final qrDataUri = t['qr_data_uri'] as String; // "data:image/png;base64,...."
final bytes = base64Decode(qrDataUri.split(',').last);
// Image.memory(bytes) direto — sem biblioteca de QR no client.
}
}
Polling status do pedido (PIX)¶
/// Apos exibir QR PIX, fazer polling ate status mudar.
Future<void> pollOrderStatus(
String token,
String orderId,
) async {
Timer.periodic(Duration(seconds: 5), (timer) async {
final resp = await http.get(
Uri.parse('$baseUrl/api/v1/me/ticket-orders/$orderId'),
headers: {'Authorization': 'Bearer $token'},
);
final data = jsonDecode(resp.body);
if (data['order']['status'] == 'paid') {
timer.cancel();
// Navegar para tela de ingressos
}
if (data['order']['status'] == 'failed') {
timer.cancel();
// Exibir erro
}
});
}
Checklist Frontend¶
- [ ] Tela de listagem de eventos a venda (
GET /store/events) - [ ] Tela de detalhe do evento com
available_ticketse botao "Comprar" - [ ] Seletor de quantidade (1-10)
- [ ] Tela de pagamento (cartao ou PIX) — mesmo padrao da assinatura
- [ ] Exibir QR PIX com timer de expiracao (30 min)
- [ ] Polling status do pedido apos pagamento PIX
- [ ] Tela "Meus Ingressos" com lista de pedidos (
GET /me/ticket-orders) - [ ] Exibir QR code do ingresso (
GET /me/tickets/{id}/qr) - [ ] Botao "Compartilhar" QR code
- [ ] Tratar 409 "Esgotado" com mensagem amigavel
- [ ] Admin: tela de escaneamento QR (
POST /tickets/consume-qr) - [ ] Admin: validacao visual (verde = valido, vermelho = ja consumido/expirado)
Guest checkout (sem cadastro):
- [ ] Opcao "comprar sem cadastro" na tela de checkout (toggle/botao separado)
- [ ] Formulario do comprador: nome, email, CPF/CNPJ, telefone (opcional)
- [ ] Chamar
/store/purchase/checkout/...semAuthorization - [ ] Cartao: renderizar
tickets[].qr_data_uridireto (ja e imagem, sem lib de QR) - [ ] PIX: nao fazer polling — avisar que a confirmacao chega por email
- [ ] Mensagem clara: "verifique seu email para ver seu ingresso" no caminho PIX
FDplay incluso no ingresso¶
Toda compra de ingresso concede acesso ao streaming, sem custo adicional e sem o comprador pedir. O periodo vai da confirmacao do pagamento ate:
Quem compra com meses de antecedencia fica coberto ate o evento; quem compra na vespera tem os 30 dias garantidos. Exemplo real: ingresso de Goiania (05/12) comprado em agosto = ~4 meses de FDplay.
| Caminho | Quando o acesso comeca |
|---|---|
| Cartao (Asaas) | na hora da compra |
| PIX (Asaas) | quando o webhook confirma o pagamento |
| Stripe | nao concede — fora de escopo nesta entrega |
Quem ja assina nao recebe nada. Se o comprador tem assinatura viva (active ou
pending), a compra segue normal e nenhum periodo e concedido: o acesso dele ja existe, e
mexer numa assinatura paga e proibido (ADR-0078).
O periodo e uma Subscription active com expires_at e sem next_billing_date —
expira sozinha, nada e cobrado por ela.
Campo subscribe — virar assinante¶
Os 4 endpoints de compra Asaas aceitam subscribe: bool.
| Valor | O que acontece |
|---|---|
false (default) |
So o periodo incluso. Nada e cobrado depois. |
true |
O periodo incluso continua igual e a mensalidade (plano ativo, preco cheio) comeca no dia em que ele termina. |
A primeira cobranca cai exatamente no fim do periodo incluso — o cliente nunca paga por um dia que o ingresso ja cobria.
Cartao: o cartao usado no ingresso e tokenizado e vira uma assinatura Asaas mensal com
nextDueDate no fim do periodo. Nenhum dado de cartao e pedido de novo.
PIX: a cobranca deixa de ser um QR comum e passa a ser uma autorizacao de PIX
Automatico. O mesmo QR que o cliente paga para levar o ingresso registra o consentimento da
mensalidade. QR PIX comum nao autoriza cobranca futura — por isso o caminho e outro, nao um
passo a mais. A resposta traz payload/encoded_image da autorizacao, no mesmo formato de
antes.
Quem ja assina nao vira assinante de novo. Sem periodo concedido nao ha recorrencia:
subscribe: truee ignorado com seguranca nesse caso.
Stripe nao suporta subscribe nem concede periodo (DEBT-105): o campo nao existe nesses
schemas.
Falhas que nao derrubam a compra¶
O ingresso e o que o cliente pagou. Se a concessao do periodo ou a montagem da recorrencia
falhar, a compra e concluida e o erro vai para o log — o cliente sai com o ingresso, e o
problema e nosso. Consequencia pratica: subscribe: true nao garante que a recorrencia
ficou armada; confira em GET /customers/me (next_billing_date preenchido).