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.
Files changed (181) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +4 -1
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/mcp/bundle.js +119 -93
  66. package/dist/mcp/check-update.d.ts +5 -0
  67. package/dist/mcp/check-update.d.ts.map +1 -1
  68. package/dist/mcp/check-update.js +21 -10
  69. package/dist/mcp/check-update.js.map +1 -1
  70. package/dist/mcp/check-update.test.d.ts +2 -0
  71. package/dist/mcp/check-update.test.d.ts.map +1 -0
  72. package/dist/mcp/check-update.test.js +33 -0
  73. package/dist/mcp/check-update.test.js.map +1 -0
  74. package/dist/mcp/index.js +2 -2
  75. package/dist/mcp/index.js.map +1 -1
  76. package/dist/mcp/prompts/templates.d.ts.map +1 -1
  77. package/dist/mcp/prompts/templates.js +35 -0
  78. package/dist/mcp/prompts/templates.js.map +1 -1
  79. package/dist/mcp/resources/documentation.d.ts +1 -1
  80. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  81. package/dist/mcp/resources/documentation.js +39 -3
  82. package/dist/mcp/resources/documentation.js.map +1 -1
  83. package/dist/mcp/tools/connect.d.ts.map +1 -1
  84. package/dist/mcp/tools/connect.js +18 -8
  85. package/dist/mcp/tools/connect.js.map +1 -1
  86. package/dist/mcp/tools/database.d.ts.map +1 -1
  87. package/dist/mcp/tools/database.js +59 -47
  88. package/dist/mcp/tools/database.js.map +1 -1
  89. package/dist/mcp/tools/database.test.js +2 -2
  90. package/dist/mcp/tools/database.test.js.map +1 -1
  91. package/dist/mcp/tools/delete.d.ts.map +1 -1
  92. package/dist/mcp/tools/delete.js +13 -3
  93. package/dist/mcp/tools/delete.js.map +1 -1
  94. package/dist/mcp/tools/execute.d.ts.map +1 -1
  95. package/dist/mcp/tools/execute.js +20 -9
  96. package/dist/mcp/tools/execute.js.map +1 -1
  97. package/dist/mcp/tools/folder.d.ts.map +1 -1
  98. package/dist/mcp/tools/folder.js +22 -12
  99. package/dist/mcp/tools/folder.js.map +1 -1
  100. package/dist/mcp/tools/get.d.ts.map +1 -1
  101. package/dist/mcp/tools/get.js +16 -6
  102. package/dist/mcp/tools/get.js.map +1 -1
  103. package/dist/mcp/tools/index.d.ts +1 -1
  104. package/dist/mcp/tools/index.d.ts.map +1 -1
  105. package/dist/mcp/tools/index.js +28 -1
  106. package/dist/mcp/tools/index.js.map +1 -1
  107. package/dist/mcp/tools/list.d.ts.map +1 -1
  108. package/dist/mcp/tools/list.js +38 -14
  109. package/dist/mcp/tools/list.js.map +1 -1
  110. package/dist/mcp/tools/logs.d.ts.map +1 -1
  111. package/dist/mcp/tools/logs.js +15 -5
  112. package/dist/mcp/tools/logs.js.map +1 -1
  113. package/dist/mcp/tools/save.d.ts.map +1 -1
  114. package/dist/mcp/tools/save.js +14 -4
  115. package/dist/mcp/tools/save.js.map +1 -1
  116. package/dist/mcp/tools/save.test.js +3 -3
  117. package/dist/mcp/tools/save.test.js.map +1 -1
  118. package/dist/mcp/tools/search-docs.d.ts +3 -0
  119. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  120. package/dist/mcp/tools/search-docs.js +102 -0
  121. package/dist/mcp/tools/search-docs.js.map +1 -0
  122. package/package.json +6 -2
  123. package/skills/acquire-funifier-knowledge/SKILL.md +155 -0
  124. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  125. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  126. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  127. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  128. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  129. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  130. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +86 -0
  131. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  132. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  133. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  134. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  135. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  136. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  137. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  138. package/skills/funifier/SKILL.md +88 -0
  139. package/skills/funifier/references/configure-security.md +96 -0
  140. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  141. package/skills/funifier/references/create-aggregate.md +144 -0
  142. package/skills/funifier/references/create-challenge.md +116 -0
  143. package/skills/funifier/references/create-competition.md +98 -0
  144. package/skills/funifier/references/create-crossword.md +574 -0
  145. package/skills/funifier/references/create-custom-object.md +91 -0
  146. package/skills/funifier/references/create-custom-page.md +135 -0
  147. package/skills/funifier/references/create-folder.md +104 -0
  148. package/skills/funifier/references/create-lastmile.md +643 -0
  149. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  150. package/skills/funifier/references/create-level.md +94 -0
  151. package/skills/funifier/references/create-lottery.md +913 -0
  152. package/skills/funifier/references/create-mystery.md +769 -0
  153. package/skills/funifier/references/create-notification.md +75 -0
  154. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  155. package/skills/funifier/references/create-quiz.md +98 -0
  156. package/skills/funifier/references/create-scheduler.md +141 -0
  157. package/skills/funifier/references/create-story.md +636 -0
  158. package/skills/funifier/references/create-swap.md +95 -0
  159. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  160. package/skills/funifier/references/create-virtual-good.md +96 -0
  161. package/skills/funifier/references/create-webhook.md +72 -0
  162. package/skills/funifier/references/create-websocket.md +71 -0
  163. package/skills/funifier/references/create-widget.md +76 -0
  164. package/skills/funifier/references/debug.md +87 -0
  165. package/skills/funifier/references/help.md +81 -0
  166. package/skills/funifier/references/implement-frontend.md +106 -0
  167. package/skills/funifier/references/import-csv.md +75 -0
  168. package/skills/funifier/references/manage-player.md +82 -0
  169. package/skills/funifier/references/manage-team.md +76 -0
  170. package/skills/funifier/references/upload-file.md +91 -0
  171. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  172. package/skills/funifier-create-challenge/SKILL.md +0 -88
  173. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  174. package/skills/funifier-create-level/SKILL.md +0 -87
  175. package/skills/funifier-create-quiz/SKILL.md +0 -87
  176. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  177. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  178. package/skills/funifier-debug/SKILL.md +0 -92
  179. package/skills/funifier-help/SKILL.md +0 -86
  180. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  181. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,102 +1,152 @@
1
- # Patterns (Design 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
- Padrões de implementação da Funifier, criados com base na experiência em projetos reais.
4
-
5
- ## Quando usar
100
+ ---
6
101
 
7
- - Ao implementar funcionalidades comuns em frontends Funifier
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
- ## Signup Pattern
109
+ ### Problema
14
110
 
15
- **Problema:** Implementar cadastro de jogador em área pública (frontend) que acessa diretamente a API da Funifier, sem expor o endpoint `/v3/player` para escrita direta.
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
- **Solução:** Usar um objeto customizado `signup__c` como intermediário, com uma trigger `before_update` que valida os dados, cria o jogador com senha criptografada, e retorna o resultado para o frontend.
113
+ ### Solução
18
114
 
19
- ### Arquitetura
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 → Trigger before_update → Cria Player
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
- > ⚠️ **DEVE usar PUT**, não POST! A trigger `before_update` só é disparada pelo método PUT. POST dispara `before_create`.
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
- **Exemplo (JavaScript):**
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
- Este token terá apenas as permissões da role `public` escrita em `signup__c` e nada mais. Seguro para usar em código público.
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
- | Campo | Valor |
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
- // Remove campos sensíveis antes de salvar no signup__c
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
- ### Uso no Frontend
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
- name: fullName,
126
- email: email,
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
- ### Extensões Comuns
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
- - **E-mail de boas-vindas:** Adicionar envio de email na trigger após `insert`
138
- - **Termos de uso:** Adicionar campo `terms` (boolean) e validar na trigger
139
- - **Atribuição de equipe:** Adicionar lógica de team assignment na trigger
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
- **Problema:** Enviar emails automáticos a partir de triggers, com conteúdo editável pelo administrador sem alterar código.
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
- **Solução:** Criar coleção `email_template__c` com templates HTML usando variáveis Mustache, e usar `MustacheUtils.parse()` + `MailContext` nas triggers para montar e enviar.
190
+ ### Problema
149
191
 
150
- ### Arquitetura
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 (templates editáveis) → Trigger (busca template + parse Mustache) → SMTP (envia)
199
+ email_template__c (editável) → trigger (findOne + parse Mustache) → SMTP (MailContext) → envia
154
200
  ```
155
201
 
156
- ### 1. Estrutura do Template (`email_template__c`)
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/>Thank you for signing up. Confirm your email:<br/><br/><a href=\"{{link}}\">Confirm your Email</a><br/><br/>The Funifier Team"
211
+ "content": "Hi {{user.name}}<br/><br/>Confirme seu email:<br/><a href=\"{{link}}\">Confirmar</a>"
167
212
  }
168
213
  ```
169
214
 
170
- **Variáveis Mustache:** Usar `{{campo}}` no subject e content. Podem ser campos da entity, do player, ou valores calculados.
171
-
172
- ### 2. Uso na Trigger
173
-
215
+ **Na trigger:**
174
216
  ```java
175
- // 1. Buscar template
176
- Object templateObj = database.findOne("email_template__c", "{_id: 'signup_welcome'}");
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. Montar valores para substituição Mustache
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. Parse Mustache — substitui {{variavel}} pelos valores
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. Enviar email
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 = com.funifier.controller.Configuration.getCurrentConfiguration().getMailContext();
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 (Simple Java Mail) |
222
- | `MailerBuilder` | Construir mailer SMTP (Simple Java Mail) |
223
- | `com.funifier.engine.mail.MailContext` | Config SMTP da gamificação |
224
- | `com.funifier.controller.Configuration` | Acesso à configuração atual |
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
- ### Propriedades do MailContext
248
+ Opcional: criar página em **Studio → Pages** para o admin editar os templates visualmente (`modules/studio-page.md`).
228
249
 
229
- - `ctx.hostName` — servidor SMTP
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
- > ⚠️ O envio de email deve acontecer **antes** do `entity.remove("email")` no Signup Pattern, para ter acesso ao endereço do destinatário.
252
+ - **Enviar email no Signup (§2) depois de `entity.remove("email")`** destinatário nulo. Causa: o campo 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
- ### ⚠️ Notas Importantes
257
+ ## 4. Google OAuth Login Pattern
239
258
 
240
- - O token Basic com apenas a API Key recebe as permissões da role `public`
241
- - Nunca expor o endpoint `/v3/player` para escrita em área pública
242
- - A trigger roda no servidor validações client-side são complementares, não substitutas
243
- - O registro em `signup__c` serve como log de tentativas de cadastro (sem dados sensíveis)
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
- ## Payment Gateway Pattern (Asaas + Public Endpoints)
264
+ ### Problema
248
265
 
249
- **Problema:** Implementar pagamento recorrente (assinatura) em um app Funifier sem backend proprio, usando apenas o frontend e a infraestrutura Funifier.
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
- **Solução:** Usar Public Endpoints do Funifier como proxy server-side para a API do gateway de pagamento (Asaas), evitando CORS e protegendo a API key.
268
+ ### Solução
252
269
 
253
- ### Arquitetura
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 (SPA)
257
- |
258
- v
259
- Funifier Public Endpoints (server-side proxy)
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
- ### Endpoints Necessarios
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
- ```json
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
- Tipos: `PERCENTAGE` (desconto %) e `FIXED` (desconto em R$).
284
+ ### Armadilhas conhecidas
311
285
 
312
- O endpoint `validate_coupon` valida e retorna o desconto. O endpoint `create_subscription` aplica o desconto no `value` da subscription.
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
- ### Campos do Player (`extra.plan`)
291
+ ---
315
292
 
316
- ```json
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
- | Campo | Descricao |
330
- |-------|-----------|
331
- | `type` | `standard` ou `premium` |
332
- | `plan_status` | `active`, `canceled`, `pending_downgrade` |
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
- ### Gestao de Assinatura
300
+ ### Problema
340
301
 
341
- | Acao | Logica |
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
- ### Webhook: Eventos Importantes
304
+ ### Solução
349
305
 
350
- | Evento Asaas | Acao |
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
- ### Seguranca do Webhook
308
+ ```
309
+ Gamificação Produto → trigger after_create (player) → HTTP POST → Gamificação CRM (/v3/database/person + /deal)
310
+ ```
357
311
 
358
- - Validar `authToken` no header/body (configurado ao criar webhook no Asaas)
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
- ### Padroes Groovy para Scripts de Pagamento
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
- ```groovy
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
- // 2. Atualizar player via Jongo (PlayerManager NAO tem update)
370
- manager.getJongoConnection().getCollection("player")
371
- .update("{_id: #}", playerId)
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
- ### Licoes Aprendidas
322
+ ---
391
323
 
392
- 1. **Asaas `billingType: UNDEFINED`** permite o cliente escolher metodo de pagamento no checkout
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
- ## Database Strict Mode Pattern (BSON Types)
331
+ ### Problema
407
332
 
