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 }