// ToDo: not currently used and need to carve out sql package messagesvc import ( "context" "database/sql" "errors" "time" accountMessageHandler "synlotto-website/internal/handlers/account/messages" ) // Service implements accountMessageHandler.MessageService. type Service struct { DB *sql.DB Dialect string // "postgres", "mysql", "sqlite" (affects INSERT id retrieval) Now func() time.Time Timeout time.Duration } func New(db *sql.DB, opts ...func(*Service)) *Service { s := &Service{ DB: db, Dialect: "mysql", // sane default for LastInsertId (works for mysql/sqlite) Now: time.Now, Timeout: 5 * time.Second, } for _, opt := range opts { opt(s) } return s } // WithDialect sets SQL dialect hints: "postgres" uses RETURNING id. func WithDialect(d string) func(*Service) { return func(s *Service) { s.Dialect = d } } // WithTimeout overrides per-call context timeout. func WithTimeout(d time.Duration) func(*Service) { return func(s *Service) { s.Timeout = d } } func (s *Service) ListInbox(userID int64) ([]accountMessageHandler.Message, error) { ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) defer cancel() const q = ` SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at FROM messages WHERE user_id = ? AND is_archived = FALSE ORDER BY created_at DESC` rows, err := s.DB.QueryContext(s.rebind(ctx), s.bind(q), userID) if err != nil { return nil, err } defer rows.Close() var out []accountMessageHandler.Message for rows.Next() { var m accountMessageHandler.Message if err := rows.Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt); err != nil { return nil, err } out = append(out, m) } return out, rows.Err() } func (s *Service) ListArchived(userID int64) ([]accountMessageHandler.Message, error) { ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) defer cancel() const q = ` SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at FROM messages WHERE user_id = ? AND is_archived = TRUE ORDER BY created_at DESC` rows, err := s.DB.QueryContext(s.rebind(ctx), s.bind(q), userID) if err != nil { return nil, err } defer rows.Close() var out []accountMessageHandler.Message for rows.Next() { var m accountMessageHandler.Message if err := rows.Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt); err != nil { return nil, err } out = append(out, m) } return out, rows.Err() } func (s *Service) GetByID(userID, id int64) (*accountMessageHandler.Message, error) { ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) defer cancel() const q = ` SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at FROM messages WHERE user_id = ? AND id = ?` var m accountMessageHandler.Message err := s.DB.QueryRowContext(s.rebind(ctx), s.bind(q), userID, id). Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, err } return &m, nil } func (s *Service) Create(userID int64, in accountMessageHandler.CreateMessageInput) (int64, error) { ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) defer cancel() switch s.Dialect { case "postgres": const q = ` INSERT INTO messages (user_id, from_email, to_email, subject, body, is_read, is_archived, created_at) VALUES ($1, $2, $3, $4, $5, FALSE, FALSE, NOW()) RETURNING id` var id int64 if err := s.DB.QueryRowContext(ctx, q, userID, "", in.To, in.Subject, in.Body).Scan(&id); err != nil { return 0, err } return id, nil default: // mysql/sqlite const q = ` INSERT INTO messages (user_id, from_email, to_email, subject, body, is_read, is_archived, created_at) VALUES (?, ?, ?, ?, ?, FALSE, FALSE, CURRENT_TIMESTAMP)` res, err := s.DB.ExecContext(s.rebind(ctx), q, userID, "", in.To, in.Subject, in.Body) if err != nil { return 0, err } return res.LastInsertId() } } // --- small helpers --- // bind replaces ? with $1.. for Postgres if needed. // We keep queries written with ? for brevity and adapt here. func (s *Service) bind(q string) string { if s.Dialect != "postgres" { return q } // cheap replacer for positional params: n := 0 out := make([]byte, 0, len(q)+10) for i := 0; i < len(q); i++ { if q[i] == '?' { n++ out = append(out, '$') out = append(out, []byte(intToStr(n))...) continue } out = append(out, q[i]) } return string(out) } func (s *Service) rebind(ctx context.Context) context.Context { return ctx } // intToStr avoids fmt for tiny helper func intToStr(n int) string { if n == 0 { return "0" } var b [12]byte i := len(b) for n > 0 { i-- b[i] = byte('0' + n%10) n /= 10 } return string(b[i:]) }