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.

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

GET /api/v1/store/events

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

GET /api/v1/store/events/{event_id}

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_id e 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 users comum 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-orders nem GET /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

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
}

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_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)

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/... sem Authorization
  • [ ] Cartao: renderizar tickets[].qr_data_uri direto (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:

max(hoje + 30 dias, fim do evento)

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.

{ "event_id": "...", "quantity": 1, "subscribe": true }

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: true e 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).