408
- **Problema:** O MongoDB armazena dados em formato BSON com tipos especiais (`$date`, `$oid`, `$numberLong`, etc.). O endpoint `/v3/database` por padrão retorna os dados em JSON puro, onde campos tipados perdem a informação de tipo. Se você lê dados sem tipagem e salva de volta, o MongoDB muda o tipo do campo (ex: `Date` vira `String` ou `Number`), quebrando consultas por data e ordenação.
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
- **Solução:** Sempre usar `strict=true` como query parameter ao consultar via `/v3/database`. Isso retorna os dados no formato BSON estendido, preservando os tipos.
335
+ ### Solução
411
336
 
412
- ### Exemplo sem strict (JSON puro PERIGOSO para escrita)
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
- GET /v3/database/player/ricardo
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
- ### Exemplo com strict (BSON estendido — CORRETO)
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
- ### Regras de Uso
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
- 1. **Sempre usar `strict=true` em GETs do `/v3/database`** leitura de coleções customizadas (`__c`) e coleções nativas
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
- ### Tipos BSON Comuns
357
+ **Cupons (`coupon__c`):** `type` (`PERCENTAGE`|`FIXED`), `value`, `maxUses`, `usedCount`, `active`. `validate_coupon` valida; `create_subscription` aplica no `value`.
454
358
 
