Skip to content

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

GET /api/v1/store/events
Authorization: Bearer <customer_token>

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

GET /api/v1/store/events/{event_id}
Authorization: Bearer <customer_token>

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

GET /api/v1/me/ticket-orders
Authorization: Bearer <customer_token>

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 status do TicketOrder atualmente permanece como pending pois os webhooks ainda nao atualizam ticket_orders (ver DEBT-037). Use payment_status para determinar se o pagamento foi confirmado. Status confirmados: CONFIRMED, RECEIVED (Asaas), succeeded (Stripe).

Detalhe do pedido

GET /api/v1/me/ticket-orders/{order_id}
Authorization: Bearer <customer_token>

QR Code do ingresso

GET /api/v1/me/tickets/{ticket_id}/qr
Authorization: Bearer <customer_token>

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'. Tickets pending (PIX aguardando confirmacao), consumed ou expired retornam 400 VALIDATION_ERROR. Para PIX, faca polling em GET /me/ticket-orders/{order_id} (campo payment_status) ate CONFIRMED/RECEIVED/succeeded antes 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_tickets e 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)