package services import ( "context" "fmt" "io" "path/filepath" "strings" "github.com/google/uuid" "go.uber.org/zap" "gorm.io/gorm" "veza-backend-api/internal/models" ) // GearDocumentService handles gear document and repair operations type GearDocumentService struct { db *gorm.DB s3Service *S3StorageService logger *zap.Logger } // NewGearDocumentService creates a new gear document service func NewGearDocumentService(db *gorm.DB, s3Service *S3StorageService, logger *zap.Logger) *GearDocumentService { return &GearDocumentService{db: db, s3Service: s3Service, logger: logger} } // CreateDocument uploads a document for a gear item func (s *GearDocumentService) CreateDocument(ctx context.Context, userID, gearID uuid.UUID, file io.Reader, filename, docType string) (*models.GearDocument, error) { if err := s.verifyGearOwnership(ctx, gearID, userID); err != nil { return nil, err } if docType == "" { docType = "invoice" } data, err := io.ReadAll(file) if err != nil { return nil, fmt.Errorf("read file: %w", err) } storageKey := fmt.Sprintf("gear/%s/documents/%s/%s", gearID, uuid.New(), sanitizeGearFilename(filename)) if s.s3Service == nil { return nil, fmt.Errorf("S3 storage not configured") } contentType := "application/pdf" if strings.HasSuffix(strings.ToLower(filename), ".png") || strings.HasSuffix(strings.ToLower(filename), ".jpg") { contentType = "image/" + strings.TrimPrefix(strings.ToLower(filepath.Ext(filename)), ".") } if _, err := s.s3Service.UploadFile(ctx, data, storageKey, contentType); err != nil { return nil, fmt.Errorf("upload to S3: %w", err) } doc := &models.GearDocument{ GearID: gearID, Type: docType, StorageKey: storageKey, Filename: filename, } if err := s.db.WithContext(ctx).Create(doc).Error; err != nil { _ = s.s3Service.DeleteFile(ctx, storageKey) return nil, fmt.Errorf("create document: %w", err) } return doc, nil } // ListDocuments returns documents for a gear item func (s *GearDocumentService) ListDocuments(ctx context.Context, userID, gearID uuid.UUID) ([]models.GearDocument, error) { if err := s.verifyGearOwnership(ctx, gearID, userID); err != nil { return nil, err } var docs []models.GearDocument if err := s.db.WithContext(ctx).Where("gear_id = ?", gearID).Order("uploaded_at DESC").Find(&docs).Error; err != nil { return nil, err } return docs, nil } // DeleteDocument removes a document func (s *GearDocumentService) DeleteDocument(ctx context.Context, userID, gearID, docID uuid.UUID) error { if err := s.verifyGearOwnership(ctx, gearID, userID); err != nil { return err } var doc models.GearDocument if err := s.db.WithContext(ctx).Where("id = ? AND gear_id = ?", docID, gearID).First(&doc).Error; err != nil { return err } if err := s.db.WithContext(ctx).Delete(&doc).Error; err != nil { return err } if s.s3Service != nil { _ = s.s3Service.DeleteFile(ctx, doc.StorageKey) } return nil } // CreateRepair adds a repair record func (s *GearDocumentService) CreateRepair(ctx context.Context, userID, gearID uuid.UUID, repair *models.GearRepair) (*models.GearRepair, error) { if err := s.verifyGearOwnership(ctx, gearID, userID); err != nil { return nil, err } repair.GearID = gearID if repair.Currency == "" { repair.Currency = "EUR" } if err := s.db.WithContext(ctx).Create(repair).Error; err != nil { return nil, fmt.Errorf("create repair: %w", err) } return repair, nil } // ListRepairs returns repairs for a gear item func (s *GearDocumentService) ListRepairs(ctx context.Context, userID, gearID uuid.UUID) ([]models.GearRepair, error) { if err := s.verifyGearOwnership(ctx, gearID, userID); err != nil { return nil, err } var repairs []models.GearRepair if err := s.db.WithContext(ctx).Where("gear_id = ?", gearID).Order("repair_date DESC").Find(&repairs).Error; err != nil { return nil, err } return repairs, nil } // DeleteRepair removes a repair record func (s *GearDocumentService) DeleteRepair(ctx context.Context, userID, gearID, repairID uuid.UUID) error { if err := s.verifyGearOwnership(ctx, gearID, userID); err != nil { return err } result := s.db.WithContext(ctx).Where("id = ? AND gear_id = ?", repairID, gearID).Delete(&models.GearRepair{}) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return fmt.Errorf("repair not found") } return nil } func (s *GearDocumentService) verifyGearOwnership(ctx context.Context, gearID, userID uuid.UUID) error { var count int64 s.db.WithContext(ctx).Model(&models.GearItem{}).Where("id = ? AND user_id = ?", gearID, userID).Count(&count) if count == 0 { return fmt.Errorf("gear not found") } return nil } func sanitizeGearFilename(name string) string { base := filepath.Base(name) return strings.Map(func(r rune) rune { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' || r == '.' { return r } return '_' }, base) }