Перевел на 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

@@ -21,8 +21,9 @@ func NewDraftsHandler(service *drafts.Service) *DraftsHandler {
return &DraftsHandler{service: service}
}
// GetDraft возвращает полные данные черновика
// GetDraft
func (h *DraftsHandler) GetDraft(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
@@ -30,7 +31,7 @@ func (h *DraftsHandler) GetDraft(c *gin.Context) {
return
}
draft, err := h.service.GetDraft(id)
draft, err := h.service.GetDraft(id, userID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "draft not found"})
return
@@ -38,17 +39,37 @@ func (h *DraftsHandler) GetDraft(c *gin.Context) {
c.JSON(http.StatusOK, draft)
}
// GetStores возвращает список складов
func (h *DraftsHandler) GetStores(c *gin.Context) {
stores, err := h.service.GetActiveStores()
// GetDictionaries (бывший GetStores)
func (h *DraftsHandler) GetDictionaries(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
data, err := h.service.GetDictionaries(userID)
if err != nil {
logger.Log.Error("GetDictionaries error", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stores)
c.JSON(http.StatusOK, data)
}
// UpdateItemDTO - тело запроса на изменение строки
// GetStores - устаревший метод для обратной совместимости
// Возвращает массив складов
func (h *DraftsHandler) GetStores(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
// Используем логику из GetDictionaries, но возвращаем только stores
dict, err := h.service.GetDictionaries(userID)
if err != nil {
logger.Log.Error("GetStores error", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// dict["stores"] уже содержит []catalog.Store
c.JSON(http.StatusOK, dict["stores"])
}
// UpdateItemDTO
type UpdateItemDTO struct {
ProductID *string `json:"product_id"`
ContainerID *string `json:"container_id"`
@@ -57,6 +78,7 @@ type UpdateItemDTO struct {
}
func (h *DraftsHandler) UpdateItem(c *gin.Context) {
// userID := c.MustGet("userID").(uuid.UUID) // Пока не используется в UpdateItem, но можно добавить проверку владельца
draftID, _ := uuid.Parse(c.Param("id"))
itemID, _ := uuid.Parse(c.Param("itemId"))
@@ -99,8 +121,8 @@ type CommitRequestDTO struct {
Comment string `json:"comment"`
}
// CommitDraft сохраняет шапку и отправляет в RMS
func (h *DraftsHandler) CommitDraft(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
draftID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid draft id"})
@@ -113,10 +135,9 @@ func (h *DraftsHandler) CommitDraft(c *gin.Context) {
return
}
// Парсинг данных шапки
date, err := time.Parse("2006-01-02", req.DateIncoming)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid date format (YYYY-MM-DD)"})
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid date format"})
return
}
storeID, err := uuid.Parse(req.StoreID)
@@ -130,35 +151,30 @@ func (h *DraftsHandler) CommitDraft(c *gin.Context) {
return
}
// 1. Обновляем шапку
if err := h.service.UpdateDraftHeader(draftID, &storeID, &supplierID, date, req.Comment); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update header: " + err.Error()})
return
}
// 2. Отправляем
docNum, err := h.service.CommitDraft(draftID)
docNum, err := h.service.CommitDraft(draftID, userID)
if err != nil {
logger.Log.Error("Commit failed", zap.Error(err))
c.JSON(http.StatusBadGateway, gin.H{"error": "RMS error: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "completed",
"document_number": docNum,
})
c.JSON(http.StatusOK, gin.H{"status": "completed", "document_number": docNum})
}
// AddContainerRequestDTO - запрос на создание фасовки
type AddContainerRequestDTO struct {
ProductID string `json:"product_id" binding:"required"`
Name string `json:"name" binding:"required"`
Count float64 `json:"count" binding:"required,gt=0"`
}
// AddContainer создает новую фасовку для товара
func (h *DraftsHandler) AddContainer(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
var req AddContainerRequestDTO
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
@@ -171,29 +187,22 @@ func (h *DraftsHandler) AddContainer(c *gin.Context) {
return
}
// Конвертация float64 -> decimal
countDec := decimal.NewFromFloat(req.Count)
// Вызов сервиса
newID, err := h.service.CreateProductContainer(pID, req.Name, countDec)
newID, err := h.service.CreateProductContainer(userID, pID, req.Name, countDec)
if err != nil {
logger.Log.Error("Failed to create container", zap.Error(err))
// Можно возвращать 502, если ошибка от RMS
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "created",
"container_id": newID.String(),
})
c.JSON(http.StatusOK, gin.H{"status": "created", "container_id": newID.String()})
}
// DraftListItemDTO - структура элемента списка
type DraftListItemDTO struct {
ID string `json:"id"`
DocumentNumber string `json:"document_number"`
DateIncoming string `json:"date_incoming"` // YYYY-MM-DD
DateIncoming string `json:"date_incoming"`
Status string `json:"status"`
ItemsCount int `json:"items_count"`
TotalSum float64 `json:"total_sum"`
@@ -201,38 +210,30 @@ type DraftListItemDTO struct {
CreatedAt string `json:"created_at"`
}
// GetDrafts возвращает список активных черновиков
func (h *DraftsHandler) GetDrafts(c *gin.Context) {
list, err := h.service.GetActiveDrafts()
userID := c.MustGet("userID").(uuid.UUID)
list, err := h.service.GetActiveDrafts(userID)
if err != nil {
logger.Log.Error("Failed to fetch drafts", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
response := make([]DraftListItemDTO, 0, len(list))
for _, d := range list {
// Расчет суммы
var totalSum decimal.Decimal
for _, item := range d.Items {
// Если item.Sum посчитана - берем её, иначе (qty * price)
if !item.Sum.IsZero() {
totalSum = totalSum.Add(item.Sum)
} else {
totalSum = totalSum.Add(item.Quantity.Mul(item.Price))
}
}
sumFloat, _ := totalSum.Float64()
// Форматирование даты
dateStr := ""
if d.DateIncoming != nil {
dateStr = d.DateIncoming.Format("2006-01-02")
}
// Имя склада
storeName := ""
if d.Store != nil {
storeName = d.Store.Name
@@ -249,12 +250,11 @@ func (h *DraftsHandler) GetDrafts(c *gin.Context) {
CreatedAt: d.CreatedAt.Format(time.RFC3339),
})
}
c.JSON(http.StatusOK, response)
}
// DeleteDraft обрабатывает запрос на удаление/отмену
func (h *DraftsHandler) DeleteDraft(c *gin.Context) {
// userID := c.MustGet("userID").(uuid.UUID) // Можно добавить проверку владельца
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
@@ -264,14 +264,9 @@ func (h *DraftsHandler) DeleteDraft(c *gin.Context) {
newStatus, err := h.service.DeleteDraft(id)
if err != nil {
logger.Log.Error("Failed to delete draft", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Возвращаем новый статус, чтобы фронтенд знал, удалился он совсем или стал CANCELED
c.JSON(http.StatusOK, gin.H{
"status": newStatus,
"id": id.String(),
})
}
c.JSON(http.StatusOK, gin.H{"status": newStatus, "id": id.String()})
}

View File

@@ -22,9 +22,19 @@ func NewOCRHandler(service *ocrService.Service) *OCRHandler {
// GetCatalog возвращает список товаров для OCR сервиса
func (h *OCRHandler) GetCatalog(c *gin.Context) {
items, err := h.service.GetCatalogForIndexing()
// Если этот эндпоинт дергает Python-скрипт без токена пользователя - это проблема безопасности.
// Либо Python скрипт должен передавать токен админа/системы и ID сервера в query.
// ПОКА: Предполагаем, что запрос идет от фронта или с заголовком X-Telegram-User-ID.
// Если заголовка нет (вызов от скрипта), пробуем взять server_id из query (небезопасно, но для MVP)
// Или лучше так: этот метод вызывается Фронтендом для поиска? Нет, название GetCatalogForIndexing намекает на OCR.
// Оставим пока требование UserID.
userID := c.MustGet("userID").(uuid.UUID)
items, err := h.service.GetCatalogForIndexing(userID)
if err != nil {
logger.Log.Error("Ошибка получения каталога для OCR", zap.Error(err))
logger.Log.Error("Ошибка получения каталога", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
@@ -38,8 +48,9 @@ type MatchRequest struct {
ContainerID *string `json:"container_id"`
}
// SaveMatch сохраняет привязку (обучение)
func (h *OCRHandler) SaveMatch(c *gin.Context) {
userID := c.MustGet("userID").(uuid.UUID)
var req MatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
@@ -64,7 +75,7 @@ func (h *OCRHandler) SaveMatch(c *gin.Context) {
}
}
if err := h.service.SaveMapping(req.RawName, pID, qty, contID); err != nil {
if err := h.service.SaveMapping(userID, req.RawName, pID, qty, contID); err != nil {
logger.Log.Error("Ошибка сохранения матчинга", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
@@ -73,18 +84,16 @@ func (h *OCRHandler) SaveMatch(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "saved"})
}
// DeleteMatch удаляет связь
func (h *OCRHandler) DeleteMatch(c *gin.Context) {
// Получаем raw_name из query параметров, так как в URL path могут быть спецсимволы
// Пример: DELETE /api/ocr/match?raw_name=Хлеб%20Бородинский
userID := c.MustGet("userID").(uuid.UUID)
rawName := c.Query("raw_name")
if rawName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "raw_name is required"})
return
}
if err := h.service.DeleteMatch(rawName); err != nil {
logger.Log.Error("Ошибка удаления матча", zap.Error(err))
if err := h.service.DeleteMatch(userID, rawName); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
@@ -92,43 +101,32 @@ func (h *OCRHandler) DeleteMatch(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
}
// SearchProducts ищет товары (для автокомплита)
func (h *OCRHandler) SearchProducts(c *gin.Context) {
query := c.Query("q") // ?q=молоко
if query == "" {
c.JSON(http.StatusOK, []interface{}{})
return
}
userID := c.MustGet("userID").(uuid.UUID)
query := c.Query("q")
products, err := h.service.SearchProducts(query)
products, err := h.service.SearchProducts(userID, query)
if err != nil {
logger.Log.Error("Search error", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Отдаем на фронт упрощенную структуру или полную, в зависимости от нужд.
// Product entity уже содержит JSON теги, так что можно отдать напрямую.
c.JSON(http.StatusOK, products)
}
// GetMatches возвращает список всех обученных связей
func (h *OCRHandler) GetMatches(c *gin.Context) {
matches, err := h.service.GetKnownMatches()
userID := c.MustGet("userID").(uuid.UUID)
matches, err := h.service.GetKnownMatches(userID)
if err != nil {
logger.Log.Error("Ошибка получения списка матчей", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, matches)
}
// GetUnmatched возвращает список нераспознанных позиций для подсказок
func (h *OCRHandler) GetUnmatched(c *gin.Context) {
items, err := h.service.GetUnmatchedItems()
userID := c.MustGet("userID").(uuid.UUID)
items, err := h.service.GetUnmatchedItems(userID)
if err != nil {
logger.Log.Error("Ошибка получения списка unmatched", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}