236 lines
5.6 KiB
Go
236 lines
5.6 KiB
Go
// Package messagesvc
|
|
// Path: /internal/platform/services/messages
|
|
// File: service.go
|
|
|
|
package messagesvc
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"synlotto-website/internal/logging"
|
|
|
|
domain "synlotto-website/internal/domain/messages"
|
|
|
|
"github.com/go-sql-driver/mysql"
|
|
)
|
|
|
|
// Service implements domain.Service.
|
|
type Service struct {
|
|
DB *sql.DB
|
|
Dialect string // "postgres", "mysql", "sqlite"
|
|
Now func() time.Time
|
|
Timeout time.Duration
|
|
}
|
|
|
|
func New(db *sql.DB, opts ...func(*Service)) *Service {
|
|
s := &Service{
|
|
DB: db,
|
|
Dialect: "mysql", // default; works with LastInsertId
|
|
Now: time.Now,
|
|
Timeout: 5 * time.Second,
|
|
}
|
|
for _, opt := range opts {
|
|
opt(s)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Ensure *Service satisfies the domain interface.
|
|
var _ domain.MessageService = (*Service)(nil)
|
|
|
|
func (s *Service) ListInbox(userID int64) ([]domain.Message, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
|
defer cancel()
|
|
|
|
q := `
|
|
SELECT id, senderId, recipientId, subject, body, is_read, is_archived, created_at
|
|
FROM user_messages
|
|
WHERE recipientId = ? AND is_archived = FALSE
|
|
ORDER BY created_at DESC`
|
|
q = s.bind(q)
|
|
|
|
rows, err := s.DB.QueryContext(ctx, q, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []domain.Message
|
|
for rows.Next() {
|
|
var m domain.Message
|
|
if err := rows.Scan(&m.ID, &m.SenderId, &m.RecipientId, &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) ([]domain.Message, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
|
defer cancel()
|
|
|
|
q := `
|
|
SELECT id, senderId, recipientId, subject, body, is_read, is_archived, created_at
|
|
FROM user_messages
|
|
WHERE recipientId = ? AND is_archived = TRUE
|
|
ORDER BY created_at DESC`
|
|
q = s.bind(q)
|
|
|
|
rows, err := s.DB.QueryContext(ctx, q, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []domain.Message
|
|
for rows.Next() {
|
|
var m domain.Message
|
|
if err := rows.Scan(&m.ID, &m.SenderId, &m.RecipientId, &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) (*domain.Message, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
|
defer cancel()
|
|
|
|
q := `
|
|
SELECT id, senderId, recipientId, subject, body, is_read, is_archived, created_at
|
|
FROM user_messages
|
|
WHERE recipientId = ? AND id = ?`
|
|
q = s.bind(q)
|
|
|
|
var m domain.Message
|
|
err := s.DB.QueryRowContext(ctx, q, userID, id).
|
|
Scan(&m.ID, &m.SenderId, &m.RecipientId, &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(senderID int64, in domain.CreateMessageInput) (int64, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
|
defer cancel()
|
|
|
|
// ✅ make sure this matches your current table/column names
|
|
const q = `
|
|
INSERT INTO user_messages
|
|
(senderId, recipientId, subject, body, is_read, is_archived, created_at)
|
|
VALUES
|
|
(?, ?, ?, ?, 0, 0, CURRENT_TIMESTAMP)
|
|
`
|
|
|
|
// 👀 Log the SQL and arguments (truncate body in logs if you prefer)
|
|
logging.Info("🧪 SQL Exec: %s | args: senderId=%d recipientId=%d subject=%q body_len=%d", compactSQL(q), senderID, in.RecipientID, in.Subject, len(in.Body))
|
|
|
|
res, err := s.DB.ExecContext(ctx, q, senderID, in.RecipientID, in.Subject, in.Body)
|
|
if err != nil {
|
|
// Surface MySQL code/message (very helpful for FK #1452 etc.)
|
|
var me *mysql.MySQLError
|
|
if errors.As(err, &me) {
|
|
wrapped := fmt.Errorf("insert user_messages: mysql #%d %s | args(senderId=%d, recipientId=%d, subject=%q, body_len=%d)",
|
|
me.Number, me.Message, senderID, in.RecipientID, in.Subject, len(in.Body))
|
|
logging.Info("❌ %v", wrapped)
|
|
return 0, wrapped
|
|
}
|
|
wrapped := fmt.Errorf("insert user_messages: %w | args(senderId=%d, recipientId=%d, subject=%q, body_len=%d)",
|
|
err, senderID, in.RecipientID, in.Subject, len(in.Body))
|
|
logging.Info("❌ %v", wrapped)
|
|
return 0, wrapped
|
|
}
|
|
|
|
id, err := res.LastInsertId()
|
|
if err != nil {
|
|
wrapped := fmt.Errorf("lastInsertId user_messages: %w", err)
|
|
logging.Info("❌ %v", wrapped)
|
|
return 0, wrapped
|
|
}
|
|
|
|
logging.Info("✅ Inserted message id=%d", id)
|
|
return id, nil
|
|
}
|
|
|
|
func compactSQL(s string) string {
|
|
out := make([]rune, 0, len(s))
|
|
space := false
|
|
for _, r := range s {
|
|
if r == '\n' || r == '\t' || r == '\r' || r == ' ' {
|
|
if !space {
|
|
out = append(out, ' ')
|
|
space = true
|
|
}
|
|
continue
|
|
}
|
|
space = false
|
|
out = append(out, r)
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
func (s *Service) bind(q string) string {
|
|
if s.Dialect != "postgres" {
|
|
return q
|
|
}
|
|
n := 0
|
|
out := make([]byte, 0, len(q)+8)
|
|
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) Archive(userID, id int64) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
|
defer cancel()
|
|
|
|
q := `
|
|
UPDATE user_messages
|
|
SET is_archived = 1, archived_at = CURRENT_TIMESTAMP
|
|
WHERE id = ? AND recipientId = ?
|
|
`
|
|
q = s.bind(q)
|
|
|
|
res, err := s.DB.ExecContext(ctx, q, id, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
if n == 0 {
|
|
return sql.ErrNoRows
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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:])
|
|
}
|