package photos import ( "errors" "os" "github.com/google/uuid" "rmser/internal/domain/account" "rmser/internal/domain/drafts" "rmser/internal/domain/photos" ) var ( ErrPhotoHasInvoice = errors.New("нельзя удалить фото: есть созданная накладная") ErrPhotoHasDraft = errors.New("фото связано с черновиком") ) type Service struct { photoRepo photos.Repository draftRepo drafts.Repository accountRepo account.Repository } func NewService(photoRepo photos.Repository, draftRepo drafts.Repository, accountRepo account.Repository) *Service { return &Service{ photoRepo: photoRepo, draftRepo: draftRepo, accountRepo: accountRepo, } } // DTO для ответа type PhotoWithStatus struct { photos.ReceiptPhoto Status photos.PhotoStatus `json:"status"` CanDelete bool `json:"can_delete"` CanRegenerate bool `json:"can_regenerate"` } func (s *Service) GetPhotosForServer(userID uuid.UUID, page, limit int) ([]PhotoWithStatus, int64, error) { server, err := s.accountRepo.GetActiveServer(userID) if err != nil || server == nil { return nil, 0, errors.New("active server not found") } items, total, err := s.photoRepo.GetByServerID(server.ID, page, limit) if err != nil { return nil, 0, err } result := make([]PhotoWithStatus, len(items)) for i, photo := range items { status := photos.PhotoStatusOrphan if photo.InvoiceID != nil { status = photos.PhotoStatusHasInvoice } else if photo.DraftID != nil { status = photos.PhotoStatusHasDraft } result[i] = PhotoWithStatus{ ReceiptPhoto: photo, Status: status, // Удалить можно только если нет накладной CanDelete: photo.InvoiceID == nil, // Пересоздать можно только если это "сирота" CanRegenerate: photo.DraftID == nil && photo.InvoiceID == nil, } } return result, total, nil } func (s *Service) DeletePhoto(userID, photoID uuid.UUID, forceDeleteDraft bool) error { // Проверка прав server, err := s.accountRepo.GetActiveServer(userID) if err != nil || server == nil { return errors.New("no active server") } // Операторы не могут удалять фото из архива (только админы) role, _ := s.accountRepo.GetUserRole(userID, server.ID) if role == account.RoleOperator { return errors.New("access denied") } photo, err := s.photoRepo.GetByID(photoID) if err != nil { return err } // Проверка: есть накладная if photo.InvoiceID != nil { return ErrPhotoHasInvoice } // Проверка: есть черновик if photo.DraftID != nil { if !forceDeleteDraft { return ErrPhotoHasDraft } // Если форсируем удаление - удаляем и черновик if err := s.draftRepo.Delete(*photo.DraftID); err != nil { return err } } // Удаляем файл с диска (физически) // В продакшене лучше делать это асинхронно или не делать вовсе (soft delete), // но для экономии места удалим. // Путь в БД может быть относительным или абсолютным, зависит от реализации загрузки. // В ocr/service мы пишем абсолютный путь. // Но в поле FilePath у нас путь. if photo.FilePath != "" { _ = os.Remove(photo.FilePath) } return s.photoRepo.Delete(photoID) }