funifier-mcp 0.2.26 → 0.2.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/check-update.d.ts +5 -0
- package/dist/mcp/check-update.d.ts.map +1 -1
- package/dist/mcp/check-update.js +21 -10
- package/dist/mcp/check-update.js.map +1 -1
- package/dist/mcp/check-update.test.d.ts +2 -0
- package/dist/mcp/check-update.test.d.ts.map +1 -0
- package/dist/mcp/check-update.test.js +33 -0
- package/dist/mcp/check-update.test.js.map +1 -0
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/prompts/templates.d.ts.map +1 -1
- package/dist/mcp/prompts/templates.js +35 -0
- package/dist/mcp/prompts/templates.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +28 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,102 +1,152 @@
|
|
|
1
|
-
# Patterns
|
|
1
|
+
# Patterns — Padrões de Implementação Funifier
|
|
2
|
+
|
|
3
|
+
## 1. Visão Geral
|
|
4
|
+
|
|
5
|
+
### 1.1 O que é este documento
|
|
6
|
+
|
|
7
|
+
Este documento descreve patterns de implementação Funifier para apps client-side sem backend próprio: cadastro em área pública, envio de email, login social, pagamento recorrente, integração entre gamificações, isolamento de dados, conversa por voz com IA e acesso correto à API de Player e Database.
|
|
8
|
+
|
|
9
|
+
### 1.2 Quando consultar
|
|
10
|
+
|
|
11
|
+
- Consulte ao implementar **cadastro (signup)** de jogador em área pública sem expor `/v3/player`.
|
|
12
|
+
- Consulte ao **enviar email** a partir de uma trigger com conteúdo editável pelo admin.
|
|
13
|
+
- Consulte ao implementar **login com Google** num app sem backend.
|
|
14
|
+
- Consulte ao criar **lead no CRM** automaticamente no signup (gamificação de produto → gamificação de CRM).
|
|
15
|
+
- Consulte ao implementar **pagamento/assinatura recorrente** usando Public Endpoints como proxy para um gateway (Asaas).
|
|
16
|
+
- Consulte ao implementar **conversa por voz/vídeo com IA** (OpenAI Realtime + WebRTC).
|
|
17
|
+
- Consulte ao corrigir **vazamento de dados entre usuários** em apps que usam `localStorage`.
|
|
18
|
+
- Consulte ao ler/escrever **datas e tipos BSON** via `/v3/database`.
|
|
19
|
+
- Consulte ao descobrir que **campos do player somem ao salvar** ou que **queries retornam tudo/vazio**.
|
|
20
|
+
- Consulte ao implementar **alteração ou reset de senha**.
|
|
21
|
+
- Consulte ao corrigir bugs de **scope, digest ou iOS** em frontends AngularJS.
|
|
22
|
+
- Consulte ao tratar **cache de browser** servindo código antigo após deploy.
|
|
23
|
+
|
|
24
|
+
### 1.3 Quando NÃO consultar
|
|
25
|
+
|
|
26
|
+
- **NÃO** consulte para entender como um módulo de feature funciona (achievement, challenge, leaderboard, level, lottery, etc.) — use o módulo correspondente via `index.md`.
|
|
27
|
+
- **NÃO** consulte para referência de campos/métodos Java — use `guides/java-entities.md` e `guides/java-managers.md`.
|
|
28
|
+
- **NÃO** consulte para a mecânica interna de triggers, public endpoints ou schedulers — use `modules/trigger.md`, `modules/public.md`, `modules/scheduler.md`. Este documento usa esses recursos; não os documenta.
|
|
29
|
+
- **NÃO** consulte para queries/aggregates avançados — use `guides/aggregates.md`.
|
|
30
|
+
|
|
31
|
+
### 1.4 Índice de decisão
|
|
32
|
+
|
|
33
|
+
| Problema / Situação | Pattern | Seção |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| Cadastro em área pública sem expor `/v3/player` | Signup | §2 |
|
|
36
|
+
| Email a partir de trigger, conteúdo editável pelo admin | Email Template | §3 |
|
|
37
|
+
| Login com Google sem backend próprio | Google OAuth | §4 |
|
|
38
|
+
| Criar lead no CRM ao cadastrar jogador (cross-gamificação) | CRM Integration | §5 |
|
|
39
|
+
| Assinatura/pagamento recorrente sem backend próprio | Payment Gateway (Asaas) | §6 |
|
|
40
|
+
| Conversa por voz/vídeo com IA | OpenAI Realtime | §7 |
|
|
41
|
+
| Vazamento de dados entre usuários (cache `localStorage`) | Cross-User Data Isolation | §8 |
|
|
42
|
+
| Datas/tipos quebrando ao ler+escrever em `/v3/database` | Database Strict Mode | §9 |
|
|
43
|
+
| Campos do player somem ao salvar / queries retornam tudo ou vazio | Acesso à API (Player & Database) | §10 |
|
|
44
|
+
| Alterar ou resetar senha de jogador | Alteração de Senha | §11 |
|
|
45
|
+
| Bugs de scope/digest/iOS em frontend AngularJS | Frontend AngularJS/iOS | §12 |
|
|
46
|
+
| Browser servindo versão em cache após deploy | Versionamento & Cache-Busting | §13 |
|
|
47
|
+
|
|
48
|
+
### 1.5 Restrições globais críticas
|
|
49
|
+
|
|
50
|
+
Internalize estas regras **antes** de implementar qualquer pattern. Elas afetam múltiplos patterns e, quando violadas, causam perda silenciosa de dados ou scripts.
|
|
51
|
+
|
|
52
|
+
> ⚠️ **PUT dispara `before_update`; POST dispara `before_create`.**
|
|
53
|
+
> Consequência de errar: a trigger esperada não roda e o signup/handler é ignorado sem erro visível.
|
|
54
|
+
> Correto: para acionar uma trigger `before_update` (ex: Signup §2), o frontend **deve** usar `PUT`. Eventos confirmados em `modules/trigger.md` §3.1.
|
|
55
|
+
|
|
56
|
+
> ⚠️ **`POST /v3/public` é upsert COMPLETO (full replace).**
|
|
57
|
+
> Consequência de enviar payload parcial (ex: `{"_id":"slug","timeout":30}`): o campo `script` é apagado e o endpoint para de funcionar.
|
|
58
|
+
> Correto: sempre reenvie **todos** os campos (`active`, `method`, `timeout`, `script`) ao atualizar. Pipeline em `modules/public.md` §4 (`POST /v3/public — upsert`).
|
|
59
|
+
|
|
60
|
+
> ⚠️ **Timeout de scripts difere por tipo.** O default não é o mesmo nos três runtimes.
|
|
61
|
+
>
|
|
62
|
+
> | Runtime | Default | Configurável via | Teto `@TimedInterrupt` (independente) |
|
|
63
|
+
> |---|---|---|---|
|
|
64
|
+
> | Public Endpoint | **10s** | `POST /v3/public` campo `timeout` | 5s (em loops/métodos) |
|
|
65
|
+
> | Trigger | **5s** | `POST /v3/trigger` campo `timeout` | 200s |
|
|
66
|
+
> | Scheduler | **30s** | `POST /v3/scheduler` campo `timeout` | 2000s |
|
|
67
|
+
>
|
|
68
|
+
> Valor em **segundos**; `null` ou `<= 0` usa o default. O campo `timeout` **não aparece no formulário do Studio** — só via API. O `@TimedInterrupt` é uma segunda camada independente: aumentar `timeout` não eleva o teto do `@TimedInterrupt`. Em Public Endpoints o teto (5s) é **menor** que o default do executor (10s). Fontes: `modules/public.md` §5, `modules/trigger.md` §2.8, `modules/scheduler.md`.
|
|
69
|
+
|
|
70
|
+
> ⚠️ **O sandbox de Public Endpoints bloqueia classes que funcionam em Triggers/Schedulers.**
|
|
71
|
+
> Consequência de assumir paridade: código que roda na trigger lança `MissingPropertyException`/`ClassCastException` no Public Endpoint.
|
|
72
|
+
> Restrições aplicam-se **apenas** a Public Endpoints (mesmo `SecureASTCustomizer`/`TriggerExpressionChecker` descrito em `modules/public.md` §2). Em Triggers e Schedulers, `com.*`/`org.*` e Unirest/BCrypt **funcionam**.
|
|
73
|
+
>
|
|
74
|
+
> | Bloqueado em Public Endpoint | Erro / motivo | Permitido em Public Endpoint |
|
|
75
|
+
> |---|---|---|
|
|
76
|
+
> | `com.*` / `org.*` fully-qualified | `MissingPropertyException: No such property: com/org` | `java.net.URL` / `openConnection()` (HTTP) |
|
|
77
|
+
> | `BCrypt` (qualquer forma) | indisponível no sandbox | `java.security.MessageDigest` (SHA-256) |
|
|
78
|
+
> | `Class.forName()` | `ClassNotFoundException` | `java.util.Base64`, `java.net.URLEncoder` |
|
|
79
|
+
> | `groovy.json.JsonSlurper` / `JsonOutput` | `ClassCastException: [B incompatible with [C` | `java.text.SimpleDateFormat` |
|
|
80
|
+
> | regex `=~` | bloqueado pelo `SecureASTCustomizer` | `while` loops |
|
|
81
|
+
> | `for` + `return` em sequência | parser error (`expecting '}', found 'return'`) | `String.split/replace`, `StringBuilder` |
|
|
82
|
+
> | `System.currentTimeMillis()` | bloqueado | `new Date().getTime()` |
|
|
83
|
+
> | `instanceof` | bloqueado | `getClass().getName()` ou try/catch |
|
|
84
|
+
> | Unirest (`com.mashape.*`) | é `com.*` → bloqueado | `manager.get*Manager()`, `new Player()` |
|
|
85
|
+
>
|
|
86
|
+
> Implicação: para hashear senha num Public Endpoint, delegue ao trigger `before_update` de `signup__c` (que tem BCrypt). Para JSON, parseie manualmente. Para HTTP, use `java.net.URL`.
|
|
87
|
+
|
|
88
|
+
> ⚠️ **Use `strict=true` em todo GET/escrita do `/v3/database`.** (Detalhe completo em §9.)
|
|
89
|
+
> Consequência de omitir: campos `Date` são lidos como número/string; ao reescrever, o MongoDB grava o tipo errado e queries por data (`$gt`, `_sort=-created`) param de funcionar.
|
|
90
|
+
> Não se aplica a entidades nativas (`/v3/player`, `/v3/action`, etc.), que já têm tipagem própria.
|
|
91
|
+
|
|
92
|
+
> ⚠️ **No `/v3/database`, o parâmetro de filtro é `q` (sintaxe Mongo), não `_filter`.**
|
|
93
|
+
> Consequência: `_filter`, `_sort`, `_limit` são **silenciosamente ignorados** — a query retorna todos os registros sem filtro. Confirmado em `modules/database.md` §4.2.
|
|
94
|
+
> Correto: `?q=campo:"valor"&strict=true`. Para ordenar, use `POST /v3/database/{collection}/aggregate` com `$sort` (o GET de listagem **não** ordena).
|
|
95
|
+
|
|
96
|
+
> ⚠️ **`player` é entidade nativa — nunca exponha escrita pública e nunca use `/v3/database/player`.** (Detalhe completo em §10.)
|
|
97
|
+
> Consequência: `PUT /v3/database/player` faz replace e perde campos; expor `/v3/player` em token público abre escrita irrestrita.
|
|
98
|
+
> O `password` enviado no `POST/PUT /v3/player` é gravado **como veio, sem hash** (`modules/player.md` §3.1) — por isso o hash é feito em trigger (§2, §11).
|
|
2
99
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Quando usar
|
|
100
|
+
---
|
|
6
101
|
|
|
7
|
-
|
|
8
|
-
- Como referência de boas práticas de segurança e arquitetura
|
|
9
|
-
- Para garantir consistência entre projetos
|
|
102
|
+
## 2. Signup Pattern
|
|
10
103
|
|
|
11
|
-
|
|
104
|
+
**Quando usar:** cadastro de jogador a partir de frontend público, sem backend próprio.
|
|
105
|
+
**Não usar quando:** o cadastro ocorre em área já autenticada (use `POST /v3/player` direto com token de sessão).
|
|
106
|
+
**Depende de:** Custom Object (`signup__c`), Trigger `before_update`, Role `public`, restrições globais §1.5 (PUT vs POST, password não-hasheado).
|
|
107
|
+
**Referências:** `modules/custom-object.md` (CRUD genérico `__c`), `modules/trigger.md` §3.1 (eventos), `guides/triggers-guide.md` (script/managers), `modules/security.md` §2.2 (role `public`, Basic público → scope da role), `modules/player.md` §3.1 (`password` gravado sem hash).
|
|
12
108
|
|
|
13
|
-
|
|
109
|
+
### Problema
|
|
14
110
|
|
|
15
|
-
|
|
111
|
+
O frontend é público, então qualquer token nele embutido é visível. Dar a esse token escrita em `/v3/player` permitiria criar/alterar qualquer jogador. Além disso, o `password` enviado a `/v3/player` é gravado sem hash — gravar senha em texto plano a partir do cliente é inaceitável.
|
|
16
112
|
|
|
17
|
-
|
|
113
|
+
### Solução
|
|
18
114
|
|
|
19
|
-
|
|
115
|
+
Expor apenas escrita na coleção `signup__c`. Uma trigger `before_update` valida os dados, cria o `Player` com senha hasheada via BCrypt no servidor e devolve o resultado. O token público recebe o scope da role `public` — escrita em `signup__c` e nada mais.
|
|
20
116
|
|
|
21
117
|
```
|
|
22
|
-
Frontend (público) → PUT /v3/database/signup__c →
|
|
118
|
+
Frontend (público) → PUT /v3/database/signup__c → trigger before_update → cria Player (BCrypt)
|
|
119
|
+
→ grava status/message em signup__c
|
|
23
120
|
```
|
|
24
121
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
### Vantagens
|
|
28
|
-
|
|
29
|
-
- O token público só tem permissão de escrita em `signup__c` — não acessa `/v3/player`
|
|
30
|
-
- A senha é criptografada no servidor via BCrypt (nunca armazenada em texto plano)
|
|
31
|
-
- Validação server-side (campos obrigatórios, duplicidade de username)
|
|
32
|
-
- Dados sensíveis (password, email) são removidos do registro `signup__c` após processamento
|
|
33
|
-
- Permite adicionar lógica extra (e-mail de boas-vindas, atribuição de equipe, etc.)
|
|
34
|
-
|
|
35
|
-
### Configuração
|
|
36
|
-
|
|
37
|
-
#### 1. Criar Role "public"
|
|
38
|
-
|
|
39
|
-
Em **Studio > Security > Roles**, criar:
|
|
40
|
-
|
|
41
|
-
| Campo | Valor |
|
|
42
|
-
|-------|-------|
|
|
43
|
-
| Role | `public` |
|
|
44
|
-
| Scope | `write_database_signup__c` |
|
|
45
|
-
| Timeout | `1d` |
|
|
46
|
-
|
|
47
|
-
#### 2. Gerar Token Basic
|
|
48
|
-
|
|
49
|
-
O token Basic é gerado a partir da API Key da gamificação, sem usuário/senha:
|
|
122
|
+
### Implementação
|
|
50
123
|
|
|
51
|
-
|
|
52
|
-
1. Concatenar: ApiKey + ":"
|
|
53
|
-
2. Codificar em Base64
|
|
54
|
-
3. Prefixar com "Basic "
|
|
55
|
-
4. Usar no header: Authorization: Basic <token>
|
|
56
|
-
```
|
|
124
|
+
**1. Role `public`** (Studio → Security → Roles): scope `write_database_signup__c`, timeout `1d`. O Basic gerado só com a API Key (sem secret) recebe o scope dessa role (`modules/security.md` §2.2).
|
|
57
125
|
|
|
58
|
-
**
|
|
126
|
+
**2. Token Basic público** — API Key + `":"`, em Base64:
|
|
59
127
|
```javascript
|
|
60
|
-
var BASIC_TOKEN = 'Basic ' + btoa(API_KEY + ':');
|
|
128
|
+
var BASIC_TOKEN = 'Basic ' + btoa(API_KEY + ':'); // só permissões da role public
|
|
61
129
|
```
|
|
62
130
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
#### 3. Habilitar Password Required
|
|
66
|
-
|
|
67
|
-
Em **Studio > Security**, habilitar a opção **Password Required**. Isso garante que o login exija senha, já que agora as senhas são gerenciadas de forma segura via trigger.
|
|
68
|
-
|
|
69
|
-
#### 4. Criar Trigger
|
|
70
|
-
|
|
71
|
-
Em **Studio > Trigger**, criar:
|
|
131
|
+
**3. Studio → Security:** habilitar **Password Required** (login passa a exigir senha, agora gerida via trigger).
|
|
72
132
|
|
|
73
|
-
|
|
74
|
-
|-------|-------|
|
|
75
|
-
| Name | Signup Handler |
|
|
76
|
-
| Entity | `signup__c` |
|
|
77
|
-
| Event | `before_update` |
|
|
78
|
-
|
|
79
|
-
**Script:**
|
|
133
|
+
**4. Trigger** (Studio → Trigger): entity `signup__c`, event `before_update`.
|
|
80
134
|
```java
|
|
81
135
|
void trigger(event, entity, player, database){
|
|
82
136
|
entity.status = "Unauthorized";
|
|
83
137
|
if(entity.password == null || entity.password.trim().length() == 0) {
|
|
84
138
|
entity.message = "Senha deve ser informada para se cadastrar!";
|
|
85
|
-
}
|
|
86
|
-
else if(entity.email == null || entity.email.trim().length() == 0) {
|
|
139
|
+
} else if(entity.email == null || entity.email.trim().length() == 0) {
|
|
87
140
|
entity.message = "Email deve ser informado para se cadastrar!";
|
|
88
|
-
}
|
|
89
|
-
else if(entity.name == null || entity.name.trim().length() == 0) {
|
|
141
|
+
} else if(entity.name == null || entity.name.trim().length() == 0) {
|
|
90
142
|
entity.message = "Nome deve ser informado para se cadastrar!";
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
143
|
+
} else {
|
|
93
144
|
Player current = manager.getPlayerManager().findById(entity._id);
|
|
94
145
|
if(current != null) {
|
|
95
146
|
entity.message = "Este usuário já está cadastrado!";
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
// Criptografa senha e salva jogador
|
|
147
|
+
} else {
|
|
99
148
|
Player p = JsonUtil.fromJson(JsonUtil.toJson(entity), Player.class);
|
|
149
|
+
// hash server-side — o POST /v3/player NÃO hasheia (ver §1.5)
|
|
100
150
|
p.password = com.funifier.engine.util.BCrypt.hashpw(
|
|
101
151
|
entity.password, com.funifier.engine.util.BCrypt.gensalt()
|
|
102
152
|
);
|
|
@@ -106,991 +156,646 @@ void trigger(event, entity, player, database){
|
|
|
106
156
|
entity.status = "OK";
|
|
107
157
|
}
|
|
108
158
|
}
|
|
109
|
-
//
|
|
159
|
+
// signup__c vira log de tentativas sem dados sensíveis
|
|
110
160
|
entity.remove("password");
|
|
111
161
|
entity.remove("email");
|
|
112
162
|
}
|
|
113
163
|
```
|
|
114
164
|
|
|
115
|
-
|
|
116
|
-
|
|
165
|
+
**5. Frontend** — usar `PUT` (não `POST`; ver §1.5):
|
|
117
166
|
```javascript
|
|
118
|
-
// Token público — seguro para código client-side
|
|
119
|
-
var BASIC_TOKEN = 'Basic ' + btoa(API_KEY + ':');
|
|
120
|
-
|
|
121
|
-
// Cadastro via signup__c — DEVE usar PUT (não POST!)
|
|
122
|
-
// PUT dispara a trigger before_update; POST dispara before_create
|
|
123
167
|
$http.put(API + '/v3/database/signup__c', {
|
|
124
|
-
_id: username,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
password: password
|
|
128
|
-
}, {
|
|
129
|
-
headers: { 'Authorization': BASIC_TOKEN }
|
|
130
|
-
}).then(function(response) {
|
|
131
|
-
// Verificar response.data.status === "OK"
|
|
132
|
-
});
|
|
168
|
+
_id: username, name: fullName, email: email, password: password
|
|
169
|
+
}, { headers: { 'Authorization': BASIC_TOKEN } })
|
|
170
|
+
.then(function(response) { /* checar response.data.status === "OK" */ });
|
|
133
171
|
```
|
|
134
172
|
|
|
135
|
-
|
|
173
|
+
Extensões na trigger após o `insert`: email de boas-vindas (§3), termos de uso (campo `terms` validado), atribuição de equipe, campos extras.
|
|
174
|
+
|
|
175
|
+
### Armadilhas conhecidas
|
|
136
176
|
|
|
137
|
-
- **
|
|
138
|
-
- **
|
|
139
|
-
- **
|
|
140
|
-
- **Campos extras:** Qualquer campo adicional pode ser passado e tratado na trigger
|
|
177
|
+
- **Usar POST em vez de PUT** → dispara `before_create`, não `before_update`; a trigger de signup não roda. Causa: eventos são distintos por método HTTP. Correção: usar `PUT` (ver §1.5).
|
|
178
|
+
- **Hashear senha no frontend ou esperar que `/v3/player` hasheie** → senha gravada errada. Causa: `POST /v3/player` grava `password` como veio (`modules/player.md` §3.1). Correção: hash via BCrypt na trigger.
|
|
179
|
+
- **Esquecer `entity.remove("password"/"email")`** → o registro `signup__c` (público) retém dados sensíveis. Correção: remover antes do retorno; remover **depois** de qualquer uso (ex: enviar email — §3).
|
|
141
180
|
|
|
142
181
|
---
|
|
143
182
|
|
|
144
|
-
## Email Template Pattern
|
|
183
|
+
## 3. Email Template Pattern
|
|
145
184
|
|
|
146
|
-
**
|
|
185
|
+
**Quando usar:** enviar email via SMTP a partir de uma trigger, com conteúdo editável pelo admin sem mexer em código.
|
|
186
|
+
**Não usar quando:** basta uma notificação push/in-app — use o módulo Notification (`modules/notification.md`).
|
|
187
|
+
**Depende de:** Trigger, Custom Object (`email_template__c`), SMTP da gamificação configurado.
|
|
188
|
+
**Referências:** `guides/java-libraries.md` (`EmailBuilder`/SimpleJavaMail, `JsonUtil`), `modules/trigger.md` (runtime Java), `modules/custom-object.md` (coleção `__c`), `modules/studio-page.md` (página de edição opcional).
|
|
147
189
|
|
|
148
|
-
|
|
190
|
+
### Problema
|
|
149
191
|
|
|
150
|
-
|
|
192
|
+
Hardcodar HTML de email dentro do script da trigger acopla conteúdo a código: qualquer ajuste de texto exige redeploy do script, e o admin não consegue editar.
|
|
193
|
+
|
|
194
|
+
### Solução
|
|
195
|
+
|
|
196
|
+
Guardar cada template como documento em `email_template__c` com variáveis Mustache. A trigger busca o template, faz `MustacheUtils.parse()` e envia via `MailContext` da gamificação. (`MustacheUtils` e `MailContext` são utilitários do runtime de trigger; não estão no guia de bibliotecas — só `EmailBuilder` está.)
|
|
151
197
|
|
|
152
198
|
```
|
|
153
|
-
email_template__c (
|
|
199
|
+
email_template__c (editável) → trigger (findOne + parse Mustache) → SMTP (MailContext) → envia
|
|
154
200
|
```
|
|
155
201
|
|
|
156
|
-
###
|
|
157
|
-
|
|
158
|
-
Cada template é um documento na coleção `email_template__c`:
|
|
202
|
+
### Implementação
|
|
159
203
|
|
|
204
|
+
**Template** (`email_template__c`):
|
|
160
205
|
```json
|
|
161
206
|
{
|
|
162
207
|
"_id": "signup_email_confirm",
|
|
163
208
|
"fromName": "Funifier Team",
|
|
164
209
|
"fromEmail": "no-reply@funifier.com",
|
|
165
210
|
"subject": "Funifier : Email Confirmation",
|
|
166
|
-
"content": "Hi {{user.name}}<br/><br/>
|
|
211
|
+
"content": "Hi {{user.name}}<br/><br/>Confirme seu email:<br/><a href=\"{{link}}\">Confirmar</a>"
|
|
167
212
|
}
|
|
168
213
|
```
|
|
169
214
|
|
|
170
|
-
**
|
|
171
|
-
|
|
172
|
-
### 2. Uso na Trigger
|
|
173
|
-
|
|
215
|
+
**Na trigger:**
|
|
174
216
|
```java
|
|
175
|
-
// 1.
|
|
176
|
-
|
|
177
|
-
org.bson.Document template = (org.bson.Document) templateObj;
|
|
217
|
+
// 1. buscar template
|
|
218
|
+
org.bson.Document template = (org.bson.Document) database.findOne("email_template__c", "{_id: 'signup_welcome'}");
|
|
178
219
|
|
|
179
|
-
// 2.
|
|
180
|
-
// Opção A: usar a própria entity (se já tem os campos necessários)
|
|
181
|
-
// Opção B: montar um mapa de valores calculados
|
|
220
|
+
// 2. valores de substituição Mustache
|
|
182
221
|
Map values = new HashMap();
|
|
183
222
|
values.put("name", entity.name);
|
|
184
|
-
values.put("email", entity.email);
|
|
185
223
|
values.put("link", "https://app.exemplo.com/confirm/" + entity._id);
|
|
186
224
|
|
|
187
|
-
// 3.
|
|
225
|
+
// 3. parse {{variavel}}
|
|
188
226
|
String subject = com.funifier.engine.util.MustacheUtils.parse(template.getString("subject"), values);
|
|
189
227
|
String content = com.funifier.engine.util.MustacheUtils.parse(template.getString("content"), values);
|
|
190
228
|
|
|
191
|
-
// 4.
|
|
229
|
+
// 4. enviar
|
|
192
230
|
Email email = EmailBuilder.startingBlank()
|
|
193
231
|
.from(template.getString("fromName"), template.getString("fromEmail"))
|
|
194
232
|
.to(entity.name, entity.email)
|
|
195
|
-
.withSubject(subject)
|
|
196
|
-
.withHTMLText(content)
|
|
197
|
-
.buildEmail();
|
|
233
|
+
.withSubject(subject).withHTMLText(content).buildEmail();
|
|
198
234
|
|
|
199
|
-
com.funifier.engine.mail.MailContext ctx =
|
|
235
|
+
com.funifier.engine.mail.MailContext ctx =
|
|
236
|
+
com.funifier.controller.Configuration.getCurrentConfiguration().getMailContext();
|
|
200
237
|
MailerBuilder.withSMTPServer(ctx.hostName, ctx.smtpPort, ctx.authUser, ctx.authPassword)
|
|
201
|
-
.buildMailer()
|
|
202
|
-
.sendMail(email);
|
|
238
|
+
.buildMailer().sendMail(email);
|
|
203
239
|
```
|
|
204
240
|
|
|
205
|
-
### 3. Página Customizada no Studio (opcional)
|
|
206
|
-
|
|
207
|
-
Criar uma página customizada em **Studio > Pages** para que o administrador possa editar os templates de email visualmente, sem acessar a API ou o banco diretamente.
|
|
208
|
-
|
|
209
|
-
### Vantagens
|
|
210
|
-
|
|
211
|
-
- **Separação de responsabilidades:** conteúdo do email separado da lógica da trigger
|
|
212
|
-
- **Administrável:** admin edita templates sem alterar código
|
|
213
|
-
- **Reutilizável:** mesmo template pode ser usado em múltiplas triggers
|
|
214
|
-
- **Flexível:** variáveis Mustache permitem personalização dinâmica
|
|
215
|
-
- **Auditável:** templates ficam versionados na coleção `email_template__c`
|
|
216
|
-
|
|
217
|
-
### Classes Disponíveis no Contexto
|
|
218
|
-
|
|
219
241
|
| Classe | Uso |
|
|
220
242
|
|--------|-----|
|
|
221
|
-
| `EmailBuilder` | Construir email (
|
|
222
|
-
| `
|
|
223
|
-
| `com.funifier.
|
|
224
|
-
| `com.funifier.
|
|
225
|
-
| `com.funifier.engine.util.MustacheUtils` | Parse de templates Mustache |
|
|
243
|
+
| `EmailBuilder` / `MailerBuilder` | Construir email/mailer (SimpleJavaMail — ver `guides/java-libraries.md`) |
|
|
244
|
+
| `com.funifier.engine.mail.MailContext` | Config SMTP (`hostName`, `smtpPort`, `authUser`, `authPassword`) |
|
|
245
|
+
| `com.funifier.controller.Configuration` | Configuração atual da gamificação |
|
|
246
|
+
| `com.funifier.engine.util.MustacheUtils` | `parse(template, valuesMap)` |
|
|
226
247
|
|
|
227
|
-
|
|
248
|
+
Opcional: criar página em **Studio → Pages** para o admin editar os templates visualmente (`modules/studio-page.md`).
|
|
228
249
|
|
|
229
|
-
|
|
230
|
-
- `ctx.smtpPort` — porta SMTP
|
|
231
|
-
- `ctx.authUser` — usuário de autenticação
|
|
232
|
-
- `ctx.authPassword` — senha de autenticação
|
|
250
|
+
### Armadilhas conhecidas
|
|
233
251
|
|
|
234
|
-
|
|
252
|
+
- **Enviar email no Signup (§2) depois de `entity.remove("email")`** → destinatário nulo. Causa: o campo já foi removido. Correção: enviar **antes** do `remove`.
|
|
253
|
+
- **Tentar enviar email a partir de um Public Endpoint** → `com.*`/`org.*` bloqueados (§1.5); `EmailBuilder` e `MailContext` não estão acessíveis. Correção: envie de trigger/scheduler, não de Public Endpoint.
|
|
235
254
|
|
|
236
255
|
---
|
|
237
256
|
|
|
238
|
-
|
|
257
|
+
## 4. Google OAuth Login Pattern
|
|
239
258
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
---
|
|
259
|
+
**Quando usar:** login com Google em app Funifier sem backend próprio.
|
|
260
|
+
**Não usar quando:** login por usuário/senha — use `POST /v3/auth/token` grant `password` (`modules/auth.md`).
|
|
261
|
+
**Depende de:** Public Endpoint (verificação de token + geração de auth token), Signup Pattern §2 (para criar player com hash), restrições §1.5 (sandbox).
|
|
262
|
+
**Referências:** `modules/public.md` (runtime/sandbox), `modules/auth.md` §2.1 (`POST /v3/auth/token`).
|
|
246
263
|
|
|
247
|
-
|
|
264
|
+
### Problema
|
|
248
265
|
|
|
249
|
-
|
|
266
|
+
A verificação do `id_token` do Google e a emissão do token Funifier precisam acontecer server-side (a API key não pode ficar exposta, e o player precisa existir com senha hasheada). Sem backend próprio, não há onde rodar essa lógica.
|
|
250
267
|
|
|
251
|
-
|
|
268
|
+
### Solução
|
|
252
269
|
|
|
253
|
-
|
|
270
|
+
Google Sign-In (GSI) no frontend obtém o `id_token`; um Public Endpoint `google_login` verifica o token, cria o player (via `signup__c` PUT — §2 — para obter o hash BCrypt, já que o sandbox do Public Endpoint não tem BCrypt) e emite o auth token.
|
|
254
271
|
|
|
255
272
|
```
|
|
256
|
-
Frontend (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
v
|
|
262
|
-
Asaas API (gateway de pagamento BR)
|
|
263
|
-
|
|
|
264
|
-
v (webhook async)
|
|
265
|
-
Funifier Public Endpoint (webhook receiver)
|
|
266
|
-
|
|
|
267
|
-
v
|
|
268
|
-
Player.extra.plan (atualiza plano do jogador via Jongo)
|
|
273
|
+
Frontend (GSI renderButton) → id_token → Public Endpoint "google_login"
|
|
274
|
+
├─ verifica token (Google tokeninfo via java.net.URL)
|
|
275
|
+
├─ se player não existe: cria via signup__c PUT (hash BCrypt na trigger)
|
|
276
|
+
├─ gera token (POST /v3/auth/token via java.net.URL)
|
|
277
|
+
└─ retorna token + player
|
|
269
278
|
```
|
|
270
279
|
|
|
271
|
-
###
|
|
272
|
-
|
|
273
|
-
| Endpoint | Metodo | Funcao |
|
|
274
|
-
|----------|--------|--------|
|
|
275
|
-
| `validate_coupon` | POST | Valida cupom de desconto em `coupon__c` |
|
|
276
|
-
| `create_subscription` | POST | Cria customer + subscription no Asaas, retorna URL de pagamento |
|
|
277
|
-
| `asaas_webhook` | POST | Recebe eventos de pagamento do Asaas, atualiza plano do jogador |
|
|
278
|
-
| `manage_subscription` | POST | Cancel, downgrade, reactivate, status — gerencia assinatura existente |
|
|
279
|
-
|
|
280
|
-
### Fluxo de Assinatura (Novo Usuario)
|
|
281
|
-
|
|
282
|
-
1. Usuario completa onboarding e escolhe plano (Standard/Premium)
|
|
283
|
-
2. Frontend chama `create_subscription` com: `playerId`, `planType`, `couponCode` (opcional)
|
|
284
|
-
3. Endpoint cria customer no Asaas (ou reutiliza existente via `externalReference`)
|
|
285
|
-
4. Endpoint cria subscription no Asaas com:
|
|
286
|
-
- `billingType: "UNDEFINED"` (cliente escolhe Pix/cartao/boleto no checkout)
|
|
287
|
-
- `nextDueDate` com trial (ex: `DateUtil.fromKeyword("+7d")`)
|
|
288
|
-
- `externalReference: playerId` (vincula ao jogador Funifier)
|
|
289
|
-
5. Endpoint retorna `invoiceUrl` (checkout hospedado do Asaas)
|
|
290
|
-
6. Frontend redireciona usuario para `invoiceUrl`
|
|
291
|
-
7. Usuario paga no checkout Asaas
|
|
292
|
-
8. Asaas envia webhook `PAYMENT_CONFIRMED` para `asaas_webhook`
|
|
293
|
-
9. Webhook atualiza `player.extra.plan` via Jongo
|
|
294
|
-
|
|
295
|
-
### Sistema de Cupons
|
|
296
|
-
|
|
297
|
-
Colecao `coupon__c` com estrutura:
|
|
280
|
+
### Implementação
|
|
298
281
|
|
|
299
|
-
|
|
300
|
-
{
|
|
301
|
-
"_id": "FITEVOLVE20",
|
|
302
|
-
"type": "PERCENTAGE",
|
|
303
|
-
"value": 20,
|
|
304
|
-
"maxUses": 100,
|
|
305
|
-
"usedCount": 0,
|
|
306
|
-
"active": true
|
|
307
|
-
}
|
|
308
|
-
```
|
|
282
|
+
No Public Endpoint, HTTP via `java.net.URL` (Unirest é `com.*` → bloqueado, §1.5). Properties do player em Groovy via acesso direto (`newPlayer._id = email`), não setters. Plano padrão para usuários Google: Standard.
|
|
309
283
|
|
|
310
|
-
|
|
284
|
+
### Armadilhas conhecidas
|
|
311
285
|
|
|
312
|
-
|
|
286
|
+
- **Usar `google.accounts.id.prompt()` (One Tap)** → falha em mobile. Causa: limitação do One Tap em browsers móveis. Correção: `google.accounts.id.renderButton()` direto no DOM.
|
|
287
|
+
- **`<a href="#" ng-click="...">`** → o `#` reseta o hash antes do `ng-click` e causa redirect. Correção: `href=""` (ver §12).
|
|
288
|
+
- **`$scope.$apply` em callback async do GSI** → erro "already in digest". Correção: `$scope.$applyAsync` (ver §12).
|
|
289
|
+
- **Tentar hashear senha no Public Endpoint** → BCrypt indisponível no sandbox (§1.5). Correção: delegar criação ao `signup__c` PUT (§2).
|
|
313
290
|
|
|
314
|
-
|
|
291
|
+
---
|
|
315
292
|
|
|
316
|
-
|
|
317
|
-
{
|
|
318
|
-
"type": "premium",
|
|
319
|
-
"plan_status": "active",
|
|
320
|
-
"asaas_customer_id": "cus_xxx",
|
|
321
|
-
"asaas_subscription_id": "sub_xxx",
|
|
322
|
-
"plan_end_date": null,
|
|
323
|
-
"pending_plan": null,
|
|
324
|
-
"plan_downgrade_date": null,
|
|
325
|
-
"changesUsed": 0
|
|
326
|
-
}
|
|
327
|
-
```
|
|
293
|
+
## 5. CRM Integration Pattern (Cross-Gamification)
|
|
328
294
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
| `asaas_customer_id` | ID do customer no Asaas |
|
|
334
|
-
| `asaas_subscription_id` | ID da subscription ativa no Asaas |
|
|
335
|
-
| `plan_end_date` | Data fim do acesso apos cancelamento |
|
|
336
|
-
| `pending_plan` | Plano futuro em caso de downgrade |
|
|
337
|
-
| `plan_downgrade_date` | Data em que o downgrade sera efetivado |
|
|
295
|
+
**Quando usar:** criar lead/deal no CRM Funifier (gamificação B) automaticamente quando um jogador se cadastra na gamificação de produto (gamificação A).
|
|
296
|
+
**Não usar quando:** produto e CRM são a mesma gamificação — escreva direto nas coleções locais.
|
|
297
|
+
**Depende de:** Trigger `after_create` (entity `player`), HTTP saindo da trigger, App token do CRM.
|
|
298
|
+
**Referências:** `guides/java-libraries.md` (Unirest), `modules/trigger.md` (eventos/runtime), `modules/database.md` (`/v3/database`), `modules/security.md` (scope do App).
|
|
338
299
|
|
|
339
|
-
###
|
|
300
|
+
### Problema
|
|
340
301
|
|
|
341
|
-
|
|
342
|
-
|------|--------|
|
|
343
|
-
| **Cancel** | Cancela subscription no Asaas. Seta `plan_status: "canceled"` e `plan_end_date` = fim do ciclo atual. Usuario mantem acesso ate `plan_end_date`. |
|
|
344
|
-
| **Downgrade** | Cancela Premium no Asaas, cria nova Standard com `nextDueDate` = fim do ciclo Premium. Seta `plan_status: "pending_downgrade"`, `pending_plan: "standard"`. |
|
|
345
|
-
| **Reactivate** | Cria nova subscription no Asaas (com trial). Reseta `plan_status: "active"`. Suporta cupom. |
|
|
346
|
-
| **Status** | Consulta subscription no Asaas e retorna status, proximo vencimento, valor. |
|
|
302
|
+
Cada gamificação é um tenant isolado. O app de produto não tem acesso direto às coleções do CRM; é preciso uma chamada autenticada de uma gamificação para a outra, no momento certo do ciclo de vida do jogador.
|
|
347
303
|
|
|
348
|
-
###
|
|
304
|
+
### Solução
|
|
349
305
|
|
|
350
|
-
|
|
351
|
-
|-------------|------|
|
|
352
|
-
| `PAYMENT_CONFIRMED` / `PAYMENT_RECEIVED` | Ativa plano. Se `pending_downgrade`, efetiva downgrade. |
|
|
353
|
-
| `PAYMENT_OVERDUE` | Marca como inadimplente (opcional). |
|
|
354
|
-
| `PAYMENT_DELETED` / `PAYMENT_REFUNDED` | Cancela plano. |
|
|
306
|
+
Trigger `after_create` na gamificação de produto faz `HTTP POST` para os endpoints `/v3/database/person` e `/v3/database/deal` da gamificação de CRM, usando um App token (não-expirável) do CRM. Em trigger, `com.*` é permitido → Unirest funciona.
|
|
355
307
|
|
|
356
|
-
|
|
308
|
+
```
|
|
309
|
+
Gamificação Produto → trigger after_create (player) → HTTP POST → Gamificação CRM (/v3/database/person + /deal)
|
|
310
|
+
```
|
|
357
311
|
|
|
358
|
-
|
|
359
|
-
- Registrar dominio do app nas configuracoes do Asaas (para callback de checkout)
|
|
360
|
-
- Webhook criado via API: `POST /v3/webhooks` no Asaas
|
|
312
|
+
### Implementação
|
|
361
313
|
|
|
362
|
-
|
|
314
|
+
Use App token do CRM (Security → Apps → ícone do olho gera o Basic). Deals exigem `owner`, `add_time`, `visible_to` para aparecer no Kanban. Um único CRM atende vários produtos — diferencie por Pipeline.
|
|
363
315
|
|
|
364
|
-
|
|
365
|
-
// 1. Escapar $ (MongoDB operators e API keys)
|
|
366
|
-
def d = String.valueOf((char)0x24) // = "$"
|
|
367
|
-
def setCmd = '{"' + d + 'set": {"extra.plan.type": "premium"}}'
|
|
316
|
+
### Armadilhas conhecidas
|
|
368
317
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
.with(setCmd)
|
|
373
|
-
|
|
374
|
-
// 3. Chamar API externa (Asaas) via Unirest
|
|
375
|
-
def response = Unirest.post("https://api.asaas.com/v3/subscriptions")
|
|
376
|
-
.header("Content-Type", "application/json")
|
|
377
|
-
.header("access_token", asaasApiKey)
|
|
378
|
-
.body(JsonUtil.toJson(bodyMap))
|
|
379
|
-
.asString()
|
|
380
|
-
def result = new groovy.json.JsonSlurper().parseText(response.getBody())
|
|
381
|
-
|
|
382
|
-
// 4. Datas com DateUtil
|
|
383
|
-
def trialEnd = DateUtil.fromKeyword("+7d") // 7 dias no futuro
|
|
384
|
-
|
|
385
|
-
// 5. Ler payload do request
|
|
386
|
-
def slurper = new groovy.json.JsonSlurper()
|
|
387
|
-
def body = slurper.parseText(JsonUtil.toJson(payload))
|
|
388
|
-
```
|
|
318
|
+
- **Usar `/v3/crm/*` com Basic auth** → 404. Causa: rotas CRM não respondem a Basic. Correção: usar `/v3/database/person` e `/v3/database/deal`.
|
|
319
|
+
- **Scope do App do CRM sem a palavra `database`** → writes falham silenciosamente (201 sem persistência). Causa: `AuthBean.getScope` exige `database` no scope para escrita em `/v3/database` (`modules/security.md` §2.1). Correção: incluir `database` no scope do App.
|
|
320
|
+
- **Deal sem `owner`/`add_time`/`visible_to`** → não aparece no Kanban. Correção: incluir esses campos.
|
|
389
321
|
|
|
390
|
-
|
|
322
|
+
---
|
|
391
323
|
|
|
392
|
-
|
|
393
|
-
2. **Asaas `billingType: CREDIT_CARD`** e obrigatorio para cobranças recorrentes automaticas; PIX requer `chargeType: DETACHED`
|
|
394
|
-
3. **`externalReference`** no Asaas e essencial para vincular subscription ao playerId
|
|
395
|
-
4. **Dominio deve ser registrado no Asaas** para callbacks de checkout funcionarem
|
|
396
|
-
5. **Scripts Groovy no Funifier tem timeout de 5s** — chamadas HTTP devem ser rapidas
|
|
397
|
-
6. **Jongo com dotted notation** (`extra.plan.type`) funciona corretamente em `$set`
|
|
398
|
-
7. **Nunca usar `import`** nos scripts — ver `public.md` → Script Runtime Environment
|
|
399
|
-
8. **NÃO usar `groovy.json.JsonSlurper`** para parsear responses — usar `JsonUtil.fromJsonToMap()` (evita `LazyMap` → `ClassCastException`)
|
|
400
|
-
9. **Player.extra** é campo público — acessar direto, salvar com `insert()` (upsert)
|
|
401
|
-
10. **`instanceof` bloqueado** pelo SecureAST — usar alternativas
|
|
402
|
-
11. **Public Endpoints podem ser atualizados via API** — `POST /v3/public` com `Basic` auth (apiKey)
|
|
324
|
+
## 6. Payment Gateway Pattern (Asaas + Public Endpoints)
|
|
403
325
|
|
|
404
|
-
|
|
326
|
+
**Quando usar:** assinatura recorrente num app Funifier sem backend próprio, integrando o gateway Asaas.
|
|
327
|
+
**Não usar quando:** pagamento avulso simples sem recorrência, ou gateway com SDK client-side seguro.
|
|
328
|
+
**Depende de:** Public Endpoints (proxy server-side), Jongo (escrita em player), restrições §1.5 (sandbox, timeouts).
|
|
329
|
+
**Referências:** `modules/public.md` (endpoints/sandbox/timeout), `guides/database-access.md` §3 (Jongo), `guides/java-libraries.md` (`DateUtil`, `JsonUtil`), `modules/player.md` (`extra`).
|
|
405
330
|
|
|
406
|
-
|
|
331
|
+
### Problema
|
|
407
332
|
|
|
408
|
-
|
|
333
|
+
Chamar a API do gateway direto do frontend expõe a API key e esbarra em CORS. Sem backend próprio, falta um lugar server-side para criar customer/subscription e receber o webhook de confirmação.
|
|
409
334
|
|
|
410
|
-
|
|
335
|
+
### Solução
|
|
411
336
|
|
|
412
|
-
|
|
337
|
+
Public Endpoints atuam como proxy server-side para o Asaas (escondem a key, evitam CORS) e como receptor de webhook. O plano do jogador vive em `player.extra.plan`, atualizado via Jongo.
|
|
413
338
|
|
|
414
339
|
```
|
|
415
|
-
|
|
340
|
+
Frontend → Public Endpoint (create_subscription) → Asaas API → invoiceUrl → checkout
|
|
341
|
+
Asaas → webhook PAYMENT_CONFIRMED → Public Endpoint (asaas_webhook) → player.extra.plan (Jongo)
|
|
416
342
|
```
|
|
417
|
-
```json
|
|
418
|
-
{
|
|
419
|
-
"_id": "ricardo",
|
|
420
|
-
"name": "Ricardo",
|
|
421
|
-
"created": 1772145212606
|
|
422
|
-
}
|
|
423
|
-
```
|
|
424
|
-
O campo `created` aparece como número (timestamp). Se salvar isso de volta, o MongoDB armazena como `Number`, não como `Date`.
|
|
425
343
|
|
|
426
|
-
|
|
344
|
+
| Endpoint | Método | Função |
|
|
345
|
+
|----------|--------|--------|
|
|
346
|
+
| `validate_coupon` | POST | Valida cupom em `coupon__c` |
|
|
347
|
+
| `create_subscription` | POST | Cria customer + subscription, retorna `invoiceUrl` |
|
|
348
|
+
| `asaas_webhook` | POST | Recebe eventos de pagamento, atualiza plano |
|
|
349
|
+
| `manage_subscription` | POST | Cancel / downgrade / reactivate / status |
|
|
427
350
|
|
|
428
|
-
|
|
429
|
-
GET /v3/database/player/ricardo?strict=true
|
|
430
|
-
```
|
|
431
|
-
```json
|
|
432
|
-
{
|
|
433
|
-
"_id": "ricardo",
|
|
434
|
-
"name": "Ricardo",
|
|
435
|
-
"created": { "$date": "2026-02-26T22:33:32.606Z" }
|
|
436
|
-
}
|
|
437
|
-
```
|
|
438
|
-
O campo `created` vem com o tipo `$date`. Ao salvar de volta, o MongoDB preserva como `Date`.
|
|
351
|
+
### Implementação
|
|
439
352
|
|
|
440
|
-
|
|
353
|
+
**Fluxo (novo usuário):** frontend chama `create_subscription` (`playerId`, `planType`, `couponCode?`) → endpoint cria customer no Asaas (reusa via `externalReference`) e subscription (`billingType:"UNDEFINED"`, `nextDueDate` com trial `DateUtil.fromKeyword("+7d")`, `externalReference: playerId`) → retorna `invoiceUrl` → usuário paga → Asaas dispara `PAYMENT_CONFIRMED` → webhook atualiza `player.extra.plan`.
|
|
441
354
|
|
|
442
|
-
|
|
443
|
-
2. **Ao acessar campos tipados, usar o formato BSON:** `record.created.$date` em vez de `record.created`
|
|
444
|
-
3. **Ao salvar (PUT/POST), enviar os dados no formato BSON** para preservar tipos:
|
|
445
|
-
```json
|
|
446
|
-
{ "created": { "$date": "2026-02-27T10:00:00.000Z" } }
|
|
447
|
-
```
|
|
448
|
-
4. **Ao criar novos registros com datas**, formatar assim:
|
|
449
|
-
```javascript
|
|
450
|
-
{ created: { $date: new Date().toISOString() } }
|
|
451
|
-
```
|
|
355
|
+
**`player.extra.plan`:** `type` (`standard`|`premium`), `plan_status` (`active`|`canceled`|`pending_downgrade`), `asaas_customer_id`, `asaas_subscription_id`, `plan_end_date`, `pending_plan`, `plan_downgrade_date`, `changesUsed`.
|
|
452
356
|
|
|
453
|
-
|
|
357
|
+
**Cupons (`coupon__c`):** `type` (`PERCENTAGE`|`FIXED`), `value`, `maxUses`, `usedCount`, `active`. `validate_coupon` valida; `create_subscription` aplica no `value`.
|
|
454
358
|
|
|
455
|
-
|
|
456
|
-
|------|-------------------|---------|
|
|
457
|
-
| Date | `{ "$date": "ISO-8601" }` | `{ "$date": "2026-02-27T10:00:00.000Z" }` |
|
|
458
|
-
| ObjectId | `{ "$oid": "hex" }` | `{ "$oid": "69a16149434ba01017676d07" }` |
|
|
459
|
-
| Long | `{ "$numberLong": "num" }` | `{ "$numberLong": "1234567890" }` |
|
|
359
|
+
**Gestão:** Cancel → cancela no Asaas, `plan_status:"canceled"` + `plan_end_date` = fim do ciclo (mantém acesso até lá). Downgrade → cancela Premium, cria Standard com `nextDueDate` = fim do ciclo Premium, `plan_status:"pending_downgrade"`. Reactivate → nova subscription com trial. Status → consulta o Asaas.
|
|
460
360
|
|
|
461
|
-
|
|
361
|
+
**Webhook:** `PAYMENT_CONFIRMED`/`PAYMENT_RECEIVED` → ativa plano (efetiva downgrade pendente); `PAYMENT_OVERDUE` → inadimplente; `PAYMENT_DELETED`/`PAYMENT_REFUNDED` → cancela. Segurança: validar `authToken` no header/body; registrar o domínio do app no Asaas (callback do checkout).
|
|
462
362
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
363
|
+
**Padrões Groovy (Public Endpoint):**
|
|
364
|
+
```groovy
|
|
365
|
+
// escapar $ (operadores Mongo / chaves)
|
|
366
|
+
def d = String.valueOf((char)0x24)
|
|
367
|
+
def setCmd = '{"' + d + 'set": {"extra.plan.type": "premium"}}'
|
|
468
368
|
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
if (!field) return null;
|
|
472
|
-
if (field.$date) return new Date(field.$date);
|
|
473
|
-
if (typeof field === 'string') return new Date(field);
|
|
474
|
-
if (typeof field === 'number') return new Date(field);
|
|
475
|
-
return null;
|
|
476
|
-
}
|
|
477
|
-
```
|
|
369
|
+
// atualizar player via Jongo (PlayerManager não tem update) — ver guides/database-access.md §3
|
|
370
|
+
manager.getJongoConnection().getCollection("player").update("{_id: #}", playerId).with(setCmd)
|
|
478
371
|
|
|
479
|
-
|
|
372
|
+
// trial: data no futuro
|
|
373
|
+
def trialEnd = DateUtil.fromKeyword("+7d")
|
|
480
374
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
- Dados perdem integridade progressivamente a cada leitura+escrita
|
|
375
|
+
// ler payload e parsear response SEM JsonSlurper (ver §1.5)
|
|
376
|
+
def body = JsonUtil.fromJsonToMap(JsonUtil.toJson(payload))
|
|
377
|
+
```
|
|
485
378
|
|
|
486
|
-
###
|
|
379
|
+
### Armadilhas conhecidas
|
|
487
380
|
|
|
488
|
-
-
|
|
489
|
-
- **
|
|
381
|
+
- **`JsonSlurper`/`JsonOutput` no Public Endpoint** → `ClassCastException: [B incompatible with [C`. Causa: bloqueio do sandbox (§1.5). Correção: `JsonUtil.fromJsonToMap()`.
|
|
382
|
+
- **Unirest no Public Endpoint** → `com.mashape.*` é `com.*` → bloqueado (§1.5). Correção: `java.net.URL`/`openConnection()`.
|
|
383
|
+
- **`billingType: CREDIT_CARD` esperando PIX recorrente** → PIX exige `chargeType: DETACHED`; `CREDIT_CARD` é obrigatório para cobrança recorrente automática. Use `UNDEFINED` para o cliente escolher no checkout.
|
|
384
|
+
- **Domínio não registrado no Asaas** → callback do checkout não funciona. Correção: registrar o domínio.
|
|
385
|
+
- **HTTP lento no Public Endpoint** → estoura o timeout de 10s (§1.5). Correção: chamadas rápidas; aumentar `timeout` via API se necessário.
|
|
386
|
+
- **`instanceof` no script** → bloqueado (§1.5). Correção: `getClass().getName()` ou try/catch.
|
|
490
387
|
|
|
491
388
|
---
|
|
492
389
|
|
|
493
|
-
##
|
|
390
|
+
## 7. OpenAI Realtime — Voz/Vídeo Pattern
|
|
391
|
+
|
|
392
|
+
**Quando usar:** conversa por voz/vídeo com IA num app Funifier (OpenAI Realtime + WebRTC).
|
|
393
|
+
**Não usar quando:** basta chat por texto (use a API de Chat Completions diretamente).
|
|
394
|
+
**Depende de:** Public Endpoint (dados do usuário + API key), OpenAI Realtime API.
|
|
395
|
+
**Referências:** `modules/public.md` (endpoint para servir dados/key).
|
|
494
396
|
|
|
495
|
-
|
|
397
|
+
### Problema
|
|
496
398
|
|
|
497
|
-
|
|
399
|
+
A chave efêmera do OpenAI e os dados do usuário precisam ser montados server-side, e as tools da IA precisam ser registradas no momento certo — caso contrário a IA não "vê" as ferramentas e nunca as chama.
|
|
498
400
|
|
|
499
|
-
###
|
|
401
|
+
### Solução
|
|
402
|
+
|
|
403
|
+
Um Public Endpoint retorna dados do usuário + API key; o frontend gera a chave efêmera com as tools **embutidas na criação** e estabelece WebRTC.
|
|
500
404
|
|
|
501
405
|
```
|
|
502
|
-
|
|
406
|
+
Frontend → Public Endpoint (dados + key)
|
|
407
|
+
→ POST /v1/realtime/client_secrets (chave efêmera, tools incluídas)
|
|
408
|
+
→ POST /v1/realtime/calls (SDP/WebRTC) → data channel "oai-events"
|
|
503
409
|
```
|
|
504
410
|
|
|
505
|
-
- **MINOR** — Incrementa +1 a cada alteração pequena (bug fix, ajuste visual, texto). Ex: `1.0` → `1.1` → `1.2` → ... → `1.120`
|
|
506
|
-
- **MAJOR** — Incrementa +1 e zera o MINOR em mudanças grandes (refatoração, nova feature significativa, redesign). Ex: `1.120` → `2.0`
|
|
507
|
-
|
|
508
411
|
### Implementação
|
|
509
412
|
|
|
510
|
-
|
|
413
|
+
Endpoints/parâmetros atuais (2026-03):
|
|
414
|
+
- Chave efêmera: `POST /v1/realtime/client_secrets` (não o antigo `/v1/realtime/sessions`).
|
|
415
|
+
- SDP: `POST /v1/realtime/calls` (não `/v1/realtime?model=...`).
|
|
416
|
+
- Modelo: `gpt-realtime-mini` (hardcode como safety net; não o nome completo).
|
|
417
|
+
- `voice` em `session.audio.output.voice` (não `session.voice`); `input_audio_transcription` não é suportado dentro de `session` no `client_secrets`.
|
|
511
418
|
|
|
512
|
-
|
|
513
|
-
var CONFIG = {
|
|
514
|
-
// ... outras configs
|
|
515
|
-
VERSION: '1.0'
|
|
516
|
-
};
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
#### 2. Expor no `$rootScope` (AngularJS)
|
|
419
|
+
> ⚠️ **Tools DEVEM ser registradas na criação da chave efêmera.** `session.update` via data channel **não** aplica tools de forma confiável. Inclua `tools` no body do `POST /v1/realtime/client_secrets`; envie `session.update` apenas como backup.
|
|
520
420
|
|
|
521
421
|
```javascript
|
|
522
|
-
|
|
523
|
-
|
|
422
|
+
fetch('https://api.openai.com/v1/realtime/client_secrets', {
|
|
423
|
+
method: 'POST',
|
|
424
|
+
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
|
|
425
|
+
body: JSON.stringify({ session: {
|
|
426
|
+
type: 'realtime', model: 'gpt-realtime-mini', instructions: instructions,
|
|
427
|
+
tools: voiceTools, // ← obrigatório aqui
|
|
428
|
+
audio: { output: { voice: 'coral' } }
|
|
429
|
+
}})
|
|
524
430
|
});
|
|
525
431
|
```
|
|
526
432
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
```css
|
|
536
|
-
.version-footer {
|
|
537
|
-
text-align: center;
|
|
538
|
-
font-size: 11px;
|
|
539
|
-
color: rgba(255,255,255,0.2);
|
|
540
|
-
padding: 8px 0 calc(env(safe-area-inset-bottom, 0px) + 80px);
|
|
541
|
-
letter-spacing: 0.5px;
|
|
542
|
-
}
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
### Regras
|
|
546
|
-
|
|
547
|
-
1. **Sempre incrementar a versão** ao fazer deploy — nunca fazer deploy sem mudar a versão
|
|
548
|
-
2. **Checar a versão no rodapé** após deploy para confirmar que o cache foi atualizado
|
|
549
|
-
3. Se a versão exibida não bate com a esperada → cache desatualizado → forçar refresh (Ctrl+Shift+R)
|
|
550
|
-
4. Em caso de dúvida sobre qual versão está rodando, basta olhar o rodapé
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
## Campo `timeout` em Scripts (Public Endpoint, Trigger, Scheduler)
|
|
554
|
-
|
|
555
|
-
O timeout de execucao dos scripts e controlado por um executor Java (`Future.get(timeout, TimeUnit.SECONDS)`). O padrao e **10 segundos**. Para scripts que precisam de mais tempo (chamadas HTTP externas, BCrypt, etc.), o timeout pode ser customizado via API:
|
|
556
|
-
|
|
557
|
-
```bash
|
|
558
|
-
# Public Endpoint
|
|
559
|
-
POST /v3/public → {"_id": "slug", "timeout": 30}
|
|
560
|
-
|
|
561
|
-
# Trigger
|
|
562
|
-
POST /v3/trigger → {"_id": "trigger_id", "timeout": 30}
|
|
563
|
-
|
|
564
|
-
# Scheduler
|
|
565
|
-
POST /v3/scheduler → {"_id": "scheduler_id", "timeout": 30}
|
|
433
|
+
Incluir sempre uma tool `end_call` para a IA encerrar a ligação ao fim natural da conversa; o handler chama a mesma função do botão "Desligar", com delay para a IA terminar de falar:
|
|
434
|
+
```javascript
|
|
435
|
+
case 'response.function_call_arguments.done':
|
|
436
|
+
if (evt.name === 'end_call') {
|
|
437
|
+
sendToolResult(evt.call_id, { success: true });
|
|
438
|
+
setTimeout(function() { $scope.endCall(); $scope.$applyAsync(); }, 2000);
|
|
439
|
+
}
|
|
440
|
+
break;
|
|
566
441
|
```
|
|
567
442
|
|
|
568
|
-
|
|
569
|
-
- Campo `timeout` **nao aparece no formulario do Studio** — so via API
|
|
570
|
-
- `null` = usa padrao de 10 segundos
|
|
571
|
-
- Fonte: classe Java `PublicEndpoint` (campo `public Long timeout`) + metodo `executor()` que usa `TimeUnit.SECONDS`
|
|
572
|
-
- Nota: `@TimedInterrupt(5s)` no wrapper Groovy e uma segunda camada de protecao independente
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
## CRITICO: POST parcial em /v3/public apaga o script!
|
|
576
|
-
|
|
577
|
-
O endpoint `POST /v3/public` faz **upsert completo** — se voce enviar apenas `{"_id": "slug", "timeout": 30}`, o campo `script` sera apagado (setado como null/vazio) porque nao foi incluido no payload.
|
|
443
|
+
### Armadilhas conhecidas
|
|
578
444
|
|
|
579
|
-
**
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
"_id": "slug",
|
|
583
|
-
"active": true,
|
|
584
|
-
"method": "POST",
|
|
585
|
-
"timeout": 30,
|
|
586
|
-
"script": "public Object handle(Object payload) { ... }"
|
|
587
|
-
}
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
**Nunca faca update parcial** como `{"_id": "slug", "timeout": 30}` — isso apaga o script!
|
|
445
|
+
- **Registrar tools só via `session.update`** → a IA pode nunca chamá-las. Causa: aplicação não confiável via data channel. Correção: incluir `tools` na criação da chave efêmera.
|
|
446
|
+
- **Usar o nome completo do modelo ou endpoints antigos** → falha de criação da sessão. Correção: `gpt-realtime-mini` + endpoints 2026-03.
|
|
447
|
+
- **`voice` em `session.voice`** → ignorado. Correção: `session.audio.output.voice`.
|
|
591
448
|
|
|
592
449
|
---
|
|
593
450
|
|
|
594
|
-
##
|
|
595
|
-
|
|
596
|
-
Resultado de testes extensivos no Orvya (março 2026). O sandbox Groovy do Funifier tem restrições severas via `SecureASTCustomizer`.
|
|
597
|
-
|
|
598
|
-
### BLOQUEADO (não usar)
|
|
599
|
-
|
|
600
|
-
| Item | Erro |
|
|
601
|
-
|------|------|
|
|
602
|
-
| `com.*` fully qualified names | `MissingPropertyException: No such property: com` |
|
|
603
|
-
| `org.*` fully qualified names | `MissingPropertyException: No such property: org` |
|
|
604
|
-
| `BCrypt` (unqualified) | Não disponível no sandbox de Public Endpoints |
|
|
605
|
-
| `Class.forName()` | `ClassNotFoundException` |
|
|
606
|
-
| `groovy.json.JsonSlurper` (para response) | `ClassCastException: [B incompatible with [C` |
|
|
607
|
-
| `groovy.json.JsonOutput` | Mesma `ClassCastException` |
|
|
608
|
-
| Regex `=~` operator | Bloqueado pelo `SecureASTCustomizer` |
|
|
609
|
-
| `for` loop + `return` na sequência | Parser error (`expecting '}', found 'return'`) |
|
|
610
|
-
| `System.currentTimeMillis()` | Bloqueado — usar `new Date().getTime()` |
|
|
611
|
-
| `instanceof` | Bloqueado — usar `getClass().getName()` ou try/catch |
|
|
612
|
-
|
|
613
|
-
### PERMITIDO (seguro para usar)
|
|
614
|
-
|
|
615
|
-
| Item | Notas |
|
|
616
|
-
|------|-------|
|
|
617
|
-
| `java.net.URL` / `openConnection()` | Para chamadas HTTP externas |
|
|
618
|
-
| `java.security.MessageDigest` (SHA-256) | Para hashing |
|
|
619
|
-
| `java.util.Base64` | Encode/decode |
|
|
620
|
-
| `java.net.URLEncoder` | URL encoding |
|
|
621
|
-
| `java.text.SimpleDateFormat` | Formatação de datas |
|
|
622
|
-
| `while` loops | Usar no lugar de `for` quando `return` segue |
|
|
623
|
-
| `String.split()`, `String.replace()`, `StringBuilder` | Manipulação de strings |
|
|
624
|
-
| `manager.getAuthenticationManager()` | Auth operations |
|
|
625
|
-
| `manager.getPlayerManager()` | Player CRUD |
|
|
626
|
-
| `new Player()` com `.id`, `.name`, `.email`, `.password`, `.extra` | Criação de player |
|
|
627
|
-
|
|
628
|
-
### Implicações para Desenvolvimento
|
|
629
|
-
|
|
630
|
-
- **Para HTTP em Public Endpoints:** usar `java.net.URL` (não Unirest — `com.mashape.*` é bloqueado)
|
|
631
|
-
- **Para JSON em Public Endpoints:** parsear manualmente com `while` loops e `String.split()`
|
|
632
|
-
- **Para hashing de senha:** delegar para `signup__c` PUT trigger (que tem acesso ao BCrypt)
|
|
633
|
-
- **Em Triggers e Schedulers:** `com.*` e `org.*` FUNCIONAM normalmente (Unirest, BCrypt, etc.)
|
|
634
|
-
|
|
635
|
-
> ⚠️ Essas restrições se aplicam APENAS a Public Endpoints. Triggers e Schedulers têm acesso completo às classes importadas no wrapper.
|
|
451
|
+
## 8. Cross-User Data Isolation Pattern
|
|
636
452
|
|
|
637
|
-
|
|
453
|
+
**Quando usar:** apps que usam `localStorage` como cache e trocam de usuário no mesmo dispositivo/browser.
|
|
454
|
+
**Não usar quando:** o app não persiste estado por usuário no cliente.
|
|
455
|
+
**Depende de:** §10 (queries corretas no `/v3/database`).
|
|
456
|
+
**Referências:** `modules/database.md` §4.2 (`q`), `guides/aggregates.md` (`$sort` em aggregate).
|
|
638
457
|
|
|
639
|
-
|
|
458
|
+
### Problema
|
|
640
459
|
|
|
641
|
-
|
|
460
|
+
Sem limpar o `localStorage` na troca de usuário, o segundo usuário vê dados em cache do primeiro. Usar o cache como fonte de verdade vaza dados entre contas.
|
|
642
461
|
|
|
643
|
-
|
|
462
|
+
### Solução
|
|
644
463
|
|
|
645
|
-
|
|
464
|
+
Defesa em três camadas: limpar no logout, limpar no login se o usuário mudou, e tratar o DB como fonte de verdade (cache nunca preenche o que o DB devolve vazio).
|
|
646
465
|
|
|
647
466
|
```
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
Funifier Public Endpoint "google_login"
|
|
652
|
-
|→ Verifica token via Google tokeninfo API (java.net.URL)
|
|
653
|
-
|→ Se player não existe: cria via signup__c PUT (para BCrypt hash)
|
|
654
|
-
|→ Gera auth token via POST /v3/auth/token (java.net.URL)
|
|
655
|
-
|→ Retorna token + player info ao frontend
|
|
467
|
+
logout → clearUserData() (remove chaves do app + zera $rootScope)
|
|
468
|
+
login → se localStorage.user != usuário atual → clearUserData()
|
|
469
|
+
loadFromDB → DB é fonte de verdade: campo ausente no DB ⇒ limpa o cache (nunca mantém valor antigo)
|
|
656
470
|
```
|
|
657
471
|
|
|
658
|
-
###
|
|
659
|
-
|
|
660
|
-
1. **Usar `google.accounts.id.renderButton()`** direto no DOM — o `prompt()` (One Tap) falha em mobile
|
|
661
|
-
2. **Nunca usar `<a href="#">` com `ng-click` em AngularJS** — o `#` reseta o hash antes do ng-click, causando redirect. Usar `href=""`
|
|
662
|
-
3. **Google users recebem plano Standard** por padrão
|
|
663
|
-
4. **`$scope.$applyAsync`** em vez de `$scope.$apply` em callbacks async (evita "already in digest")
|
|
664
|
-
5. **Player properties em Groovy:** usar acesso direto (`newPlayer._id = email`) não setter methods
|
|
665
|
-
|
|
666
|
-
---
|
|
667
|
-
|
|
668
|
-
## Cross-User Data Isolation Pattern
|
|
669
|
-
|
|
670
|
-
**Problema:** Em apps que usam localStorage para cache, trocar de usuário sem limpar cache causa vazamento de dados entre usuários.
|
|
671
|
-
|
|
672
|
-
**Solução:** Defesa em 3 camadas:
|
|
472
|
+
### Implementação
|
|
673
473
|
|
|
674
|
-
### Camada 1: Limpar dados no logout
|
|
675
474
|
```javascript
|
|
475
|
+
// Camada 1 — logout
|
|
676
476
|
function clearUserData() {
|
|
677
|
-
// Limpar todas as chaves do app no localStorage
|
|
678
477
|
Object.keys(localStorage).forEach(function(key) {
|
|
679
|
-
if (key.startsWith('fitness_') || key.startsWith('water_'))
|
|
680
|
-
localStorage.removeItem(key);
|
|
681
|
-
}
|
|
478
|
+
if (key.startsWith('fitness_') || key.startsWith('water_')) localStorage.removeItem(key);
|
|
682
479
|
});
|
|
683
|
-
|
|
684
|
-
$rootScope.player = null;
|
|
685
|
-
$rootScope.profileData = null;
|
|
686
|
-
// ... etc
|
|
480
|
+
$rootScope.player = null; $rootScope.profileData = null;
|
|
687
481
|
}
|
|
688
|
-
```
|
|
689
482
|
|
|
690
|
-
|
|
691
|
-
```javascript
|
|
483
|
+
// Camada 2 — login com usuário diferente
|
|
692
484
|
var lastUser = localStorage.getItem('fitness_user');
|
|
693
|
-
if (lastUser && lastUser !== currentUser)
|
|
694
|
-
clearUserData();
|
|
695
|
-
}
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
### Camada 3: DB é source of truth
|
|
699
|
-
- `loadFromDB()` sempre roda após login
|
|
700
|
-
- Se DB não tem dados para um campo, localStorage é LIMPO (não mantém valor antigo)
|
|
701
|
-
- Nunca usar localStorage como fallback quando DB retorna vazio
|
|
702
|
-
|
|
703
|
-
### Lição: Parâmetro correto de filtro é `q`, não `_filter`
|
|
704
|
-
|
|
705
|
-
O endpoint `/v3/database` **NÃO tem** parâmetro `_filter`. O parâmetro correto é `q` com sintaxe MongoDB:
|
|
485
|
+
if (lastUser && lastUser !== currentUser) clearUserData();
|
|
706
486
|
|
|
487
|
+
// Camada 3 — DB é source of truth: loadFromDB() roda após login;
|
|
488
|
+
// se o DB não tem o campo, o localStorage é LIMPO (nunca mantém valor antigo).
|
|
707
489
|
```
|
|
708
|
-
GET /v3/database/body_checkin__c?q=userId:"ricardo@funifier.com"&strict=true
|
|
709
|
-
POST /v3/database/body_checkin__c/aggregate?q=userId:"ricardo@funifier.com"&strict=true
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
`_filter`, `_sort`, `_limit` são **silenciosamente ignorados** pelo backend, fazendo com que a query retorne todos os registros sem filtro.
|
|
713
490
|
|
|
714
|
-
|
|
491
|
+
Query por usuário (filtro com `q`, ordenação com aggregate — ver §1.5):
|
|
715
492
|
```javascript
|
|
716
493
|
$http({
|
|
717
494
|
method: 'POST',
|
|
718
|
-
url: API + '/v3/database/
|
|
495
|
+
url: API + '/v3/database/body_checkin__c/aggregate?q=userId:"' + userId + '"&strict=true',
|
|
719
496
|
headers: { 'Authorization': 'Bearer ' + token, 'Range': 'items=0-19' },
|
|
720
497
|
data: [{ $sort: { created: -1 } }]
|
|
721
498
|
});
|
|
722
499
|
```
|
|
500
|
+
Defense-in-depth client-side: `results = results.filter(function(r){ return r.userId === currentUserId; });`
|
|
723
501
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
502
|
+
### Armadilhas conhecidas
|
|
503
|
+
|
|
504
|
+
- **Usar `_filter`/`_sort`/`_limit` no `/v3/database`** → ignorados; a query traz todos os registros (vazamento entre usuários). Correção: `q` + aggregate `$sort` (§1.5).
|
|
505
|
+
- **Usar `localStorage` como fallback quando o DB devolve vazio** → mantém dado do usuário anterior. Correção: vazio do DB limpa o cache.
|
|
728
506
|
|
|
729
507
|
---
|
|
730
508
|
|
|
731
|
-
##
|
|
509
|
+
## 9. Database Strict Mode Pattern (BSON Types)
|
|
732
510
|
|
|
733
|
-
**
|
|
511
|
+
**Quando usar:** qualquer leitura ou escrita de campos tipados (datas, ObjectId, long) via `/v3/database`.
|
|
512
|
+
**Não usar quando:** acessando entidades nativas (`/v3/player`, `/v3/action`, etc.) — já têm tipagem própria.
|
|
513
|
+
**Depende de:** —
|
|
514
|
+
**Referências:** `modules/database.md` §4.1–§4.2 (`strict`), `guides/database-access.md` §1.
|
|
734
515
|
|
|
735
|
-
|
|
516
|
+
### Problema
|
|
736
517
|
|
|
737
|
-
|
|
518
|
+
Sem `strict`, o `/v3/database` devolve JSON puro: um `Date` vira número (timestamp). Ao reescrever esse valor, o MongoDB grava o campo como `Number` — e consultas por data (`$gt`, `$lt`, `_sort=-created`) param de funcionar. A integridade degrada a cada ciclo leitura+escrita.
|
|
519
|
+
|
|
520
|
+
### Solução
|
|
521
|
+
|
|
522
|
+
Usar `strict=true` em GETs e enviar/ler campos no formato BSON estendido.
|
|
738
523
|
|
|
739
524
|
```
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
→ Trigger after_create (entity: player)
|
|
743
|
-
|
|
|
744
|
-
→ HTTP POST para Gamificação "CRM" (/v3/database/person + /v3/database/deal)
|
|
525
|
+
GET /v3/database/player/ricardo → "created": 1772145212606 (Number — perigoso)
|
|
526
|
+
GET /v3/database/player/ricardo?strict=true → "created": { "$date": "...Z" } (Date — correto)
|
|
745
527
|
```
|
|
746
528
|
|
|
747
|
-
###
|
|
529
|
+
### Implementação
|
|
748
530
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
531
|
+
| Tipo | Formato strict | Exemplo |
|
|
532
|
+
|------|----------------|---------|
|
|
533
|
+
| Date | `{ "$date": "ISO-8601" }` | `{ "$date": "2026-02-27T10:00:00.000Z" }` |
|
|
534
|
+
| ObjectId | `{ "$oid": "hex" }` | `{ "$oid": "69a16149434ba01017676d07" }` |
|
|
535
|
+
| Long | `{ "$numberLong": "num" }` | `{ "$numberLong": "1234567890" }` |
|
|
754
536
|
|
|
755
|
-
|
|
537
|
+
```javascript
|
|
538
|
+
function bsonDate(date) { return { $date: (date || new Date()).toISOString() }; } // escrever
|
|
539
|
+
function readDate(field) { // ler
|
|
540
|
+
if (!field) return null;
|
|
541
|
+
if (field.$date) return new Date(field.$date);
|
|
542
|
+
if (typeof field === 'string' || typeof field === 'number') return new Date(field);
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
Ao acessar campo tipado, usar a forma BSON (`record.created.$date`), não `record.created`.
|
|
756
547
|
|
|
757
|
-
|
|
548
|
+
### Armadilhas conhecidas
|
|
758
549
|
|
|
759
|
-
|
|
550
|
+
- **GET sem `strict=true` seguido de PUT** → o campo `Date` é regravado como `String`/`Number`. Causa: a leitura perdeu o tipo. Correção: ler com `strict=true` e reescrever no formato `{ $date: ... }`.
|
|
551
|
+
- **Aplicar strict em `/v3/player`** → desnecessário e sem efeito útil. Correção: só em `/v3/database`.
|
|
760
552
|
|
|
761
|
-
|
|
762
|
-
// Ler player
|
|
763
|
-
Player p = manager.getPlayerManager().findById(playerId)
|
|
553
|
+
---
|
|
764
554
|
|
|
765
|
-
|
|
766
|
-
p.extra.plan = [type: "premium"]
|
|
555
|
+
## 10. Acesso à API Funifier — Player & Database
|
|
767
556
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
557
|
+
**Quando usar:** ao ler/escrever Player ou coleções customizadas e os campos somem ou as queries retornam tudo/vazio.
|
|
558
|
+
**Não usar quando:** operações cobertas por um módulo específico (use o módulo).
|
|
559
|
+
**Depende de:** §9 (strict), restrições §1.5 (`player` nativo, `q` vs `_filter`).
|
|
560
|
+
**Referências:** `modules/player.md` (entidade, `insert` único ponto de escrita), `modules/database.md` (CRUD/`q`/aggregate), `guides/java-managers.md` (PlayerManager), `modules/custom-object.md` (`__c`).
|
|
771
561
|
|
|
772
|
-
###
|
|
562
|
+
### Problema
|
|
773
563
|
|
|
774
|
-
|
|
564
|
+
`player` é entidade **nativa**, não uma coleção do `/v3/database`. Misturar os dois endpoints, ou enviar objetos parciais, causa perda silenciosa: campos somem, queries voltam vazias e o bug é difícil de rastrear.
|
|
775
565
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
566
|
+
### Solução
|
|
567
|
+
|
|
568
|
+
Endpoints distintos por tipo de recurso, e escrita de player sempre via **read-merge-write** (objeto completo).
|
|
569
|
+
|
|
570
|
+
```
|
|
571
|
+
GET /v3/player/{id} → objeto completo → muta só os campos alvo → POST /v3/player (objeto completo)
|
|
572
|
+
(extra/name/email preservados)
|
|
783
573
|
```
|
|
784
574
|
|
|
785
|
-
|
|
575
|
+
| Recurso | Ler | Salvar | Errado |
|
|
576
|
+
|---------|-----|--------|--------|
|
|
577
|
+
| Player (nativo) | `GET /v3/player/{id}` | `POST /v3/player` (objeto completo) | `GET/PUT /v3/database/player` (404 / replace) |
|
|
578
|
+
| Database (`__c`) | `GET /v3/database/{c}?strict=true&q=_id:'{id}'` | `PUT /v3/database/{c}` (objeto completo) | `GET /v3/database/{c}/{id}` (não existe) |
|
|
579
|
+
| Aggregate | `POST /v3/database/{c}/aggregate?strict=true` | — | `POST /v3/database/{c}` (CRIA documento!) |
|
|
786
580
|
|
|
787
|
-
|
|
581
|
+
### Implementação
|
|
788
582
|
|
|
583
|
+
**Read-merge-write do player** (preserva `extra`, `image`, etc.):
|
|
584
|
+
```javascript
|
|
585
|
+
ApiService.getPlayer(playerId).then(function(res) {
|
|
586
|
+
var player = res.data;
|
|
587
|
+
player.image = imageObj; // muda só o que precisa; extra fica intacto
|
|
588
|
+
$http.post(API + '/v3/player', player, authHeader);
|
|
589
|
+
});
|
|
590
|
+
```
|
|
591
|
+
Ao salvar player com `extra`, **sempre** inclua `name` e `email` (senão são apagados):
|
|
789
592
|
```json
|
|
790
|
-
{
|
|
791
|
-
"image": {
|
|
792
|
-
"small": { "url": "https://...", "size": 0, "width": 0, "height": 0, "depth": 0 },
|
|
793
|
-
"medium": { "url": "https://...", "size": 0, "width": 0, "height": 0, "depth": 0 },
|
|
794
|
-
"original": { "url": "https://...", "size": 0, "width": 0, "height": 0, "depth": 0 }
|
|
795
|
-
}
|
|
796
|
-
}
|
|
593
|
+
{ "_id": "user@email.com", "name": "Nome", "email": "user@email.com", "extra": { } }
|
|
797
594
|
```
|
|
798
595
|
|
|
799
|
-
|
|
596
|
+
**`player.image`** exige a estrutura completa (`size`/`width`/`height`/`depth` obrigatórios, mesmo `0`):
|
|
597
|
+
```json
|
|
598
|
+
{ "image": { "small": {"url":"…","size":0,"width":0,"height":0,"depth":0},
|
|
599
|
+
"medium": {…}, "original": {…} } }
|
|
600
|
+
```
|
|
800
601
|
|
|
801
|
-
|
|
602
|
+
**Query por ID em `__c` retorna ARRAY** — normalizar:
|
|
603
|
+
```javascript
|
|
604
|
+
$http.get(API + "/v3/database/profile__c?strict=true&q=_id:'" + userId + "'", authHeader)
|
|
605
|
+
.then(function(res){ res.data = (Array.isArray(res.data) && res.data[0]) || null; return res; });
|
|
606
|
+
```
|
|
802
607
|
|
|
608
|
+
**PlayerManager em Groovy** (triggers/schedulers/endpoints — `guides/java-managers.md`):
|
|
803
609
|
| Correto | Errado |
|
|
804
610
|
|---------|--------|
|
|
805
611
|
| `pm.findById(id)` | — |
|
|
806
|
-
| `pm.find()` (sem args) | `pm.find("{}")` (signature
|
|
807
|
-
| `pm.insert(player)` | `pm.save(player)` |
|
|
808
|
-
| `player.id`
|
|
612
|
+
| `pm.find()` (sem args) | `pm.find("{}")` (signature inexistente) |
|
|
613
|
+
| `pm.insert(player)` (upsert; único ponto de escrita) | `pm.save(player)` |
|
|
614
|
+
| `player.id` | `player._id` (não funciona em Groovy) |
|
|
809
615
|
| `player.extra` (acesso direto) | `player.getExtra()` (não existe) |
|
|
810
616
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
## AngularJS Patterns (Frontend)
|
|
814
|
-
|
|
815
|
-
Lições aprendidas em projetos AngularJS 1.x com Funifier:
|
|
816
|
-
|
|
817
|
-
### ng-if cria child scope
|
|
818
|
-
`ng-model="varName"` dentro de `ng-if` escreve no child scope, não no controller. **Fix:** usar `$parent.varName`:
|
|
819
|
-
|
|
820
|
-
```html
|
|
821
|
-
<div ng-if="editing">
|
|
822
|
-
<input ng-model="$parent.editName"> <!-- correto -->
|
|
823
|
-
<input ng-model="editName"> <!-- BUG: escreve no child scope -->
|
|
824
|
-
</div>
|
|
825
|
-
```
|
|
826
|
-
|
|
827
|
-
### $q.reject() em vez de Promise.reject()
|
|
828
|
-
Dentro de `$http.then()` chains, nunca usar `Promise.reject()` — o digest cycle do AngularJS não detecta. Usar `$q.reject()`.
|
|
829
|
-
|
|
830
|
-
### input[type="time"] requer Date object
|
|
831
|
-
AngularJS `<input type="time">` não aceita strings `"07:00"`. Converter para `Date` ao carregar:
|
|
832
|
-
|
|
833
|
-
```javascript
|
|
834
|
-
var parts = timeStr.split(':');
|
|
835
|
-
var d = new Date(1970, 0, 1, parseInt(parts[0]), parseInt(parts[1]), 0);
|
|
836
|
-
```
|
|
617
|
+
### Armadilhas conhecidas
|
|
837
618
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
- `navigator.vibrate` não funciona — usar `new Audio('audio/beep.mp3')`
|
|
843
|
-
- `Notification` API não disponível (non-PWA) — `'Notification' in window` é false
|
|
844
|
-
- `beforeinstallprompt` não existe — Apple usa Share → Add to Home Screen
|
|
845
|
-
- `google.accounts.id.prompt()` falha em mobile — renderizar botão oficial
|
|
619
|
+
- **`POST /v3/player` com só `_id` + `extra`** → `name`/`email` apagados. Causa: o save persiste o objeto enviado. Correção: read-merge-write com objeto completo.
|
|
620
|
+
- **`PUT /v3/database/{c}` parcial** → replace total apaga campos não enviados. Correção: enviar objeto completo.
|
|
621
|
+
- **`POST /v3/database/{c}` esperando query** → cria um documento novo. Correção: usar `GET ?q=` para filtrar, `POST .../aggregate` para pipeline.
|
|
622
|
+
- **Tratar a resposta de `?q=_id:` como objeto** → é array. Correção: pegar `[0]`.
|
|
846
623
|
|
|
847
624
|
---
|
|
848
625
|
|
|
849
|
-
##
|
|
626
|
+
## 11. Alteração de Senha Pattern
|
|
850
627
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
3. Frontend estabelece WebRTC via `/v1/realtime/calls` (SDP exchange)
|
|
856
|
-
4. Data channel `oai-events` para controle bidirecional
|
|
628
|
+
**Quando usar:** trocar ou resetar a senha de um jogador (área logada, "esqueci a senha", ou reset administrativo).
|
|
629
|
+
**Não usar quando:** o jogador ainda não existe — use Signup §2.
|
|
630
|
+
**Depende de:** BCrypt (server-side, em trigger), template de email da gamificação (fluxo "esqueci").
|
|
631
|
+
**Referências:** `modules/auth.md` (autenticação), `modules/player.md` §3.1 (`password` não-hasheado no POST/PUT), `modules/security.md`, `modules/trigger.md`.
|
|
857
632
|
|
|
858
|
-
###
|
|
859
|
-
- **Chave efêmera:** `POST /v1/realtime/client_secrets` (NÃO usar o antigo `/v1/realtime/sessions`)
|
|
860
|
-
- **SDP exchange:** `POST /v1/realtime/calls` (NÃO usar o antigo `/v1/realtime?model=...`)
|
|
861
|
-
- **Modelo:** `gpt-realtime-mini` (NÃO usar nome completo `gpt-4o-realtime-mini-2025-01-21`)
|
|
862
|
-
- `input_audio_transcription` NÃO é suportado dentro do objeto `session` no endpoint `client_secrets`
|
|
863
|
-
- `voice` deve estar em `session.audio.output.voice`, NÃO em `session.voice`
|
|
633
|
+
### Problema
|
|
864
634
|
|
|
865
|
-
|
|
635
|
+
A senha é armazenada com BCrypt; não pode ser trocada gravando o campo `password` direto (o `POST/PUT /v3/player` grava sem hash — §1.5). Cada cenário (lembra/esqueceu/admin) usa um caminho distinto.
|
|
866
636
|
|
|
867
|
-
|
|
637
|
+
### Solução
|
|
868
638
|
|
|
869
|
-
|
|
639
|
+
Usar os endpoints nativos de senha; para reset administrativo server-side, hashear com BCrypt numa trigger.
|
|
870
640
|
|
|
871
|
-
```
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
body: JSON.stringify({
|
|
876
|
-
session: {
|
|
877
|
-
type: 'realtime',
|
|
878
|
-
model: 'gpt-realtime-mini',
|
|
879
|
-
instructions: instructions,
|
|
880
|
-
tools: voiceTools, // ← CRITICAL: incluir aqui!
|
|
881
|
-
audio: { output: { voice: 'coral' } }
|
|
882
|
-
}
|
|
883
|
-
})
|
|
884
|
-
});
|
|
641
|
+
```
|
|
642
|
+
Lembra a senha → PUT /v3/player/password?old_password=…&new_password=…
|
|
643
|
+
Esqueceu → GET /v3/player/password/change (email c/ código) → PUT …?code=…&new_password=…
|
|
644
|
+
Reset admin → PUT /v3/database/change_password__c → trigger BCrypt → PlayerManager.insert
|
|
885
645
|
```
|
|
886
646
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
### Tool end_call — Finalização automática de ligação
|
|
647
|
+
### Implementação
|
|
890
648
|
|
|
891
|
-
|
|
649
|
+
**Jogador lembra a senha (área logada):**
|
|
650
|
+
```
|
|
651
|
+
PUT /v3/player/password?player={id}&old_password={atual}&new_password={nova}
|
|
652
|
+
Authorization: Basic {token}
|
|
653
|
+
```
|
|
892
654
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
description: 'Encerra a ligacao quando o usuario disser tchau, que ja entendeu, ou quando a conversa terminar. Sempre se despeca antes de chamar.',
|
|
900
|
-
parameters: { type: 'object', properties: {}, required: [] }
|
|
901
|
-
}
|
|
902
|
-
];
|
|
655
|
+
**Esqueceu a senha (área pública):**
|
|
656
|
+
```
|
|
657
|
+
# 1. solicitar código (enviado por email — usa o template configurado na gamificação)
|
|
658
|
+
GET /v3/player/password/change?player={id}
|
|
659
|
+
# 2. definir nova senha com o código
|
|
660
|
+
PUT /v3/player/password?player={id}&code={código}&new_password={nova}
|
|
903
661
|
```
|
|
904
662
|
|
|
905
|
-
|
|
906
|
-
```
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
}, 2000); // delay para IA terminar de falar
|
|
663
|
+
**Reset via trigger (admin):**
|
|
664
|
+
```groovy
|
|
665
|
+
void trigger(event, entity, player, database) {
|
|
666
|
+
Player p = manager.getPlayerManager().findById(entity._id);
|
|
667
|
+
if (entity.password != null) {
|
|
668
|
+
p.password = com.funifier.engine.util.BCrypt.hashpw(
|
|
669
|
+
entity.password, com.funifier.engine.util.BCrypt.gensalt());
|
|
670
|
+
entity.remove("password");
|
|
914
671
|
}
|
|
915
|
-
|
|
672
|
+
manager.getPlayerManager().insert(p);
|
|
673
|
+
}
|
|
916
674
|
```
|
|
917
|
-
|
|
918
|
-
Nas instruções, mencionar a tool explicitamente:
|
|
919
675
|
```
|
|
920
|
-
|
|
921
|
-
Quando o usuario disser tchau ou pedir para desligar, despeca-se e chame end_call.
|
|
676
|
+
PUT /v3/database/change_password__c { "_id": "playerId", "password": "novaSenha" }
|
|
922
677
|
```
|
|
923
678
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
6. [ ] Tool `end_call` deve chamar a mesma função do botão manual "Desligar"
|
|
931
|
-
7. [ ] Enviar `session.update` via data channel como backup (não como fonte primária)
|
|
932
|
-
8. [ ] Incluir instruções em português com contexto completo do usuário
|
|
679
|
+
Observações: `player` aceita `_id` ou `email`; o `code` expira após uso/timeout; Basic auth é suficiente.
|
|
680
|
+
|
|
681
|
+
### Armadilhas conhecidas
|
|
682
|
+
|
|
683
|
+
- **Gravar `password` direto via `POST /v3/player`** → fica em texto plano (não hasheado, §1.5) e o login falha. Correção: usar os endpoints de senha ou hashear em trigger.
|
|
684
|
+
- **Esquecer `entity.remove("password")` na trigger** → senha em texto plano fica no `change_password__c`. Correção: remover após hashear.
|
|
933
685
|
|
|
934
686
|
---
|
|
935
687
|
|
|
936
|
-
##
|
|
688
|
+
## 12. Frontend AngularJS / iOS Pattern
|
|
937
689
|
|
|
938
|
-
**
|
|
690
|
+
**Quando usar:** apps frontend Funifier em AngularJS 1.x, especialmente em iOS Safari.
|
|
691
|
+
**Não usar quando:** o frontend não usa AngularJS.
|
|
692
|
+
**Depende de:** —
|
|
693
|
+
**Referências:** —
|
|
939
694
|
|
|
940
|
-
###
|
|
695
|
+
### Problema
|
|
941
696
|
|
|
942
|
-
|
|
697
|
+
AngularJS 1.x tem armadilhas de scope/digest que produzem bugs silenciosos (binding no scope errado, promises não detectadas), e o iOS Safari não suporta APIs comuns de PWA.
|
|
943
698
|
|
|
944
|
-
|
|
945
|
-
|----------|-----------|--------------------------|
|
|
946
|
-
| **Ler por ID** | `GET /v3/player/{id}` | `GET /v3/database/player/{id}` (404 ou vazio) |
|
|
947
|
-
| **Salvar/Atualizar** | `POST /v3/player` (com objeto completo) | `PUT /v3/database/player` (faz REPLACE, perde campos) |
|
|
948
|
-
| **Deletar** | `DELETE /v3/player/{id}` | — |
|
|
699
|
+
### Solução
|
|
949
700
|
|
|
950
|
-
|
|
951
|
-
```javascript
|
|
952
|
-
// 1. Ler player completo
|
|
953
|
-
ApiService.getPlayer(playerId).then(function(res) {
|
|
954
|
-
var player = res.data;
|
|
955
|
-
|
|
956
|
-
// 2. Merge apenas os campos que mudaram
|
|
957
|
-
player.image = imageObj; // ex: atualizar foto
|
|
958
|
-
// player.extra permanece intacto!
|
|
959
|
-
|
|
960
|
-
// 3. Salvar objeto completo via POST /v3/player
|
|
961
|
-
$http.post(API + '/v3/player', player, authHeader);
|
|
962
|
-
});
|
|
963
|
-
```
|
|
701
|
+
Aplicar as correções abaixo nos pontos onde cada armadilha aparece.
|
|
964
702
|
|
|
965
|
-
|
|
966
|
-
```javascript
|
|
967
|
-
$http.post(API + '/v3/player', {
|
|
968
|
-
_id: userId,
|
|
969
|
-
name: $rootScope.player.name,
|
|
970
|
-
email: $rootScope.player.email,
|
|
971
|
-
image: imgObj,
|
|
972
|
-
extra: $rootScope.player.extra // SEMPRE incluir extra!
|
|
973
|
-
}, AuthService.authHeader());
|
|
974
|
-
```
|
|
703
|
+
### Implementação
|
|
975
704
|
|
|
976
|
-
|
|
705
|
+
- **`ng-if` cria child scope:** `ng-model` dentro de `ng-if` escreve no child scope. Use `$parent`:
|
|
706
|
+
```html
|
|
707
|
+
<div ng-if="editing"><input ng-model="$parent.editName"></div>
|
|
708
|
+
```
|
|
709
|
+
- **`$q.reject()` (não `Promise.reject()`)** dentro de `$http.then()` — o digest do Angular não detecta `Promise.reject()`.
|
|
710
|
+
- **`<input type="time">` exige `Date`**, não string `"07:00"`:
|
|
711
|
+
```javascript
|
|
712
|
+
var p = timeStr.split(':'); var d = new Date(1970,0,1, +p[0], +p[1], 0);
|
|
713
|
+
```
|
|
977
714
|
|
|
978
|
-
|
|
715
|
+
### Armadilhas conhecidas
|
|
979
716
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
| **Salvar** | `PUT /v3/database/{collection}` (com objeto completo) | — |
|
|
984
|
-
| **Listar/Filtrar** | `GET /v3/database/{collection}?q={mongo-query}&strict=true` | — |
|
|
985
|
-
| **Aggregate** | `POST /v3/database/{collection}/aggregate?strict=true` | `POST /v3/database/{collection}` (CRIA documento!) |
|
|
717
|
+
- **`<a href="#" ng-click="fn()">`** → reseta o hash antes do `ng-click` e causa redirect. Correção: `href=""`. (Também relevante no §4.)
|
|
718
|
+
- **`$scope.$apply` em callback async** → "already in digest". Correção: `$scope.$applyAsync`. (Também no §4 e §7.)
|
|
719
|
+
- **iOS Safari (non-PWA):** `navigator.vibrate` não funciona (use `new Audio('audio/beep.mp3')`); `Notification` indisponível (`'Notification' in window` é `false`); `beforeinstallprompt` não existe (Apple usa Share → Add to Home Screen); `google.accounts.id.prompt()` falha (renderizar botão — §4).
|
|
986
720
|
|
|
987
|
-
|
|
721
|
+
---
|
|
988
722
|
|
|
989
|
-
|
|
723
|
+
## 13. Versionamento & Cache-Busting Pattern
|
|
990
724
|
|
|
991
|
-
**
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
API + "/v3/database/profile__c?strict=true&q=_id:'" + userId + "'",
|
|
996
|
-
authHeader
|
|
997
|
-
).then(function(res) {
|
|
998
|
-
// q= retorna array, pegar primeiro elemento
|
|
999
|
-
res.data = Array.isArray(res.data) && res.data.length > 0 ? res.data[0] : null;
|
|
1000
|
-
return res;
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
|
-
```
|
|
725
|
+
**Quando usar:** SPAs hospedadas (ex: Netlify) onde o browser serve JS/CSS de cache após deploy.
|
|
726
|
+
**Não usar quando:** o host já faz hash de assets por build (ex: bundlers com fingerprint).
|
|
727
|
+
**Depende de:** —
|
|
728
|
+
**Referências:** —
|
|
1004
729
|
|
|
1005
|
-
###
|
|
730
|
+
### Problema
|
|
1006
731
|
|
|
1007
|
-
|
|
1008
|
-
- Datas: ler como `field.$date`, escrever como `{ $date: "ISO-8601" }`
|
|
1009
|
-
- Sem strict, datas viram strings/números e quebram queries
|
|
1010
|
-
- **Não se aplica** a `/v3/player`, `/v3/action`, etc. (entidades nativas)
|
|
732
|
+
O browser serve `app.js`/`api.js`/CSS do cache mesmo após deploy. O usuário vê a versão nova no rodapé (porque `config.js` mudou) mas executa código antigo — bugs "impossíveis" que não reproduzem.
|
|
1011
733
|
|
|
1012
|
-
|
|
734
|
+
### Solução
|
|
1013
735
|
|
|
1014
|
-
|
|
736
|
+
Versão visível no rodapé + query string de versão em todos os assets + headers anti-cache.
|
|
1015
737
|
|
|
1016
|
-
|
|
738
|
+
### Implementação
|
|
1017
739
|
|
|
1018
|
-
**
|
|
740
|
+
**Versão (`MAJOR.MINOR`)** — incrementar MINOR a cada ajuste pequeno; MAJOR (zera MINOR) em mudança grande. Definir em `config.js` e expor:
|
|
741
|
+
```javascript
|
|
742
|
+
var CONFIG = { VERSION: '1.0' };
|
|
743
|
+
app.run(function($rootScope){ $rootScope.appVersion = CONFIG.VERSION; });
|
|
744
|
+
```
|
|
745
|
+
```html
|
|
746
|
+
<div class="version-footer">version {{appVersion}}</div>
|
|
747
|
+
```
|
|
1019
748
|
|
|
749
|
+
**Cache-busting** — `?v=` em todos os `<script>`/`<link>` e headers Netlify:
|
|
1020
750
|
```html
|
|
1021
|
-
<script src="app.js?v=0.20.2"></script>
|
|
1022
751
|
<script src="services/api.js?v=0.20.2"></script>
|
|
1023
752
|
<link rel="stylesheet" href="css/style.css?v=0.20.2">
|
|
1024
753
|
```
|
|
1025
|
-
|
|
1026
|
-
**Complemento:** Arquivo `_headers` na raiz do projeto (Netlify):
|
|
1027
754
|
```
|
|
755
|
+
# _headers (raiz)
|
|
1028
756
|
/*.js
|
|
1029
757
|
Cache-Control: no-cache, must-revalidate
|
|
1030
758
|
/*.css
|
|
1031
759
|
Cache-Control: no-cache, must-revalidate
|
|
1032
760
|
```
|
|
1033
761
|
|
|
1034
|
-
**
|
|
1035
|
-
|
|
1036
|
-
**Checklist de deploy:**
|
|
1037
|
-
1. [ ] Bumpar VERSION nos dois `config.js` (`app/config.js` e `config.js` raiz)
|
|
1038
|
-
2. [ ] Atualizar `?v=` em todos os `<script>` e `<link>` do `index.html`
|
|
1039
|
-
3. [ ] `git add -A && git commit && git push`
|
|
1040
|
-
4. [ ] Deploy via API Netlify (zip)
|
|
1041
|
-
5. [ ] Informar versão ao Ricardo
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
## Alteração de Senha
|
|
1045
|
-
|
|
1046
|
-
A senha do jogador é armazenada criptografada (BCrypt). Não pode ser alterada simplesmente editando o campo password diretamente.
|
|
1047
|
-
|
|
1048
|
-
### Jogador lembra a senha atual (área logada)
|
|
1049
|
-
|
|
1050
|
-
```
|
|
1051
|
-
PUT /v3/player/password?player={playerId}&old_password={senhaAtual}&new_password={novaSenha}
|
|
1052
|
-
Authorization: Basic {token}
|
|
1053
|
-
```
|
|
1054
|
-
|
|
1055
|
-
### Jogador esqueceu a senha (área pública — "Forgot Password")
|
|
1056
|
-
|
|
1057
|
-
**Passo 1:** Solicitar código de recuperação (enviado por email):
|
|
1058
|
-
```
|
|
1059
|
-
GET /v3/player/password/change?player={playerId}
|
|
1060
|
-
Authorization: Basic {token}
|
|
1061
|
-
```
|
|
1062
|
-
|
|
1063
|
-
**Passo 2:** Usar o código recebido por email para definir nova senha:
|
|
1064
|
-
```
|
|
1065
|
-
PUT /v3/player/password?player={playerId}&code={codigoRecebido}&new_password={novaSenha}
|
|
1066
|
-
Authorization: Basic {token}
|
|
1067
|
-
```
|
|
762
|
+
**Checklist de deploy:** (1) bumpar `VERSION` em todos os `config.js`; (2) atualizar `?v=` em todos os assets do `index.html`; (3) commit + push; (4) deploy; (5) conferir a versão no rodapé.
|
|
1068
763
|
|
|
1069
|
-
###
|
|
764
|
+
### Armadilhas conhecidas
|
|
1070
765
|
|
|
1071
|
-
|
|
766
|
+
- **Deploy sem bumpar versão / `?v=`** → browser serve assets antigos; o rodapé não confirma a atualização real do código. Correção: seguir o checklist por completo.
|
|
767
|
+
- **Versão no rodapé não bate com a esperada** → cache desatualizado. Correção: forçar refresh (Ctrl+Shift+R) e revisar `?v=`.
|
|
1072
768
|
|
|
1073
|
-
|
|
1074
|
-
void trigger(event, entity, player, database) {
|
|
1075
|
-
Player p = manager.getPlayerManager().findById(entity._id);
|
|
1076
|
-
if (entity.password != null) {
|
|
1077
|
-
p.password = com.funifier.engine.util.BCrypt.hashpw(
|
|
1078
|
-
entity.password, com.funifier.engine.util.BCrypt.gensalt()
|
|
1079
|
-
);
|
|
1080
|
-
entity.remove("password");
|
|
1081
|
-
}
|
|
1082
|
-
manager.getPlayerManager().insert(p);
|
|
1083
|
-
}
|
|
1084
|
-
```
|
|
1085
|
-
|
|
1086
|
-
Chamada:
|
|
1087
|
-
```
|
|
1088
|
-
PUT /v3/database/change_password__c
|
|
1089
|
-
{"_id": "playerId", "password": "novaSenha"}
|
|
1090
|
-
```
|
|
769
|
+
---
|
|
1091
770
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
771
|
+
## Fontes consultadas
|
|
772
|
+
|
|
773
|
+
Arquivos lidos/inspecionados em `datasource-funifier-docs/knowledge/` durante a Etapa 1. "Inspecionado via grep" significa que confirmei headings e âncoras específicas (não li o arquivo inteiro).
|
|
774
|
+
|
|
775
|
+
| Arquivo | Como foi usado | O que contribuiu |
|
|
776
|
+
|---------|----------------|------------------|
|
|
777
|
+
| `index.md` | Lido | Router da base; confirmou nomes de módulos/guias para as linhas de **Referências**. |
|
|
778
|
+
| `modules/patterns.md` (original) | Lido integral | Fonte de todo o conteúdo experiencial reescrito. |
|
|
779
|
+
| `modules/public.md` | Inspecionado (grep) | **Referenciado.** Confirmou: timeout default 10s + `@TimedInterrupt` 5s (§5), upsert completo do `POST /v3/public` (§4), sandbox `SecureASTCustomizer`/`TriggerExpressionChecker` (§2), vazamento de threads no `executor()`. Base das restrições §1.5 e patterns §4/§6/§7. |
|
|
780
|
+
| `modules/trigger.md` | Inspecionado (grep) | **Referenciado + correção incorporada.** Confirmou eventos `before_create`/`before_update` por método (§3.1) e timeout default **5s** (§2.8) — corrigi o original, que dizia 10s para triggers. |
|
|
781
|
+
| `modules/scheduler.md` | Inspecionado (grep) | **Correção incorporada.** Timeout default **30s** (não 10s) e `@TimedInterrupt` 2000s — corrigiu a tabela de timeouts em §1.5. |
|
|
782
|
+
| `modules/database.md` | Inspecionado (grep) | **Referenciado.** Confirmou `strict` (§4.1/§4.2), `q` como fragmento Mongo cru, GET de listagem **não ordena** (usar aggregate), PUT = full replace. Base de §9, §10 e restrições §1.5. |
|
|
783
|
+
| `modules/player.md` | Inspecionado (grep) | **Referenciado + incorporado.** Confirmou que `password` é gravado **sem hash** no POST/PUT (§3.1) — fundamenta §2, §11 e a restrição §1.5; `extra`/`image` como campos; `insert()` como único ponto de escrita. |
|
|
784
|
+
| `modules/security.md` | Inspecionado (grep) | **Referenciado.** Confirmou role `public`, Basic público (só apiKey) → scope da role, e exigência de `database` no scope para escrita em `/v3/database`. Base de §2 e §5. |
|
|
785
|
+
| `modules/custom-object.md` | Inspecionado (grep) | **Referenciado.** Confirmou que `__c` é só convenção sobre o `DatabaseRest` genérico. Contextualiza `signup__c`, `email_template__c`, `coupon__c`. |
|
|
786
|
+
| `modules/auth.md` | Inspecionado (grep) | **Referenciado.** Confirmou `POST /v3/auth/token` e grants. Base de §4 e §11. |
|
|
787
|
+
| `guides/java-libraries.md` | Inspecionado (grep) | **Referenciado.** Confirmou `JsonUtil.fromJsonToMap`, `DateUtil.fromKeyword`, `EmailBuilder` (SimpleJavaMail) e que Unirest é `com.mashape.*` (→ bloqueado em Public Endpoint). Base de §3, §6 e restrição do sandbox. |
|
|
788
|
+
| `guides/triggers-guide.md` | Inspecionado (grep) | **Referenciado.** Tabela de eventos×entidades e managers — base das triggers de §2, §5, §11. |
|
|
789
|
+
| `guides/database-access.md` | Inspecionado (grep) | **Referenciado.** Acesso Jongo via `manager.getJongoConnection()` — base do update de player em §6. |
|
|
790
|
+
| `guides/aggregates.md` | Não lido | **Referenciado** (via `index.md`) para `$sort`/relatórios em §8. Não inspecionado em detalhe; citado apenas como ponteiro de aprofundamento. |
|
|
791
|
+
| `guides/java-managers.md` / `guides/java-entities.md` | Não lidos | **Referenciados** (via `index.md`) em §10 para métodos de manager e campos de entidade. Citados como ponteiros. |
|
|
792
|
+
| `modules/studio-page.md` | Não lido | **Referenciado** em §3 (página opcional de edição de templates). Citado como ponteiro. |
|
|
793
|
+
| `modules/notification.md` | Não lido | **Ignorado como referência primária.** O Email Template Pattern envia SMTP via trigger (caminho distinto do módulo Notification). Mencionado em §3 apenas como alternativa. |
|
|
794
|
+
| Demais `modules/*.md` (achievement, action, action-log, avatar, backup, challenge, compact, competition, crossword, csv-data, folder, kpi-formulas, lastmile, leaderboard, level, lottery, marketplace, mystery, point, question, quiz, story, swap, team, upload, virtual-good, webhook, websocket, widget, staging, static-repo) | Listados (não lidos) | **Ignorados.** São módulos de feature sem sobreposição com os patterns de implementação deste documento. |
|
|
795
|
+
|
|
796
|
+
### Decisões de classificação (Etapa 1 → Etapa 2)
|
|
797
|
+
|
|
798
|
+
- **Restrições movidas para §1.5** (eram seções/sub-itens soltos no original): `POST /v3/public` upsert, campo `timeout`, sandbox de Public Endpoints, `strict=true`, `q` vs `_filter`, PUT vs POST, `player` nativo + `password` sem hash. Motivo: afetam múltiplos patterns; centralizar evita repetição (regra "sem redundância").
|
|
799
|
+
- **Consolidações:** "Player Management" + "Funifier API Endpoints Corretos" → §10; "App Version" + "Cache-Busting" → §13. Motivo: tratavam do mesmo problema em seções separadas.
|
|
800
|
+
- **Deduplicações:** `strict` agora só em §9 (one-liner em §1.5); BCrypt em §2/§11 sem repetir a explicação do sandbox (que vive em §1.5); endpoints `/v3/player` vs `/v3/database` só em §10/§1.5; sandbox só em §1.5 (patterns referenciam).
|
|
801
|
+
- **Mantidos como patterns** (decisão de não reduzir): App Version/Cache-Busting (§13) e AngularJS/iOS (§12), embora sejam convenções de frontend e não recursos exclusivos do Funifier.
|