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.
Fluxo de Compra¶
┌─────────────────────────────────────────────────────────────┐
│ FRONTEND (Flutter) │
└─────────────┬───────────────────────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ 1. Listar eventos a venda │
│ GET /store/events │
│ (autenticado) │
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ 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 (autenticado)¶
Listar eventos a venda¶
Requer autenticacao (customer ou admin). Retorna eventos com is_sale_box_office=true, is_active=true e event_date > now.
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. Requer autenticacao.
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 |
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
}
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)