455
- | Tipo | Formato JSON strict | Exemplo |
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
- ### Helper JavaScript para Frontend
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
- ```javascript
464
- // Criar campo date no formato BSON
465
- function bsonDate(date) {
466
- return { $date: (date || new Date()).toISOString() };
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
- // Ler campo date do formato BSON
470
- function readDate(field) {
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
- ### ⚠️ Impacto de NÃO usar strict
372
+ // trial: data no futuro
373
+ def trialEnd = DateUtil.fromKeyword("+7d")
480
374
 
481
- - Campos `Date` viram `String` ou `Number` no MongoDB
482
- - Consultas com `$gt`, `$lt` por data param de funcionar
483
- - Ordenação por data (`_sort=-created`) retorna resultados incorretos
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
- ### Onde se aplica
379
+ ### Armadilhas conhecidas
487
380
 
488
- - **Apenas** no endpoint `/v3/database` (coleções customizadas `__c` e nativas via database)
489
- - **NÃO** se aplica a endpoints específicos como `/v3/player`, `/v3/action`, `/v3/challenge` etc. (estes já têm tipagem própria)
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
- ## App Version Pattern
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
- **Problema:** Equipe de desenvolvimento e testes precisa saber se está vendo a versão correta do app, evitando trabalhar sobre versão em cache do browser.
397
+ ### Problema
496
398
 
497
- **Solução:** Exibir a versão no rodapé de todas as páginas e gerenciá-la de forma consistente.
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
- ### Convenção de Versionamento
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
- MAJOR.MINOR
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
- #### 1. Definir versão no `config.js`
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
- ```javascript
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
- app.run(function($rootScope) {
523
- $rootScope.appVersion = CONFIG.VERSION;
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
- #### 3. Rodapé no `index.html`
528
-
529
- ```html
530
- <div class="version-footer">version {{appVersion}}</div>
531
- ```
532
-
533
- #### 4. CSS
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
- - Valor em **segundos** (30 = 30s)
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
- **Regra:** Sempre inclua TODOS os campos importantes ao atualizar um endpoint via API:
580
- ```json
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 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
- ## Public Endpoint Sandbox Classes Bloqueadas vs Permitidas
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
- ## Google OAuth Login Pattern
458
+ ### Problema
640
459
 
641
- **Problema:** Implementar login com Google em app Funifier sem backend próprio.
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
- **Solução:** Google Sign-In (GSI) no frontend + Public Endpoint no Funifier para verificação do token e criação/login do player.
462
+ ### Solução
644
463
 
645
- ### Arquitetura
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
- Frontend: Google GSI renderButton User clica Google retorna id_token
649
- |
650
- v
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
- ### Lições Importantes
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
- // Limpar $rootScope
684
- $rootScope.player = null;
685
- $rootScope.profileData = null;
686
- // ... etc
480
+ $rootScope.player = null; $rootScope.profileData = null;
687
481
  }
688
- ```
689
482
 
690
- ### Camada 2: Limpar dados no login (se usuário diferente)
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
- Para queries com ordenação, usar o endpoint `aggregate` com pipeline `$sort`:
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/collection__c/aggregate?q=userId:"' + userId + '"&strict=true',
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
- **Defense-in-depth:** Ainda é boa prática verificar userId client-side:
725
- ```javascript
726
- results = results.filter(function(r) { return r.userId === currentUserId; });
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
- ## CRM Integration Pattern (Cross-Gamification)
509
+ ## 9. Database Strict Mode Pattern (BSON Types)
732
510
 
733
- **Problema:** Integrar app de produto (gamificação A) com CRM Funifier (gamificação B) para criar leads automaticamente no signup.
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
- **Solução:** Trigger na gamificação do produto faz HTTP POST para a gamificação do CRM.
516
+ ### Problema
736
517
 
737
- ### Arquitetura
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
- Gamificação "Produto" (Orvya Fitness)
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
- ### Lições Importantes
529
+ ### Implementação
748
530
 
749
- 1. **CRM endpoints `/v3/crm/*` retornam 404 com Basic auth** — usar `/v3/database/person` e `/v3/database/deal`
750
- 2. **Deals precisam de campos específicos para Kanban:** `owner`, `add_time`, `visible_to`
751
- 3. **CRM App scope DEVE incluir a palavra "database"** sem ela, writes silenciosamente falham (201 sem persistência)
752
- 4. **Usar App token (não-expirável)** da Security Apps gerar Basic token pelo ícone do olho
753
- 5. **Um CRM para todos os produtos** diferenciar por Pipeline
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
- ## Player Management Pattern
548
+ ### Armadilhas conhecidas
758
549
 
759
- ### Leitura e Escrita de Player
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
- ```groovy
762
- // Ler player
763
- Player p = manager.getPlayerManager().findById(playerId)
553
+ ---
764
554
 
765
- // Modificar
766
- p.extra.plan = [type: "premium"]
555
+ ## 10. Acesso à API Funifier — Player & Database
767
556
 
768
- // Salvar (upsert)
769
- manager.getPlayerManager().insert(p)
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
- ### Campos obrigatórios no POST /v3/player
562
+ ### Problema
773
563
 
774
- Ao atualizar player via `POST /v3/player` com apenas `_id` + `extra`, os campos `name` e `email` são APAGADOS. **Sempre incluir:**
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
- ```json
777
- {
778
- "_id": "user@email.com",
779
- "name": "Nome do Usuário",
780
- "email": "user@email.com",
781
- "extra": { ... }
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
- ### Player.image Structure
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
- Para salvar foto do player, usar a estrutura completa:
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
- Os campos `size`, `width`, `height`, `depth` são obrigatórios (mesmo que 0) — Funifier espera essa estrutura.
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
- ### PlayerManagerMétodos Corretos
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 não existe) |
807
- | `pm.insert(player)` | `pm.save(player)` |
808
- | `player.id` (em Groovy) | `player._id` (não funciona em Groovy) |
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
- ### href="#" causa redirect
839
- `<a href="#" ng-click="fn()">` reseta hash antes de ng-click causa redirect. **Fix:** usar `href=""`
840
-
841
- ### iOS Safari limitations
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
- ## OpenAI Realtime API Voice/Video Call Pattern
626
+ ## 11. Alteração de Senha Pattern
850
627
 
851
- ### Arquitetura
852
- Stack: OpenAI Realtime API + WebRTC. Fluxo:
853
- 1. Frontend chama Funifier Public Endpoint para obter dados do usuário + API key
854
- 2. Frontend gera chave efêmera via `/v1/realtime/client_secrets`
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
- ### Endpoints OpenAI (atualizados 2026-03)
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
- ### ⚠️ CRITICAL: Tools DEVEM ser registradas na criação da chave efêmera
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
- **Problema:** `session.update` via data channel NÃO aplica tools de forma confiável. A IA pode não "ver" as ferramentas e nunca chamá-las.
637
+ ### Solução
868
638
 
869
- **Solução:** Incluir `tools` no body do `POST /v1/realtime/client_secrets`:
639
+ Usar os endpoints nativos de senha; para reset administrativo server-side, hashear com BCrypt numa trigger.
870
640
 
871
- ```javascript
872
- fetch('https://api.openai.com/v1/realtime/client_secrets', {
873
- method: 'POST',
874
- headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
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
- O `session.update` via data channel pode ser enviado como **backup**, mas NÃO como fonte primária de configuração.
888
-
889
- ### Tool end_call — Finalização automática de ligação
647
+ ### Implementação
890
648
 
891
- Sempre incluir uma tool `end_call` para que a IA possa desligar a ligação quando a conversa terminar naturalmente:
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
- ```javascript
894
- var voiceTools = [
895
- // ... outras tools ...
896
- {
897
- type: 'function',
898
- name: 'end_call',
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
- No handler de eventos:
906
- ```javascript
907
- case 'response.function_call_arguments.done':
908
- if (evt.name === 'end_call') {
909
- sendToolResult(evt.call_id, { success: true });
910
- setTimeout(function() {
911
- $scope.endCall(); // mesma função do botão "Desligar"
912
- $scope.$applyAsync();
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
- break;
672
+ manager.getPlayerManager().insert(p);
673
+ }
916
674
  ```
917
-
918
- Nas instruções, mencionar a tool explicitamente:
919
675
  ```
920
- Voce tem a ferramenta end_call para encerrar a ligacao.
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
- ### Checklist para implementar conversa por voz
925
- 1. [ ] Criar Public Endpoint no Funifier para retornar dados do usuário + API key
926
- 2. [ ] Incluir **todas as tools** (incluindo `end_call`) na criação da chave efêmera
927
- 3. [ ] Usar modelo `gpt-realtime-mini` (hardcode no frontend como safety net)
928
- 4. [ ] Usar `voice` em `session.audio.output.voice`
929
- 5. [ ] Implementar handler para `response.function_call_arguments.done`
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
- ## Funifier API Endpoints Corretos (CRÍTICO)
688
+ ## 12. Frontend AngularJS / iOS Pattern
937
689
 
938
- **Problema:** Usar endpoints errados causa perda silenciosa de dados — campos desaparecem, queries retornam vazio, e é difícil debugar.
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
- ### Player (entidade nativa)
695
+ ### Problema
941
696
 
942
- O `player` é uma entidade nativa do Funifier, **NÃO** uma collection do `/v3/database`.
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
- | Operação | ✅ Correto | ❌ Errado (não funciona) |
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
- **Padrão correto para atualizar player (read-merge-write):**
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
- **Exemplo do fitness (funciona):**
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
- ### Database (collections customizadas `__c`)
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
- O endpoint `/v3/database` serve para collections customizadas (ex: `profile__c`, `signup__c`).
715
+ ### Armadilhas conhecidas
979
716
 
980
- | Operação | Correto | Errado (não funciona) |
981
- |----------|-----------|--------------------------|
982
- | **Ler por ID** | `GET /v3/database/{collection}?strict=true&q=_id:'{id}'` | `GET /v3/database/{collection}/{id}` (padrão não existe) |
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
- **⚠️ CUIDADO:** `POST /v3/database/{collection}` com um body JSON **CRIA** um novo documento em vez de fazer query!
721
+ ---
988
722
 
989
- **⚠️ `PUT /v3/database/{collection}` faz REPLACE:** Substitui o documento inteiro. Se não enviar `extra`, `password`, etc., esses campos são apagados.
723
+ ## 13. Versionamento & Cache-Busting Pattern
990
724
 
991
- **Query por ID retorna ARRAY normalizar:**
992
- ```javascript
993
- getProfile: function(userId) {
994
- return $http.get(
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
- ### strict=true (BSON types)
730
+ ### Problema
1006
731
 
1007
- - Sempre usar `?strict=true` no `/v3/database` para preservar tipos BSON
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
- ## Cache-Busting em SPAs (Netlify)
736
+ Versão visível no rodapé + query string de versão em todos os assets + headers anti-cache.
1015
737
 
1016
- **Problema:** Browser serve arquivos JS/CSS do cache, ignorando deploys novos. Usuário vê versão nova no rodapé (config.js atualizado) mas executa código antigo (api.js, controllers do cache).
738
+ ### Implementação
1017
739
 
1018
- **Solução:** Query string com versão em todos os `<script>` e `<link>`:
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
- **Regra:** A cada deploy, atualizar o `?v=` em todos os tags do `index.html` para a versão nova. Isso garante que o browser baixa os arquivos atualizados.
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
- ### Alteração via Trigger (server-side)
764
+ ### Armadilhas conhecidas
1070
765
 
1071
- Para alterar a senha via trigger (ex: admin reset), usar BCrypt:
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
- ```groovy
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
- ### Observações
1093
- - O endpoint `GET /v3/player/password/change` envia email automaticamente usando o template de email configurado na gamificação
1094
- - O `player` param aceita tanto o `_id` quanto o `email` do jogador
1095
- - O `code` tem validade limitada (expira após uso ou timeout)
1096
- - Basic auth é suficiente (não precisa de Bearer/session token)
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.