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"
|
||||
@@ -33,8 +34,8 @@ func NewContractorService(contractorRepo *repositories.ContractorRepository, res
|
||||
}
|
||||
|
||||
// GetContractor gets a contractor by ID with access check
|
||||
func (s *ContractorService) GetContractor(contractorID, userID uint) (*responses.ContractorResponse, error) {
|
||||
contractor, err := s.contractorRepo.FindByID(contractorID)
|
||||
func (s *ContractorService) GetContractor(ctx context.Context, contractorID, userID uint) (*responses.ContractorResponse, error) {
|
||||
contractor, err := s.contractorRepo.WithContext(ctx).FindByID(contractorID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.contractor_not_found")
|
||||
@@ -43,7 +44,7 @@ func (s *ContractorService) GetContractor(contractorID, userID uint) (*responses
|
||||
}
|
||||
|
||||
// Check access
|
||||
if !s.hasContractorAccess(contractor, userID) {
|
||||
if !s.hasContractorAccess(ctx, contractor, userID) {
|
||||
return nil, apperrors.Forbidden("error.contractor_access_denied")
|
||||
}
|
||||
|
||||
@@ -55,14 +56,14 @@ func (s *ContractorService) GetContractor(contractorID, userID uint) (*responses
|
||||
// Access rules:
|
||||
// - If contractor has no residence: only the creator has access
|
||||
// - If contractor has a residence: all users with access to that residence
|
||||
func (s *ContractorService) hasContractorAccess(contractor *models.Contractor, userID uint) bool {
|
||||
func (s *ContractorService) hasContractorAccess(ctx context.Context, contractor *models.Contractor, userID uint) bool {
|
||||
if contractor.ResidenceID == nil {
|
||||
// Personal contractor - only creator has access
|
||||
return contractor.CreatedByID == userID
|
||||
}
|
||||
|
||||
// Residence contractor - check residence access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(*contractor.ResidenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(*contractor.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -70,15 +71,15 @@ func (s *ContractorService) hasContractorAccess(contractor *models.Contractor, u
|
||||
}
|
||||
|
||||
// ListContractors lists all contractors accessible to a user
|
||||
func (s *ContractorService) ListContractors(userID uint) ([]responses.ContractorResponse, error) {
|
||||
func (s *ContractorService) ListContractors(ctx context.Context, userID uint) ([]responses.ContractorResponse, 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)
|
||||
}
|
||||
|
||||
// FindByUser now handles both personal and residence contractors
|
||||
contractors, err := s.contractorRepo.FindByUser(userID, residenceIDs)
|
||||
contractors, err := s.contractorRepo.WithContext(ctx).FindByUser(userID, residenceIDs)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -87,10 +88,10 @@ func (s *ContractorService) ListContractors(userID uint) ([]responses.Contractor
|
||||
}
|
||||
|
||||
// CreateContractor creates a new contractor
|
||||
func (s *ContractorService) CreateContractor(req *requests.CreateContractorRequest, userID uint) (*responses.ContractorResponse, error) {
|
||||
func (s *ContractorService) CreateContractor(ctx context.Context, req *requests.CreateContractorRequest, userID uint) (*responses.ContractorResponse, error) {
|
||||
// If residence is provided, check access
|
||||
if req.ResidenceID != nil {
|
||||
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)
|
||||
}
|
||||
@@ -122,19 +123,19 @@ func (s *ContractorService) CreateContractor(req *requests.CreateContractorReque
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := s.contractorRepo.Create(contractor); err != nil {
|
||||
if err := s.contractorRepo.WithContext(ctx).Create(contractor); err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Set specialties if provided
|
||||
if len(req.SpecialtyIDs) > 0 {
|
||||
if err := s.contractorRepo.SetSpecialties(contractor.ID, req.SpecialtyIDs); err != nil {
|
||||
if err := s.contractorRepo.WithContext(ctx).SetSpecialties(contractor.ID, req.SpecialtyIDs); err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload with relations
|
||||
contractor, reloadErr := s.contractorRepo.FindByID(contractor.ID)
|
||||
contractor, reloadErr := s.contractorRepo.WithContext(ctx).FindByID(contractor.ID)
|
||||
if reloadErr != nil {
|
||||
return nil, apperrors.Internal(reloadErr)
|
||||
}
|
||||
@@ -144,8 +145,8 @@ func (s *ContractorService) CreateContractor(req *requests.CreateContractorReque
|
||||
}
|
||||
|
||||
// UpdateContractor updates a contractor
|
||||
func (s *ContractorService) UpdateContractor(contractorID, userID uint, req *requests.UpdateContractorRequest) (*responses.ContractorResponse, error) {
|
||||
contractor, err := s.contractorRepo.FindByID(contractorID)
|
||||
func (s *ContractorService) UpdateContractor(ctx context.Context, contractorID, userID uint, req *requests.UpdateContractorRequest) (*responses.ContractorResponse, error) {
|
||||
contractor, err := s.contractorRepo.WithContext(ctx).FindByID(contractorID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.contractor_not_found")
|
||||
@@ -154,7 +155,7 @@ func (s *ContractorService) UpdateContractor(contractorID, userID uint, req *req
|
||||
}
|
||||
|
||||
// Check access
|
||||
if !s.hasContractorAccess(contractor, userID) {
|
||||
if !s.hasContractorAccess(ctx, contractor, userID) {
|
||||
return nil, apperrors.Forbidden("error.contractor_access_denied")
|
||||
}
|
||||
|
||||
@@ -198,7 +199,7 @@ func (s *ContractorService) UpdateContractor(contractorID, userID uint, req *req
|
||||
// If residence_id is provided, verify the user has access to the NEW residence.
|
||||
// This prevents an attacker from reassigning a contractor to someone else's residence.
|
||||
if req.ResidenceID != nil {
|
||||
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)
|
||||
}
|
||||
@@ -211,19 +212,19 @@ func (s *ContractorService) UpdateContractor(contractorID, userID uint, req *req
|
||||
// removed the residence association - contractor becomes personal
|
||||
contractor.ResidenceID = req.ResidenceID
|
||||
|
||||
if err := s.contractorRepo.Update(contractor); err != nil {
|
||||
if err := s.contractorRepo.WithContext(ctx).Update(contractor); err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Update specialties if provided
|
||||
if req.SpecialtyIDs != nil {
|
||||
if err := s.contractorRepo.SetSpecialties(contractorID, req.SpecialtyIDs); err != nil {
|
||||
if err := s.contractorRepo.WithContext(ctx).SetSpecialties(contractorID, req.SpecialtyIDs); err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload
|
||||
contractor, err = s.contractorRepo.FindByID(contractorID)
|
||||
contractor, err = s.contractorRepo.WithContext(ctx).FindByID(contractorID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -233,8 +234,8 @@ func (s *ContractorService) UpdateContractor(contractorID, userID uint, req *req
|
||||
}
|
||||
|
||||
// DeleteContractor soft-deletes a contractor
|
||||
func (s *ContractorService) DeleteContractor(contractorID, userID uint) error {
|
||||
contractor, err := s.contractorRepo.FindByID(contractorID)
|
||||
func (s *ContractorService) DeleteContractor(ctx context.Context, contractorID, userID uint) error {
|
||||
contractor, err := s.contractorRepo.WithContext(ctx).FindByID(contractorID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return apperrors.NotFound("error.contractor_not_found")
|
||||
@@ -243,11 +244,11 @@ func (s *ContractorService) DeleteContractor(contractorID, userID uint) error {
|
||||
}
|
||||
|
||||
// Check access
|
||||
if !s.hasContractorAccess(contractor, userID) {
|
||||
if !s.hasContractorAccess(ctx, contractor, userID) {
|
||||
return apperrors.Forbidden("error.contractor_access_denied")
|
||||
}
|
||||
|
||||
if err := s.contractorRepo.Delete(contractorID); err != nil {
|
||||
if err := s.contractorRepo.WithContext(ctx).Delete(contractorID); err != nil {
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
|
||||
@@ -255,8 +256,8 @@ func (s *ContractorService) DeleteContractor(contractorID, userID uint) error {
|
||||
}
|
||||
|
||||
// ToggleFavorite toggles the favorite status of a contractor and returns the updated contractor
|
||||
func (s *ContractorService) ToggleFavorite(contractorID, userID uint) (*responses.ContractorResponse, error) {
|
||||
contractor, err := s.contractorRepo.FindByID(contractorID)
|
||||
func (s *ContractorService) ToggleFavorite(ctx context.Context, contractorID, userID uint) (*responses.ContractorResponse, error) {
|
||||
contractor, err := s.contractorRepo.WithContext(ctx).FindByID(contractorID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.contractor_not_found")
|
||||
@@ -265,17 +266,17 @@ func (s *ContractorService) ToggleFavorite(contractorID, userID uint) (*response
|
||||
}
|
||||
|
||||
// Check access
|
||||
if !s.hasContractorAccess(contractor, userID) {
|
||||
if !s.hasContractorAccess(ctx, contractor, userID) {
|
||||
return nil, apperrors.Forbidden("error.contractor_access_denied")
|
||||
}
|
||||
|
||||
_, err = s.contractorRepo.ToggleFavorite(contractorID)
|
||||
_, err = s.contractorRepo.WithContext(ctx).ToggleFavorite(contractorID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Re-fetch the contractor to get the updated state with all relations
|
||||
contractor, err = s.contractorRepo.FindByID(contractorID)
|
||||
contractor, err = s.contractorRepo.WithContext(ctx).FindByID(contractorID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -285,8 +286,8 @@ func (s *ContractorService) ToggleFavorite(contractorID, userID uint) (*response
|
||||
}
|
||||
|
||||
// GetContractorTasks gets all tasks for a contractor
|
||||
func (s *ContractorService) GetContractorTasks(contractorID, userID uint) ([]responses.TaskResponse, error) {
|
||||
contractor, err := s.contractorRepo.FindByID(contractorID)
|
||||
func (s *ContractorService) GetContractorTasks(ctx context.Context, contractorID, userID uint) ([]responses.TaskResponse, error) {
|
||||
contractor, err := s.contractorRepo.WithContext(ctx).FindByID(contractorID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, apperrors.NotFound("error.contractor_not_found")
|
||||
@@ -295,11 +296,11 @@ func (s *ContractorService) GetContractorTasks(contractorID, userID uint) ([]res
|
||||
}
|
||||
|
||||
// Check access
|
||||
if !s.hasContractorAccess(contractor, userID) {
|
||||
if !s.hasContractorAccess(ctx, contractor, userID) {
|
||||
return nil, apperrors.Forbidden("error.contractor_access_denied")
|
||||
}
|
||||
|
||||
tasks, err := s.contractorRepo.GetTasksForContractor(contractorID)
|
||||
tasks, err := s.contractorRepo.WithContext(ctx).GetTasksForContractor(contractorID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -308,9 +309,9 @@ func (s *ContractorService) GetContractorTasks(contractorID, userID uint) ([]res
|
||||
}
|
||||
|
||||
// ListContractorsByResidence lists all contractors for a specific residence
|
||||
func (s *ContractorService) ListContractorsByResidence(residenceID, userID uint) ([]responses.ContractorResponse, error) {
|
||||
func (s *ContractorService) ListContractorsByResidence(ctx context.Context, residenceID, userID uint) ([]responses.ContractorResponse, error) {
|
||||
// Check user has access to the residence
|
||||
hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID)
|
||||
hasAccess, err := s.residenceRepo.WithContext(ctx).HasAccess(residenceID, userID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -318,7 +319,7 @@ func (s *ContractorService) ListContractorsByResidence(residenceID, userID uint)
|
||||
return nil, apperrors.Forbidden("error.residence_access_denied")
|
||||
}
|
||||
|
||||
contractors, err := s.contractorRepo.FindByResidence(residenceID)
|
||||
contractors, err := s.contractorRepo.WithContext(ctx).FindByResidence(residenceID)
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
@@ -327,8 +328,8 @@ func (s *ContractorService) ListContractorsByResidence(residenceID, userID uint)
|
||||
}
|
||||
|
||||
// GetSpecialties returns all contractor specialties
|
||||
func (s *ContractorService) GetSpecialties() ([]responses.ContractorSpecialtyResponse, error) {
|
||||
specialties, err := s.contractorRepo.GetAllSpecialties()
|
||||
func (s *ContractorService) GetSpecialties(ctx context.Context) ([]responses.ContractorSpecialtyResponse, error) {
|
||||
specialties, err := s.contractorRepo.WithContext(ctx).GetAllSpecialties()
|
||||
if err != nil {
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user