primeira versão do e-li-nps construido com IA
This commit is contained in:
commit
06950d6e2c
34 changed files with 2524 additions and 0 deletions
154
internal/elinps/queries.go
Normal file
154
internal/elinps/queries.go
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
package elinps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"e-li.nps/internal/db"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewStore(pool *pgxpool.Pool) *Store { return &Store{pool: pool} }
|
||||
|
||||
func (s *Store) poolRef() *pgxpool.Pool { return s.pool }
|
||||
|
||||
func ipReal(r *http.Request) string {
|
||||
// IP real do cliente.
|
||||
//
|
||||
// Importante:
|
||||
// - No servidor, usamos middleware.RealIP (chi) que resolve o IP considerando
|
||||
// headers comuns de proxy (X-Forwarded-For / X-Real-IP).
|
||||
// - Aqui usamos o r.RemoteAddr já processado e extraímos apenas o host.
|
||||
// - Se não for possível parsear, retornamos vazio.
|
||||
ip := r.RemoteAddr
|
||||
if host, _, err := net.SplitHostPort(ip); err == nil {
|
||||
ip = host
|
||||
}
|
||||
if net.ParseIP(ip) == nil {
|
||||
return ""
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func (s *Store) EnsureTableForProduto(ctx context.Context, produtoNome string) (table string, err error) {
|
||||
prod, err := db.NormalizeProduto(produtoNome)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
table = db.TableNameForProduto(prod)
|
||||
if err := db.EnsureNPSTable(ctx, s.pool, table); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return table, nil
|
||||
}
|
||||
|
||||
func (s *Store) HasRespostaValidaRecente(ctx context.Context, table, inquilinoCodigo, usuarioCodigo string) (bool, error) {
|
||||
q := fmt.Sprintf(`
|
||||
SELECT 1
|
||||
FROM %s
|
||||
WHERE inquilino_codigo=$1 AND usuario_codigo=$2
|
||||
AND status='respondido' AND valida=true
|
||||
AND respondido_em >= now() - interval '45 days'
|
||||
LIMIT 1`, table)
|
||||
|
||||
var one int
|
||||
err := s.pool.QueryRow(ctx, q, inquilinoCodigo, usuarioCodigo).Scan(&one)
|
||||
if err == pgx.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *Store) HasPedidoEmAbertoRecente(ctx context.Context, table, inquilinoCodigo, usuarioCodigo string) (bool, error) {
|
||||
q := fmt.Sprintf(`
|
||||
SELECT 1
|
||||
FROM %s
|
||||
WHERE inquilino_codigo=$1 AND usuario_codigo=$2
|
||||
AND status='pedido'
|
||||
AND pedido_criado_em >= now() - interval '10 days'
|
||||
LIMIT 1`, table)
|
||||
var one int
|
||||
err := s.pool.QueryRow(ctx, q, inquilinoCodigo, usuarioCodigo).Scan(&one)
|
||||
if err == pgx.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreatePedido(ctx context.Context, table string, in PedidoInput, r *http.Request) (string, error) {
|
||||
q := fmt.Sprintf(`
|
||||
INSERT INTO %s (
|
||||
produto_nome,
|
||||
inquilino_codigo, inquilino_nome,
|
||||
usuario_codigo, usuario_nome, usuario_email, usuario_telefone,
|
||||
status, origem, user_agent, ip_real
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,'pedido','widget_iframe',$8,$9)
|
||||
RETURNING id`, table)
|
||||
|
||||
var id string
|
||||
err := s.pool.QueryRow(ctx, q,
|
||||
in.ProdutoNome,
|
||||
in.InquilinoCodigo, in.InquilinoNome,
|
||||
in.UsuarioCodigo, in.UsuarioNome, in.UsuarioEmail, in.UsuarioTelefone,
|
||||
r.UserAgent(), ipReal(r),
|
||||
).Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (s *Store) GetRegistro(ctx context.Context, table, id string) (Registro, error) {
|
||||
q := fmt.Sprintf(`
|
||||
SELECT id, produto_nome, status, nota, justificativa, pedido_criado_em, respondido_em
|
||||
FROM %s
|
||||
WHERE id=$1`, table)
|
||||
|
||||
var reg Registro
|
||||
err := s.pool.QueryRow(ctx, q, id).Scan(
|
||||
®.ID, ®.ProdutoNome, ®.Status, ®.Nota, ®.Justificativa, ®.PedidoCriadoEm, ®.RespondidoEm,
|
||||
)
|
||||
return reg, err
|
||||
}
|
||||
|
||||
func (s *Store) PatchRegistro(ctx context.Context, table, id string, in PatchInput) error {
|
||||
// UPDATE único com campos opcionais.
|
||||
q := fmt.Sprintf(`
|
||||
UPDATE %s
|
||||
SET
|
||||
nota = COALESCE($2, nota),
|
||||
justificativa = COALESCE($3, justificativa),
|
||||
status = CASE WHEN $4 THEN 'respondido' ELSE status END,
|
||||
respondido_em = CASE WHEN $4 THEN COALESCE(respondido_em, now()) ELSE respondido_em END,
|
||||
atualizado_em = now()
|
||||
WHERE id=$1`, table)
|
||||
|
||||
_, err := s.pool.Exec(ctx, q, id, in.Nota, in.Justificativa, in.Finalizar)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) TouchAtualizadoEm(ctx context.Context, table, id string) error {
|
||||
q := fmt.Sprintf(`UPDATE %s SET atualizado_em=now() WHERE id=$1`, table)
|
||||
_, err := s.pool.Exec(ctx, q, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) CooldownSuggested(reg Registro) time.Duration {
|
||||
// Não é usado pelo servidor hoje; fica como helper se precisarmos.
|
||||
if reg.Status == "respondido" {
|
||||
return 45 * 24 * time.Hour
|
||||
}
|
||||
return 24 * time.Hour
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue