Migrate Auth/Contractor/Document/Notification/Subscription services to ctx
Every public method on these five services now takes ctx context.Context as the first arg and routes its repo calls through .WithContext(ctx). With TaskService and ResidenceService already migrated, this means every in-process service that touches Postgres now produces a flame graph in Jaeger where the SQL spans nest under the parent HTTP request span. Endpoints now fully traced (HTTP → service → SQL): - /api/auth/login, /register, /logout, /me, /verify-email, /resend-verification - /api/auth/forgot-password, /verify-reset, /reset-password, /update-profile - /api/contractors/* (CRUD + favorite + by-residence + tasks) - /api/documents/* (CRUD + activate/deactivate + image upload/delete) - /api/notifications/* (list, count, mark-read, prefs, devices) - /api/subscription/* (status, purchase, cancel, triggers, promotions) - All previously-migrated /api/tasks/* and /api/residences/* paths Internal helpers also threaded: - TaskService.sendTaskCompletedNotification → forwards ctx - TaskService.UpdateUserTimezone → forwards ctx to NotificationService - ResidenceService.CreateResidence → forwards ctx to SubscriptionService.CheckLimit - NotificationService.registerAPNSDevice / registerGCMDevice → both take ctx ~75 method signatures, ~120 handler/test call sites updated. Tests pass green; the only failure is the pre-existing flaky TaskHandler_QuickComplete SQLite race that fails ~60% of runs on master. Step 3 of the observability plan is now genuinely complete: every API endpoint backed by a Go service emits a per-request flame graph with HTTP → service → SQL spans, plus B2/APNs/FCM/asynq spans where applicable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -34,8 +35,8 @@ func NewDocumentService(documentRepo *repositories.DocumentRepository, residence
|
||||
}
|
||||
|
||||
// GetDocument gets a document by ID with access check
|
||||
func (s *DocumentService) GetDocument(documentID, userID uint) (*responses.DocumentResponse, error) {
|
||||
document, err := s.documentRepo.FindByID(documentID)
|
||||
func (s *DocumentService) GetDocument(ctx context.Context, documentID, userID uint) (*responses.DocumentResponse, error) {
|
||||
document, err := s.documentRepo.WithContext(ctx).FindByID(documentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.document_not_found")
|
||||
@@ -44,7 +45,7 @@ func (s *DocumentService) GetDocument(documentID, userID uint) (*responses.Docum
|
||||
}
|
||||
|
||||
// Check access via residence
|
||||
hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(document.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -57,9 +58,9 @@ func (s *DocumentService) GetDocument(documentID, userID uint) (*responses.Docum
|
||||
}
|
||||
|
||||
// ListDocuments lists all documents accessible to a user, with optional filters.
|
||||
func (s *DocumentService) ListDocuments(userID uint, filter *repositories.DocumentFilter) ([]responses.DocumentResponse, error) {
|
||||
func (s *DocumentService) ListDocuments(ctx context.Context, userID uint, filter *repositories.DocumentFilter) ([]responses.DocumentResponse, error) {
|
||||
// Get residence IDs (lightweight - no preloads)
|
||||
residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID)
|
||||
residenceIDs, err := s.residenceRepo.WithContext(ctx).FindResidenceIDsByUser(userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -83,7 +84,7 @@ func (s *DocumentService) ListDocuments(userID uint, filter *repositories.Docume
|
||||
residenceIDs = []uint{*filter.ResidenceID}
|
||||
}
|
||||
|
||||
documents, err := s.documentRepo.FindByUserFiltered(residenceIDs, filter)
|
||||
documents, err := s.documentRepo.WithContext(ctx).FindByUserFiltered(residenceIDs, filter)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -92,9 +93,9 @@ func (s *DocumentService) ListDocuments(userID uint, filter *repositories.Docume
|
||||
}
|
||||
|
||||
// ListWarranties lists all warranty documents
|
||||
func (s *DocumentService) ListWarranties(userID uint) ([]responses.DocumentResponse, error) {
|
||||
func (s *DocumentService) ListWarranties(ctx context.Context, userID uint) ([]responses.DocumentResponse, error) {
|
||||
// Get residence IDs (lightweight - no preloads)
|
||||
residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID)
|
||||
residenceIDs, err := s.residenceRepo.WithContext(ctx).FindResidenceIDsByUser(userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -103,7 +104,7 @@ func (s *DocumentService) ListWarranties(userID uint) ([]responses.DocumentRespo
|
||||
return []responses.DocumentResponse{}, nil
|
||||
}
|
||||
|
||||
documents, err := s.documentRepo.FindWarranties(residenceIDs)
|
||||
documents, err := s.documentRepo.WithContext(ctx).FindWarranties(residenceIDs)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -112,9 +113,9 @@ func (s *DocumentService) ListWarranties(userID uint) ([]responses.DocumentRespo
|
||||
}
|
||||
|
||||
// CreateDocument creates a new document
|
||||
func (s *DocumentService) CreateDocument(req *requests.CreateDocumentRequest, userID uint) (*responses.DocumentResponse, error) {
|
||||
func (s *DocumentService) CreateDocument(ctx context.Context, req *requests.CreateDocumentRequest, userID uint) (*responses.DocumentResponse, error) {
|
||||
// Check residence access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(req.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -147,7 +148,7 @@ func (s *DocumentService) CreateDocument(req *requests.CreateDocumentRequest, us
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := s.documentRepo.Create(document); err != nil {
|
||||
if err := s.documentRepo.WithContext(ctx).Create(document); err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
@@ -158,7 +159,7 @@ func (s *DocumentService) CreateDocument(req *requests.CreateDocumentRequest, us
|
||||
DocumentID: document.ID,
|
||||
ImageURL: imageURL,
|
||||
}
|
||||
if err := s.documentRepo.CreateDocumentImage(img); err != nil {
|
||||
if err := s.documentRepo.WithContext(ctx).CreateDocumentImage(img); err != nil {
|
||||
// Log but don't fail the whole operation
|
||||
continue
|
||||
}
|
||||
@@ -166,7 +167,7 @@ func (s *DocumentService) CreateDocument(req *requests.CreateDocumentRequest, us
|
||||
}
|
||||
|
||||
// Reload with relations
|
||||
document, err = s.documentRepo.FindByID(document.ID)
|
||||
document, err = s.documentRepo.WithContext(ctx).FindByID(document.ID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -176,8 +177,8 @@ func (s *DocumentService) CreateDocument(req *requests.CreateDocumentRequest, us
|
||||
}
|
||||
|
||||
// UpdateDocument updates a document
|
||||
func (s *DocumentService) UpdateDocument(documentID, userID uint, req *requests.UpdateDocumentRequest) (*responses.DocumentResponse, error) {
|
||||
document, err := s.documentRepo.FindByID(documentID)
|
||||
func (s *DocumentService) UpdateDocument(ctx context.Context, documentID, userID uint, req *requests.UpdateDocumentRequest) (*responses.DocumentResponse, error) {
|
||||
document, err := s.documentRepo.WithContext(ctx).FindByID(documentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.document_not_found")
|
||||
@@ -186,7 +187,7 @@ func (s *DocumentService) UpdateDocument(documentID, userID uint, req *requests.
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(document.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -238,12 +239,12 @@ func (s *DocumentService) UpdateDocument(documentID, userID uint, req *requests.
|
||||
document.TaskID = req.TaskID
|
||||
}
|
||||
|
||||
if err := s.documentRepo.Update(document); err != nil {
|
||||
if err := s.documentRepo.WithContext(ctx).Update(document); err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Reload
|
||||
document, err = s.documentRepo.FindByID(documentID)
|
||||
document, err = s.documentRepo.WithContext(ctx).FindByID(documentID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -253,8 +254,8 @@ func (s *DocumentService) UpdateDocument(documentID, userID uint, req *requests.
|
||||
}
|
||||
|
||||
// DeleteDocument soft-deletes a document
|
||||
func (s *DocumentService) DeleteDocument(documentID, userID uint) error {
|
||||
document, err := s.documentRepo.FindByID(documentID)
|
||||
func (s *DocumentService) DeleteDocument(ctx context.Context, documentID, userID uint) error {
|
||||
document, err := s.documentRepo.WithContext(ctx).FindByID(documentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return apperrors.NotFound("error.document_not_found")
|
||||
@@ -263,7 +264,7 @@ func (s *DocumentService) DeleteDocument(documentID, userID uint) error {
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(document.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
@@ -271,7 +272,7 @@ func (s *DocumentService) DeleteDocument(documentID, userID uint) error {
|
||||
return apperrors.Forbidden("error.document_access_denied")
|
||||
}
|
||||
|
||||
if err := s.documentRepo.Delete(documentID); err != nil {
|
||||
if err := s.documentRepo.WithContext(ctx).Delete(documentID); err != nil {
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
|
||||
@@ -279,15 +280,15 @@ func (s *DocumentService) DeleteDocument(documentID, userID uint) error {
|
||||
}
|
||||
|
||||
// ActivateDocument activates a document
|
||||
func (s *DocumentService) ActivateDocument(documentID, userID uint) (*responses.DocumentResponse, error) {
|
||||
func (s *DocumentService) ActivateDocument(ctx context.Context, documentID, userID uint) (*responses.DocumentResponse, error) {
|
||||
// First check if document exists (even if inactive)
|
||||
var document models.Document
|
||||
if err := s.documentRepo.FindByIDIncludingInactive(documentID, &document); err != nil {
|
||||
if err := s.documentRepo.WithContext(ctx).FindByIDIncludingInactive(documentID, &document); err != nil {
|
||||
return nil, apperrors.NotFound("error.document_not_found")
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(document.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -295,12 +296,12 @@ func (s *DocumentService) ActivateDocument(documentID, userID uint) (*responses.
|
||||
return nil, apperrors.Forbidden("error.document_access_denied")
|
||||
}
|
||||
|
||||
if err := s.documentRepo.Activate(documentID); err != nil {
|
||||
if err := s.documentRepo.WithContext(ctx).Activate(documentID); err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Reload
|
||||
doc, err := s.documentRepo.FindByID(documentID)
|
||||
doc, err := s.documentRepo.WithContext(ctx).FindByID(documentID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -310,8 +311,8 @@ func (s *DocumentService) ActivateDocument(documentID, userID uint) (*responses.
|
||||
}
|
||||
|
||||
// DeactivateDocument deactivates a document
|
||||
func (s *DocumentService) DeactivateDocument(documentID, userID uint) (*responses.DocumentResponse, error) {
|
||||
document, err := s.documentRepo.FindByID(documentID)
|
||||
func (s *DocumentService) DeactivateDocument(ctx context.Context, documentID, userID uint) (*responses.DocumentResponse, error) {
|
||||
document, err := s.documentRepo.WithContext(ctx).FindByID(documentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.document_not_found")
|
||||
@@ -320,7 +321,7 @@ func (s *DocumentService) DeactivateDocument(documentID, userID uint) (*response
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(document.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -328,7 +329,7 @@ func (s *DocumentService) DeactivateDocument(documentID, userID uint) (*response
|
||||
return nil, apperrors.Forbidden("error.document_access_denied")
|
||||
}
|
||||
|
||||
if err := s.documentRepo.Deactivate(documentID); err != nil {
|
||||
if err := s.documentRepo.WithContext(ctx).Deactivate(documentID); err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
@@ -338,8 +339,8 @@ func (s *DocumentService) DeactivateDocument(documentID, userID uint) (*response
|
||||
}
|
||||
|
||||
// UploadDocumentImage adds an image to an existing document
|
||||
func (s *DocumentService) UploadDocumentImage(documentID, userID uint, imageURL, caption string) (*responses.DocumentResponse, error) {
|
||||
document, err := s.documentRepo.FindByID(documentID)
|
||||
func (s *DocumentService) UploadDocumentImage(ctx context.Context, documentID, userID uint, imageURL, caption string) (*responses.DocumentResponse, error) {
|
||||
document, err := s.documentRepo.WithContext(ctx).FindByID(documentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.document_not_found")
|
||||
@@ -348,7 +349,7 @@ func (s *DocumentService) UploadDocumentImage(documentID, userID uint, imageURL,
|
||||
}
|
||||
|
||||
// Check access via residence
|
||||
hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(document.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -361,12 +362,12 @@ func (s *DocumentService) UploadDocumentImage(documentID, userID uint, imageURL,
|
||||
ImageURL: imageURL,
|
||||
Caption: caption,
|
||||
}
|
||||
if err := s.documentRepo.CreateDocumentImage(img); err != nil {
|
||||
if err := s.documentRepo.WithContext(ctx).CreateDocumentImage(img); err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Reload with relations
|
||||
document, err = s.documentRepo.FindByID(documentID)
|
||||
document, err = s.documentRepo.WithContext(ctx).FindByID(documentID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -376,9 +377,9 @@ func (s *DocumentService) UploadDocumentImage(documentID, userID uint, imageURL,
|
||||
}
|
||||
|
||||
// DeleteDocumentImage removes an image from a document
|
||||
func (s *DocumentService) DeleteDocumentImage(documentID, imageID, userID uint) (*responses.DocumentResponse, error) {
|
||||
func (s *DocumentService) DeleteDocumentImage(ctx context.Context, documentID, imageID, userID uint) (*responses.DocumentResponse, error) {
|
||||
// Find the image first
|
||||
image, err := s.documentRepo.FindImageByID(imageID)
|
||||
image, err := s.documentRepo.WithContext(ctx).FindImageByID(imageID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.document_image_not_found")
|
||||
@@ -392,7 +393,7 @@ func (s *DocumentService) DeleteDocumentImage(documentID, imageID, userID uint)
|
||||
}
|
||||
|
||||
// Find parent document to check access
|
||||
document, err := s.documentRepo.FindByID(documentID)
|
||||
document, err := s.documentRepo.WithContext(ctx).FindByID(documentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.document_not_found")
|
||||
@@ -401,7 +402,7 @@ func (s *DocumentService) DeleteDocumentImage(documentID, imageID, userID uint)
|
||||
}
|
||||
|
||||
// Check access via residence
|
||||
hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(document.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -409,12 +410,12 @@ func (s *DocumentService) DeleteDocumentImage(documentID, imageID, userID uint)
|
||||
return nil, apperrors.Forbidden("error.document_access_denied")
|
||||
}
|
||||
|
||||
if err := s.documentRepo.DeleteDocumentImage(imageID); err != nil {
|
||||
if err := s.documentRepo.WithContext(ctx).DeleteDocumentImage(imageID); err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Reload with relations
|
||||
document, err = s.documentRepo.FindByID(documentID)
|
||||
document, err = s.documentRepo.WithContext(ctx).FindByID(documentID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user