veza/veza-backend-api/internal/services/gear_document_service.go
senke 7692c4b8b9 feat(v0.802): frontend Cloud/Gear, MSW, docs, scope v0.803, archive
- Cloud: CloudFileVersions, CloudShareModal, versions/share in CloudView
- Gear: GearDocumentsTab, GearRepairsTab, warranty badge, initialTab
- MSW: cloud versions/share, gear documents/repairs, tags suggest
- Stories: CloudFileVersions, CloudShareModal, GearDetailModal variants
- gearService: listDocuments, uploadDocument, deleteDocument, listRepairs, createRepair, deleteRepair
- cloudService: listVersions, restoreVersion, shareFile, getSharedFile
- gear_warranty_notifier: 24h ticker, notifications for expiring warranty
- tag_handler_test: unit tests
- docs: API_REFERENCE, CHANGELOG, PROJECT_STATE, FEATURE_STATUS v0.802
- SCOPE_CONTROL, .cursorrules: scope v0.803
- archive: V0_802_RELEASE_SCOPE, RETROSPECTIVE_V0802
2026-02-25 14:00:58 +01:00

164 lines
4.9 KiB
Go

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)
}