mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-05 03:12:34 -06:00
120 lines
3.5 KiB
Go
120 lines
3.5 KiB
Go
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)
|
||
}
|