diff --git a/internal/domain/messages/domain.go b/internal/domain/messages/domain.go new file mode 100644 index 0000000..e2d052d --- /dev/null +++ b/internal/domain/messages/domain.go @@ -0,0 +1,20 @@ +package domainMessages + +import ( + "synlotto-website/internal/models" +) + +type Message = models.Message + +type CreateMessageInput struct { + RecipientID int64 `form:"to" binding:"required,username"` + Subject string `form:"subject" binding:"required,max=200"` + Body string `form:"body" binding:"required"` +} + +type MessageService interface { + ListInbox(userID int64) ([]Message, error) + ListArchived(userID int64) ([]Message, error) + GetByID(userID, id int64) (*Message, error) + Create(userID int64, in CreateMessageInput) (int64, error) +} diff --git a/internal/domain/notifications/domain.go b/internal/domain/notifications/domain.go new file mode 100644 index 0000000..f17c4d6 --- /dev/null +++ b/internal/domain/notifications/domain.go @@ -0,0 +1,14 @@ +package domainMessages + +import ( + "synlotto-website/internal/models" +) + +// ToDo: Should be taken from model. +type Notification = models.Notification + +// ToDo: Should interfaces be else where? +type NotificationService interface { + List(userID int64) ([]Notification, error) + GetByID(userID, id int64) (*Notification, error) +} diff --git a/internal/handlers/account/messages/archive.go b/internal/handlers/account/messages/archive.go index f9705e0..2c9e1b1 100644 --- a/internal/handlers/account/messages/archive.go +++ b/internal/handlers/account/messages/archive.go @@ -7,20 +7,46 @@ package accountMessageHandler import ( "net/http" + templateHelpers "synlotto-website/internal/helpers/template" + + "synlotto-website/internal/logging" + "synlotto-website/internal/models" + "synlotto-website/internal/platform/bootstrap" + "github.com/gin-gonic/gin" + "github.com/justinas/nosurf" ) // GET /account/messages/archived // Renders: web/templates/account/messages/archived.html func (h *AccountMessageHandlers) ArchivedList(c *gin.Context) { + app := c.MustGet("app").(*bootstrap.App) + sm := app.SessionManager + userID := mustUserID(c) msgs, err := h.Svc.ListArchived(userID) if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to load archived messages"}) + logging.Info("❌ list archived error: %v", err) + c.String(http.StatusInternalServerError, "Failed to load archived messages") return } - c.HTML(http.StatusOK, "account/messages/archived.html", gin.H{ - "title": "Archived Messages", - "messages": msgs, - }) + + ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{}) + if f := sm.PopString(c.Request.Context(), "flash"); f != "" { + ctx["Flash"] = f + } + ctx["CSRFToken"] = nosurf.Token(c.Request) + ctx["Title"] = "Archived Messages" + ctx["Messages"] = msgs + + tmpl := templateHelpers.LoadTemplateFiles( + "layout.html", + "web/templates/account/messages/archived.html", + ) + + c.Status(http.StatusOK) + if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { + logging.Info("❌ Template render error: %v", err) + c.String(http.StatusInternalServerError, "Error rendering archived messages") + } } diff --git a/internal/handlers/account/messages/read.go b/internal/handlers/account/messages/read.go index d687463..953a4c9 100644 --- a/internal/handlers/account/messages/read.go +++ b/internal/handlers/account/messages/read.go @@ -7,40 +7,88 @@ package accountMessageHandler import ( "net/http" + templateHelpers "synlotto-website/internal/helpers/template" + + "synlotto-website/internal/logging" + "synlotto-website/internal/models" + "synlotto-website/internal/platform/bootstrap" + "github.com/gin-gonic/gin" + "github.com/justinas/nosurf" ) // GET /account/messages // Renders: web/templates/account/messages/index.html func (h *AccountMessageHandlers) List(c *gin.Context) { + app := c.MustGet("app").(*bootstrap.App) + sm := app.SessionManager + userID := mustUserID(c) + + // Pull messages (via service) msgs, err := h.Svc.ListInbox(userID) if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to load messages"}) + logging.Info("❌ list inbox error: %v", err) + c.String(http.StatusInternalServerError, "Failed to load messages") return } - c.HTML(http.StatusOK, "account/messages/index.html", gin.H{ - "title": "Messages", - "messages": msgs, - }) + + // Build template context just like LoginGet + ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{}) + + if f := sm.PopString(c.Request.Context(), "flash"); f != "" { + ctx["Flash"] = f + } + + ctx["CSRFToken"] = nosurf.Token(c.Request) + ctx["Title"] = "Messages" + ctx["Messages"] = msgs + + // Use the same loader + layout pattern + tmpl := templateHelpers.LoadTemplateFiles("layout.html", "web/templates/account/messages/index.html") + + c.Status(http.StatusOK) + if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { + logging.Info("❌ Template render error: %v", err) + c.String(http.StatusInternalServerError, "Error rendering messages page") + } } // GET /account/messages/:id // Renders: web/templates/account/messages/read.html func (h *AccountMessageHandlers) ReadGet(c *gin.Context) { + app := c.MustGet("app").(*bootstrap.App) + sm := app.SessionManager + userID := mustUserID(c) id, err := parseIDParam(c, "id") if err != nil { c.AbortWithStatus(http.StatusNotFound) return } + msg, err := h.Svc.GetByID(userID, id) if err != nil || msg == nil { c.AbortWithStatus(http.StatusNotFound) return } - c.HTML(http.StatusOK, "account/messages/read.html", gin.H{ - "title": msg.Subject, - "message": msg, - }) + + ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{}) + if f := sm.PopString(c.Request.Context(), "flash"); f != "" { + ctx["Flash"] = f + } + ctx["CSRFToken"] = nosurf.Token(c.Request) + ctx["Title"] = msg.Subject + ctx["Message"] = msg + + tmpl := templateHelpers.LoadTemplateFiles( + "layout.html", + "web/templates/account/messages/read.html", + ) + + c.Status(http.StatusOK) + if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { + logging.Info("❌ Template render error: %v", err) + c.String(http.StatusInternalServerError, "Error rendering message") + } } diff --git a/internal/handlers/account/messages/send.go b/internal/handlers/account/messages/send.go index 5790f07..d2d8e93 100644 --- a/internal/handlers/account/messages/send.go +++ b/internal/handlers/account/messages/send.go @@ -7,38 +7,101 @@ package accountMessageHandler import ( "net/http" + domain "synlotto-website/internal/domain/messages" + templateHelpers "synlotto-website/internal/helpers/template" + + "synlotto-website/internal/logging" + "synlotto-website/internal/models" + "synlotto-website/internal/platform/bootstrap" + "github.com/gin-gonic/gin" + "github.com/justinas/nosurf" ) // GET /account/messages/add // Renders: web/templates/account/messages/send.html func (h *AccountMessageHandlers) AddGet(c *gin.Context) { - c.HTML(http.StatusOK, "account/messages/send.html", gin.H{ - "title": "Send Message", - }) + app := c.MustGet("app").(*bootstrap.App) + sm := app.SessionManager + + ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{}) + if f := sm.PopString(c.Request.Context(), "flash"); f != "" { + ctx["Flash"] = f + } + ctx["CSRFToken"] = nosurf.Token(c.Request) + ctx["Title"] = "Send Message" + + tmpl := templateHelpers.LoadTemplateFiles( + "layout.html", + "web/templates/account/messages/send.html", + ) + + c.Status(http.StatusOK) + if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { + logging.Info("❌ Template render error: %v", err) + c.String(http.StatusInternalServerError, "Error rendering send message page") + } } // POST /account/messages/add func (h *AccountMessageHandlers) AddPost(c *gin.Context) { + app := c.MustGet("app").(*bootstrap.App) + sm := app.SessionManager + userID := mustUserID(c) - var in CreateMessageInput + + var in domain.CreateMessageInput if err := c.ShouldBind(&in); err != nil { // Re-render form with validation errors - c.HTML(http.StatusBadRequest, "account/messages/send.html", gin.H{ - "title": "Send Message", - "error": "Please correct the errors below.", - "form": in, - }) + ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{}) + if f := sm.PopString(c.Request.Context(), "flash"); f != "" { + ctx["Flash"] = f + } + ctx["CSRFToken"] = nosurf.Token(c.Request) + ctx["Title"] = "Send Message" + ctx["Error"] = "Please correct the errors below." + ctx["Form"] = in + + tmpl := templateHelpers.LoadTemplateFiles( + "layout.html", + "web/templates/account/messages/send.html", + ) + + c.Status(http.StatusBadRequest) + if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { + logging.Info("❌ Template render error: %v", err) + c.String(http.StatusInternalServerError, "Error rendering send message page") + } return } + if _, err := h.Svc.Create(userID, in); err != nil { - c.HTML(http.StatusInternalServerError, "account/messages/send.html", gin.H{ - "title": "Send Message", - "error": "Could not send message.", - "form": in, - }) + ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{}) + if f := sm.PopString(c.Request.Context(), "flash"); f != "" { + ctx["Flash"] = f + } + ctx["CSRFToken"] = nosurf.Token(c.Request) + ctx["Title"] = "Send Message" + ctx["Error"] = "Could not send message." + ctx["Form"] = in + + tmpl := templateHelpers.LoadTemplateFiles( + "layout.html", + "web/templates/account/messages/send.html", + ) + + c.Status(http.StatusInternalServerError) + if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { + logging.Info("❌ Template render error: %v", err) + c.String(http.StatusInternalServerError, "Error rendering send message page") + } return } + + // Optional: set a flash message for success (since you already PopString elsewhere) + // If you're using scs/v2, Put is available: + sm.Put(c.Request.Context(), "flash", "Message sent!") + // Redirect back to inbox c.Redirect(http.StatusSeeOther, "/account/messages") } diff --git a/internal/handlers/account/messages/types.go b/internal/handlers/account/messages/types.go index 5b8eb01..d1407f2 100644 --- a/internal/handlers/account/messages/types.go +++ b/internal/handlers/account/messages/types.go @@ -4,33 +4,8 @@ package accountMessageHandler -import "time" +import domain "synlotto-website/internal/domain/messages" type AccountMessageHandlers struct { - Svc MessageService -} - -type CreateMessageInput struct { - To string `form:"to" binding:"required,email"` - Subject string `form:"subject" binding:"required,max=200"` - Body string `form:"body" binding:"required"` -} - -type Message struct { - ID int64 - From string - To string - Subject string - Body string - IsRead bool - IsArchived bool - CreatedAt time.Time -} - -// ToDo: Should interfaces be else where? -type MessageService interface { - ListInbox(userID int64) ([]Message, error) - ListArchived(userID int64) ([]Message, error) - GetByID(userID, id int64) (*Message, error) - Create(userID int64, in CreateMessageInput) (int64, error) + Svc domain.MessageService } diff --git a/internal/handlers/account/notifications/list.go b/internal/handlers/account/notifications/list.go index d5a5b78..8735067 100644 --- a/internal/handlers/account/notifications/list.go +++ b/internal/handlers/account/notifications/list.go @@ -3,66 +3,32 @@ package accountNotificationHandler import ( "net/http" + templateHelpers "synlotto-website/internal/helpers/template" + + "synlotto-website/internal/logging" + "synlotto-website/internal/models" + "synlotto-website/internal/platform/bootstrap" + "github.com/gin-gonic/gin" + "github.com/justinas/nosurf" ) -func NewAccountNotificationHandlers(svc NotificationService) *AccountNotificationHandlers { - return &AccountNotificationHandlers{Svc: svc} -} - -// GET /account/notifications -// Renders: web/templates/account/notifications/index.html -func (h *AccountNotificationHandlers) List(c *gin.Context) { - userID := mustUserID(c) - items, err := h.Svc.List(userID) - if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to load notifications"}) - return - } - c.HTML(http.StatusOK, "account/notifications/index.html", gin.H{ - "title": "Notifications", - "notifications": items, - }) -} - -// --- Optional since you have read.html --- - -// GET /account/notifications/:id -// Renders: web/templates/account/notifications/read.html -func (h *AccountNotificationHandlers) ReadGet(c *gin.Context) { - userID := mustUserID(c) - id, err := parseIDParam(c, "id") - if err != nil { - c.AbortWithStatus(http.StatusNotFound) - return - } - nt, err := h.Svc.GetByID(userID, id) - if err != nil || nt == nil { - c.AbortWithStatus(http.StatusNotFound) - return - } - c.HTML(http.StatusOK, "account/notifications/read.html", gin.H{ - "title": nt.Title, - "notification": nt, - }) -} - -// --- helpers (same as in messages; consider sharing in a pkg) --- - +// ToDo: functional also in messages needs to come out func mustUserID(c *gin.Context) int64 { + // Pull from your auth middleware/session. Panic-unsafe alternative: if v, ok := c.Get("userID"); ok { if id, ok2 := v.(int64); ok2 { return id } } + // Fallback for stubs: return 1 } -func parseIDParam(c *gin.Context, name string) (int64, error) { - return atoi64(c.Param(name)) -} - +// ToDo: functional also in messages needs to come out func atoi64(s string) (int64, error) { + // small helper to keep imports focused + // replace with strconv.ParseInt in real code var n int64 for _, ch := range []byte(s) { if ch < '0' || ch > '9' { @@ -76,3 +42,34 @@ func atoi64(s string) (int64, error) { type strconvNumErr struct{} func (e *strconvNumErr) Error() string { return "invalid number" } + +// GET /account/notifications/:id +// Renders: web/templates/account/notifications/read.html +func (h *AccountNotificationHandlers) List(c *gin.Context) { + app := c.MustGet("app").(*bootstrap.App) + sm := app.SessionManager + + userID := mustUserID(c) + notes, err := h.Svc.List(userID) // or ListAll/ListUnread – use your method name + if err != nil { + logging.Info("❌ list notifications error: %v", err) + c.String(http.StatusInternalServerError, "Failed to load notifications") + return + } + + ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{}) + if f := sm.PopString(c.Request.Context(), "flash"); f != "" { + ctx["Flash"] = f + } + ctx["CSRFToken"] = nosurf.Token(c.Request) + ctx["Title"] = "Notifications" + ctx["Notifications"] = notes + + tmpl := templateHelpers.LoadTemplateFiles("layout.html", "web/templates/account/notifications/index.html") + + c.Status(http.StatusOK) + if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { + logging.Info("❌ Template render error: %v", err) + c.String(http.StatusInternalServerError, "Error rendering notifications page") + } +} diff --git a/internal/handlers/account/notifications/read.go b/internal/handlers/account/notifications/read.go new file mode 100644 index 0000000..31b8bbe --- /dev/null +++ b/internal/handlers/account/notifications/read.go @@ -0,0 +1,58 @@ +// internal/handlers/account/notifications/read.go +package accountNotificationHandler + +import ( + "net/http" + + templateHelpers "synlotto-website/internal/helpers/template" + "synlotto-website/internal/logging" + "synlotto-website/internal/models" + "synlotto-website/internal/platform/bootstrap" + + "github.com/gin-gonic/gin" + "github.com/justinas/nosurf" +) + +// ToDo: functional also in messages needs to come out +func parseIDParam(c *gin.Context, name string) (int64, error) { + // typical atoi wrapper + // (implement: strconv.ParseInt(c.Param(name), 10, 64)) + return atoi64(c.Param(name)) +} + +func (h *AccountNotificationHandlers) ReadGet(c *gin.Context) { + app := c.MustGet("app").(*bootstrap.App) + sm := app.SessionManager + + userID := mustUserID(c) + id, err := parseIDParam(c, "id") + if err != nil { + c.AbortWithStatus(http.StatusNotFound) + return + } + + n, err := h.Svc.GetByID(userID, id) + if err != nil || n == nil { + c.AbortWithStatus(http.StatusNotFound) + return + } + + ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{}) + if f := sm.PopString(c.Request.Context(), "flash"); f != "" { + ctx["Flash"] = f + } + ctx["CSRFToken"] = nosurf.Token(c.Request) + ctx["Title"] = n.Title // or Subject/Heading depending on your struct + ctx["Notification"] = n + + tmpl := templateHelpers.LoadTemplateFiles( + "layout.html", + "web/templates/account/notifications/read.html", + ) + + c.Status(http.StatusOK) + if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { + logging.Info("❌ Template render error: %v", err) + c.String(http.StatusInternalServerError, "Error rendering notification") + } +} diff --git a/internal/handlers/account/notifications/types.go b/internal/handlers/account/notifications/types.go index dc3bd13..3cf5603 100644 --- a/internal/handlers/account/notifications/types.go +++ b/internal/handlers/account/notifications/types.go @@ -1,21 +1,7 @@ package accountNotificationHandler -import "time" - -type Notification struct { - ID int64 - Title string - Body string - IsRead bool - CreatedAt time.Time -} +import domain "synlotto-website/internal/domain/notifications" type AccountNotificationHandlers struct { - Svc NotificationService -} - -// ToDo: Should interfaces be else where? -type NotificationService interface { - List(userID int64) ([]Notification, error) - GetByID(userID, id int64) (*Notification, error) + Svc domain.NotificationService } diff --git a/internal/http/routes/accountroutes.go b/internal/http/routes/accountroutes.go index a85d6eb..28bfbb8 100644 --- a/internal/http/routes/accountroutes.go +++ b/internal/http/routes/accountroutes.go @@ -22,7 +22,7 @@ package routes import ( accountHandler "synlotto-website/internal/handlers/account" - accoutMessageHandler "synlotto-website/internal/handlers/account/messages" + accountMsgHandlers "synlotto-website/internal/handlers/account/messages" accountNotificationHandler "synlotto-website/internal/handlers/account/notifications" accountTicketHandler "synlotto-website/internal/handlers/account/tickets" @@ -33,6 +33,16 @@ import ( func RegisterAccountRoutes(app *bootstrap.App) { r := app.Router + // Instantiate handlers that have method receivers + messageSvc := app.Services.Messages + msgH := &accountMsgHandlers.AccountMessageHandlers{Svc: messageSvc} + + notificationSvc := app.Services.Notifications + notifH := &accountNotificationHandler.AccountNotificationHandlers{Svc: notificationSvc} + + // ticketSvc := app.Services.TicketService + // ticketH := &accountTickets.AccountTicketHandlers{Svc: ticketSvc} + // Public account pages acc := r.Group("/account") acc.Use(middleware.PublicOnly()) @@ -55,19 +65,19 @@ func RegisterAccountRoutes(app *bootstrap.App) { messages := r.Group("/account/messages") messages.Use(middleware.AuthMiddleware(), middleware.RequireAuth()) { - messages.GET("/", accoutMessageHandler.List) - messages.GET("/add", accoutMessageHandler.AddGet) - messages.POST("/add", accoutMessageHandler.AddPost) - messages.GET("/archived", accoutMessageHandler.ArchivedList) // renders archived.html - messages.GET("/:id", accoutMessageHandler.ReadGet) // renders read.html + messages.GET("/", msgH.List) + messages.GET("/add", msgH.AddGet) + messages.POST("/add", msgH.AddPost) + messages.GET("/archived", msgH.ArchivedList) // renders archived.html + messages.GET("/:id", msgH.ReadGet) // renders read.html } // Notifications (auth-required) notifications := r.Group("/account/notifications") notifications.Use(middleware.AuthMiddleware(), middleware.RequireAuth()) { - notifications.GET("/", accountNotificationHandler.List) - notifications.GET("/:id", accountNotificationHandler.ReadGet) // renders read.html + notifications.GET("/", notifH.List) + notifications.GET("/:id", notifH.ReadGet) // renders read.html } // Tickets (auth-required) diff --git a/internal/models/user.go b/internal/models/user.go index f49960e..f7d362d 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -18,7 +18,7 @@ type User struct { type Notification struct { ID int UserId int - Subject string + Title string Body string IsRead bool CreatedAt time.Time @@ -30,8 +30,9 @@ type Message struct { SenderId int RecipientId int Subject string - Message string + Body string IsRead bool + IsArchived bool CreatedAt time.Time ArchivedAt *time.Time } diff --git a/internal/platform/bootstrap/loader.go b/internal/platform/bootstrap/loader.go index e92ba2f..4899370 100644 --- a/internal/platform/bootstrap/loader.go +++ b/internal/platform/bootstrap/loader.go @@ -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) diff --git a/internal/platform/services/messages/service.go b/internal/platform/services/messages/service.go index 6b14749..2226a7d 100644 --- a/internal/platform/services/messages/service.go +++ b/internal/platform/services/messages/service.go @@ -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" diff --git a/internal/platform/services/notifications/service.go b/internal/platform/services/notifications/service.go index 821faf6..f9014f1 100644 --- a/internal/platform/services/notifications/service.go +++ b/internal/platform/services/notifications/service.go @@ -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) {