249 lines
6.8 KiB
Go
249 lines
6.8 KiB
Go
//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()
|
|
}
|