# Integração Unity — API de Brindes Fanzone Este documento descreve como integrar o projeto Unity com a API REST de brindes hospedada. ## URL base ``` http://localhost/fanzone/api/index.php ``` Todas as rotas usam o parâmetro `route`: ``` http://localhost/fanzone/api/index.php?route=brindes/inventario ``` ### Headers recomendados | Header | Valor | | -------------- | ------------------ | | `Content-Type` | `application/json` | | `Accept` | `application/json` | A API envia CORS com `Access-Control-Allow-Origin: *`, permitindo requisições diretas do Unity Editor e builds locais. ### Segurança A API **não possui autenticação**. Use apenas em rede local/confiável (evento, kiosk, LAN). --- ## Formato padrão de resposta ### Sucesso ```json { "success": true, "data": { }, "message": "Mensagem opcional" } ``` ### Erro ```json { "success": false, "error": { "code": "ESTOQUE_ESGOTADO", "message": "Descrição legível do erro" } } ``` ### Códigos de erro comuns | Código | HTTP | Descrição | | ------------------------ | ---- | ------------------------------------------------------- | | `ROTA_INVALIDA` | 404 | Parâmetro `route` ausente | | `ROTA_NAO_ENCONTRADA` | 404 | Endpoint inexistente | | `JSON_INVALIDO` | 400 | Body não é JSON válido | | `VALIDACAO` | 422 | Dados inválidos (nome/e-mail/telefone, peso zero, etc.) | | `BRINDE_NAO_ENCONTRADO` | 404 | ID inexistente ou inativo (estoque) | | `NENHUM_BRINDE_LIBERADO` | 409 | Nenhum item liberado com estoque | | `INGRESSO_ESGOTADO` | 409 | Ingressos sem estoque disponível | | `INGRESSO_INDISPONIVEL` | 409 | Ingresso inativo ou indisponível para habilitação | | `ESTOQUE_ESGOTADO` | 409 | Todos os brindes esgotados | | `ERRO_INTERNO` | 500 | Falha inesperada no servidor | --- ## Endpoints | Método | Route | Descrição | | ------ | ---------------------------- | ------------------------------------------------------------ | | `POST` | `brindes/verificar-email` | Consulta se o e-mail já foi usado (liberado para participar) | | `POST` | `brindes/sortear` | Registra lead e entrega brinde (online ou sync offline com `brinde`) | | `POST` | `brindes/habilitar-ingresso` | Admin: próximo sorteio será ingresso Village | | `GET` | `brindes/ingresso/status` | Consulta se ingresso está habilitado para o próximo sorteio | | `GET` | `brindes/inventario` | Consulta inventário completo | | `GET` | `brindes/premios` | Snapshot de quantidades para cache offline (tablet Unity) | | `GET` | `brindes/leads` | Lista leads com brindes atribuídos | | `GET` | `brindes` | Lista brindes ativos | | `GET` | `brindes/{id}` | Detalhe de um brinde | | `POST` | `brindes` | Cadastra novo brinde | | `PUT` | `brindes/{id}` | Edita nome, peso, liberação, ativo | | `POST` | `brindes/{id}/estoque` | Adiciona quantidade ao estoque | --- ## Modelos C# (JsonUtility) > **Nota:** `JsonUtility` não suporta genéricos nem `Dictionary`. Para produção, considere [Newtonsoft.Json](https://docs.unity3d.com/Packages/com.unity.nuget.newtonsoft-json@latest). Os modelos abaixo funcionam com `JsonUtility` usando wrappers. ```csharp using System; [Serializable] public class ApiResponseWrapper { public bool success; } [Serializable] public class BrindeData { public int id; public string nome; public string descricao; public int quantidade_cadastrada; public int quantidade_distribuida; public int quantidade_disponivel; public int peso; public string codigo; public bool participa_sorteio_automatico; public bool liberado; public bool ativo; public string created_at; public string updated_at; } [Serializable] public class InventarioResumo { public int total_cadastrado; public int total_distribuido; public int total_disponivel; public int itens_ativos; public int itens_liberados; } [Serializable] public class InventarioData { public InventarioResumo resumo; public BrindeData[] itens; } [Serializable] public class InventarioResponse { public bool success; public InventarioData data; } [Serializable] public class PremioQuantidadeData { public int id; public string codigo; public string nome; public int quantidade_cadastrada; public int quantidade_distribuida; public int quantidade_disponivel; public int peso; public bool participa_sorteio_automatico; public bool liberado; public bool ativo; public bool sorteavel_offline; } [Serializable] public class PremiosIngressoData { public string codigo; public bool proximo_sorteio_ingresso; public int quantidade_disponivel; } [Serializable] public class PremiosResumoData { public int total_cadastrado; public int total_distribuido; public int total_disponivel; public int itens_ativos; public int itens_sorteaveis_offline; public int unidades_sorteaveis_offline; } [Serializable] public class PremiosData { public string atualizado_em; public int pontuacao_minima; public PremiosIngressoData ingresso; public PremiosResumoData resumo; public PremioQuantidadeData[] premios; } [Serializable] public class PremiosResponse { public bool success; public PremiosData data; public string message; } [Serializable] public class LeadData { public int id; public int distribuicao_id; public string nome; public string email; public string telefone; public string created_at; } [Serializable] public class SorteioBrindeWrapper { public bool ganhou_brinde; public int distribuicao_id; public string referencia; public int pontuacao; public int total_perguntas; public int pontuacao_minima; public bool sorteio_reservado_ingresso; public LeadData lead; public BrindeData brinde; } [Serializable] public class SorteioResponse { public bool success; public SorteioBrindeWrapper data; public string message; } [Serializable] public class BrindeResponse { public bool success; public BrindeData data; public string message; } [Serializable] public class BrindeListResponse { public bool success; public BrindeData[] data; } [Serializable] public class ApiErrorBody { public bool success; public ApiErrorDetail error; } [Serializable] public class ApiErrorDetail { public string code; public string message; } ``` ### Payloads de requisição ```csharp [Serializable] public class SortearRequest { public string nome; public string email; public string telefone; public string referencia; public int pontuacao; public int total_perguntas; } [Serializable] public class CriarBrindeRequest { public string nome; public string descricao; public int quantidade_inicial; public int peso; public bool liberado; } [Serializable] public class AtualizarBrindeRequest { public string nome; public int peso; public bool liberado; public bool ativo; } [Serializable] public class ReporEstoqueRequest { public int quantidade_adicionar; } [Serializable] public class VerificarEmailRequest { public string email; } [Serializable] public class VerificarEmailData { public string email; public bool liberado; public bool email_existe; } [Serializable] public class VerificarEmailResponse { public bool success; public VerificarEmailData data; public string message; } ``` --- ## Cliente HTTP reutilizável Crie um `MonoBehaviour` ou serviço singleton com a URL configurável no Inspector. ```csharp using System; using System.Text; using System.Collections; using UnityEngine; using UnityEngine.Networking; public class BrindeApiClient : MonoBehaviour { [SerializeField] private string baseUrl = "http://localhost/fanzone/api/index.php"; public IEnumerator SortearBrinde( string nome, string email, string telefone, Action onSuccess, Action onError, string referencia = null) { var body = new SortearRequest { nome = nome, email = email, telefone = telefone, referencia = referencia }; yield return Post("brindes/sortear", JsonUtility.ToJson(body), (responseText) => onSuccess?.Invoke(JsonUtility.FromJson(responseText)), onError); } public IEnumerator VerificarEmail( string email, Action onSuccess, Action onError) { var body = new VerificarEmailRequest { email = email }; yield return Post("brindes/verificar-email", JsonUtility.ToJson(body), (responseText) => onSuccess?.Invoke(JsonUtility.FromJson(responseText)), onError); } public IEnumerator ObterInventario(Action onSuccess, Action onError, bool apenasLiberados = false) { string route = apenasLiberados ? "brindes/inventario&apenas_liberados=1" : "brindes/inventario"; yield return Get(route, (responseText) => onSuccess?.Invoke(JsonUtility.FromJson(responseText)), onError); } public IEnumerator ObterPremios(Action onSuccess, Action onError) { yield return Get("brindes/premios", (responseText) => onSuccess?.Invoke(JsonUtility.FromJson(responseText)), onError); } public IEnumerator CriarBrinde(CriarBrindeRequest request, Action onSuccess, Action onError) { yield return Post("brindes", JsonUtility.ToJson(request), (responseText) => onSuccess?.Invoke(JsonUtility.FromJson(responseText)), onError); } public IEnumerator AtualizarBrinde(int id, AtualizarBrindeRequest request, Action onSuccess, Action onError) { yield return Send("PUT", $"brindes/{id}", JsonUtility.ToJson(request), (responseText) => onSuccess?.Invoke(JsonUtility.FromJson(responseText)), onError); } public IEnumerator ReporEstoque(int id, int quantidade, Action onSuccess, Action onError) { var body = new ReporEstoqueRequest { quantidade_adicionar = quantidade }; yield return Post($"brindes/{id}/estoque", JsonUtility.ToJson(body), (responseText) => onSuccess?.Invoke(JsonUtility.FromJson(responseText)), onError); } private IEnumerator Get(string route, Action onSuccess, Action onError) { yield return Send("GET", route, null, onSuccess, onError); } private IEnumerator Post(string route, string jsonBody, Action onSuccess, Action onError) { yield return Send("POST", route, jsonBody, onSuccess, onError); } private IEnumerator Send(string method, string route, string jsonBody, Action onSuccess, Action onError) { string url = $"{baseUrl}?route={route}"; using var request = new UnityWebRequest(url, method); if (!string.IsNullOrEmpty(jsonBody)) { byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonBody); request.uploadHandler = new UploadHandlerRaw(bodyRaw); } request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); request.SetRequestHeader("Accept", "application/json"); yield return request.SendWebRequest(); #if UNITY_2020_2_OR_NEWER bool failed = request.result != UnityWebRequest.Result.Success; #else bool failed = request.isNetworkError || request.isHttpError; #endif string responseText = request.downloadHandler.text; if (failed) { onError?.Invoke(new ApiErrorDetail { code = "NETWORK_ERROR", message = request.error ?? "Falha na requisição" }); yield break; } if (responseText.Contains("\"success\":false")) { var error = JsonUtility.FromJson(responseText); onError?.Invoke(error.error); yield break; } onSuccess?.Invoke(responseText); } } ``` --- ## Cena Jogo — Tablet (lead + brinde) Fluxo recomendado no tablet Unity: 1. Usuário preenche **e-mail** na UI. 2. Chamar `POST brindes/verificar-email` para saber se o e-mail está liberado. 3. Se `data.liberado == true`, continuar com nome e telefone e seguir para o jogo. 4. Se `data.liberado == false`, informar que o e-mail já participou. 5. Ao finalizar o jogo, gerar UUID único como `referencia` (idempotência em caso de retry). 6. Chamar `POST brindes/sortear` com os dados do lead. 7. Exibir `data.brinde.nome` na tela de resultado. 8. Tratar erros de validação (`VALIDACAO`) e estoque (`NENHUM_BRINDE_LIBERADO`). ### Verificar e-mail (somente consulta) **Request:** ```http POST /fanzone/api/index.php?route=brindes/verificar-email Content-Type: application/json { "email": "maria@email.com" } ``` **Response — e-mail liberado (200):** ```json { "success": true, "data": { "email": "maria@email.com", "liberado": true, "email_existe": false }, "message": "E-mail liberado para participação." } ``` **Response — e-mail já utilizado (200):** ```json { "success": true, "data": { "email": "maria@email.com", "liberado": false, "email_existe": true }, "message": "Este e-mail já foi utilizado." } ``` ```csharp public void ValidarEmailAntesDoJogo() { StartCoroutine(apiClient.VerificarEmail( email: inputEmail.text, onSuccess: (response) => { if (response.data.liberado) { Debug.Log("E-mail liberado — pode iniciar o jogo."); } else { Debug.Log("E-mail já utilizado — bloquear acesso."); } }, onError: (error) => { Debug.LogError($"[{error.code}] {error.message}"); } )); } ``` ```csharp using System; using UnityEngine; using UnityEngine.UI; public class BrindeGameController : MonoBehaviour { [SerializeField] private BrindeApiClient apiClient; [SerializeField] private InputField inputNome; [SerializeField] private InputField inputEmail; [SerializeField] private InputField inputTelefone; public void PegarBrinde() { string referencia = Guid.NewGuid().ToString(); StartCoroutine(apiClient.SortearBrinde( nome: inputNome.text, email: inputEmail.text, telefone: inputTelefone.text, onSuccess: (response) => { Debug.Log($"Lead: {response.data.lead.nome}"); Debug.Log($"Brinde entregue: {response.data.brinde.nome}"); Debug.Log($"Restam {response.data.brinde.quantidade_disponivel} unidades."); }, onError: (error) => { Debug.LogError($"[{error.code}] {error.message}"); }, referencia: referencia )); } } ``` ### Exemplo de resposta — sortear **Request:** ```http POST /fanzone/api/index.php?route=brindes/sortear Content-Type: application/json { "nome": "Maria Silva", "email": "maria@email.com", "telefone": "11987654321", "referencia": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "pontuacao": 3, "total_perguntas": 3 } ``` **Response — ganhou brinde (200):** ```json { "success": true, "data": { "ganhou_brinde": true, "distribuicao_id": 42, "referencia": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "pontuacao": 3, "total_perguntas": 3, "pontuacao_minima": 3, "sorteio_reservado_ingresso": false, "lead": { "id": 15, "nome": "Maria Silva", "pontuacao": 3, "total_perguntas": 3, "ganhou_brinde": true }, "brinde": { "id": 1, "nome": "Leque", "quantidade_disponivel": 69 } }, "message": "Brinde sorteado com sucesso." } ``` **Response — não ganhou brinde (200):** ```json { "success": true, "data": { "ganhou_brinde": false, "distribuicao_id": null, "referencia": "uuid-do-participante", "pontuacao": 1, "total_perguntas": 3, "pontuacao_minima": 3, "sorteio_reservado_ingresso": false, "lead": { "id": 16, "nome": "João", "pontuacao": 1, "total_perguntas": 3, "ganhou_brinde": false }, "brinde": null }, "message": "Participante registrado. Pontuação insuficiente para brinde." } ``` ### Pontuação mínima - Configure no painel admin: **Quiz — pontuação para brinde** (padrão: **3**). - Unity envia `pontuacao` e `total_perguntas` no mesmo `POST brindes/sortear`. - Se `pontuacao < pontuacao_minima`: **não sorteia**, registra o participante e retorna `ganhou_brinde: false`. - Se `pontuacao >= pontuacao_minima`: sorteia normalmente. ### Campos obrigatórios do lead | Campo | Regra | | ----------------- | -------------------------------------------- | | `nome` | Obrigatório, até 255 caracteres | | `email` | Obrigatório, formato válido | | `telefone` | Obrigatório, ao menos 8 dígitos numéricos | | `pontuacao` | Obrigatório, inteiro ≥ 0 | | `total_perguntas` | Obrigatório, inteiro > 0 | | `referencia` | Opcional, UUID recomendado para retry seguro | | `brinde` | Opcional, código do brinde sorteado offline (sincronização) | ### Sincronização offline (tablet sem internet) Quando o tablet perde conexão, a Unity sorteia localmente com o inventário em cache e guarda lead + brinde na fila. Ao reconectar, envie o **mesmo** `POST brindes/sortear` incluindo o campo `brinde` com o **código** do item já entregue ao participante. A API **não sorteia de novo**: registra o lead, deduz 1 unidade do brinde informado e responde como uma entrega normal. Use a mesma `referencia` gerada offline para idempotência (retry seguro). **Request — sync offline:** ```http POST /fanzone/api/index.php?route=brindes/sortear Content-Type: application/json { "nome": "Maria Silva", "email": "maria@email.com", "telefone": "11987654321", "referencia": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "pontuacao": 3, "total_perguntas": 3, "brinde": "cordinha_oculos" } ``` **Comportamento:** | Campo `brinde` | Pontuação | Resultado | | -------------- | --------- | --------- | | omitido | ≥ mínima | Sorteio online normal (servidor escolhe) | | `"codigo"` | ≥ mínima | Deduz o brinde informado (`sincronizacao_offline: true`) | | omitido ou enviado | < mínima | Registra lead sem brinde (`ganhou_brinde: false`) | Códigos disponíveis no seed: `leque`, `cordinha_oculos`, `bone`, `bolsa_estacio`, `ingresso_village`. **Erros comuns na sync:** `422` se o código não existir ou estiver inativo/liberado; `409 ESTOQUE_ESGOTADO` se o estoque no servidor já acabou. **Fluxo sugerido na Unity:** 1. Antes do evento (e ao reconectar), cachear prêmios via `GET brindes/premios`. 2. Offline: sortear localmente **somente** entre itens com `sorteavel_offline = true` (mesma lógica de peso) e persistir `{ lead, brinde.codigo, referencia }`. 3. Ao entregar offline, decrementar `quantidade_disponivel` no cache local. 4. Online: coroutine que consome a fila chamando `POST brindes/sortear` com `brinde` preenchido. 5. Se `409 ESTOQUE_ESGOTADO`, tratar na UI (brinde já esgotado no servidor) e recarregar `GET brindes/premios`. ### Consultar quantidades de prêmios (cache offline) **GET** `?route=brindes/premios` Retorno pensado para a Unity manter estoque local sincronizado: ```json { "success": true, "data": { "atualizado_em": "2026-06-13 14:30:00", "pontuacao_minima": 3, "ingresso": { "codigo": "ingresso_village", "proximo_sorteio_ingresso": false, "quantidade_disponivel": 2 }, "resumo": { "total_cadastrado": 397, "total_distribuido": 12, "total_disponivel": 385, "itens_ativos": 5, "itens_sorteaveis_offline": 4, "unidades_sorteaveis_offline": 383 }, "premios": [ { "id": 1, "codigo": "leque", "nome": "Leque", "quantidade_cadastrada": 70, "quantidade_distribuida": 5, "quantidade_disponivel": 65, "peso": 70, "participa_sorteio_automatico": true, "liberado": true, "ativo": true, "sorteavel_offline": true } ] }, "message": "Quantidades de prêmios consultadas com sucesso." } ``` | Campo | Uso na Unity | | ----- | ------------ | | `atualizado_em` | Comparar com cache local para saber se precisa atualizar | | `pontuacao_minima` | Validar offline se o participante ganha brinde | | `sorteavel_offline` | `true` = entra no sorteio ponderado local (ingresso fica `false`) | | `ingresso.proximo_sorteio_ingresso` | Só online: próximo sorteio no servidor entrega ingresso | ```csharp StartCoroutine(apiClient.ObterPremios( onSuccess: (response) => { PlayerPrefs.SetString("premios_cache_json", JsonUtility.ToJson(response.data)); foreach (var premio in response.data.premios) { if (!premio.sorteavel_offline) continue; Debug.Log($"{premio.nome}: {premio.quantidade_disponivel} disponíveis | peso {premio.peso}"); } }, onError: (error) => Debug.LogError(error.message) )); ``` --- ## Cena Admin — Gerenciar inventário Interface sugerida com painel listando itens e formulários de edição. ### Carregar inventário ```csharp StartCoroutine(apiClient.ObterInventario( onSuccess: (response) => { foreach (var item in response.data.itens) { Debug.Log($"{item.nome}: {item.quantidade_disponivel} disponíveis | peso {item.peso}"); } }, onError: (error) => Debug.LogError(error.message) )); ``` **GET** `?route=brindes/inventario` Retorno inclui resumo global: ```json { "success": true, "data": { "resumo": { "total_cadastrado": 230, "total_distribuido": 60, "total_disponivel": 170, "itens_ativos": 3, "itens_liberados": 3 }, "itens": [ ] } } ``` ### Adicionar novo brinde **POST** `?route=brindes` ```json { "nome": "Canecas", "descricao": "Caneca térmica", "quantidade_inicial": 50, "peso": 6, "liberado": true } ``` ```csharp var request = new CriarBrindeRequest { nome = "Canecas", descricao = "Caneca térmica", quantidade_inicial = 50, peso = 6, liberado = true }; StartCoroutine(apiClient.CriarBrinde(request, onSuccess, onError)); ``` ### Repor estoque (reposição dinâmica) Quando chegam mais unidades de um item já cadastrado: **POST** `?route=brindes/1/estoque` ```json { "quantidade_adicionar": 100 } ``` Exemplo: Copos tinham 100 cadastrados, 30 entregues (70 disponíveis). Após repor +100, ficam 200 cadastrados, 30 entregues (**170 disponíveis**). ```csharp StartCoroutine(apiClient.ReporEstoque(1, 100, onSuccess, onError)); ``` ### Editar peso e liberação Controla o que pode sair no sorteio e com qual frequência: - `**peso**`: quanto maior, mais chance no sorteio global. - `**liberado**`: `false` remove o item do pool sem apagar estoque. **PUT** `?route=brindes/2` ```json { "peso": 3, "liberado": false } ``` ```csharp var request = new AtualizarBrindeRequest { peso = 3, liberado = false }; StartCoroutine(apiClient.AtualizarBrinde(2, request, onSuccess, onError)); ``` ### UI Admin sugerida | Campo | Tipo | Ação na API | | ------------------------------------- | ----------------- | --------------------------- | | Nome | Input | POST/PUT `nome` | | Quantidade inicial | Input int | POST `quantidade_inicial` | | Repor estoque | Input int + botão | POST `brindes/{id}/estoque` | | Peso | Slider 1–100 | PUT `peso` | | Liberado | Toggle | PUT `liberado` | | Ativo | Toggle | PUT `ativo` | | Cadastrados / Entregues / Disponíveis | Labels read-only | GET `brindes/inventario` | --- ## Sorteio ponderado — como funciona O endpoint `brindes/sortear` considera apenas itens que atendem **simultaneamente**: - `ativo = true` - `liberado = true` - `quantidade_disponivel > 0` (estoque restante) - `peso > 0` - `participa_sorteio_automatico = true` (ingressos ficam de fora) A probabilidade de cada item é: ``` chance(item) = peso(item) / soma(peso de todos elegíveis) ``` ### Estoque do evento Estoque **único** por brinde: `quantidade_disponivel = quantidade_cadastrada - quantidade_distribuida`. | Brinde | Estoque inicial | Peso | | ----------------------- | --------------- | ---------- | | Leque | 2.100 | 70 | | Cordinha de óculos | 2.400 | 80 | | Boné | 150 | 5 | | Bolsa de 10% na Estácio | 7.200 | 240 | | Ingresso Village | 60 | — (manual) | Reponha estoque via painel admin ou `POST brindes/{id}/estoque`. Exemplo com pesos Leque=70, Cordinha=80, Boné=5, Bolsa=240 (total 395): | Brinde | Peso | Chance aproximada | | --------- | ---- | ----------------- | | Leque | 70 | 17,7% | | Cordinha | 80 | 20,3% | | Boné | 5 | 1,3% | | Bolsa 10% | 240 | 60,8% | --- ## Ingresso Village — sorteio manual (admin Unity) O **Par de ingressos Village** (`codigo: ingresso_village`) **nunca** participa do sorteio automático. ### Fluxo 1. Admin Unity chama `POST brindes/habilitar-ingresso` quando quiser que **o próximo** sorteio entregue um ingresso. 2. Participante sorteia normalmente via `POST brindes/sortear`. 3. Se o ingresso estava habilitado, **esse** sorteio entrega o ingresso (100% garantido). 4. A habilitação é **consumida** — sorteios seguintes voltam ao automático até nova habilitação. 5. Consulte `GET brindes/ingresso/status` para saber se há ingresso pendente. ### Habilitar próximo sorteio como ingresso **POST** `?route=brindes/habilitar-ingresso` (body vazio `{}`) **Response (200):** ```json { "success": true, "data": { "proximo_sorteio_ingresso": true, "habilitado_em": "2026-06-09 14:30:00", "ingresso": { "id": 5, "nome": "Par de ingressos para o Village", "codigo": "ingresso_village", "quantidade_cadastrada": 60, "quantidade_distribuida": 0, "quantidade_disponivel": 60 } }, "message": "Próximo sorteio habilitado para ingresso." } ``` ### Consultar status **GET** `?route=brindes/ingresso/status` Retorna `proximo_sorteio_ingresso: true/false` e dados do brinde ingresso. ### Resposta do sorteio quando ingresso reservado O campo `sorteio_reservado_ingresso: true` indica que aquele sorteio usou a reserva manual do admin. ```csharp public IEnumerator HabilitarProximoIngresso(Action onSuccess, Action onError) { yield return Post("brindes/habilitar-ingresso", "{}", (responseText) => onSuccess?.Invoke(JsonUtility.FromJson(responseText)), onError); } public IEnumerator ObterStatusIngresso(Action onSuccess, Action onError) { yield return Get("brindes/ingresso/status", (responseText) => onSuccess?.Invoke(JsonUtility.FromJson(responseText)), onError); } ``` ## Idempotência com `referencia` Se a Unity reenviar a mesma requisição de sorteio (timeout, clique duplo, retry), envie o mesmo UUID em `referencia`. A API retorna a entrega original sem consumir outro brinde. --- ## Configuração Unity 1. **Player Settings → Other Settings → Allow downloads over HTTP**: habilitar para `http://localhost` (Unity 2022+). 2. **Api Compatibility Level**: `.NET 4.x` ou `.NET Standard 2.1`. 3. Testar primeiro no Editor apontando para `http://localhost/fanzone/api/index.php`. 4. Em build para dispositivo na mesma rede, trocar `baseUrl` para o IP da máquina XAMPP (ex.: `http://192.168.1.10/fanzone/api/index.php`). --- ## Checklist de integração - [ ] XAMPP Apache + MySQL rodando - [ ] SQL do banco executado (`database/fanzone.sql`) - [ ] `BrindeApiClient` na cena com URL correta - [ ] Cena Tablet: formulário nome/e-mail/telefone + botão sortear - [ ] Cena Tablet: exibir brinde retornado em `data.brinde.nome` - [ ] Cena Admin: lista inventário, repõe estoque, edita peso/liberação - [ ] Tratamento de erros 409 (estoque esgotado) na UI - [ ] Teste de retry com mesma `referencia` - [ ] Fila offline: sync com campo `brinde` ao reconectar