Continued work on messages and notifications.

This commit is contained in:
2025-10-30 11:11:22 +00:00
parent b41e92629b
commit 8650b1fd63
14 changed files with 387 additions and 172 deletions

View File

@@ -58,8 +58,12 @@ import (
"net/http"
"time"
domainMsgs "synlotto-website/internal/domain/messages"
domainNotifs "synlotto-website/internal/domain/notifications"
weberr "synlotto-website/internal/http/error"
databasePlatform "synlotto-website/internal/platform/database"
messagesvc "synlotto-website/internal/platform/services/messages"
notifysvc "synlotto-website/internal/platform/services/notifications"
"synlotto-website/internal/platform/config"
"synlotto-website/internal/platform/csrf"
@@ -78,6 +82,11 @@ type App struct {
Router *gin.Engine
Handler http.Handler
Server *http.Server
Services struct {
Messages domainMsgs.MessageService
Notifications domainNotifs.NotificationService
}
}
func Load(configPath string) (*App, error) {
@@ -119,6 +128,9 @@ func Load(configPath string) (*App, error) {
Router: router,
}
app.Services.Messages = messagesvc.New(db)
app.Services.Notifications = notifysvc.New(db)
// Inject *App into Gin context for handler access
router.Use(func(c *gin.Context) {
c.Set("app", app)

View File

@@ -1,4 +1,7 @@
// ToDo: not currently used and need to carve out sql
// Package messagesvc
// Path: /internal/platform/services/messages
// File: service.go
package messagesvc
import (
@@ -7,13 +10,13 @@ import (
"errors"
"time"
accountMessageHandler "synlotto-website/internal/handlers/account/messages"
domain "synlotto-website/internal/domain/messages"
)
// Service implements accountMessageHandler.MessageService.
// Service implements domain.Service.
type Service struct {
DB *sql.DB
Dialect string // "postgres", "mysql", "sqlite" (affects INSERT id retrieval)
Dialect string // "postgres", "mysql", "sqlite"
Now func() time.Time
Timeout time.Duration
}
@@ -21,7 +24,7 @@ type Service struct {
func New(db *sql.DB, opts ...func(*Service)) *Service {
s := &Service{
DB: db,
Dialect: "mysql", // sane default for LastInsertId (works for mysql/sqlite)
Dialect: "mysql", // default; works with LastInsertId
Now: time.Now,
Timeout: 5 * time.Second,
}
@@ -31,56 +34,58 @@ func New(db *sql.DB, opts ...func(*Service)) *Service {
return s
}
// WithDialect sets SQL dialect hints: "postgres" uses RETURNING id.
func WithDialect(d string) func(*Service) { return func(s *Service) { s.Dialect = d } }
// Ensure *Service satisfies the domain interface.
var _ domain.MessageService = (*Service)(nil)
// 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) {
func (s *Service) ListInbox(userID int64) ([]domain.Message, error) {
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
defer cancel()
const q = `
q := `
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
FROM messages
FROM users_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)
q = s.bind(q)
rows, err := s.DB.QueryContext(ctx, q, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []accountMessageHandler.Message
var out []domain.Message
for rows.Next() {
var m accountMessageHandler.Message
var m domain.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) {
func (s *Service) ListArchived(userID int64) ([]domain.Message, error) {
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
defer cancel()
const q = `
q := `
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
FROM messages
FROM users_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)
q = s.bind(q)
rows, err := s.DB.QueryContext(ctx, q, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []accountMessageHandler.Message
var out []domain.Message
for rows.Next() {
var m accountMessageHandler.Message
var m domain.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
}
@@ -89,16 +94,18 @@ func (s *Service) ListArchived(userID int64) ([]accountMessageHandler.Message, e
return out, rows.Err()
}
func (s *Service) GetByID(userID, id int64) (*accountMessageHandler.Message, error) {
func (s *Service) GetByID(userID, id int64) (*domain.Message, error) {
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
defer cancel()
const q = `
q := `
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
FROM messages
FROM users_messages
WHERE user_id = ? AND id = ?`
var m accountMessageHandler.Message
err := s.DB.QueryRowContext(s.rebind(ctx), s.bind(q), userID, id).
q = s.bind(q)
var m domain.Message
err := s.DB.QueryRowContext(ctx, 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
@@ -109,7 +116,7 @@ func (s *Service) GetByID(userID, id int64) (*accountMessageHandler.Message, err
return &m, nil
}
func (s *Service) Create(userID int64, in accountMessageHandler.CreateMessageInput) (int64, error) {
func (s *Service) Create(userID int64, in domain.CreateMessageInput) (int64, error) {
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
defer cancel()
@@ -128,7 +135,7 @@ func (s *Service) Create(userID int64, in accountMessageHandler.CreateMessageInp
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)
res, err := s.DB.ExecContext(ctx, q, userID, "", in.To, in.Subject, in.Body)
if err != nil {
return 0, err
}
@@ -136,17 +143,13 @@ func (s *Service) Create(userID int64, in accountMessageHandler.CreateMessageInp
}
}
// --- small helpers ---
// bind replaces ? with $1.. for Postgres if needed.
// We keep queries written with ? for brevity and adapt here.
// bind replaces '?' with '$1..' only for Postgres. For MySQL/SQLite it returns q unchanged.
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)
out := make([]byte, 0, len(q)+8)
for i := 0; i < len(q); i++ {
if q[i] == '?' {
n++
@@ -159,9 +162,7 @@ func (s *Service) bind(q string) string {
return string(out)
}
func (s *Service) rebind(ctx context.Context) context.Context { return ctx }
// intToStr avoids fmt for tiny helper
// ToDo: helper dont think it should be here.
func intToStr(n int) string {
if n == 0 {
return "0"

View File

@@ -1,4 +1,8 @@
// Package notifysvc
// Path: /internal/platform/services/notifications
// File: service.go
// ToDo: carve out sql
package notifysvc
import (
@@ -7,7 +11,7 @@ import (
"errors"
"time"
accountNotificationHandler "synlotto-website/internal/handlers/account/notifications"
domain "synlotto-website/internal/domain/notifications"
)
type Service struct {
@@ -31,7 +35,7 @@ func New(db *sql.DB, opts ...func(*Service)) *Service {
func WithTimeout(d time.Duration) func(*Service) { return func(s *Service) { s.Timeout = d } }
// List returns newest-first notifications for a user.
func (s *Service) List(userID int64) ([]accountNotificationHandler.Notification, error) {
func (s *Service) List(userID int64) ([]domain.Notification, error) {
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
defer cancel()
@@ -47,9 +51,9 @@ ORDER BY created_at DESC`
}
defer rows.Close()
var out []accountNotificationHandler.Notification
var out []domain.Notification
for rows.Next() {
var n accountNotificationHandler.Notification
var n domain.Notification
if err := rows.Scan(&n.ID, &n.Title, &n.Body, &n.IsRead, &n.CreatedAt); err != nil {
return nil, err
}
@@ -58,7 +62,7 @@ ORDER BY created_at DESC`
return out, rows.Err()
}
func (s *Service) GetByID(userID, id int64) (*accountNotificationHandler.Notification, error) {
func (s *Service) GetByID(userID, id int64) (*domain.Notification, error) {
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
defer cancel()
@@ -67,7 +71,7 @@ SELECT id, title, body, is_read, created_at
FROM notifications
WHERE user_id = ? AND id = ?`
var n accountNotificationHandler.Notification
var n domain.Notification
err := s.DB.QueryRowContext(ctx, q, userID, id).
Scan(&n.ID, &n.Title, &n.Body, &n.IsRead, &n.CreatedAt)
if errors.Is(err, sql.ErrNoRows) {