refatoração de tipagem go
This commit is contained in:
parent
6f78511946
commit
0c41ed4279
12 changed files with 175 additions and 117 deletions
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"e-li.nps/internal/contratos"
|
||||
"e-li.nps/internal/db"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
|
@ -29,7 +30,7 @@ func NewHandlers(pool *pgxpool.Pool) *Handlers {
|
|||
func (h *Handlers) PostPedido(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var in PedidoInput
|
||||
var in contratos.PedidoInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "json_invalido"})
|
||||
return
|
||||
|
|
@ -55,29 +56,29 @@ func (h *Handlers) PostPedido(w http.ResponseWriter, r *http.Request) {
|
|||
if err != nil {
|
||||
slog.Error("erro ao checar resposta recente", "err", err)
|
||||
// Fail-closed.
|
||||
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "erro"})
|
||||
writeJSON(w, http.StatusOK, contratos.PedidoResponse{PodeAbrir: false, Motivo: "erro"})
|
||||
return
|
||||
}
|
||||
if respRecente {
|
||||
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "resposta_recente"})
|
||||
writeJSON(w, http.StatusOK, contratos.PedidoResponse{PodeAbrir: false, Motivo: "resposta_recente"})
|
||||
return
|
||||
}
|
||||
|
||||
pedidoAberto, err := h.store.HasPedidoEmAbertoRecente(ctx, table, in.InquilinoCodigo, in.UsuarioCodigo)
|
||||
if err != nil {
|
||||
slog.Error("erro ao checar pedido em aberto", "err", err)
|
||||
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "erro"})
|
||||
writeJSON(w, http.StatusOK, contratos.PedidoResponse{PodeAbrir: false, Motivo: "erro"})
|
||||
return
|
||||
}
|
||||
if pedidoAberto {
|
||||
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "pedido_em_aberto"})
|
||||
writeJSON(w, http.StatusOK, contratos.PedidoResponse{PodeAbrir: false, Motivo: "pedido_em_aberto"})
|
||||
return
|
||||
}
|
||||
|
||||
id, err := h.store.CreatePedido(ctx, table, in, r)
|
||||
if err != nil {
|
||||
slog.Error("erro ao criar pedido", "err", err)
|
||||
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "erro"})
|
||||
writeJSON(w, http.StatusOK, contratos.PedidoResponse{PodeAbrir: false, Motivo: "erro"})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -102,7 +103,7 @@ func (h *Handlers) PatchResposta(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
var in PatchInput
|
||||
var in contratos.PatchInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "json_invalido"})
|
||||
return
|
||||
|
|
@ -132,7 +133,7 @@ func (h *Handlers) PatchResposta(w http.ResponseWriter, r *http.Request) {
|
|||
w.Write([]byte("db"))
|
||||
return
|
||||
}
|
||||
data := FormPageData{Produto: prod, ID: id, Reg: reg}
|
||||
data := contratos.FormPageData{Produto: prod, ID: id, Reg: reg}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
h.tpl.Render(w, "form_inner.html", data)
|
||||
return
|
||||
|
|
@ -173,7 +174,7 @@ func (h *Handlers) GetForm(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
data := FormPageData{
|
||||
data := contratos.FormPageData{
|
||||
Produto: prod,
|
||||
ID: id,
|
||||
Reg: reg,
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
package elinps
|
||||
|
||||
import "time"
|
||||
|
||||
type PedidoInput struct {
|
||||
ProdutoNome string `json:"produto_nome"`
|
||||
InquilinoCodigo string `json:"inquilino_codigo"`
|
||||
InquilinoNome string `json:"inquilino_nome"`
|
||||
UsuarioCodigo string `json:"usuario_codigo"`
|
||||
UsuarioNome string `json:"usuario_nome"`
|
||||
UsuarioTelefone string `json:"usuario_telefone"`
|
||||
UsuarioEmail string `json:"usuario_email"`
|
||||
}
|
||||
|
||||
type PedidoResponse struct {
|
||||
PodeAbrir bool `json:"pode_abrir"`
|
||||
Motivo string `json:"motivo,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
type PatchInput struct {
|
||||
Nota *int `json:"nota,omitempty"`
|
||||
Justificativa *string `json:"justificativa,omitempty"`
|
||||
Finalizar bool `json:"finalizar,omitempty"`
|
||||
}
|
||||
|
||||
type Registro struct {
|
||||
// ProdutoNome é o nome original do produto como enviado pela integração/widget.
|
||||
// Ele existe apenas para exibição ao usuário.
|
||||
//
|
||||
// Importante: a normalização (remoção de acentos/símbolos) é usada apenas
|
||||
// para formar o nome da tabela no Postgres e o parâmetro {produto} da rota.
|
||||
ProdutoNome string
|
||||
ID string
|
||||
Status string
|
||||
Nota *int
|
||||
Justificativa *string
|
||||
PedidoCriadoEm time.Time
|
||||
RespondidoEm *time.Time
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"e-li.nps/internal/contratos"
|
||||
"e-li.nps/internal/db"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
|
@ -57,37 +58,11 @@ func (a AuthPainel) handlerLoginPost(w http.ResponseWriter, r *http.Request) {
|
|||
http.Redirect(w, r, "/painel", http.StatusFound)
|
||||
}
|
||||
|
||||
// NPSMensal representa o cálculo do NPS agregado por mês.
|
||||
type NPSMensal struct {
|
||||
Mes string
|
||||
Detratores int
|
||||
Neutros int
|
||||
Promotores int
|
||||
Total int
|
||||
NPS int
|
||||
}
|
||||
type NPSMensal = contratos.NPSMensal
|
||||
|
||||
// RespostaPainel representa uma resposta para listagem no painel.
|
||||
type RespostaPainel struct {
|
||||
ID string
|
||||
RespondidoEm *time.Time
|
||||
PedidoCriadoEm time.Time
|
||||
UsuarioCodigo *string
|
||||
UsuarioNome string
|
||||
UsuarioEmail *string
|
||||
Nota *int
|
||||
Justificativa *string
|
||||
}
|
||||
type RespostaPainel = contratos.RespostaPainel
|
||||
|
||||
type PainelDados struct {
|
||||
Produto string
|
||||
Produtos []string
|
||||
Meses []NPSMensal
|
||||
Respostas []RespostaPainel
|
||||
Pagina int
|
||||
SomenteBaixas bool
|
||||
MsgErro string
|
||||
}
|
||||
type PainelDados = contratos.PainelDados
|
||||
|
||||
func (a AuthPainel) handlerPainel(w http.ResponseWriter, r *http.Request, store *Store) {
|
||||
ctx := r.Context()
|
||||
|
|
@ -145,7 +120,7 @@ func (a AuthPainel) handlerPainel(w http.ResponseWriter, r *http.Request, store
|
|||
if err != nil {
|
||||
// Se a tabela ainda não tem coluna ip_real/ etc, EnsureNPSTable deveria ajustar.
|
||||
if err == pgx.ErrNoRows {
|
||||
respostas = []RespostaPainel{}
|
||||
respostas = []contratos.RespostaPainel{}
|
||||
} else {
|
||||
dados.MsgErro = "erro ao listar respostas"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"e-li.nps/internal/contratos"
|
||||
"e-li.nps/internal/db"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
|
@ -43,7 +44,7 @@ ORDER BY tablename`)
|
|||
// - 1–6 detratores
|
||||
// - 7–8 neutros
|
||||
// - 9–10 promotores
|
||||
func (s *Store) NPSMesAMes(ctx context.Context, tabela string, meses int) ([]NPSMensal, error) {
|
||||
func (s *Store) NPSMesAMes(ctx context.Context, tabela string, meses int) ([]contratos.NPSMensal, error) {
|
||||
// Segurança: a tabela é um identificador interpolado. Validamos sempre.
|
||||
if !db.TableNameValido(tabela) {
|
||||
return nil, fmt.Errorf("tabela invalida")
|
||||
|
|
@ -75,9 +76,9 @@ ORDER BY mes ASC`, tabela)
|
|||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := []NPSMensal{}
|
||||
out := []contratos.NPSMensal{}
|
||||
for rows.Next() {
|
||||
var m NPSMensal
|
||||
var m contratos.NPSMensal
|
||||
if err := rows.Scan(&m.Mes, &m.Detratores, &m.Neutros, &m.Promotores, &m.Total); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -97,17 +98,10 @@ type ListarRespostasFiltro struct {
|
|||
PorPagina int
|
||||
}
|
||||
|
||||
func (f *ListarRespostasFiltro) normalizar() {
|
||||
if f.Pagina <= 0 {
|
||||
f.Pagina = 1
|
||||
}
|
||||
if f.PorPagina <= 0 || f.PorPagina > 200 {
|
||||
f.PorPagina = 50
|
||||
}
|
||||
}
|
||||
func (f *ListarRespostasFiltro) normalizar() { (*contratos.ListarRespostasFiltro)(f).Normalizar() }
|
||||
|
||||
// ListarRespostas retorna respostas respondidas, com paginação e filtro.
|
||||
func (s *Store) ListarRespostas(ctx context.Context, tabela string, filtro ListarRespostasFiltro) ([]RespostaPainel, error) {
|
||||
func (s *Store) ListarRespostas(ctx context.Context, tabela string, filtro ListarRespostasFiltro) ([]contratos.RespostaPainel, error) {
|
||||
// Segurança: a tabela é um identificador interpolado. Validamos sempre.
|
||||
if !db.TableNameValido(tabela) {
|
||||
return nil, fmt.Errorf("tabela invalida")
|
||||
|
|
@ -145,9 +139,9 @@ LIMIT $1 OFFSET $2`, tabela, cond)
|
|||
}
|
||||
defer rows.Close()
|
||||
|
||||
respostas := []RespostaPainel{}
|
||||
respostas := []contratos.RespostaPainel{}
|
||||
for rows.Next() {
|
||||
var r RespostaPainel
|
||||
var r contratos.RespostaPainel
|
||||
if err := rows.Scan(
|
||||
&r.ID,
|
||||
&r.RespondidoEm,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"e-li.nps/internal/contratos"
|
||||
"e-li.nps/internal/db"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
|
@ -98,7 +99,7 @@ LIMIT 1`, table)
|
|||
return true, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreatePedido(ctx context.Context, table string, in PedidoInput, r *http.Request) (string, error) {
|
||||
func (s *Store) CreatePedido(ctx context.Context, table string, in contratos.PedidoInput, r *http.Request) (string, error) {
|
||||
// Segurança: a tabela é um identificador interpolado. Validamos sempre.
|
||||
if !db.TableNameValido(table) {
|
||||
return "", fmt.Errorf("tabela invalida")
|
||||
|
|
@ -122,24 +123,24 @@ RETURNING id`, table)
|
|||
return id, err
|
||||
}
|
||||
|
||||
func (s *Store) GetRegistro(ctx context.Context, table, id string) (Registro, error) {
|
||||
func (s *Store) GetRegistro(ctx context.Context, table, id string) (contratos.Registro, error) {
|
||||
// Segurança: a tabela é um identificador interpolado. Validamos sempre.
|
||||
if !db.TableNameValido(table) {
|
||||
return Registro{}, fmt.Errorf("tabela invalida")
|
||||
return contratos.Registro{}, fmt.Errorf("tabela invalida")
|
||||
}
|
||||
q := fmt.Sprintf(`
|
||||
SELECT id, produto_nome, status, nota, justificativa, pedido_criado_em, respondido_em
|
||||
FROM %s
|
||||
WHERE id=$1`, table)
|
||||
|
||||
var reg Registro
|
||||
var reg contratos.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 {
|
||||
func (s *Store) PatchRegistro(ctx context.Context, table, id string, in contratos.PatchInput) error {
|
||||
// Segurança: a tabela é um identificador interpolado. Validamos sempre.
|
||||
if !db.TableNameValido(table) {
|
||||
return fmt.Errorf("tabela invalida")
|
||||
|
|
@ -169,7 +170,7 @@ func (s *Store) TouchAtualizadoEm(ctx context.Context, table, id string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (s *Store) CooldownSuggested(reg Registro) time.Duration {
|
||||
func (s *Store) CooldownSuggested(reg contratos.Registro) time.Duration {
|
||||
// Não é usado pelo servidor hoje; fica como helper se precisarmos.
|
||||
if reg.Status == "respondido" {
|
||||
return 45 * 24 * time.Hour
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"e-li.nps/internal/contratos"
|
||||
)
|
||||
|
||||
type TemplateRenderer struct {
|
||||
|
|
@ -23,8 +25,5 @@ func (r *TemplateRenderer) Render(w http.ResponseWriter, name string, data any)
|
|||
}
|
||||
}
|
||||
|
||||
type FormPageData struct {
|
||||
Produto string
|
||||
ID string
|
||||
Reg Registro
|
||||
}
|
||||
// Alias para manter as chamadas concisas dentro do pacote elinps.
|
||||
type FormPageData = contratos.FormPageData
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"e-li.nps/internal/contratos"
|
||||
)
|
||||
|
||||
var emailRe = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
|
||||
|
|
@ -12,7 +14,7 @@ func normalizeEmail(s string) string {
|
|||
return strings.ToLower(strings.TrimSpace(s))
|
||||
}
|
||||
|
||||
func ValidatePedidoInput(in *PedidoInput) error {
|
||||
func ValidatePedidoInput(in *contratos.PedidoInput) error {
|
||||
in.ProdutoNome = strings.TrimSpace(in.ProdutoNome)
|
||||
in.InquilinoCodigo = strings.TrimSpace(in.InquilinoCodigo)
|
||||
in.InquilinoNome = strings.TrimSpace(in.InquilinoNome)
|
||||
|
|
@ -49,7 +51,7 @@ func ValidatePedidoInput(in *PedidoInput) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func ValidatePatchInput(in *PatchInput) error {
|
||||
func ValidatePatchInput(in *contratos.PatchInput) error {
|
||||
if in.Nota != nil {
|
||||
if *in.Nota < 1 || *in.Nota > 10 {
|
||||
return errors.New("nota invalida")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue