Перевел на multi-tenant

Добавил поставщиков
Накладные успешно создаются из фронта
This commit is contained in:
2025-12-18 03:56:21 +03:00
parent 47ec8094e5
commit 542beafe0e
38 changed files with 1942 additions and 977 deletions

View File

@@ -6,58 +6,66 @@ import (
"github.com/google/uuid"
"github.com/shopspring/decimal"
"go.uber.org/zap"
"rmser/internal/domain/account"
"rmser/internal/domain/catalog"
"rmser/internal/domain/drafts"
"rmser/internal/domain/ocr"
"rmser/internal/infrastructure/ocr_client"
"rmser/pkg/logger"
)
type Service struct {
ocrRepo ocr.Repository
catalogRepo catalog.Repository
draftRepo drafts.Repository
pyClient *ocr_client.Client // Клиент к Python сервису
accountRepo account.Repository // <-- NEW
pyClient *ocr_client.Client
}
func NewService(
ocrRepo ocr.Repository,
catalogRepo catalog.Repository,
draftRepo drafts.Repository,
accountRepo account.Repository, // <-- NEW
pyClient *ocr_client.Client,
) *Service {
return &Service{
ocrRepo: ocrRepo,
catalogRepo: catalogRepo,
draftRepo: draftRepo,
accountRepo: accountRepo,
pyClient: pyClient,
}
}
// ProcessReceiptImage - Создает черновик, распознает, сохраняет результаты
func (s *Service) ProcessReceiptImage(ctx context.Context, chatID int64, imgData []byte) (*drafts.DraftInvoice, error) {
// 1. Создаем заготовку черновика
// ProcessReceiptImage
func (s *Service) ProcessReceiptImage(ctx context.Context, userID uuid.UUID, imgData []byte) (*drafts.DraftInvoice, error) {
// 1. Получаем активный сервер для UserID
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, fmt.Errorf("no active server for user")
}
serverID := server.ID
// 2. Создаем черновик
draft := &drafts.DraftInvoice{
ChatID: chatID,
Status: drafts.StatusProcessing,
UserID: userID, // <-- Исправлено с ChatID на UserID
RMSServerID: serverID, // <-- NEW
Status: drafts.StatusProcessing,
}
if err := s.draftRepo.Create(draft); err != nil {
return nil, fmt.Errorf("failed to create draft: %w", err)
}
logger.Log.Info("Создан черновик", zap.String("draft_id", draft.ID.String()))
// 2. Отправляем в Python OCR
// 3. Отправляем в Python OCR
rawResult, err := s.pyClient.ProcessImage(ctx, imgData, "receipt.jpg")
if err != nil {
// Ставим статус ошибки
draft.Status = drafts.StatusError
_ = s.draftRepo.Update(draft)
return nil, fmt.Errorf("python ocr error: %w", err)
}
// 3. Обрабатываем результаты и создаем Items
// 4. Матчинг (с учетом ServerID)
var draftItems []drafts.DraftInvoiceItem
for _, rawItem := range rawResult.Items {
@@ -66,60 +74,33 @@ func (s *Service) ProcessReceiptImage(ctx context.Context, chatID int64, imgData
RawName: rawItem.RawName,
RawAmount: decimal.NewFromFloat(rawItem.Amount),
RawPrice: decimal.NewFromFloat(rawItem.Price),
// Quantity/Price по умолчанию берем как Raw, если не будет пересчета
Quantity: decimal.NewFromFloat(rawItem.Amount),
Price: decimal.NewFromFloat(rawItem.Price),
Sum: decimal.NewFromFloat(rawItem.Sum),
Quantity: decimal.NewFromFloat(rawItem.Amount),
Price: decimal.NewFromFloat(rawItem.Price),
Sum: decimal.NewFromFloat(rawItem.Sum),
}
// Пытаемся найти матчинг
match, err := s.ocrRepo.FindMatch(rawItem.RawName)
if err != nil {
logger.Log.Error("db error finding match", zap.Error(err))
}
match, _ := s.ocrRepo.FindMatch(serverID, rawItem.RawName) // <-- ServerID
if match != nil {
item.IsMatched = true
item.ProductID = &match.ProductID
item.ContainerID = match.ContainerID
} else {
// Если не нашли - сохраняем в Unmatched для статистики и подсказок
if err := s.ocrRepo.UpsertUnmatched(rawItem.RawName); err != nil {
logger.Log.Warn("failed to save unmatched", zap.Error(err))
}
s.ocrRepo.UpsertUnmatched(serverID, rawItem.RawName) // <-- ServerID
}
draftItems = append(draftItems, item)
}
// 4. Сохраняем позиции в БД
// 5. Сохраняем
draft.Status = drafts.StatusReadyToVerify
if err := s.draftRepo.Update(draft); err != nil {
return nil, fmt.Errorf("failed to update draft status: %w", err)
}
draft.Items = draftItems
if err := s.draftRepo.CreateItems(draftItems); err != nil {
return nil, fmt.Errorf("failed to save items: %w", err)
}
s.draftRepo.Update(draft)
s.draftRepo.CreateItems(draftItems)
return draft, nil
}
// ProcessedItem - результат обработки одной строки чека
type ProcessedItem struct {
RawName string
Amount decimal.Decimal
Price decimal.Decimal
Sum decimal.Decimal
IsMatched bool
ProductID *uuid.UUID
MatchSource string // "learned", "auto", "manual"
}
// Добавить структуры в конец файла
type ContainerForIndex struct {
ID string `json:"id"`
Name string `json:"name"`
@@ -134,9 +115,14 @@ type ProductForIndex struct {
Containers []ContainerForIndex `json:"containers"`
}
// GetCatalogForIndexing - возвращает облегченный каталог
func (s *Service) GetCatalogForIndexing() ([]ProductForIndex, error) {
products, err := s.catalogRepo.GetActiveGoods()
// GetCatalogForIndexing
func (s *Service) GetCatalogForIndexing(userID uuid.UUID) ([]ProductForIndex, error) {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, fmt.Errorf("no server")
}
products, err := s.catalogRepo.GetActiveGoods(server.ID)
if err != nil {
return nil, err
}
@@ -169,37 +155,45 @@ func (s *Service) GetCatalogForIndexing() ([]ProductForIndex, error) {
return result, nil
}
// SaveMapping сохраняет привязку с количеством и фасовкой
func (s *Service) SaveMapping(rawName string, productID uuid.UUID, quantity decimal.Decimal, containerID *uuid.UUID) error {
return s.ocrRepo.SaveMatch(rawName, productID, quantity, containerID)
}
// DeleteMatch удаляет ошибочную привязку
func (s *Service) DeleteMatch(rawName string) error {
return s.ocrRepo.DeleteMatch(rawName)
}
// GetKnownMatches возвращает список всех обученных связей
func (s *Service) GetKnownMatches() ([]ocr.ProductMatch, error) {
return s.ocrRepo.GetAllMatches()
}
// GetUnmatchedItems возвращает список частых нераспознанных строк
func (s *Service) GetUnmatchedItems() ([]ocr.UnmatchedItem, error) {
// Берем топ 50 нераспознанных
return s.ocrRepo.GetTopUnmatched(50)
}
// FindKnownMatch ищет, знаем ли мы уже этот товар
func (s *Service) FindKnownMatch(rawName string) (*ocr.ProductMatch, error) {
return s.ocrRepo.FindMatch(rawName)
}
// SearchProducts ищет товары в БД по части названия, коду или артикулу
func (s *Service) SearchProducts(query string) ([]catalog.Product, error) {
func (s *Service) SearchProducts(userID uuid.UUID, query string) ([]catalog.Product, error) {
if len(query) < 2 {
// Слишком короткий запрос, возвращаем пустой список
return []catalog.Product{}, nil
}
return s.catalogRepo.Search(query)
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, fmt.Errorf("no server")
}
return s.catalogRepo.Search(server.ID, query)
}
func (s *Service) SaveMapping(userID uuid.UUID, rawName string, productID uuid.UUID, quantity decimal.Decimal, containerID *uuid.UUID) error {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return fmt.Errorf("no server")
}
return s.ocrRepo.SaveMatch(server.ID, rawName, productID, quantity, containerID)
}
func (s *Service) DeleteMatch(userID uuid.UUID, rawName string) error {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return fmt.Errorf("no server")
}
return s.ocrRepo.DeleteMatch(server.ID, rawName)
}
func (s *Service) GetKnownMatches(userID uuid.UUID) ([]ocr.ProductMatch, error) {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, fmt.Errorf("no server")
}
return s.ocrRepo.GetAllMatches(server.ID)
}
func (s *Service) GetUnmatchedItems(userID uuid.UUID) ([]ocr.UnmatchedItem, error) {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, fmt.Errorf("no server")
}
return s.ocrRepo.GetTopUnmatched(server.ID, 50)
}