//go:build js && wasm package main import ( "regexp" "strings" "syscall/js" ) // IMPORTANTE (.agent): o backend Go continua sendo a autoridade das regras. // Este WASM existe para concentrar regras de negócio do widget (cliente) em Go, // mantendo o arquivo JS pequeno e fácil de auditar. var dataISORe = regexp.MustCompile(`^([0-9]{4})-([0-9]{2})-([0-9]{2})$`) type cfgWidget struct { ProdutoNome string InquilinoCodigo string InquilinoNome string UsuarioCodigo string UsuarioNome string UsuarioTelefone string UsuarioEmail string CooldownHours float64 DataMinimaAbertura string } func main() { js.Global().Set("__eli_nps_wasm_preflight", js.FuncOf(preflight)) js.Global().Set("__eli_nps_wasm_decidir", js.FuncOf(decidir)) js.Global().Set("__eli_nps_wasm_cooldown_ativo", js.FuncOf(cooldownAtivo)) js.Global().Set("__eli_nps_wasm_set_cooldown", js.FuncOf(setCooldown)) js.Global().Set("__eli_nps_wasm_ready", true) // Mantém o módulo vivo. select {} } func cooldownAtivo(this js.Value, args []js.Value) any { if len(args) < 1 { return false } chave := strings.TrimSpace(args[0].String()) if chave == "" { return false } // Best-effort: se der erro, tratamos como não ativo. storage := js.Global().Get("localStorage") if storage.IsUndefined() || storage.IsNull() { return false } v := storage.Call("getItem", chave) if v.IsNull() || v.IsUndefined() { return false } parsed := js.Global().Get("JSON").Call("parse", v) until := parsed.Get("until") if until.IsUndefined() || until.IsNull() { return false } agora := js.Global().Get("Date").Call("now").Int() return int64(agora) < int64(until.Int()) } func setCooldown(this js.Value, args []js.Value) any { if len(args) < 2 { return nil } chave := strings.TrimSpace(args[0].String()) if chave == "" { return nil } ateMs := int64(args[1].Int()) if ateMs <= 0 { return nil } storage := js.Global().Get("localStorage") if storage.IsUndefined() || storage.IsNull() { return nil } obj := js.Global().Get("Object").New() obj.Set("until", ateMs) json := js.Global().Get("JSON").Call("stringify", obj) storage.Call("setItem", chave, json) return nil } func preflight(this js.Value, args []js.Value) any { if len(args) < 1 { return map[string]any{"ok": false, "motivo": "sem_cfg"} } cfg := lerCfg(args[0]) // Bloqueio por data mínima. if antesDaDataMinima(cfg.DataMinimaAbertura) { return map[string]any{"ok": false, "motivo": "antes_da_data_minima"} } // controle de exibição: produto + inquilino_codigo + usuario_codigo if cfg.ProdutoNome == "" || cfg.InquilinoCodigo == "" || cfg.UsuarioCodigo == "" { return map[string]any{"ok": false, "motivo": "contexto_incompleto"} } chaveCooldown := chaveCooldown(cfg.ProdutoNome, cfg.InquilinoCodigo, cfg.UsuarioCodigo) untilMs := calcularCooldownAteMs(cfg.CooldownHours) // Enviamos exatamente o produto_nome informado. payload := map[string]any{ "produto_nome": cfg.ProdutoNome, "inquilino_codigo": cfg.InquilinoCodigo, "inquilino_nome": cfg.InquilinoNome, "usuario_codigo": cfg.UsuarioCodigo, "usuario_nome": cfg.UsuarioNome, "usuario_telefone": cfg.UsuarioTelefone, "usuario_email": normalizarEmail(cfg.UsuarioEmail), } return map[string]any{ "ok": true, "chave_cooldown": chaveCooldown, "cooldown_ate_ms": untilMs, "payload": payload, } } func decidir(this js.Value, args []js.Value) any { // Entrada: // - args[0] = cfg (para cooldownHours) // - args[1] = resposta JSON do servidor (obj) if len(args) < 2 { return map[string]any{"abrir": false, "motivo": "sem_dados"} } cfg := lerCfg(args[0]) data := args[1] untilMs := calcularCooldownAteMs(cfg.CooldownHours) // Fail-closed. if data.IsUndefined() || data.IsNull() { return map[string]any{"abrir": false, "motivo": "resposta_invalida", "aplicar_cooldown": true, "cooldown_ate_ms": untilMs} } // Esperado: {pode_abrir:bool, id:string, produto:string} podeAbrir := data.Get("pode_abrir") if !podeAbrir.Truthy() { return map[string]any{"abrir": false, "motivo": "nao_pode_abrir", "aplicar_cooldown": true, "cooldown_ate_ms": untilMs} } id := strings.TrimSpace(data.Get("id").String()) produtoRota := strings.TrimSpace(data.Get("produto").String()) if id == "" || produtoRota == "" { // Não dá para montar URL segura. return map[string]any{"abrir": false, "motivo": "sem_produto_ou_id", "aplicar_cooldown": true, "cooldown_ate_ms": untilMs} } return map[string]any{ "abrir": true, "id": id, "produto_rota": produtoRota, "aplicar_cooldown": true, "cooldown_ate_ms": untilMs, } } func lerCfg(v js.Value) cfgWidget { // Leitura defensiva: tudo é best-effort. getStr := func(k string) string { if v.Type() != js.TypeObject { return "" } vv := v.Get(k) if vv.IsUndefined() || vv.IsNull() { return "" } return strings.TrimSpace(vv.String()) } getNum := func(k string) float64 { if v.Type() != js.TypeObject { return 0 } vv := v.Get(k) if vv.IsUndefined() || vv.IsNull() { return 0 } return vv.Float() } return cfgWidget{ ProdutoNome: getStr("produto_nome"), InquilinoCodigo: getStr("inquilino_codigo"), InquilinoNome: getStr("inquilino_nome"), UsuarioCodigo: getStr("usuario_codigo"), UsuarioNome: getStr("usuario_nome"), UsuarioTelefone: getStr("usuario_telefone"), UsuarioEmail: getStr("usuario_email"), CooldownHours: getNum("cooldownHours"), DataMinimaAbertura: getStr("data_minima_abertura"), } } func normalizarEmail(email string) string { return strings.ToLower(strings.TrimSpace(email)) } func chaveCooldown(produto, inquilino, usuarioCodigo string) string { // Prefixo de storage atualizado para o novo nome do projeto. return "eli-nps:cooldown:" + produto + ":" + inquilino + ":" + usuarioCodigo } func calcularCooldownAteMs(hours float64) int64 { if hours <= 0 { hours = 24 } agora := js.Global().Get("Date").Call("now").Int() return int64(agora) + int64(hours*3600*1000) } func antesDaDataMinima(s string) bool { // Aceita somente ISO (data) YYYY-MM-DD. v := strings.TrimSpace(s) if v == "" { return false } m := dataISORe.FindStringSubmatch(v) if m == nil { return false } // new Date(ano, mes-1, dia, 0, 0, 0, 0) ano := m[1] mes := m[2] dia := m[3] // Converte para números via JS (simplifica validação e compatibilidade de timezone). nAno := js.Global().Get("Number").Invoke(ano).Int() nMes := js.Global().Get("Number").Invoke(mes).Int() nDia := js.Global().Get("Number").Invoke(dia).Int() if nAno <= 0 || nMes < 1 || nMes > 12 || nDia < 1 || nDia > 31 { return false } dataMin := js.Global().Get("Date").New(nAno, nMes-1, nDia, 0, 0, 0, 0) agora := js.Global().Get("Date").New() return agora.Call("getTime").Int() < dataMin.Call("getTime").Int() }