Files
rmser/internal/services/drafts/service.go
SERTY 542beafe0e Перевел на multi-tenant
Добавил поставщиков
Накладные успешно создаются из фронта
2025-12-18 03:56:21 +03:00

292 lines
7.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package drafts
import (
"errors"
"fmt"
"strconv"
"time"
"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/invoices"
"rmser/internal/domain/ocr"
"rmser/internal/domain/suppliers"
"rmser/internal/infrastructure/rms"
"rmser/pkg/logger"
)
type Service struct {
draftRepo drafts.Repository
ocrRepo ocr.Repository
catalogRepo catalog.Repository
accountRepo account.Repository
supplierRepo suppliers.Repository
rmsFactory *rms.Factory
}
func NewService(
draftRepo drafts.Repository,
ocrRepo ocr.Repository,
catalogRepo catalog.Repository,
accountRepo account.Repository,
supplierRepo suppliers.Repository,
rmsFactory *rms.Factory,
) *Service {
return &Service{
draftRepo: draftRepo,
ocrRepo: ocrRepo,
catalogRepo: catalogRepo,
accountRepo: accountRepo,
supplierRepo: supplierRepo,
rmsFactory: rmsFactory,
}
}
func (s *Service) GetDraft(draftID, userID uuid.UUID) (*drafts.DraftInvoice, error) {
// TODO: Проверить что userID совпадает с draft.UserID
return s.draftRepo.GetByID(draftID)
}
func (s *Service) GetActiveDrafts(userID uuid.UUID) ([]drafts.DraftInvoice, error) {
return s.draftRepo.GetActive(userID)
}
// GetDictionaries возвращает Склады и Поставщиков для пользователя
func (s *Service) GetDictionaries(userID uuid.UUID) (map[string]interface{}, error) {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, fmt.Errorf("active server not found")
}
stores, _ := s.catalogRepo.GetActiveStores(server.ID)
// Ранжированные поставщики (топ за 90 дней)
suppliersList, _ := s.supplierRepo.GetRankedByUsage(server.ID, 90)
return map[string]interface{}{
"stores": stores,
"suppliers": suppliersList,
}, nil
}
func (s *Service) DeleteDraft(id uuid.UUID) (string, error) {
// Без изменений логики, только вызов репо
draft, err := s.draftRepo.GetByID(id)
if err != nil {
return "", err
}
if draft.Status == drafts.StatusCanceled {
draft.Status = drafts.StatusDeleted
s.draftRepo.Update(draft)
return drafts.StatusDeleted, nil
}
if draft.Status != drafts.StatusCompleted {
draft.Status = drafts.StatusCanceled
s.draftRepo.Update(draft)
return drafts.StatusCanceled, nil
}
return draft.Status, nil
}
func (s *Service) UpdateDraftHeader(id uuid.UUID, storeID *uuid.UUID, supplierID *uuid.UUID, date time.Time, comment string) error {
draft, err := s.draftRepo.GetByID(id)
if err != nil {
return err
}
if draft.Status == drafts.StatusCompleted {
return errors.New("черновик уже отправлен")
}
draft.StoreID = storeID
draft.SupplierID = supplierID
draft.DateIncoming = &date
draft.Comment = comment
return s.draftRepo.Update(draft)
}
func (s *Service) UpdateItem(draftID, itemID uuid.UUID, productID *uuid.UUID, containerID *uuid.UUID, qty, price decimal.Decimal) error {
draft, err := s.draftRepo.GetByID(draftID)
if err != nil {
return err
}
if draft.Status == drafts.StatusCanceled {
draft.Status = drafts.StatusReadyToVerify
s.draftRepo.Update(draft)
}
return s.draftRepo.UpdateItem(itemID, productID, containerID, qty, price)
}
// CommitDraft отправляет накладную
func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
// 1. Клиент для пользователя
client, err := s.rmsFactory.GetClientForUser(userID)
if err != nil {
return "", err
}
// 2. Черновик
draft, err := s.draftRepo.GetByID(draftID)
if err != nil {
return "", err
}
if draft.Status == drafts.StatusCompleted {
return "", errors.New("накладная уже отправлена")
}
// 3. Сборка Invoice
inv := invoices.Invoice{
ID: uuid.Nil,
DocumentNumber: draft.DocumentNumber,
DateIncoming: *draft.DateIncoming,
SupplierID: *draft.SupplierID,
DefaultStoreID: *draft.StoreID,
Status: "NEW",
Items: make([]invoices.InvoiceItem, 0, len(draft.Items)),
}
for _, dItem := range draft.Items {
if dItem.ProductID == nil {
continue // Skip unrecognized
}
// Если суммы нет, считаем
sum := dItem.Sum
if sum.IsZero() {
sum = dItem.Quantity.Mul(dItem.Price)
}
invItem := invoices.InvoiceItem{
ProductID: *dItem.ProductID,
Amount: dItem.Quantity,
Price: dItem.Price,
Sum: sum,
ContainerID: dItem.ContainerID,
}
inv.Items = append(inv.Items, invItem)
}
if len(inv.Items) == 0 {
return "", errors.New("нет распознанных позиций для отправки")
}
// 4. Отправка в RMS
docNum, err := client.CreateIncomingInvoice(inv)
if err != nil {
return "", err
}
// 5. Обновление статуса
draft.Status = drafts.StatusCompleted
s.draftRepo.Update(draft)
// 6. БИЛЛИНГ: Увеличиваем счетчик накладных
server, _ := s.accountRepo.GetActiveServer(userID)
if server != nil {
if err := s.accountRepo.IncrementInvoiceCount(server.ID); err != nil {
logger.Log.Error("Billing increment failed", zap.Error(err))
}
// 7. Обучение (передаем ID сервера для сохранения маппинга)
go s.learnFromDraft(draft, server.ID)
}
return docNum, nil
}
func (s *Service) learnFromDraft(draft *drafts.DraftInvoice, serverID uuid.UUID) {
for _, item := range draft.Items {
if item.RawName != "" && item.ProductID != nil {
qty := decimal.NewFromFloat(1.0)
err := s.ocrRepo.SaveMatch(serverID, item.RawName, *item.ProductID, qty, item.ContainerID)
if err != nil {
logger.Log.Warn("Failed to learn match", zap.Error(err))
}
}
}
}
func (s *Service) CreateProductContainer(userID uuid.UUID, productID uuid.UUID, name string, count decimal.Decimal) (uuid.UUID, error) {
client, err := s.rmsFactory.GetClientForUser(userID)
if err != nil {
return uuid.Nil, err
}
server, _ := s.accountRepo.GetActiveServer(userID) // нужен ServerID для сохранения в локальную БД
fullProduct, err := client.GetProductByID(productID)
if err != nil {
return uuid.Nil, fmt.Errorf("error fetching product: %w", err)
}
// Валидация на дубли
targetCount, _ := count.Float64()
for _, c := range fullProduct.Containers {
if !c.Deleted && (c.Name == name || (c.Count == targetCount)) {
if c.ID != nil && *c.ID != "" {
return uuid.Parse(*c.ID)
}
}
}
// Next Num
maxNum := 0
for _, c := range fullProduct.Containers {
if n, err := strconv.Atoi(c.Num); err == nil {
if n > maxNum {
maxNum = n
}
}
}
nextNum := strconv.Itoa(maxNum + 1)
// Add
newContainerDTO := rms.ContainerFullDTO{
ID: nil,
Num: nextNum,
Name: name,
Count: targetCount,
UseInFront: true,
Deleted: false,
}
fullProduct.Containers = append(fullProduct.Containers, newContainerDTO)
// Update RMS
updatedProduct, err := client.UpdateProduct(*fullProduct)
if err != nil {
return uuid.Nil, fmt.Errorf("error updating product: %w", err)
}
// Find created ID
var createdID uuid.UUID
found := false
for _, c := range updatedProduct.Containers {
if c.Name == name && c.Count == targetCount && !c.Deleted {
if c.ID != nil {
createdID, err = uuid.Parse(*c.ID)
if err == nil {
found = true
break
}
}
}
}
if !found {
return uuid.Nil, errors.New("container created but id not found")
}
// Save Local
newLocalContainer := catalog.ProductContainer{
ID: createdID,
RMSServerID: server.ID, // <-- NEW
ProductID: productID,
Name: name,
Count: count,
}
s.catalogRepo.SaveContainer(newLocalContainer)
return createdID, nil
}