Migrate from Gin to Echo framework and add comprehensive integration tests
Major changes: - Migrate all handlers from Gin to Echo framework - Add new apperrors, echohelpers, and validator packages - Update middleware for Echo compatibility - Add ArchivedHandler to task categorization chain (archived tasks go to cancelled_tasks column) - Add 6 new integration tests: - RecurringTaskLifecycle: NextDueDate advancement for weekly/monthly tasks - MultiUserSharing: Complex sharing with user removal - TaskStateTransitions: All state transitions and kanban column changes - DateBoundaryEdgeCases: Threshold boundary testing - CascadeOperations: Residence deletion cascade effects - MultiUserOperations: Shared residence collaboration - Add single-purpose repository functions for kanban columns (GetOverdueTasks, GetDueSoonTasks, etc.) - Fix RemoveUser route param mismatch (userId -> user_id) - Fix determineExpectedColumn helper to correctly prioritize in_progress over overdue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/apperrors"
|
||||
"github.com/treytartt/casera-api/internal/models"
|
||||
"github.com/treytartt/casera-api/internal/push"
|
||||
"github.com/treytartt/casera-api/internal/repositories"
|
||||
@@ -15,8 +16,11 @@ import (
|
||||
|
||||
// Notification-related errors
|
||||
var (
|
||||
// Deprecated: Use apperrors.NotFound("error.notification_not_found") instead
|
||||
ErrNotificationNotFound = errors.New("notification not found")
|
||||
// Deprecated: Use apperrors.NotFound("error.device_not_found") instead
|
||||
ErrDeviceNotFound = errors.New("device not found")
|
||||
// Deprecated: Use apperrors.BadRequest("error.invalid_platform") instead
|
||||
ErrInvalidPlatform = errors.New("invalid platform, must be 'ios' or 'android'")
|
||||
)
|
||||
|
||||
@@ -40,7 +44,7 @@ func NewNotificationService(notificationRepo *repositories.NotificationRepositor
|
||||
func (s *NotificationService) GetNotifications(userID uint, limit, offset int) ([]NotificationResponse, error) {
|
||||
notifications, err := s.notificationRepo.FindByUser(userID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
result := make([]NotificationResponse, len(notifications))
|
||||
@@ -52,7 +56,11 @@ func (s *NotificationService) GetNotifications(userID uint, limit, offset int) (
|
||||
|
||||
// GetUnreadCount gets the count of unread notifications
|
||||
func (s *NotificationService) GetUnreadCount(userID uint) (int64, error) {
|
||||
return s.notificationRepo.CountUnread(userID)
|
||||
count, err := s.notificationRepo.CountUnread(userID)
|
||||
if err != nil {
|
||||
return 0, apperrors.Internal(err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// MarkAsRead marks a notification as read
|
||||
@@ -60,21 +68,27 @@ func (s *NotificationService) MarkAsRead(notificationID, userID uint) error {
|
||||
notification, err := s.notificationRepo.FindByID(notificationID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrNotificationNotFound
|
||||
return apperrors.NotFound("error.notification_not_found")
|
||||
}
|
||||
return err
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
|
||||
if notification.UserID != userID {
|
||||
return ErrNotificationNotFound
|
||||
return apperrors.NotFound("error.notification_not_found")
|
||||
}
|
||||
|
||||
return s.notificationRepo.MarkAsRead(notificationID)
|
||||
if err := s.notificationRepo.MarkAsRead(notificationID); err != nil {
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkAllAsRead marks all notifications as read
|
||||
func (s *NotificationService) MarkAllAsRead(userID uint) error {
|
||||
return s.notificationRepo.MarkAllAsRead(userID)
|
||||
if err := s.notificationRepo.MarkAllAsRead(userID); err != nil {
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateAndSendNotification creates a notification and sends it via push
|
||||
@@ -82,7 +96,7 @@ func (s *NotificationService) CreateAndSendNotification(ctx context.Context, use
|
||||
// Check user preferences
|
||||
prefs, err := s.notificationRepo.GetOrCreatePreferences(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Check if notification type is enabled
|
||||
@@ -101,13 +115,13 @@ func (s *NotificationService) CreateAndSendNotification(ctx context.Context, use
|
||||
}
|
||||
|
||||
if err := s.notificationRepo.Create(notification); err != nil {
|
||||
return err
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Get device tokens
|
||||
iosTokens, androidTokens, err := s.notificationRepo.GetActiveTokensForUser(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Convert data for push
|
||||
@@ -128,11 +142,14 @@ func (s *NotificationService) CreateAndSendNotification(ctx context.Context, use
|
||||
err = s.pushClient.SendToAll(ctx, iosTokens, androidTokens, title, body, pushData)
|
||||
if err != nil {
|
||||
s.notificationRepo.SetError(notification.ID, err.Error())
|
||||
return err
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return s.notificationRepo.MarkAsSent(notification.ID)
|
||||
if err := s.notificationRepo.MarkAsSent(notification.ID); err != nil {
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isNotificationEnabled checks if a notification type is enabled for user
|
||||
@@ -161,7 +178,7 @@ func (s *NotificationService) isNotificationEnabled(prefs *models.NotificationPr
|
||||
func (s *NotificationService) GetPreferences(userID uint) (*NotificationPreferencesResponse, error) {
|
||||
prefs, err := s.notificationRepo.GetOrCreatePreferences(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
return NewNotificationPreferencesResponse(prefs), nil
|
||||
}
|
||||
@@ -170,7 +187,7 @@ func (s *NotificationService) GetPreferences(userID uint) (*NotificationPreferen
|
||||
func (s *NotificationService) UpdatePreferences(userID uint, req *UpdatePreferencesRequest) (*NotificationPreferencesResponse, error) {
|
||||
prefs, err := s.notificationRepo.GetOrCreatePreferences(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
if req.TaskDueSoon != nil {
|
||||
@@ -214,7 +231,7 @@ func (s *NotificationService) UpdatePreferences(userID uint, req *UpdatePreferen
|
||||
}
|
||||
|
||||
if err := s.notificationRepo.UpdatePreferences(prefs); err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
return NewNotificationPreferencesResponse(prefs), nil
|
||||
@@ -230,7 +247,7 @@ func (s *NotificationService) RegisterDevice(userID uint, req *RegisterDeviceReq
|
||||
case push.PlatformAndroid:
|
||||
return s.registerGCMDevice(userID, req)
|
||||
default:
|
||||
return nil, ErrInvalidPlatform
|
||||
return nil, apperrors.BadRequest("error.invalid_platform")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +261,7 @@ func (s *NotificationService) registerAPNSDevice(userID uint, req *RegisterDevic
|
||||
existing.Name = req.Name
|
||||
existing.DeviceID = req.DeviceID
|
||||
if err := s.notificationRepo.UpdateAPNSDevice(existing); err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
return NewAPNSDeviceResponse(existing), nil
|
||||
}
|
||||
@@ -258,7 +275,7 @@ func (s *NotificationService) registerAPNSDevice(userID uint, req *RegisterDevic
|
||||
Active: true,
|
||||
}
|
||||
if err := s.notificationRepo.CreateAPNSDevice(device); err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
return NewAPNSDeviceResponse(device), nil
|
||||
}
|
||||
@@ -273,7 +290,7 @@ func (s *NotificationService) registerGCMDevice(userID uint, req *RegisterDevice
|
||||
existing.Name = req.Name
|
||||
existing.DeviceID = req.DeviceID
|
||||
if err := s.notificationRepo.UpdateGCMDevice(existing); err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
return NewGCMDeviceResponse(existing), nil
|
||||
}
|
||||
@@ -288,7 +305,7 @@ func (s *NotificationService) registerGCMDevice(userID uint, req *RegisterDevice
|
||||
Active: true,
|
||||
}
|
||||
if err := s.notificationRepo.CreateGCMDevice(device); err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
return NewGCMDeviceResponse(device), nil
|
||||
}
|
||||
@@ -297,12 +314,12 @@ func (s *NotificationService) registerGCMDevice(userID uint, req *RegisterDevice
|
||||
func (s *NotificationService) ListDevices(userID uint) ([]DeviceResponse, error) {
|
||||
iosDevices, err := s.notificationRepo.FindAPNSDevicesByUser(userID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
androidDevices, err := s.notificationRepo.FindGCMDevicesByUser(userID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
result := make([]DeviceResponse, 0, len(iosDevices)+len(androidDevices))
|
||||
@@ -317,14 +334,19 @@ func (s *NotificationService) ListDevices(userID uint) ([]DeviceResponse, error)
|
||||
|
||||
// DeleteDevice deletes a device
|
||||
func (s *NotificationService) DeleteDevice(deviceID uint, platform string, userID uint) error {
|
||||
var err error
|
||||
switch platform {
|
||||
case push.PlatformIOS:
|
||||
return s.notificationRepo.DeactivateAPNSDevice(deviceID)
|
||||
err = s.notificationRepo.DeactivateAPNSDevice(deviceID)
|
||||
case push.PlatformAndroid:
|
||||
return s.notificationRepo.DeactivateGCMDevice(deviceID)
|
||||
err = s.notificationRepo.DeactivateGCMDevice(deviceID)
|
||||
default:
|
||||
return ErrInvalidPlatform
|
||||
return apperrors.BadRequest("error.invalid_platform")
|
||||
}
|
||||
if err != nil {
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// === Response/Request Types ===
|
||||
@@ -490,7 +512,7 @@ func (s *NotificationService) CreateAndSendTaskNotification(
|
||||
// Check user notification preferences
|
||||
prefs, err := s.notificationRepo.GetOrCreatePreferences(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
if !s.isNotificationEnabled(prefs, notificationType) {
|
||||
return nil // Skip silently
|
||||
@@ -527,13 +549,13 @@ func (s *NotificationService) CreateAndSendTaskNotification(
|
||||
}
|
||||
|
||||
if err := s.notificationRepo.Create(notification); err != nil {
|
||||
return err
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Get device tokens
|
||||
iosTokens, androidTokens, err := s.notificationRepo.GetActiveTokensForUser(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Convert data for push payload
|
||||
@@ -556,9 +578,12 @@ func (s *NotificationService) CreateAndSendTaskNotification(
|
||||
err = s.pushClient.SendActionableNotification(ctx, iosTokens, androidTokens, title, body, pushData, iosCategoryID)
|
||||
if err != nil {
|
||||
s.notificationRepo.SetError(notification.ID, err.Error())
|
||||
return err
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return s.notificationRepo.MarkAsSent(notification.ID)
|
||||
if err := s.notificationRepo.MarkAsSent(notification.ID); err != nil {
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user