dana-medical-site/internal/storage/leads.go
2026-10-14 13:33:13 +00:00

107 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package storage
import (
"context"
"encoding/json"
"errors"
"net/netip"
"dana/internal/domain"
)
// ErrFormNotFound — формы с таким кодом нет или она выключена.
var ErrFormNotFound = errors.New("form not found")
// FormByCode отдаёт описание формы вместе с полями — фронт по нему строит форму,
// а сервер по нему же проверяет присланное.
func (s *Storage) FormByCode(ctx context.Context, code string) (*domain.Form, error) {
var f domain.Form
err := s.pool.QueryRow(ctx, `
SELECT id, code, title, success_text
FROM forms WHERE code = $1 AND published`, code).
Scan(&f.ID, &f.Code, &f.Title, &f.SuccessText)
if errors.Is(wrapNotFound(err), ErrNotFound) {
return nil, ErrFormNotFound
}
if err != nil {
return nil, err
}
rows, err := s.pool.Query(ctx, `
SELECT code, label, type, required, options
FROM form_fields WHERE form_id = $1 ORDER BY sort, id`, f.ID)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var ff domain.FormField
if err := rows.Scan(&ff.Code, &ff.Label, &ff.Type, &ff.Required, &ff.Options); err != nil {
return nil, err
}
f.Fields = append(f.Fields, ff)
}
return &f, rows.Err()
}
// Forms отдаёт все опубликованные формы (для админки и для фронта разом).
func (s *Storage) Forms(ctx context.Context) ([]domain.Form, error) {
rows, err := s.pool.Query(ctx, `
SELECT code FROM forms WHERE published ORDER BY id`)
if err != nil {
return nil, err
}
var codes []string
for rows.Next() {
var c string
if err := rows.Scan(&c); err != nil {
rows.Close()
return nil, err
}
codes = append(codes, c)
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, err
}
out := make([]domain.Form, 0, len(codes))
for _, c := range codes {
f, err := s.FormByCode(ctx, c)
if err != nil {
return nil, err
}
out = append(out, *f)
}
return out, nil
}
// CreateLead сохраняет заявку с сайта. Согласие на обработку персональных данных
// фиксируется временем записи — без него заявку принимать нельзя (152-ФЗ).
func (s *Storage) CreateLead(ctx context.Context, formID *int64, in domain.LeadInput, ip, userAgent string) (int64, error) {
extra, err := json.Marshal(in.Extra)
if err != nil {
return 0, err
}
utm, err := json.Marshal(in.UTM)
if err != nil {
return 0, err
}
// inet не принимает пустую строку и мусор — кладём NULL, если адрес не разобрался.
var addr any
if parsed, err := netip.ParseAddr(ip); err == nil {
addr = parsed.String()
}
var id int64
err = s.pool.QueryRow(ctx, `
INSERT INTO leads (form_id, name, phone, email, service_id, doctor_id, branch_id,
message, payload, page_url, utm, ip, user_agent, consent_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13, now())
RETURNING id`,
formID, in.Name, in.Phone, in.Email, in.ServiceID, in.DoctorID, in.BranchID,
in.Message, extra, in.PageURL, utm, addr, userAgent).Scan(&id)
return id, err
}