funifier-mcp 0.2.0 → 0.2.4

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 (64) hide show
  1. package/.cursor/rules/funifier.mdc +91 -0
  2. package/.github/copilot-instructions.md +83 -0
  3. package/AGENTS.md +97 -0
  4. package/README.md +247 -78
  5. package/datasource-funifier-docs/knowledge/guides/aggregates.md +152 -152
  6. package/datasource-funifier-docs/knowledge/guides/database-access.md +132 -132
  7. package/datasource-funifier-docs/knowledge/guides/java-entities.md +373 -373
  8. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +330 -330
  9. package/datasource-funifier-docs/knowledge/guides/java-managers.md +509 -509
  10. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +271 -271
  11. package/datasource-funifier-docs/knowledge/index.md +121 -121
  12. package/datasource-funifier-docs/knowledge/modules/achievement.md +46 -46
  13. package/datasource-funifier-docs/knowledge/modules/action-log.md +88 -88
  14. package/datasource-funifier-docs/knowledge/modules/action.md +80 -80
  15. package/datasource-funifier-docs/knowledge/modules/auth.md +104 -104
  16. package/datasource-funifier-docs/knowledge/modules/avatar.md +28 -28
  17. package/datasource-funifier-docs/knowledge/modules/backup.md +40 -40
  18. package/datasource-funifier-docs/knowledge/modules/challenge.md +91 -91
  19. package/datasource-funifier-docs/knowledge/modules/compact.md +40 -40
  20. package/datasource-funifier-docs/knowledge/modules/competition.md +149 -149
  21. package/datasource-funifier-docs/knowledge/modules/crossword.md +41 -41
  22. package/datasource-funifier-docs/knowledge/modules/csv-data.md +30 -30
  23. package/datasource-funifier-docs/knowledge/modules/custom-object.md +53 -53
  24. package/datasource-funifier-docs/knowledge/modules/database.md +241 -241
  25. package/datasource-funifier-docs/knowledge/modules/folder.md +111 -111
  26. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +23 -23
  27. package/datasource-funifier-docs/knowledge/modules/lastmile.md +45 -45
  28. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +98 -98
  29. package/datasource-funifier-docs/knowledge/modules/level.md +83 -83
  30. package/datasource-funifier-docs/knowledge/modules/lottery.md +112 -112
  31. package/datasource-funifier-docs/knowledge/modules/marketplace.md +27 -27
  32. package/datasource-funifier-docs/knowledge/modules/mystery.md +82 -82
  33. package/datasource-funifier-docs/knowledge/modules/notification.md +40 -40
  34. package/datasource-funifier-docs/knowledge/modules/patterns.md +1096 -1096
  35. package/datasource-funifier-docs/knowledge/modules/player.md +101 -101
  36. package/datasource-funifier-docs/knowledge/modules/point.md +67 -67
  37. package/datasource-funifier-docs/knowledge/modules/public.md +253 -253
  38. package/datasource-funifier-docs/knowledge/modules/question.md +136 -136
  39. package/datasource-funifier-docs/knowledge/modules/quiz.md +163 -163
  40. package/datasource-funifier-docs/knowledge/modules/scheduler.md +58 -58
  41. package/datasource-funifier-docs/knowledge/modules/security.md +169 -169
  42. package/datasource-funifier-docs/knowledge/modules/staging.md +28 -28
  43. package/datasource-funifier-docs/knowledge/modules/static-repo.md +41 -41
  44. package/datasource-funifier-docs/knowledge/modules/story.md +42 -42
  45. package/datasource-funifier-docs/knowledge/modules/studio-page.md +180 -180
  46. package/datasource-funifier-docs/knowledge/modules/swap.md +132 -132
  47. package/datasource-funifier-docs/knowledge/modules/team.md +75 -75
  48. package/datasource-funifier-docs/knowledge/modules/trigger.md +189 -189
  49. package/datasource-funifier-docs/knowledge/modules/upload.md +155 -155
  50. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +99 -99
  51. package/datasource-funifier-docs/knowledge/modules/webhook.md +41 -41
  52. package/datasource-funifier-docs/knowledge/modules/websocket.md +41 -41
  53. package/datasource-funifier-docs/knowledge/modules/widget.md +42 -42
  54. package/datasource-funifier-docs/process-gtm-saas.md +143 -143
  55. package/datasource-funifier-docs/process-instagram.md +88 -88
  56. package/datasource-funifier-docs/process.md +1826 -1826
  57. package/datasource-funifier-docs/readme.md +132 -132
  58. package/dist/cli/config-writers.js +11 -11
  59. package/dist/mcp/bundle.js +82 -77
  60. package/package.json +70 -67
  61. package/skills/funifier-create-aggregate/SKILL.md +126 -126
  62. package/skills/funifier-create-custom-page/SKILL.md +126 -126
  63. package/skills/funifier-create-scheduler/SKILL.md +126 -126
  64. package/skills/funifier-create-trigger/SKILL.md +127 -127
@@ -1,1096 +1,1096 @@
1
- # Patterns (Design Patterns)
2
-
3
- Padrões de implementação da Funifier, criados com base na experiência em projetos reais.
4
-
5
- ## Quando usar
6
-
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
10
-
11
- ---
12
-
13
- ## Signup Pattern
14
-
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.
16
-
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.
18
-
19
- ### Arquitetura
20
-
21
- ```
22
- Frontend (público) → PUT /v3/database/signup__c → Trigger before_update → Cria Player
23
- ```
24
-
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:
50
-
51
- ```
52
- 1. Concatenar: ApiKey + ":"
53
- 2. Codificar em Base64
54
- 3. Prefixar com "Basic "
55
- 4. Usar no header: Authorization: Basic <token>
56
- ```
57
-
58
- **Exemplo (JavaScript):**
59
- ```javascript
60
- var BASIC_TOKEN = 'Basic ' + btoa(API_KEY + ':');
61
- ```
62
-
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:
72
-
73
- | Campo | Valor |
74
- |-------|-------|
75
- | Name | Signup Handler |
76
- | Entity | `signup__c` |
77
- | Event | `before_update` |
78
-
79
- **Script:**
80
- ```java
81
- void trigger(event, entity, player, database){
82
- entity.status = "Unauthorized";
83
- if(entity.password == null || entity.password.trim().length() == 0) {
84
- entity.message = "Senha deve ser informada para se cadastrar!";
85
- }
86
- else if(entity.email == null || entity.email.trim().length() == 0) {
87
- entity.message = "Email deve ser informado para se cadastrar!";
88
- }
89
- else if(entity.name == null || entity.name.trim().length() == 0) {
90
- entity.message = "Nome deve ser informado para se cadastrar!";
91
- }
92
- else {
93
- Player current = manager.getPlayerManager().findById(entity._id);
94
- if(current != null) {
95
- entity.message = "Este usuário já está cadastrado!";
96
- }
97
- else {
98
- // Criptografa senha e salva jogador
99
- Player p = JsonUtil.fromJson(JsonUtil.toJson(entity), Player.class);
100
- p.password = com.funifier.engine.util.BCrypt.hashpw(
101
- entity.password, com.funifier.engine.util.BCrypt.gensalt()
102
- );
103
- p.setCreated(new Date());
104
- manager.getPlayerManager().insert(p);
105
- entity.message = "Usuário registrado com sucesso!";
106
- entity.status = "OK";
107
- }
108
- }
109
- // Remove campos sensíveis antes de salvar no signup__c
110
- entity.remove("password");
111
- entity.remove("email");
112
- }
113
- ```
114
-
115
- ### Uso no Frontend
116
-
117
- ```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
- $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
- });
133
- ```
134
-
135
- ### Extensões Comuns
136
-
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
141
-
142
- ---
143
-
144
- ## Email Template Pattern
145
-
146
- **Problema:** Enviar emails automáticos a partir de triggers, com conteúdo editável pelo administrador sem alterar código.
147
-
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.
149
-
150
- ### Arquitetura
151
-
152
- ```
153
- email_template__c (templates editáveis) → Trigger (busca template + parse Mustache) → SMTP (envia)
154
- ```
155
-
156
- ### 1. Estrutura do Template (`email_template__c`)
157
-
158
- Cada template é um documento na coleção `email_template__c`:
159
-
160
- ```json
161
- {
162
- "_id": "signup_email_confirm",
163
- "fromName": "Funifier Team",
164
- "fromEmail": "no-reply@funifier.com",
165
- "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"
167
- }
168
- ```
169
-
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
-
174
- ```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;
178
-
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
182
- Map values = new HashMap();
183
- values.put("name", entity.name);
184
- values.put("email", entity.email);
185
- values.put("link", "https://app.exemplo.com/confirm/" + entity._id);
186
-
187
- // 3. Parse Mustache — substitui {{variavel}} pelos valores
188
- String subject = com.funifier.engine.util.MustacheUtils.parse(template.getString("subject"), values);
189
- String content = com.funifier.engine.util.MustacheUtils.parse(template.getString("content"), values);
190
-
191
- // 4. Enviar email
192
- Email email = EmailBuilder.startingBlank()
193
- .from(template.getString("fromName"), template.getString("fromEmail"))
194
- .to(entity.name, entity.email)
195
- .withSubject(subject)
196
- .withHTMLText(content)
197
- .buildEmail();
198
-
199
- com.funifier.engine.mail.MailContext ctx = com.funifier.controller.Configuration.getCurrentConfiguration().getMailContext();
200
- MailerBuilder.withSMTPServer(ctx.hostName, ctx.smtpPort, ctx.authUser, ctx.authPassword)
201
- .buildMailer()
202
- .sendMail(email);
203
- ```
204
-
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
- | Classe | Uso |
220
- |--------|-----|
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 |
226
-
227
- ### Propriedades do MailContext
228
-
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
233
-
234
- > ⚠️ O envio de email deve acontecer **antes** do `entity.remove("email")` no Signup Pattern, para ter acesso ao endereço do destinatário.
235
-
236
- ---
237
-
238
- ### ⚠️ Notas Importantes
239
-
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
- ---
246
-
247
- ## Payment Gateway Pattern (Asaas + Public Endpoints)
248
-
249
- **Problema:** Implementar pagamento recorrente (assinatura) em um app Funifier sem backend proprio, usando apenas o frontend e a infraestrutura Funifier.
250
-
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.
252
-
253
- ### Arquitetura
254
-
255
- ```
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)
269
- ```
270
-
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:
298
-
299
- ```json
300
- {
301
- "_id": "FITEVOLVE20",
302
- "type": "PERCENTAGE",
303
- "value": 20,
304
- "maxUses": 100,
305
- "usedCount": 0,
306
- "active": true
307
- }
308
- ```
309
-
310
- Tipos: `PERCENTAGE` (desconto %) e `FIXED` (desconto em R$).
311
-
312
- O endpoint `validate_coupon` valida e retorna o desconto. O endpoint `create_subscription` aplica o desconto no `value` da subscription.
313
-
314
- ### Campos do Player (`extra.plan`)
315
-
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
- ```
328
-
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 |
338
-
339
- ### Gestao de Assinatura
340
-
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. |
347
-
348
- ### Webhook: Eventos Importantes
349
-
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. |
355
-
356
- ### Seguranca do Webhook
357
-
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
361
-
362
- ### Padroes Groovy para Scripts de Pagamento
363
-
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"}}'
368
-
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
- ```
389
-
390
- ### Licoes Aprendidas
391
-
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)
403
-
404
- ---
405
-
406
- ## Database Strict Mode Pattern (BSON Types)
407
-
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.
409
-
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.
411
-
412
- ### Exemplo sem strict (JSON puro — PERIGOSO para escrita)
413
-
414
- ```
415
- GET /v3/database/player/ricardo
416
- ```
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
-
426
- ### Exemplo com strict (BSON estendido — CORRETO)
427
-
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`.
439
-
440
- ### Regras de Uso
441
-
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
- ```
452
-
453
- ### Tipos BSON Comuns
454
-
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" }` |
460
-
461
- ### Helper JavaScript para Frontend
462
-
463
- ```javascript
464
- // Criar campo date no formato BSON
465
- function bsonDate(date) {
466
- return { $date: (date || new Date()).toISOString() };
467
- }
468
-
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
- ```
478
-
479
- ### ⚠️ Impacto de NÃO usar strict
480
-
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
485
-
486
- ### Onde se aplica
487
-
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)
490
-
491
- ---
492
-
493
- ## App Version Pattern
494
-
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.
496
-
497
- **Solução:** Exibir a versão no rodapé de todas as páginas e gerenciá-la de forma consistente.
498
-
499
- ### Convenção de Versionamento
500
-
501
- ```
502
- MAJOR.MINOR
503
- ```
504
-
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
- ### Implementação
509
-
510
- #### 1. Definir versão no `config.js`
511
-
512
- ```javascript
513
- var CONFIG = {
514
- // ... outras configs
515
- VERSION: '1.0'
516
- };
517
- ```
518
-
519
- #### 2. Expor no `$rootScope` (AngularJS)
520
-
521
- ```javascript
522
- app.run(function($rootScope) {
523
- $rootScope.appVersion = CONFIG.VERSION;
524
- });
525
- ```
526
-
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}
566
- ```
567
-
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.
578
-
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!
591
-
592
- ---
593
-
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.
636
-
637
- ---
638
-
639
- ## Google OAuth Login Pattern
640
-
641
- **Problema:** Implementar login com Google em app Funifier sem backend próprio.
642
-
643
- **Solução:** Google Sign-In (GSI) no frontend + Public Endpoint no Funifier para verificação do token e criação/login do player.
644
-
645
- ### Arquitetura
646
-
647
- ```
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
656
- ```
657
-
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:
673
-
674
- ### Camada 1: Limpar dados no logout
675
- ```javascript
676
- function clearUserData() {
677
- // Limpar todas as chaves do app no localStorage
678
- Object.keys(localStorage).forEach(function(key) {
679
- if (key.startsWith('fitness_') || key.startsWith('water_')) {
680
- localStorage.removeItem(key);
681
- }
682
- });
683
- // Limpar $rootScope
684
- $rootScope.player = null;
685
- $rootScope.profileData = null;
686
- // ... etc
687
- }
688
- ```
689
-
690
- ### Camada 2: Limpar dados no login (se usuário diferente)
691
- ```javascript
692
- 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:
706
-
707
- ```
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
-
714
- Para queries com ordenação, usar o endpoint `aggregate` com pipeline `$sort`:
715
- ```javascript
716
- $http({
717
- method: 'POST',
718
- url: API + '/v3/database/collection__c/aggregate?q=userId:"' + userId + '"&strict=true',
719
- headers: { 'Authorization': 'Bearer ' + token, 'Range': 'items=0-19' },
720
- data: [{ $sort: { created: -1 } }]
721
- });
722
- ```
723
-
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
- ```
728
-
729
- ---
730
-
731
- ## CRM Integration Pattern (Cross-Gamification)
732
-
733
- **Problema:** Integrar app de produto (gamificação A) com CRM Funifier (gamificação B) para criar leads automaticamente no signup.
734
-
735
- **Solução:** Trigger na gamificação do produto faz HTTP POST para a gamificação do CRM.
736
-
737
- ### Arquitetura
738
-
739
- ```
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)
745
- ```
746
-
747
- ### Lições Importantes
748
-
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
754
-
755
- ---
756
-
757
- ## Player Management Pattern
758
-
759
- ### Leitura e Escrita de Player
760
-
761
- ```groovy
762
- // Ler player
763
- Player p = manager.getPlayerManager().findById(playerId)
764
-
765
- // Modificar
766
- p.extra.plan = [type: "premium"]
767
-
768
- // Salvar (upsert)
769
- manager.getPlayerManager().insert(p)
770
- ```
771
-
772
- ### Campos obrigatórios no POST /v3/player
773
-
774
- Ao atualizar player via `POST /v3/player` com apenas `_id` + `extra`, os campos `name` e `email` são APAGADOS. **Sempre incluir:**
775
-
776
- ```json
777
- {
778
- "_id": "user@email.com",
779
- "name": "Nome do Usuário",
780
- "email": "user@email.com",
781
- "extra": { ... }
782
- }
783
- ```
784
-
785
- ### Player.image Structure
786
-
787
- Para salvar foto do player, usar a estrutura completa:
788
-
789
- ```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
- }
797
- ```
798
-
799
- Os campos `size`, `width`, `height`, `depth` são obrigatórios (mesmo que 0) — Funifier espera essa estrutura.
800
-
801
- ### PlayerManager — Métodos Corretos
802
-
803
- | Correto | Errado |
804
- |---------|--------|
805
- | `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) |
809
- | `player.extra` (acesso direto) | `player.getExtra()` (não existe) |
810
-
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
- ```
837
-
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
846
-
847
- ---
848
-
849
- ## OpenAI Realtime API — Voice/Video Call Pattern
850
-
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
857
-
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`
864
-
865
- ### ⚠️ CRITICAL: Tools DEVEM ser registradas na criação da chave efêmera
866
-
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.
868
-
869
- **Solução:** Incluir `tools` no body do `POST /v1/realtime/client_secrets`:
870
-
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
- });
885
- ```
886
-
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
890
-
891
- Sempre incluir uma tool `end_call` para que a IA possa desligar a ligação quando a conversa terminar naturalmente:
892
-
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
- ];
903
- ```
904
-
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
914
- }
915
- break;
916
- ```
917
-
918
- Nas instruções, mencionar a tool explicitamente:
919
- ```
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.
922
- ```
923
-
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
933
-
934
- ---
935
-
936
- ## Funifier API — Endpoints Corretos (CRÍTICO)
937
-
938
- **Problema:** Usar endpoints errados causa perda silenciosa de dados — campos desaparecem, queries retornam vazio, e é difícil debugar.
939
-
940
- ### Player (entidade nativa)
941
-
942
- O `player` é uma entidade nativa do Funifier, **NÃO** uma collection do `/v3/database`.
943
-
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}` | — |
949
-
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
- ```
964
-
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
- ```
975
-
976
- ### Database (collections customizadas `__c`)
977
-
978
- O endpoint `/v3/database` serve para collections customizadas (ex: `profile__c`, `signup__c`).
979
-
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!) |
986
-
987
- **⚠️ CUIDADO:** `POST /v3/database/{collection}` com um body JSON **CRIA** um novo documento em vez de fazer query!
988
-
989
- **⚠️ `PUT /v3/database/{collection}` faz REPLACE:** Substitui o documento inteiro. Se não enviar `extra`, `password`, etc., esses campos são apagados.
990
-
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
- ```
1004
-
1005
- ### strict=true (BSON types)
1006
-
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)
1011
-
1012
- ---
1013
-
1014
- ## Cache-Busting em SPAs (Netlify)
1015
-
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).
1017
-
1018
- **Solução:** Query string com versão em todos os `<script>` e `<link>`:
1019
-
1020
- ```html
1021
- <script src="app.js?v=0.20.2"></script>
1022
- <script src="services/api.js?v=0.20.2"></script>
1023
- <link rel="stylesheet" href="css/style.css?v=0.20.2">
1024
- ```
1025
-
1026
- **Complemento:** Arquivo `_headers` na raiz do projeto (Netlify):
1027
- ```
1028
- /*.js
1029
- Cache-Control: no-cache, must-revalidate
1030
- /*.css
1031
- Cache-Control: no-cache, must-revalidate
1032
- ```
1033
-
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
- ```
1068
-
1069
- ### Alteração via Trigger (server-side)
1070
-
1071
- Para alterar a senha via trigger (ex: admin reset), usar BCrypt:
1072
-
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
- ```
1091
-
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)
1
+ # Patterns (Design Patterns)
2
+
3
+ Padrões de implementação da Funifier, criados com base na experiência em projetos reais.
4
+
5
+ ## Quando usar
6
+
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
10
+
11
+ ---
12
+
13
+ ## Signup Pattern
14
+
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.
16
+
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.
18
+
19
+ ### Arquitetura
20
+
21
+ ```
22
+ Frontend (público) → PUT /v3/database/signup__c → Trigger before_update → Cria Player
23
+ ```
24
+
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:
50
+
51
+ ```
52
+ 1. Concatenar: ApiKey + ":"
53
+ 2. Codificar em Base64
54
+ 3. Prefixar com "Basic "
55
+ 4. Usar no header: Authorization: Basic <token>
56
+ ```
57
+
58
+ **Exemplo (JavaScript):**
59
+ ```javascript
60
+ var BASIC_TOKEN = 'Basic ' + btoa(API_KEY + ':');
61
+ ```
62
+
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:
72
+
73
+ | Campo | Valor |
74
+ |-------|-------|
75
+ | Name | Signup Handler |
76
+ | Entity | `signup__c` |
77
+ | Event | `before_update` |
78
+
79
+ **Script:**
80
+ ```java
81
+ void trigger(event, entity, player, database){
82
+ entity.status = "Unauthorized";
83
+ if(entity.password == null || entity.password.trim().length() == 0) {
84
+ entity.message = "Senha deve ser informada para se cadastrar!";
85
+ }
86
+ else if(entity.email == null || entity.email.trim().length() == 0) {
87
+ entity.message = "Email deve ser informado para se cadastrar!";
88
+ }
89
+ else if(entity.name == null || entity.name.trim().length() == 0) {
90
+ entity.message = "Nome deve ser informado para se cadastrar!";
91
+ }
92
+ else {
93
+ Player current = manager.getPlayerManager().findById(entity._id);
94
+ if(current != null) {
95
+ entity.message = "Este usuário já está cadastrado!";
96
+ }
97
+ else {
98
+ // Criptografa senha e salva jogador
99
+ Player p = JsonUtil.fromJson(JsonUtil.toJson(entity), Player.class);
100
+ p.password = com.funifier.engine.util.BCrypt.hashpw(
101
+ entity.password, com.funifier.engine.util.BCrypt.gensalt()
102
+ );
103
+ p.setCreated(new Date());
104
+ manager.getPlayerManager().insert(p);
105
+ entity.message = "Usuário registrado com sucesso!";
106
+ entity.status = "OK";
107
+ }
108
+ }
109
+ // Remove campos sensíveis antes de salvar no signup__c
110
+ entity.remove("password");
111
+ entity.remove("email");
112
+ }
113
+ ```
114
+
115
+ ### Uso no Frontend
116
+
117
+ ```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
+ $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
+ });
133
+ ```
134
+
135
+ ### Extensões Comuns
136
+
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
141
+
142
+ ---
143
+
144
+ ## Email Template Pattern
145
+
146
+ **Problema:** Enviar emails automáticos a partir de triggers, com conteúdo editável pelo administrador sem alterar código.
147
+
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.
149
+
150
+ ### Arquitetura
151
+
152
+ ```
153
+ email_template__c (templates editáveis) → Trigger (busca template + parse Mustache) → SMTP (envia)
154
+ ```
155
+
156
+ ### 1. Estrutura do Template (`email_template__c`)
157
+
158
+ Cada template é um documento na coleção `email_template__c`:
159
+
160
+ ```json
161
+ {
162
+ "_id": "signup_email_confirm",
163
+ "fromName": "Funifier Team",
164
+ "fromEmail": "no-reply@funifier.com",
165
+ "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"
167
+ }
168
+ ```
169
+
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
+
174
+ ```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;
178
+
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
182
+ Map values = new HashMap();
183
+ values.put("name", entity.name);
184
+ values.put("email", entity.email);
185
+ values.put("link", "https://app.exemplo.com/confirm/" + entity._id);
186
+
187
+ // 3. Parse Mustache — substitui {{variavel}} pelos valores
188
+ String subject = com.funifier.engine.util.MustacheUtils.parse(template.getString("subject"), values);
189
+ String content = com.funifier.engine.util.MustacheUtils.parse(template.getString("content"), values);
190
+
191
+ // 4. Enviar email
192
+ Email email = EmailBuilder.startingBlank()
193
+ .from(template.getString("fromName"), template.getString("fromEmail"))
194
+ .to(entity.name, entity.email)
195
+ .withSubject(subject)
196
+ .withHTMLText(content)
197
+ .buildEmail();
198
+
199
+ com.funifier.engine.mail.MailContext ctx = com.funifier.controller.Configuration.getCurrentConfiguration().getMailContext();
200
+ MailerBuilder.withSMTPServer(ctx.hostName, ctx.smtpPort, ctx.authUser, ctx.authPassword)
201
+ .buildMailer()
202
+ .sendMail(email);
203
+ ```
204
+
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
+ | Classe | Uso |
220
+ |--------|-----|
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 |
226
+
227
+ ### Propriedades do MailContext
228
+
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
233
+
234
+ > ⚠️ O envio de email deve acontecer **antes** do `entity.remove("email")` no Signup Pattern, para ter acesso ao endereço do destinatário.
235
+
236
+ ---
237
+
238
+ ### ⚠️ Notas Importantes
239
+
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
+ ---
246
+
247
+ ## Payment Gateway Pattern (Asaas + Public Endpoints)
248
+
249
+ **Problema:** Implementar pagamento recorrente (assinatura) em um app Funifier sem backend proprio, usando apenas o frontend e a infraestrutura Funifier.
250
+
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.
252
+
253
+ ### Arquitetura
254
+
255
+ ```
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)
269
+ ```
270
+
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:
298
+
299
+ ```json
300
+ {
301
+ "_id": "FITEVOLVE20",
302
+ "type": "PERCENTAGE",
303
+ "value": 20,
304
+ "maxUses": 100,
305
+ "usedCount": 0,
306
+ "active": true
307
+ }
308
+ ```
309
+
310
+ Tipos: `PERCENTAGE` (desconto %) e `FIXED` (desconto em R$).
311
+
312
+ O endpoint `validate_coupon` valida e retorna o desconto. O endpoint `create_subscription` aplica o desconto no `value` da subscription.
313
+
314
+ ### Campos do Player (`extra.plan`)
315
+
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
+ ```
328
+
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 |
338
+
339
+ ### Gestao de Assinatura
340
+
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. |
347
+
348
+ ### Webhook: Eventos Importantes
349
+
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. |
355
+
356
+ ### Seguranca do Webhook
357
+
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
361
+
362
+ ### Padroes Groovy para Scripts de Pagamento
363
+
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"}}'
368
+
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
+ ```
389
+
390
+ ### Licoes Aprendidas
391
+
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)
403
+
404
+ ---
405
+
406
+ ## Database Strict Mode Pattern (BSON Types)
407
+
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.
409
+
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.
411
+
412
+ ### Exemplo sem strict (JSON puro — PERIGOSO para escrita)
413
+
414
+ ```
415
+ GET /v3/database/player/ricardo
416
+ ```
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
+
426
+ ### Exemplo com strict (BSON estendido — CORRETO)
427
+
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`.
439
+
440
+ ### Regras de Uso
441
+
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
+ ```
452
+
453
+ ### Tipos BSON Comuns
454
+
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" }` |
460
+
461
+ ### Helper JavaScript para Frontend
462
+
463
+ ```javascript
464
+ // Criar campo date no formato BSON
465
+ function bsonDate(date) {
466
+ return { $date: (date || new Date()).toISOString() };
467
+ }
468
+
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
+ ```
478
+
479
+ ### ⚠️ Impacto de NÃO usar strict
480
+
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
485
+
486
+ ### Onde se aplica
487
+
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)
490
+
491
+ ---
492
+
493
+ ## App Version Pattern
494
+
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.
496
+
497
+ **Solução:** Exibir a versão no rodapé de todas as páginas e gerenciá-la de forma consistente.
498
+
499
+ ### Convenção de Versionamento
500
+
501
+ ```
502
+ MAJOR.MINOR
503
+ ```
504
+
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
+ ### Implementação
509
+
510
+ #### 1. Definir versão no `config.js`
511
+
512
+ ```javascript
513
+ var CONFIG = {
514
+ // ... outras configs
515
+ VERSION: '1.0'
516
+ };
517
+ ```
518
+
519
+ #### 2. Expor no `$rootScope` (AngularJS)
520
+
521
+ ```javascript
522
+ app.run(function($rootScope) {
523
+ $rootScope.appVersion = CONFIG.VERSION;
524
+ });
525
+ ```
526
+
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}
566
+ ```
567
+
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.
578
+
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!
591
+
592
+ ---
593
+
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.
636
+
637
+ ---
638
+
639
+ ## Google OAuth Login Pattern
640
+
641
+ **Problema:** Implementar login com Google em app Funifier sem backend próprio.
642
+
643
+ **Solução:** Google Sign-In (GSI) no frontend + Public Endpoint no Funifier para verificação do token e criação/login do player.
644
+
645
+ ### Arquitetura
646
+
647
+ ```
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
656
+ ```
657
+
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:
673
+
674
+ ### Camada 1: Limpar dados no logout
675
+ ```javascript
676
+ function clearUserData() {
677
+ // Limpar todas as chaves do app no localStorage
678
+ Object.keys(localStorage).forEach(function(key) {
679
+ if (key.startsWith('fitness_') || key.startsWith('water_')) {
680
+ localStorage.removeItem(key);
681
+ }
682
+ });
683
+ // Limpar $rootScope
684
+ $rootScope.player = null;
685
+ $rootScope.profileData = null;
686
+ // ... etc
687
+ }
688
+ ```
689
+
690
+ ### Camada 2: Limpar dados no login (se usuário diferente)
691
+ ```javascript
692
+ 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:
706
+
707
+ ```
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
+
714
+ Para queries com ordenação, usar o endpoint `aggregate` com pipeline `$sort`:
715
+ ```javascript
716
+ $http({
717
+ method: 'POST',
718
+ url: API + '/v3/database/collection__c/aggregate?q=userId:"' + userId + '"&strict=true',
719
+ headers: { 'Authorization': 'Bearer ' + token, 'Range': 'items=0-19' },
720
+ data: [{ $sort: { created: -1 } }]
721
+ });
722
+ ```
723
+
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
+ ```
728
+
729
+ ---
730
+
731
+ ## CRM Integration Pattern (Cross-Gamification)
732
+
733
+ **Problema:** Integrar app de produto (gamificação A) com CRM Funifier (gamificação B) para criar leads automaticamente no signup.
734
+
735
+ **Solução:** Trigger na gamificação do produto faz HTTP POST para a gamificação do CRM.
736
+
737
+ ### Arquitetura
738
+
739
+ ```
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)
745
+ ```
746
+
747
+ ### Lições Importantes
748
+
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
754
+
755
+ ---
756
+
757
+ ## Player Management Pattern
758
+
759
+ ### Leitura e Escrita de Player
760
+
761
+ ```groovy
762
+ // Ler player
763
+ Player p = manager.getPlayerManager().findById(playerId)
764
+
765
+ // Modificar
766
+ p.extra.plan = [type: "premium"]
767
+
768
+ // Salvar (upsert)
769
+ manager.getPlayerManager().insert(p)
770
+ ```
771
+
772
+ ### Campos obrigatórios no POST /v3/player
773
+
774
+ Ao atualizar player via `POST /v3/player` com apenas `_id` + `extra`, os campos `name` e `email` são APAGADOS. **Sempre incluir:**
775
+
776
+ ```json
777
+ {
778
+ "_id": "user@email.com",
779
+ "name": "Nome do Usuário",
780
+ "email": "user@email.com",
781
+ "extra": { ... }
782
+ }
783
+ ```
784
+
785
+ ### Player.image Structure
786
+
787
+ Para salvar foto do player, usar a estrutura completa:
788
+
789
+ ```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
+ }
797
+ ```
798
+
799
+ Os campos `size`, `width`, `height`, `depth` são obrigatórios (mesmo que 0) — Funifier espera essa estrutura.
800
+
801
+ ### PlayerManager — Métodos Corretos
802
+
803
+ | Correto | Errado |
804
+ |---------|--------|
805
+ | `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) |
809
+ | `player.extra` (acesso direto) | `player.getExtra()` (não existe) |
810
+
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
+ ```
837
+
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
846
+
847
+ ---
848
+
849
+ ## OpenAI Realtime API — Voice/Video Call Pattern
850
+
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
857
+
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`
864
+
865
+ ### ⚠️ CRITICAL: Tools DEVEM ser registradas na criação da chave efêmera
866
+
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.
868
+
869
+ **Solução:** Incluir `tools` no body do `POST /v1/realtime/client_secrets`:
870
+
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
+ });
885
+ ```
886
+
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
890
+
891
+ Sempre incluir uma tool `end_call` para que a IA possa desligar a ligação quando a conversa terminar naturalmente:
892
+
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
+ ];
903
+ ```
904
+
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
914
+ }
915
+ break;
916
+ ```
917
+
918
+ Nas instruções, mencionar a tool explicitamente:
919
+ ```
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.
922
+ ```
923
+
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
933
+
934
+ ---
935
+
936
+ ## Funifier API — Endpoints Corretos (CRÍTICO)
937
+
938
+ **Problema:** Usar endpoints errados causa perda silenciosa de dados — campos desaparecem, queries retornam vazio, e é difícil debugar.
939
+
940
+ ### Player (entidade nativa)
941
+
942
+ O `player` é uma entidade nativa do Funifier, **NÃO** uma collection do `/v3/database`.
943
+
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}` | — |
949
+
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
+ ```
964
+
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
+ ```
975
+
976
+ ### Database (collections customizadas `__c`)
977
+
978
+ O endpoint `/v3/database` serve para collections customizadas (ex: `profile__c`, `signup__c`).
979
+
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!) |
986
+
987
+ **⚠️ CUIDADO:** `POST /v3/database/{collection}` com um body JSON **CRIA** um novo documento em vez de fazer query!
988
+
989
+ **⚠️ `PUT /v3/database/{collection}` faz REPLACE:** Substitui o documento inteiro. Se não enviar `extra`, `password`, etc., esses campos são apagados.
990
+
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
+ ```
1004
+
1005
+ ### strict=true (BSON types)
1006
+
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)
1011
+
1012
+ ---
1013
+
1014
+ ## Cache-Busting em SPAs (Netlify)
1015
+
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).
1017
+
1018
+ **Solução:** Query string com versão em todos os `<script>` e `<link>`:
1019
+
1020
+ ```html
1021
+ <script src="app.js?v=0.20.2"></script>
1022
+ <script src="services/api.js?v=0.20.2"></script>
1023
+ <link rel="stylesheet" href="css/style.css?v=0.20.2">
1024
+ ```
1025
+
1026
+ **Complemento:** Arquivo `_headers` na raiz do projeto (Netlify):
1027
+ ```
1028
+ /*.js
1029
+ Cache-Control: no-cache, must-revalidate
1030
+ /*.css
1031
+ Cache-Control: no-cache, must-revalidate
1032
+ ```
1033
+
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
+ ```
1068
+
1069
+ ### Alteração via Trigger (server-side)
1070
+
1071
+ Para alterar a senha via trigger (ex: admin reset), usar BCrypt:
1072
+
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
+ ```
1091
+
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)