107 lines
3.2 KiB
Go
107 lines
3.2 KiB
Go
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
|
||
}
|