Files
rmser/internal/services/drafts/service.go
SERTY 5f35d7a75f добавлен биллинг и тарифы
добавлена интеграция с юкасса
2025-12-24 09:06:19 +03:00

427 lines
12 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/internal/services/billing"
"rmser/pkg/logger"
)
type Service struct {
draftRepo drafts.Repository
ocrRepo ocr.Repository
catalogRepo catalog.Repository
accountRepo account.Repository
supplierRepo suppliers.Repository
rmsFactory *rms.Factory
billingService *billing.Service
}
func NewService(
draftRepo drafts.Repository,
ocrRepo ocr.Repository,
catalogRepo catalog.Repository,
accountRepo account.Repository,
supplierRepo suppliers.Repository,
rmsFactory *rms.Factory,
billingService *billing.Service,
) *Service {
return &Service{
draftRepo: draftRepo,
ocrRepo: ocrRepo,
catalogRepo: catalogRepo,
accountRepo: accountRepo,
supplierRepo: supplierRepo,
rmsFactory: rmsFactory,
billingService: billingService,
}
}
// checkWriteAccess проверяет, что пользователь имеет право редактировать данные на сервере (ADMIN/OWNER)
func (s *Service) checkWriteAccess(userID, serverID uuid.UUID) error {
role, err := s.accountRepo.GetUserRole(userID, serverID)
if err != nil {
return err
}
if role == account.RoleOperator {
return errors.New("доступ запрещен: оператор не может редактировать данные")
}
return nil
}
func (s *Service) GetDraft(draftID, userID uuid.UUID) (*drafts.DraftInvoice, error) {
draft, err := s.draftRepo.GetByID(draftID)
if err != nil {
return nil, err
}
// Проверяем, что черновик принадлежит активному серверу пользователя
// И пользователь не Оператор (операторы вообще не ходят в API)
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, errors.New("нет активного сервера")
}
if draft.RMSServerID != server.ID {
return nil, errors.New("черновик не принадлежит активному серверу")
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
return draft, nil
}
func (s *Service) GetActiveDrafts(userID uuid.UUID) ([]drafts.DraftInvoice, error) {
// 1. Узнаем активный сервер
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, errors.New("активный сервер не выбран")
}
// 2. Проверяем роль (Security)
// Операторам список недоступен
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
// 3. Возвращаем все черновики СЕРВЕРА
return s.draftRepo.GetActive(server.ID)
}
// 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")
}
// Словари нужны только тем, кто редактирует
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return nil, err
}
stores, _ := s.catalogRepo.GetActiveStores(server.ID)
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
}
// TODO: Здесь тоже бы проверить userID и права, но пока оставим как есть,
// так как DeleteDraft вызывается из хендлера, где мы можем добавить проверку,
// но лучше передавать userID в сигнатуру DeleteDraft(id, userID).
// Для скорости пока оставим, полагаясь на то, что фронт не покажет кнопку.
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)
}
// AddItem добавляет пустую строку в черновик
func (s *Service) AddItem(draftID uuid.UUID) (*drafts.DraftInvoiceItem, error) {
newItem := &drafts.DraftInvoiceItem{
ID: uuid.New(),
DraftID: draftID,
RawName: "Новая позиция",
RawAmount: decimal.NewFromFloat(1),
RawPrice: decimal.Zero,
Quantity: decimal.NewFromFloat(1),
Price: decimal.Zero,
Sum: decimal.Zero,
IsMatched: false,
}
if err := s.draftRepo.CreateItem(newItem); err != nil {
return nil, err
}
return newItem, nil
}
// DeleteItem удаляет строку и возвращает обновленную сумму черновика
func (s *Service) DeleteItem(draftID, itemID uuid.UUID) (float64, error) {
if err := s.draftRepo.DeleteItem(itemID); err != nil {
return 0, err
}
draft, err := s.draftRepo.GetByID(draftID)
if err != nil {
return 0, err
}
var totalSum decimal.Decimal
for _, item := range draft.Items {
if !item.Sum.IsZero() {
totalSum = totalSum.Add(item.Sum)
} else {
totalSum = totalSum.Add(item.Quantity.Mul(item.Price))
}
}
sumFloat, _ := totalSum.Float64()
return sumFloat, nil
}
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. Получаем сервер и права
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil {
return "", fmt.Errorf("active server not found: %w", err)
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return "", err
}
// --- BILLING CHECK ---
if can, err := s.billingService.CanProcessInvoice(server.ID); !can {
return "", fmt.Errorf("ошибка биллинга: %w", err)
}
// 2. Черновик
draft, err := s.draftRepo.GetByID(draftID)
if err != nil {
return "", err
}
// Проверка принадлежности черновика серверу
if draft.RMSServerID != server.ID {
return "", errors.New("черновик принадлежит другому серверу")
}
if draft.Status == drafts.StatusCompleted {
return "", errors.New("накладная уже отправлена")
}
// 3. Клиент (использует права текущего юзера - Админа/Владельца)
client, err := s.rmsFactory.GetClientForUser(userID)
if err != nil {
return "", err
}
targetStatus := "NEW"
if server.AutoProcess {
targetStatus = "PROCESSED"
}
// 4. Сборка Invoice
inv := invoices.Invoice{
ID: uuid.Nil,
DocumentNumber: draft.DocumentNumber,
DateIncoming: *draft.DateIncoming,
SupplierID: *draft.SupplierID,
DefaultStoreID: *draft.StoreID,
Status: targetStatus,
Comment: draft.Comment,
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("нет распознанных позиций для отправки")
}
// 5. Отправка в RMS
docNum, err := client.CreateIncomingInvoice(inv)
if err != nil {
return "", err
}
// 6. Обновление статуса черновика
draft.Status = drafts.StatusCompleted
s.draftRepo.Update(draft)
// --- БИЛЛИНГ: Списание баланса и инкремент счетчика ---
if err := s.accountRepo.DecrementBalance(server.ID); err != nil {
logger.Log.Error("Billing decrement failed", zap.Error(err), zap.String("server_id", server.ID.String()))
}
if err := s.accountRepo.IncrementInvoiceCount(server.ID); err != nil {
logger.Log.Error("Billing increment failed", zap.Error(err))
}
// 7. Запуск обучения
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) {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return uuid.Nil, errors.New("no active server")
}
if err := s.checkWriteAccess(userID, server.ID); err != nil {
return uuid.Nil, err
}
client, err := s.rmsFactory.GetClientForUser(userID)
if err != nil {
return uuid.Nil, err
}
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,
ProductID: productID,
Name: name,
Count: count,
}
s.catalogRepo.SaveContainer(newLocalContainer)
return createdID, nil
}