Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
181 lines
7.2 KiB
Go
181 lines
7.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
)
|
|
|
|
type NotificationListOptions struct {
|
|
IncludeRead bool
|
|
IncludeSnoozed bool
|
|
SortOrder string
|
|
}
|
|
|
|
type NotificationListResult struct {
|
|
Notifications []model.Notification
|
|
Total int64
|
|
UnreadCount int64
|
|
}
|
|
|
|
// NotificationService provides business logic for notifications and notification preferences.
|
|
// Reference: Chatwoot app/services/notification_service.rb
|
|
type NotificationService struct {
|
|
db *gorm.DB
|
|
notifRepo *repository.NotificationRepo
|
|
prefRepo *repository.NotificationPreferenceRepo
|
|
}
|
|
|
|
// NewNotificationService creates a new Notification service with required dependencies.
|
|
func NewNotificationService(
|
|
db *gorm.DB,
|
|
notifRepo *repository.NotificationRepo,
|
|
prefRepo *repository.NotificationPreferenceRepo,
|
|
) *NotificationService {
|
|
return &NotificationService{
|
|
db: db,
|
|
notifRepo: notifRepo,
|
|
prefRepo: prefRepo,
|
|
}
|
|
}
|
|
|
|
func (s *NotificationService) DB() *gorm.DB {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
return s.db
|
|
}
|
|
|
|
// --- Notification CRUD operations ---
|
|
|
|
// GetNotification retrieves a single notification by ID.
|
|
func (s *NotificationService) GetNotification(ctx context.Context, id uint) (*model.Notification, error) {
|
|
return s.notifRepo.FindByID(ctx, id)
|
|
}
|
|
|
|
// GetNotificationByAccount retrieves a notification scoped like Chatwoot fetch_notification.
|
|
func (s *NotificationService) GetNotificationByAccount(ctx context.Context, id, userID, accountID uint) (*model.Notification, error) {
|
|
return s.notifRepo.FindByUserAndAccount(ctx, id, userID, accountID)
|
|
}
|
|
|
|
// ListNotifications retrieves all notifications for a user with pagination.
|
|
func (s *NotificationService) ListNotifications(ctx context.Context, userID uint, page, perPage int) ([]model.Notification, int64, error) {
|
|
offset := (page - 1) * perPage
|
|
return s.notifRepo.ListByUser(ctx, userID, offset, perPage)
|
|
}
|
|
|
|
// ListNotificationsByAccount retrieves notifications for a user within an account.
|
|
func (s *NotificationService) ListNotificationsByAccount(ctx context.Context, userID uint, accountID uint, page, perPage int) ([]model.Notification, int64, error) {
|
|
offset := (page - 1) * perPage
|
|
return s.notifRepo.ListByUserAndAccount(ctx, userID, accountID, offset, perPage)
|
|
}
|
|
|
|
func (s *NotificationService) ListNotificationsByAccountWithOptions(ctx context.Context, userID uint, accountID uint, page, perPage int, opts NotificationListOptions) (*NotificationListResult, error) {
|
|
offset := (page - 1) * perPage
|
|
items, total, unreadCount, err := s.notifRepo.ListByUserAndAccountFiltered(ctx, userID, accountID, offset, perPage, repository.NotificationListFilter{
|
|
IncludeRead: opts.IncludeRead,
|
|
IncludeSnoozed: opts.IncludeSnoozed,
|
|
SortOrder: opts.SortOrder,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &NotificationListResult{Notifications: items, Total: total, UnreadCount: unreadCount}, nil
|
|
}
|
|
|
|
// CreateNotification creates a new notification.
|
|
func (s *NotificationService) CreateNotification(ctx context.Context, notification *model.Notification) error {
|
|
return s.notifRepo.Create(ctx, notification)
|
|
}
|
|
|
|
// MarkRead marks a single notification as read.
|
|
func (s *NotificationService) MarkRead(ctx context.Context, id uint) error {
|
|
return s.notifRepo.MarkRead(ctx, id)
|
|
}
|
|
|
|
// MarkReadByAccount marks a single notification as read within current user/account scope.
|
|
func (s *NotificationService) MarkReadByAccount(ctx context.Context, id, userID, accountID uint) (*model.Notification, error) {
|
|
return s.notifRepo.MarkReadByUserAndAccount(ctx, id, userID, accountID)
|
|
}
|
|
|
|
// MarkAllRead marks all unread notifications for a user as read.
|
|
func (s *NotificationService) MarkAllRead(ctx context.Context, userID uint) error {
|
|
return s.notifRepo.MarkAllReadByUser(ctx, userID)
|
|
}
|
|
|
|
// MarkAllReadByAccount marks all unread notifications for a user within an account as read.
|
|
func (s *NotificationService) MarkAllReadByAccount(ctx context.Context, userID uint, accountID uint) error {
|
|
return s.notifRepo.MarkAllReadByUserAndAccount(ctx, userID, accountID)
|
|
}
|
|
|
|
func (s *NotificationService) MarkPrimaryActorReadByAccount(ctx context.Context, userID, accountID uint, actorType string, actorID uint) error {
|
|
return s.notifRepo.MarkPrimaryActorReadByUserAndAccount(ctx, userID, accountID, actorType, actorID)
|
|
}
|
|
|
|
// DeleteNotification soft-deletes a notification.
|
|
func (s *NotificationService) DeleteNotification(ctx context.Context, id uint) error {
|
|
return s.notifRepo.Delete(ctx, id)
|
|
}
|
|
|
|
// DeleteNotificationByAccount deletes a scoped notification.
|
|
func (s *NotificationService) DeleteNotificationByAccount(ctx context.Context, id, userID, accountID uint) error {
|
|
return s.notifRepo.DeleteByUserAndAccount(ctx, id, userID, accountID)
|
|
}
|
|
|
|
// GetUnreadCount returns the number of unread notifications for a user.
|
|
func (s *NotificationService) GetUnreadCount(ctx context.Context, userID uint) (int64, error) {
|
|
return s.notifRepo.CountUnreadByUser(ctx, userID)
|
|
}
|
|
|
|
// GetUnreadCountByAccount returns the unread count for the current account only.
|
|
func (s *NotificationService) GetUnreadCountByAccount(ctx context.Context, userID, accountID uint) (int64, error) {
|
|
return s.notifRepo.CountUnreadByUserAndAccount(ctx, userID, accountID)
|
|
}
|
|
|
|
func (s *NotificationService) CountNotificationsByAccount(ctx context.Context, userID, accountID uint) (int64, error) {
|
|
return s.notifRepo.CountByUserAndAccount(ctx, userID, accountID)
|
|
}
|
|
|
|
// --- Notification Preference operations ---
|
|
|
|
// GetPreferences retrieves all notification preferences for a user within an account.
|
|
func (s *NotificationService) GetPreferences(ctx context.Context, userID uint, accountID uint) ([]model.NotificationPreference, error) {
|
|
return s.prefRepo.ListByUserAndAccount(ctx, userID, accountID)
|
|
}
|
|
|
|
// UpdatePreferences upserts notification preferences for a user within an account.
|
|
func (s *NotificationService) UpdatePreferences(ctx context.Context, userID uint, accountID uint, prefs []model.NotificationPreference) error {
|
|
// Enforce userID and accountID on each preference
|
|
for i := range prefs {
|
|
prefs[i].UserID = userID
|
|
prefs[i].AccountID = &accountID
|
|
}
|
|
return s.prefRepo.BatchUpsert(ctx, prefs)
|
|
}
|
|
|
|
// --- Notification extension operations (G8) ---
|
|
// Reference: Chatwoot notifications_controller.rb#snooze, #unread, #destroy_all
|
|
|
|
// SnoozeNotification sets a snooze timestamp on a notification and clears its read_at.
|
|
func (s *NotificationService) SnoozeNotification(ctx context.Context, id, userID, accountID uint, snoozedUntil time.Time) (*model.Notification, error) {
|
|
return s.notifRepo.Snooze(ctx, id, userID, accountID, snoozedUntil)
|
|
}
|
|
|
|
// MarkNotificationUnread clears read_at on a notification, marking it as unread.
|
|
func (s *NotificationService) MarkNotificationUnread(ctx context.Context, id, userID, accountID uint) (*model.Notification, error) {
|
|
return s.notifRepo.MarkUnread(ctx, id, userID, accountID)
|
|
}
|
|
|
|
// DeleteAllNotifications deletes all notifications for a user within an account.
|
|
func (s *NotificationService) DeleteAllNotifications(ctx context.Context, userID, accountID uint) error {
|
|
return s.notifRepo.DeleteAllByUser(ctx, userID, accountID)
|
|
}
|
|
|
|
func (s *NotificationService) DeleteReadNotifications(ctx context.Context, userID, accountID uint) error {
|
|
return s.notifRepo.DeleteReadByUser(ctx, userID, accountID)
|
|
}
|