- 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
164 lines
4.9 KiB
Go
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)
|
|
}
|