Add webhook logging, pagination, middleware, migrations, and prod hardening

- Webhook event logging repo and subscription webhook idempotency
- Pagination helper (echohelpers) with cursor/offset support
- Request ID and structured logging middleware
- Push client improvements (FCM HTTP v1, better error handling)
- Task model version column, business constraint migrations, targeted indexes
- Expanded categorization chain tests
- Email service and config hardening
- CI workflow updates, .gitignore additions, .env.example updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
treyt
2026-02-24 21:32:09 -06:00
parent 806bd07f80
commit e26116e2cf
50 changed files with 1681 additions and 97 deletions

View File

@@ -271,6 +271,9 @@ func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRe
}
if err := s.taskRepo.Update(task); err != nil {
if errors.Is(err, repositories.ErrVersionConflict) {
return nil, apperrors.Conflict("error.version_conflict")
}
return nil, apperrors.Internal(err)
}
@@ -337,7 +340,10 @@ func (s *TaskService) MarkInProgress(taskID, userID uint, now time.Time) (*respo
return nil, apperrors.Forbidden("error.task_access_denied")
}
if err := s.taskRepo.MarkInProgress(taskID); err != nil {
if err := s.taskRepo.MarkInProgress(taskID, task.Version); err != nil {
if errors.Is(err, repositories.ErrVersionConflict) {
return nil, apperrors.Conflict("error.version_conflict")
}
return nil, apperrors.Internal(err)
}
@@ -377,7 +383,10 @@ func (s *TaskService) CancelTask(taskID, userID uint, now time.Time) (*responses
return nil, apperrors.BadRequest("error.task_already_cancelled")
}
if err := s.taskRepo.Cancel(taskID); err != nil {
if err := s.taskRepo.Cancel(taskID, task.Version); err != nil {
if errors.Is(err, repositories.ErrVersionConflict) {
return nil, apperrors.Conflict("error.version_conflict")
}
return nil, apperrors.Internal(err)
}
@@ -413,7 +422,10 @@ func (s *TaskService) UncancelTask(taskID, userID uint, now time.Time) (*respons
return nil, apperrors.Forbidden("error.task_access_denied")
}
if err := s.taskRepo.Uncancel(taskID); err != nil {
if err := s.taskRepo.Uncancel(taskID, task.Version); err != nil {
if errors.Is(err, repositories.ErrVersionConflict) {
return nil, apperrors.Conflict("error.version_conflict")
}
return nil, apperrors.Internal(err)
}
@@ -453,7 +465,10 @@ func (s *TaskService) ArchiveTask(taskID, userID uint, now time.Time) (*response
return nil, apperrors.BadRequest("error.task_already_archived")
}
if err := s.taskRepo.Archive(taskID); err != nil {
if err := s.taskRepo.Archive(taskID, task.Version); err != nil {
if errors.Is(err, repositories.ErrVersionConflict) {
return nil, apperrors.Conflict("error.version_conflict")
}
return nil, apperrors.Internal(err)
}
@@ -489,7 +504,10 @@ func (s *TaskService) UnarchiveTask(taskID, userID uint, now time.Time) (*respon
return nil, apperrors.Forbidden("error.task_access_denied")
}
if err := s.taskRepo.Unarchive(taskID); err != nil {
if err := s.taskRepo.Unarchive(taskID, task.Version); err != nil {
if errors.Is(err, repositories.ErrVersionConflict) {
return nil, apperrors.Conflict("error.version_conflict")
}
return nil, apperrors.Internal(err)
}
@@ -581,6 +599,9 @@ func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest
task.InProgress = false
}
if err := s.taskRepo.Update(task); err != nil {
if errors.Is(err, repositories.ErrVersionConflict) {
return nil, apperrors.Conflict("error.version_conflict")
}
log.Error().Err(err).Uint("task_id", task.ID).Msg("Failed to update task after completion")
}
@@ -702,6 +723,9 @@ func (s *TaskService) QuickComplete(taskID uint, userID uint) error {
task.InProgress = false
}
if err := s.taskRepo.Update(task); err != nil {
if errors.Is(err, repositories.ErrVersionConflict) {
return apperrors.Conflict("error.version_conflict")
}
log.Error().Err(err).Uint("task_id", task.ID).Msg("Failed to update task after quick completion")
return apperrors.Internal(err) // Return error so caller knows the update failed
}