← back to jearl4__PhotoGallery

Function bodies 1,000 total

All specs Real LLM only Function bodies
proofing.Service.CreateComment method · go · L160-L205 (46 LOC)
backend/internal/domain/proofing/service.go
func (s *Service) CreateComment(ctx context.Context, req CreateCommentRequest) (*repository.PhotoComment, error) {
	// Verify proofing is enabled
	enabled, err := s.IsProofingEnabled(ctx, req.GalleryID)
	if err != nil {
		return nil, err
	}
	if !enabled {
		return nil, errors.NewForbidden("Client proofing is not enabled for this gallery")
	}

	// Verify photo exists and belongs to this gallery (prevents IDOR)
	photo, err := s.photoRepo.GetByID(ctx, req.PhotoID)
	if err != nil {
		return nil, fmt.Errorf("failed to verify photo: %w", err)
	}
	if photo == nil {
		return nil, errors.NewNotFound("Photo")
	}
	if photo.GalleryID != req.GalleryID {
		return nil, errors.NewBadRequest("Photo does not belong to this gallery")
	}

	// Validate comment text
	if req.Text == "" {
		return nil, errors.NewBadRequest("Comment text is required")
	}
	if len(req.Text) > 1000 {
		return nil, errors.NewBadRequest("Comment text must be 1000 characters or less")
	}

	comment := &repository.PhotoComment{
		Comm
proofing.Service.ListCommentsForGallery method · go · L208-L224 (17 LOC)
backend/internal/domain/proofing/service.go
func (s *Service) ListCommentsForGallery(ctx context.Context, galleryID, photographerID string) ([]*repository.PhotoComment, error) {
	// Verify ownership
	gallery, err := s.galleryRepo.GetByIDForPhotographer(ctx, galleryID, photographerID)
	if err != nil {
		return nil, fmt.Errorf("failed to get gallery: %w", err)
	}
	if gallery == nil {
		return nil, errors.NewNotFound("Gallery")
	}

	comments, err := s.commentRepo.ListByGallery(ctx, galleryID)
	if err != nil {
		return nil, fmt.Errorf("failed to list comments: %w", err)
	}

	return comments, nil
}
proofing.Service.ListCommentsForSession method · go · L227-L243 (17 LOC)
backend/internal/domain/proofing/service.go
func (s *Service) ListCommentsForSession(ctx context.Context, galleryID, sessionID string) ([]*repository.PhotoComment, error) {
	// Verify proofing is enabled
	enabled, err := s.IsProofingEnabled(ctx, galleryID)
	if err != nil {
		return nil, err
	}
	if !enabled {
		return nil, errors.NewForbidden("Client proofing is not enabled for this gallery")
	}

	comments, err := s.commentRepo.ListBySession(ctx, galleryID, sessionID)
	if err != nil {
		return nil, fmt.Errorf("failed to list comments: %w", err)
	}

	return comments, nil
}
proofing.Service.DeleteComment method · go · L247-L280 (34 LOC)
backend/internal/domain/proofing/service.go
func (s *Service) DeleteComment(ctx context.Context, galleryID, photoID, commentID, sessionID string) error {
	comment, err := s.commentRepo.GetByID(ctx, photoID, commentID)
	if err != nil {
		return fmt.Errorf("failed to get comment: %w", err)
	}
	if comment == nil {
		return errors.NewNotFound("Comment")
	}

	// Verify the comment belongs to the specified gallery (prevents cross-gallery IDOR)
	if comment.GalleryID != galleryID {
		return errors.NewForbidden("Comment does not belong to this gallery")
	}

	// Verify proofing is enabled for the gallery
	enabled, err := s.IsProofingEnabled(ctx, galleryID)
	if err != nil {
		return err
	}
	if !enabled {
		return errors.NewForbidden("Client proofing is not enabled for this gallery")
	}

	// Only the session owner can delete their own comment
	if comment.SessionID != sessionID {
		return errors.NewForbidden("You can only delete your own comments")
	}

	if err := s.commentRepo.Delete(ctx, photoID, commentID); err != nil {
		return fmt.Errorf
proofing.Service.CreateEditRequest method · go · L292-L350 (59 LOC)
backend/internal/domain/proofing/service.go
func (s *Service) CreateEditRequest(ctx context.Context, req CreateEditRequestInput) (*repository.EditRequest, error) {
	// Verify proofing is enabled
	enabled, err := s.IsProofingEnabled(ctx, req.GalleryID)
	if err != nil {
		return nil, err
	}
	if !enabled {
		return nil, errors.NewForbidden("Client proofing is not enabled for this gallery")
	}

	// Verify photo exists and belongs to this gallery (prevents IDOR)
	photo, err := s.photoRepo.GetByID(ctx, req.PhotoID)
	if err != nil {
		return nil, fmt.Errorf("failed to verify photo: %w", err)
	}
	if photo == nil {
		return nil, errors.NewNotFound("Photo")
	}
	if photo.GalleryID != req.GalleryID {
		return nil, errors.NewBadRequest("Photo does not belong to this gallery")
	}

	// Validate request type
	validTypes := map[string]bool{
		repository.EditRequestTypeCrop:    true,
		repository.EditRequestTypeRetouch: true,
		repository.EditRequestTypeColor:   true,
		repository.EditRequestTypeOther:   true,
	}
	if !validTypes[req.RequestType] 
proofing.Service.ListEditRequestsForGallery method · go · L353-L369 (17 LOC)
backend/internal/domain/proofing/service.go
func (s *Service) ListEditRequestsForGallery(ctx context.Context, galleryID, photographerID, status string) ([]*repository.EditRequest, error) {
	// Verify ownership
	gallery, err := s.galleryRepo.GetByIDForPhotographer(ctx, galleryID, photographerID)
	if err != nil {
		return nil, fmt.Errorf("failed to get gallery: %w", err)
	}
	if gallery == nil {
		return nil, errors.NewNotFound("Gallery")
	}

	requests, err := s.editRequestRepo.ListByGallery(ctx, galleryID, status)
	if err != nil {
		return nil, fmt.Errorf("failed to list edit requests: %w", err)
	}

	return requests, nil
}
proofing.Service.ListEditRequestsForSession method · go · L372-L388 (17 LOC)
backend/internal/domain/proofing/service.go
func (s *Service) ListEditRequestsForSession(ctx context.Context, galleryID, sessionID string) ([]*repository.EditRequest, error) {
	// Verify proofing is enabled
	enabled, err := s.IsProofingEnabled(ctx, galleryID)
	if err != nil {
		return nil, err
	}
	if !enabled {
		return nil, errors.NewForbidden("Client proofing is not enabled for this gallery")
	}

	requests, err := s.editRequestRepo.ListBySession(ctx, galleryID, sessionID)
	if err != nil {
		return nil, fmt.Errorf("failed to list edit requests: %w", err)
	}

	return requests, nil
}
Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
proofing.Service.UpdateEditRequestStatus method · go · L392-L432 (41 LOC)
backend/internal/domain/proofing/service.go
func (s *Service) UpdateEditRequestStatus(ctx context.Context, photoID, requestID, photographerID, status string) (*repository.EditRequest, error) {
	// Validate status
	validStatuses := map[string]bool{
		repository.EditRequestStatusApproved:  true,
		repository.EditRequestStatusRejected:  true,
		repository.EditRequestStatusCompleted: true,
	}
	if !validStatuses[status] {
		return nil, errors.NewBadRequest("Invalid status. Valid statuses: approved, rejected, completed")
	}

	// Get the request to verify ownership
	request, err := s.editRequestRepo.GetByID(ctx, photoID, requestID)
	if err != nil {
		return nil, fmt.Errorf("failed to get edit request: %w", err)
	}
	if request == nil {
		return nil, errors.NewNotFound("Edit request")
	}

	// Verify photographer owns the gallery
	gallery, err := s.galleryRepo.GetByIDForPhotographer(ctx, request.GalleryID, photographerID)
	if err != nil {
		return nil, fmt.Errorf("failed to verify gallery ownership: %w", err)
	}
	if gallery == nil {
		ret
proofing.Service.GetProofingSummary method · go · L445-L491 (47 LOC)
backend/internal/domain/proofing/service.go
func (s *Service) GetProofingSummary(ctx context.Context, galleryID, photographerID string) (*ProofingSummary, error) {
	// Verify ownership
	gallery, err := s.galleryRepo.GetByIDForPhotographer(ctx, galleryID, photographerID)
	if err != nil {
		return nil, fmt.Errorf("failed to get gallery: %w", err)
	}
	if gallery == nil {
		return nil, errors.NewNotFound("Gallery")
	}

	summary := &ProofingSummary{
		GalleryID:       galleryID,
		ProofingEnabled: isProofingPhase(gallery.Phase),
	}

	// Get selections (favorites in proofing context)
	if isProofingPhase(gallery.Phase) {
		favorites, err := s.favoriteRepo.ListByGallery(ctx, galleryID)
		if err != nil {
			return nil, fmt.Errorf("failed to get selections: %w", err)
		}
		summary.TotalSelections = len(favorites)

		// Get comment count
		commentCount, err := s.commentRepo.CountByGallery(ctx, galleryID)
		if err != nil {
			return nil, fmt.Errorf("failed to count comments: %w", err)
		}
		summary.TotalComments = commentCount

		// Get pen
proofing.Service.GetSelectionsForGallery method · go · L494-L514 (21 LOC)
backend/internal/domain/proofing/service.go
func (s *Service) GetSelectionsForGallery(ctx context.Context, galleryID, photographerID string) ([]*repository.Favorite, error) {
	// Verify ownership
	gallery, err := s.galleryRepo.GetByIDForPhotographer(ctx, galleryID, photographerID)
	if err != nil {
		return nil, fmt.Errorf("failed to get gallery: %w", err)
	}
	if gallery == nil {
		return nil, errors.NewNotFound("Gallery")
	}

	if !isProofingPhase(gallery.Phase) {
		return nil, errors.NewForbidden("Client proofing is not enabled for this gallery")
	}

	favorites, err := s.favoriteRepo.ListByGallery(ctx, galleryID)
	if err != nil {
		return nil, fmt.Errorf("failed to get selections: %w", err)
	}

	return favorites, nil
}
selection.NewService function · go · L28-L42 (15 LOC)
backend/internal/domain/selection/service.go
func NewService(
	submissionRepo repository.SelectionSubmissionRepository,
	galleryRepo repository.GalleryRepository,
	photoRepo repository.PhotoRepository,
	favoriteRepo repository.FavoriteRepository,
	eventBus events.EventBus,
) *Service {
	return &Service{
		submissionRepo: submissionRepo,
		galleryRepo:    galleryRepo,
		photoRepo:      photoRepo,
		favoriteRepo:   favoriteRepo,
		eventBus:       eventBus,
	}
}
selection.Service.SubmitSelections method · go · L48-L182 (135 LOC)
backend/internal/domain/selection/service.go
func (s *Service) SubmitSelections(ctx context.Context, galleryID, sessionID string, photoIDs []string, notes string) (*repository.SelectionSubmission, error) {
	// Validate minimum photo count
	if len(photoIDs) == 0 {
		return nil, errors.NewBadRequest("Must select at least one photo")
	}

	// Validate photoIDs array length to prevent DoS
	if len(photoIDs) > MaxSelectionPhotos {
		return nil, errors.NewBadRequest(fmt.Sprintf("Cannot submit more than %d photos at once", MaxSelectionPhotos))
	}

	// Validate for duplicate photo IDs
	uniquePhotoIDs := make(map[string]bool)
	for _, photoID := range photoIDs {
		if uniquePhotoIDs[photoID] {
			return nil, errors.NewBadRequest(fmt.Sprintf("Duplicate photo ID: %s", photoID))
		}
		uniquePhotoIDs[photoID] = true
	}

	// Get gallery to verify it exists and is in proofing phase
	gallery, err := s.galleryRepo.GetByID(ctx, galleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil {
		return nil
selection.Service.GetSubmissionForSession method · go · L185-L191 (7 LOC)
backend/internal/domain/selection/service.go
func (s *Service) GetSubmissionForSession(ctx context.Context, galleryID, sessionID string) (*repository.SelectionSubmission, error) {
	submission, err := s.submissionRepo.GetByGalleryAndSession(ctx, galleryID, sessionID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get submission")
	}
	return submission, nil
}
selection.Service.GetSubmissionByGallery method · go · L194-L212 (19 LOC)
backend/internal/domain/selection/service.go
func (s *Service) GetSubmissionByGallery(ctx context.Context, galleryID, photographerID string) (*repository.SelectionSubmission, error) {
	// Verify ownership
	gallery, err := s.galleryRepo.GetByID(ctx, galleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil {
		return nil, errors.NewNotFound("Gallery")
	}
	if gallery.PhotographerID != photographerID {
		return nil, errors.NewForbidden("You do not have permission to view this gallery")
	}

	submission, err := s.submissionRepo.GetByGallery(ctx, galleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get submission")
	}
	return submission, nil
}
selection.Service.ListSubmissionsByGallery method · go · L215-L233 (19 LOC)
backend/internal/domain/selection/service.go
func (s *Service) ListSubmissionsByGallery(ctx context.Context, galleryID, photographerID string) ([]*repository.SelectionSubmission, error) {
	// Verify ownership
	gallery, err := s.galleryRepo.GetByID(ctx, galleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil {
		return nil, errors.NewNotFound("Gallery")
	}
	if gallery.PhotographerID != photographerID {
		return nil, errors.NewForbidden("You do not have permission to view this gallery")
	}

	submissions, err := s.submissionRepo.ListByGallery(ctx, galleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to list submissions")
	}
	return submissions, nil
}
Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
selection.Service.UnlockSubmission method · go · L236-L312 (77 LOC)
backend/internal/domain/selection/service.go
func (s *Service) UnlockSubmission(ctx context.Context, galleryID, submissionID, photographerID string) (*repository.SelectionSubmission, error) {
	// Verify ownership
	gallery, err := s.galleryRepo.GetByID(ctx, galleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil {
		return nil, errors.NewNotFound("Gallery")
	}
	if gallery.PhotographerID != photographerID {
		return nil, errors.NewForbidden("You do not have permission to modify this gallery")
	}

	// Get the submission
	submissions, err := s.submissionRepo.ListByGallery(ctx, galleryID)
	if err != nil {
		return nil, errors.Wrap(err, 500, "Failed to get submissions")
	}

	var submission *repository.SelectionSubmission
	for _, sub := range submissions {
		if sub.SubmissionID == submissionID {
			submission = sub
			break
		}
	}

	if submission == nil {
		return nil, errors.NewNotFound("Submission")
	}

	// Preserve original lock state for potential rollback
	originalLockedAt := g
selection.Service.ExportSelections method · go · L315-L390 (76 LOC)
backend/internal/domain/selection/service.go
func (s *Service) ExportSelections(ctx context.Context, galleryID, photographerID string, format string) ([]byte, string, error) {
	logger.Info("ExportSelections called", map[string]interface{}{
		"galleryId":      galleryID,
		"photographerId": photographerID,
		"format":         format,
	})

	// Verify ownership
	gallery, err := s.galleryRepo.GetByID(ctx, galleryID)
	if err != nil {
		return nil, "", errors.Wrap(err, 500, "Failed to get gallery")
	}
	if gallery == nil {
		return nil, "", errors.NewNotFound("Gallery")
	}
	if gallery.PhotographerID != photographerID {
		return nil, "", errors.NewForbidden("You do not have permission to view this gallery")
	}

	// Get the latest submission
	submission, err := s.submissionRepo.GetByGallery(ctx, galleryID)
	if err != nil {
		return nil, "", errors.Wrap(err, 500, "Failed to get submission")
	}
	if submission == nil {
		logger.Warn("No submission found for gallery", map[string]interface{}{
			"galleryId": galleryID,
		})
		return nil, "", e
selection.Service.exportLightroom method · go · L394-L418 (25 LOC)
backend/internal/domain/selection/service.go
func (s *Service) exportLightroom(photos []*repository.Photo) ([]byte, string, error) {
	type lightroomExport struct {
		Filenames []string `json:"filenames"`
		Count     int      `json:"count"`
		Format    string   `json:"format"`
	}

	filenames := make([]string, 0, len(photos))
	for _, photo := range photos {
		filenames = append(filenames, photo.FileName)
	}

	export := lightroomExport{
		Filenames: filenames,
		Count:     len(filenames),
		Format:    "lightroom",
	}

	data, err := json.Marshal(export)
	if err != nil {
		return nil, "", errors.Wrap(err, 500, "Failed to marshal export data")
	}

	return data, "application/json", nil
}
selection.Service.exportJSON method · go · L421-L458 (38 LOC)
backend/internal/domain/selection/service.go
func (s *Service) exportJSON(photos []*repository.Photo, submission *repository.SelectionSubmission) ([]byte, string, error) {
	type exportPhoto struct {
		PhotoID  string `json:"photoId"`
		FileName string `json:"fileName"`
		SetID    string `json:"setId,omitempty"`
	}

	type exportData struct {
		GalleryID   string        `json:"galleryId"`
		SubmittedAt time.Time     `json:"submittedAt"`
		Notes       string        `json:"notes,omitempty"`
		PhotoCount  int           `json:"photoCount"`
		Photos      []exportPhoto `json:"photos"`
	}

	data := exportData{
		GalleryID:   submission.GalleryID,
		SubmittedAt: submission.SubmittedAt,
		Notes:       submission.Notes,
		PhotoCount:  len(photos),
		Photos:      make([]exportPhoto, 0, len(photos)),
	}

	for _, photo := range photos {
		data.Photos = append(data.Photos, exportPhoto{
			PhotoID:  photo.PhotoID,
			FileName: photo.FileName,
			SetID:    photo.SetID,
		})
	}

	jsonData, err := json.MarshalIndent(data, "", "  ")
	if err != nil {
selection.Service.exportCSV method · go · L461-L477 (17 LOC)
backend/internal/domain/selection/service.go
func (s *Service) exportCSV(photos []*repository.Photo) ([]byte, string, error) {
	var buf bytes.Buffer

	// Header row
	buf.WriteString("PhotoID,FileName,SetID\n")

	// Data rows
	for _, photo := range photos {
		buf.WriteString(fmt.Sprintf("%s,%s,%s\n",
			escapeCSV(photo.PhotoID),
			escapeCSV(photo.FileName),
			escapeCSV(photo.SetID),
		))
	}

	return buf.Bytes(), "text/csv", nil
}
selection.escapeCSV function · go · L480-L485 (6 LOC)
backend/internal/domain/selection/service.go
func escapeCSV(s string) string {
	if strings.ContainsAny(s, ",\"\n") {
		return "\"" + strings.ReplaceAll(s, "\"", "\"\"") + "\""
	}
	return s
}
subscription.BandwidthService.CheckBandwidthStatus method · go · L42-L50 (9 LOC)
backend/internal/domain/subscription/bandwidth_repository.go
func (s *BandwidthService) CheckBandwidthStatus(ctx context.Context, userID string, tier Tier) (BandwidthStatus, error) {
	usage, err := s.GetCurrentMonthUsage(ctx, userID)
	if err != nil {
		// If we can't get usage, assume 0 (don't block on bandwidth errors)
		return CheckBandwidthSoftLimit(tier, 0), nil
	}

	return CheckBandwidthSoftLimit(tier, usage.BytesUsed), nil
}
subscription.CheckGalleryLimit function · go · L15-L33 (19 LOC)
backend/internal/domain/subscription/gates.go
func CheckGalleryLimit(tier Tier, currentCount int) GateResult {
	limits := GetTierLimits(tier)

	if IsUnlimited(limits.MaxGalleries) {
		return GateResult{Allowed: true}
	}

	if currentCount >= limits.MaxGalleries {
		return GateResult{
			Allowed:      false,
			Reason:       fmt.Sprintf("Gallery limit reached (%d/%d)", currentCount, limits.MaxGalleries),
			UpgradeTier:  GetUpgradeTier(tier, "galleries"),
			CurrentValue: int64(currentCount),
			LimitValue:   int64(limits.MaxGalleries),
		}
	}

	return GateResult{Allowed: true}
}
Open data scored by Repobility · https://repobility.com
subscription.CheckPhotosPerGalleryLimit function · go · L36-L54 (19 LOC)
backend/internal/domain/subscription/gates.go
func CheckPhotosPerGalleryLimit(tier Tier, currentCount int) GateResult {
	limits := GetTierLimits(tier)

	if IsUnlimited(limits.MaxPhotosPerGallery) {
		return GateResult{Allowed: true}
	}

	if currentCount >= limits.MaxPhotosPerGallery {
		return GateResult{
			Allowed:      false,
			Reason:       fmt.Sprintf("Photo limit per gallery reached (%d/%d)", currentCount, limits.MaxPhotosPerGallery),
			UpgradeTier:  GetUpgradeTier(tier, "photos_per_gallery"),
			CurrentValue: int64(currentCount),
			LimitValue:   int64(limits.MaxPhotosPerGallery),
		}
	}

	return GateResult{Allowed: true}
}
subscription.CheckStorageLimit function · go · L57-L73 (17 LOC)
backend/internal/domain/subscription/gates.go
func CheckStorageLimit(tier Tier, currentBytes, additionalBytes int64) GateResult {
	limits := GetTierLimits(tier)

	totalAfterUpload := currentBytes + additionalBytes

	if totalAfterUpload > limits.StorageBytes {
		return GateResult{
			Allowed:      false,
			Reason:       fmt.Sprintf("Storage limit would be exceeded (%d GB / %d GB)", totalAfterUpload/GB, limits.StorageBytes/GB),
			UpgradeTier:  GetUpgradeTier(tier, "storage"),
			CurrentValue: currentBytes,
			LimitValue:   limits.StorageBytes,
		}
	}

	return GateResult{Allowed: true}
}
subscription.CheckGalleryExpirationLimit function · go · L77-L107 (31 LOC)
backend/internal/domain/subscription/gates.go
func CheckGalleryExpirationLimit(tier Tier, days int) GateResult {
	limits := GetTierLimits(tier)

	// Unlimited expiration
	if IsUnlimited(limits.MaxGalleryExpirationDays) {
		return GateResult{Allowed: true}
	}

	// No expiration (0) is only allowed for unlimited tiers
	if days == 0 {
		return GateResult{
			Allowed:      false,
			Reason:       fmt.Sprintf("Galleries must expire within %d days", limits.MaxGalleryExpirationDays),
			UpgradeTier:  GetUpgradeTier(tier, "unlimited_expiration"),
			CurrentValue: int64(days),
			LimitValue:   int64(limits.MaxGalleryExpirationDays),
		}
	}

	if days > limits.MaxGalleryExpirationDays {
		return GateResult{
			Allowed:      false,
			Reason:       fmt.Sprintf("Gallery expiration exceeds limit (%d days max)", limits.MaxGalleryExpirationDays),
			UpgradeTier:  GetUpgradeTier(tier, "unlimited_expiration"),
			CurrentValue: int64(days),
			LimitValue:   int64(limits.MaxGalleryExpirationDays),
		}
	}

	return GateResult{Allowed: true}
}
subscription.CheckVideoUpload function · go · L110-L132 (23 LOC)
backend/internal/domain/subscription/gates.go
func CheckVideoUpload(tier Tier, durationSeconds int) GateResult {
	limits := GetTierLimits(tier)

	if !limits.VideoEnabled {
		return GateResult{
			Allowed:     false,
			Reason:      "Video uploads require Pro or Studio tier",
			UpgradeTier: GetUpgradeTier(tier, "video"),
		}
	}

	if durationSeconds > limits.VideoMaxDurationSeconds {
		return GateResult{
			Allowed:      false,
			Reason:       fmt.Sprintf("Video duration exceeds limit (%d seconds max)", limits.VideoMaxDurationSeconds),
			UpgradeTier:  GetUpgradeTier(tier, "long_video"),
			CurrentValue: int64(durationSeconds),
			LimitValue:   int64(limits.VideoMaxDurationSeconds),
		}
	}

	return GateResult{Allowed: true}
}
subscription.CheckRAWDelivery function · go · L135-L147 (13 LOC)
backend/internal/domain/subscription/gates.go
func CheckRAWDelivery(tier Tier) GateResult {
	limits := GetTierLimits(tier)

	if !limits.RAWDelivery {
		return GateResult{
			Allowed:     false,
			Reason:      "RAW file delivery requires Studio tier",
			UpgradeTier: GetUpgradeTier(tier, "raw_delivery"),
		}
	}

	return GateResult{Allowed: true}
}
subscription.CheckCustomDomain function · go · L150-L162 (13 LOC)
backend/internal/domain/subscription/gates.go
func CheckCustomDomain(tier Tier) GateResult {
	limits := GetTierLimits(tier)

	if !limits.CustomDomain {
		return GateResult{
			Allowed:     false,
			Reason:      "Custom domains require Pro or Studio tier",
			UpgradeTier: GetUpgradeTier(tier, "custom_domain"),
		}
	}

	return GateResult{Allowed: true}
}
subscription.CheckCustomSubdomain function · go · L165-L177 (13 LOC)
backend/internal/domain/subscription/gates.go
func CheckCustomSubdomain(tier Tier) GateResult {
	limits := GetTierLimits(tier)

	if !limits.CustomSubdomain {
		return GateResult{
			Allowed:     false,
			Reason:      "Custom subdomains require Pro or Studio tier",
			UpgradeTier: GetUpgradeTier(tier, "custom_subdomain"),
		}
	}

	return GateResult{Allowed: true}
}
subscription.CheckFullResDownload function · go · L198-L210 (13 LOC)
backend/internal/domain/subscription/gates.go
func CheckFullResDownload(tier Tier) GateResult {
	limits := GetTierLimits(tier)

	if !limits.FullResDownload {
		return GateResult{
			Allowed:     false,
			Reason:      "Full resolution downloads require Starter tier or higher",
			UpgradeTier: GetUpgradeTier(tier, "full_res_download"),
		}
	}

	return GateResult{Allowed: true}
}
Repobility analyzer · published findings · https://repobility.com
subscription.CheckClientProofing function · go · L219-L231 (13 LOC)
backend/internal/domain/subscription/gates.go
func CheckClientProofing(tier Tier) GateResult {
	limits := GetTierLimits(tier)

	if !limits.ClientProofing {
		return GateResult{
			Allowed:     false,
			Reason:      "Client proofing requires Pro or Studio tier",
			UpgradeTier: GetUpgradeTier(tier, "client_proofing"),
		}
	}

	return GateResult{Allowed: true}
}
subscription.CheckGalleryTemplates function · go · L234-L246 (13 LOC)
backend/internal/domain/subscription/gates.go
func CheckGalleryTemplates(tier Tier) GateResult {
	limits := GetTierLimits(tier)

	if !limits.GalleryTemplates {
		return GateResult{
			Allowed:     false,
			Reason:      "Gallery templates require Pro or Studio tier",
			UpgradeTier: GetUpgradeTier(tier, "gallery_templates"),
		}
	}

	return GateResult{Allowed: true}
}
subscription.CheckTeamMemberLimit function · go · L249-L263 (15 LOC)
backend/internal/domain/subscription/gates.go
func CheckTeamMemberLimit(tier Tier, currentCount int) GateResult {
	limits := GetTierLimits(tier)

	if currentCount >= limits.MaxTeamMembers {
		return GateResult{
			Allowed:      false,
			Reason:       fmt.Sprintf("Team member limit reached (%d/%d)", currentCount, limits.MaxTeamMembers),
			UpgradeTier:  GetUpgradeTier(tier, "team_members"),
			CurrentValue: int64(currentCount),
			LimitValue:   int64(limits.MaxTeamMembers),
		}
	}

	return GateResult{Allowed: true}
}
subscription.CheckGalleryScheduling function · go · L266-L276 (11 LOC)
backend/internal/domain/subscription/gates.go
func CheckGalleryScheduling(tier Tier) GateResult {
	if !MeetsTierRequirement(tier, TierPro) {
		return GateResult{
			Allowed:     false,
			Reason:      "Gallery scheduling requires Pro or Studio tier",
			UpgradeTier: GetUpgradeTier(tier, "gallery_scheduling"),
		}
	}

	return GateResult{Allowed: true}
}
subscription.NewFreeSubscription function · go · L96-L105 (10 LOC)
backend/internal/domain/subscription/models.go
func NewFreeSubscription(userID string) *Subscription {
	now := time.Now().UTC().Format(time.RFC3339)
	return &Subscription{
		UserID:    userID,
		Tier:      TierFree,
		Status:    StatusNone,
		CreatedAt: now,
		UpdatedAt: now,
	}
}
subscription.ValidateTier function · go · L118-L125 (8 LOC)
backend/internal/domain/subscription/models.go
func ValidateTier(tier string) bool {
	switch Tier(tier) {
	case TierFree, TierStarter, TierPro, TierStudio:
		return true
	default:
		return false
	}
}
subscription.TierFromString function · go · L128-L133 (6 LOC)
backend/internal/domain/subscription/models.go
func TierFromString(s string) (Tier, error) {
	if !ValidateTier(s) {
		return "", ErrInvalidTier
	}
	return Tier(s), nil
}
subscription.NewService function · go · L53-L59 (7 LOC)
backend/internal/domain/subscription/service.go
func NewService(repo Repository, stripeClient StripeClient, config *ServiceConfig) *Service {
	return &Service{
		repo:         repo,
		stripeClient: stripeClient,
		config:       config,
	}
}
Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
subscription.Service.GetSubscription method · go · L62-L72 (11 LOC)
backend/internal/domain/subscription/service.go
func (s *Service) GetSubscription(ctx context.Context, userID string) (*Subscription, error) {
	sub, err := s.repo.GetByUserID(ctx, userID)
	if err == ErrNotFound {
		// Return a free subscription for users without a subscription record
		return NewFreeSubscription(userID), nil
	}
	if err != nil {
		return nil, fmt.Errorf("failed to get subscription: %w", err)
	}
	return sub, nil
}
subscription.Service.GetSubscriptionWithLimits method · go · L75-L86 (12 LOC)
backend/internal/domain/subscription/service.go
func (s *Service) GetSubscriptionWithLimits(ctx context.Context, userID string) (*SubscriptionResponse, error) {
	sub, err := s.GetSubscription(ctx, userID)
	if err != nil {
		return nil, err
	}

	limits := GetTierLimits(sub.Tier)
	return &SubscriptionResponse{
		Subscription: sub,
		Limits:       limits,
	}, nil
}
subscription.Service.CreateOrGetSubscription method · go · L89-L105 (17 LOC)
backend/internal/domain/subscription/service.go
func (s *Service) CreateOrGetSubscription(ctx context.Context, userID string) (*Subscription, error) {
	existing, err := s.repo.GetByUserID(ctx, userID)
	if err == nil {
		return existing, nil
	}
	if err != ErrNotFound {
		return nil, fmt.Errorf("failed to check existing subscription: %w", err)
	}

	// Create new free subscription
	sub := NewFreeSubscription(userID)
	if err := s.repo.Create(ctx, sub); err != nil {
		return nil, fmt.Errorf("failed to create subscription: %w", err)
	}

	return sub, nil
}
subscription.Service.CreateCheckoutSession method · go · L108-L149 (42 LOC)
backend/internal/domain/subscription/service.go
func (s *Service) CreateCheckoutSession(ctx context.Context, userID, email, name, priceID string) (*CheckoutSession, error) {
	// Get or create subscription
	sub, err := s.CreateOrGetSubscription(ctx, userID)
	if err != nil {
		return nil, err
	}

	// Get or create Stripe customer
	customerID := sub.StripeCustomerID
	if customerID == "" {
		customerID, err = s.stripeClient.CreateCustomer(ctx, userID, email, name)
		if err != nil {
			return nil, fmt.Errorf("failed to create Stripe customer: %w", err)
		}

		// Update subscription with customer ID - this MUST succeed for sync to work later
		sub.StripeCustomerID = customerID
		sub.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
		if err := s.repo.Update(ctx, sub); err != nil {
			return nil, fmt.Errorf("failed to save Stripe customer ID: %w", err)
		}
	}

	// Create checkout session
	metadata := map[string]string{
		"user_id": userID,
	}

	session, err := s.stripeClient.CreateCheckoutSession(
		ctx,
		customerID,
		priceID,
		s.config
subscription.Service.CreatePortalSession method · go · L152-L168 (17 LOC)
backend/internal/domain/subscription/service.go
func (s *Service) CreatePortalSession(ctx context.Context, userID string) (*PortalSession, error) {
	sub, err := s.repo.GetByUserID(ctx, userID)
	if err != nil {
		return nil, fmt.Errorf("failed to get subscription: %w", err)
	}

	if sub.StripeCustomerID == "" {
		return nil, fmt.Errorf("no Stripe customer associated with this user")
	}

	session, err := s.stripeClient.CreatePortalSession(ctx, sub.StripeCustomerID, s.config.ReturnURL)
	if err != nil {
		return nil, fmt.Errorf("failed to create portal session: %w", err)
	}

	return session, nil
}
subscription.Service.GetProrationPreview method · go · L171-L187 (17 LOC)
backend/internal/domain/subscription/service.go
func (s *Service) GetProrationPreview(ctx context.Context, userID, newPriceID string) (*ProrationPreview, error) {
	sub, err := s.repo.GetByUserID(ctx, userID)
	if err != nil {
		return nil, fmt.Errorf("failed to get subscription: %w", err)
	}

	if sub.StripeSubscriptionID == "" {
		return nil, fmt.Errorf("no active subscription to preview changes for")
	}

	preview, err := s.stripeClient.GetProrationPreview(ctx, sub.StripeSubscriptionID, newPriceID)
	if err != nil {
		return nil, fmt.Errorf("failed to get proration preview: %w", err)
	}

	return preview, nil
}
subscription.Service.ChangeTier method · go · L190-L207 (18 LOC)
backend/internal/domain/subscription/service.go
func (s *Service) ChangeTier(ctx context.Context, userID, newPriceID string) error {
	sub, err := s.repo.GetByUserID(ctx, userID)
	if err != nil {
		return fmt.Errorf("failed to get subscription: %w", err)
	}

	if sub.StripeSubscriptionID == "" {
		return fmt.Errorf("no active subscription to change")
	}

	err = s.stripeClient.UpdateSubscriptionPrice(ctx, sub.StripeSubscriptionID, newPriceID, true)
	if err != nil {
		return fmt.Errorf("failed to update subscription: %w", err)
	}

	// The webhook will update the local subscription record
	return nil
}
subscription.Service.CancelSubscription method · go · L210-L256 (47 LOC)
backend/internal/domain/subscription/service.go
func (s *Service) CancelSubscription(ctx context.Context, userID string) (*Subscription, error) {
	sub, err := s.repo.GetByUserID(ctx, userID)
	if err != nil {
		return nil, fmt.Errorf("failed to get subscription: %w", err)
	}

	if sub.StripeSubscriptionID == "" {
		return nil, fmt.Errorf("no active subscription to cancel")
	}

	stripeData, err := s.stripeClient.CancelSubscription(ctx, sub.StripeSubscriptionID, true)
	if err != nil {
		return nil, fmt.Errorf("failed to cancel subscription: %w", err)
	}

	// Update local record with data from Stripe
	sub.CancelAtPeriodEnd = true
	sub.CanceledAt = time.Now().UTC().Format(time.RFC3339)
	sub.UpdatedAt = time.Now().UTC().Format(time.RFC3339)

	// Update period end from Stripe response
	if periodEnd, ok := stripeData["current_period_end"].(string); ok && periodEnd != "" {
		sub.CurrentPeriodEnd = periodEnd
	} else if stripeData["current_period_end"] != nil {
		logger.Warn("Unexpected type for current_period_end in Stripe response", map[strin
Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
subscription.Service.ReactivateSubscription method · go · L259-L309 (51 LOC)
backend/internal/domain/subscription/service.go
func (s *Service) ReactivateSubscription(ctx context.Context, userID string) (*Subscription, error) {
	sub, err := s.repo.GetByUserID(ctx, userID)
	if err != nil {
		return nil, fmt.Errorf("failed to get subscription: %w", err)
	}

	if sub.StripeSubscriptionID == "" {
		return nil, fmt.Errorf("no subscription to reactivate")
	}

	if !sub.CancelAtPeriodEnd {
		return nil, fmt.Errorf("subscription is not scheduled for cancellation")
	}

	stripeData, err := s.stripeClient.ReactivateSubscription(ctx, sub.StripeSubscriptionID)
	if err != nil {
		return nil, fmt.Errorf("failed to reactivate subscription: %w", err)
	}

	// Update local record with data from Stripe
	sub.CancelAtPeriodEnd = false
	sub.CanceledAt = ""
	sub.UpdatedAt = time.Now().UTC().Format(time.RFC3339)

	// Update period times from Stripe response
	if periodEnd, ok := stripeData["current_period_end"].(string); ok && periodEnd != "" {
		sub.CurrentPeriodEnd = periodEnd
	} else if stripeData["current_period_end"] != nil {
		log
subscription.Service.SyncSubscriptionFromStripe method · go · L317-L404 (88 LOC)
backend/internal/domain/subscription/service.go
func (s *Service) SyncSubscriptionFromStripe(ctx context.Context, userID string, priceToTier map[string]Tier) (*Subscription, error) {
	sub, err := s.repo.GetByUserID(ctx, userID)
	if err == ErrNotFound {
		// No subscription record - nothing to sync
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("failed to get subscription: %w", err)
	}

	var stripeData map[string]interface{}

	// Try to get subscription data from Stripe
	switch {
	case sub.StripeSubscriptionID != "":
		// We have a subscription ID, fetch directly
		stripeData, err = s.stripeClient.GetSubscription(ctx, sub.StripeSubscriptionID)
		if err != nil {
			return nil, fmt.Errorf("failed to get subscription from Stripe: %w", err)
		}
	case sub.StripeCustomerID != "":
		// No subscription ID yet - this can happen after checkout before webhooks process
		// Try to find active subscription by customer ID
		stripeData, err = s.stripeClient.GetActiveSubscriptionByCustomer(ctx, sub.StripeCustomerID)
		if err != nil {
subscription.mapStripeStatus function · go · L407-L422 (16 LOC)
backend/internal/domain/subscription/service.go
func mapStripeStatus(stripeStatus string) Status {
	switch stripeStatus {
	case "active":
		return StatusActive
	case "trialing":
		return StatusTrialing
	case "past_due":
		return StatusPastDue
	case "canceled":
		return StatusCanceled
	case "paused":
		return StatusPaused
	default:
		return StatusNone
	}
}
‹ prevpage 7 / 20next ›