Files
rmser/internal/infrastructure/repository/recommendations/postgres.go
2025-11-29 08:40:24 +03:00

244 lines
7.4 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 recommendations
import (
"fmt"
"strconv"
"time"
"rmser/internal/domain/operations"
"rmser/internal/domain/recommendations"
"gorm.io/gorm"
)
type pgRepository struct {
db *gorm.DB
}
func NewRepository(db *gorm.DB) recommendations.Repository {
return &pgRepository{db: db}
}
// --- Методы Хранения ---
func (r *pgRepository) SaveAll(items []recommendations.Recommendation) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Delete(&recommendations.Recommendation{}).Error; err != nil {
return err
}
if len(items) > 0 {
if err := tx.CreateInBatches(items, 100).Error; err != nil {
return err
}
}
return nil
})
}
func (r *pgRepository) GetAll() ([]recommendations.Recommendation, error) {
var items []recommendations.Recommendation
err := r.db.Find(&items).Error
return items, err
}
// --- Методы Аналитики ---
// 1. Товары (GOODS/PREPARED), не используемые в техкартах
func (r *pgRepository) FindUnusedGoods() ([]recommendations.Recommendation, error) {
var results []recommendations.Recommendation
query := `
SELECT
p.id as product_id,
p.name as product_name,
'Товар не используется ни в одной техкарте' as reason,
? as type
FROM products p
WHERE p.type IN ('GOODS', 'PREPARED')
AND p.is_deleted = false -- Проверка на удаление
AND p.id NOT IN (
SELECT DISTINCT product_id FROM recipe_items
)
AND p.id NOT IN (
SELECT DISTINCT product_id FROM recipes
)
ORDER BY p.name ASC
`
if err := r.db.Raw(query, recommendations.TypeUnused).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
// 2. Закупается, но нет в техкартах
func (r *pgRepository) FindPurchasedButUnused(days int) ([]recommendations.Recommendation, error) {
var results []recommendations.Recommendation
dateFrom := time.Now().AddDate(0, 0, -days)
query := `
SELECT DISTINCT
p.id as product_id,
p.name as product_name,
'Товар активно закупается, но не включен ни в одну техкарту' as reason,
? as type
FROM store_operations so
JOIN products p ON so.product_id = p.id
WHERE
so.op_type = ?
AND so.period_from >= ?
AND p.is_deleted = false -- Проверка на удаление
AND p.id NOT IN (
SELECT DISTINCT product_id FROM recipe_items
)
ORDER BY p.name ASC
`
if err := r.db.Raw(query, recommendations.TypePurchasedButUnused, operations.OpTypePurchase, dateFrom).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
// 3. Ингредиенты в актуальных техкартах без закупок
func (r *pgRepository) FindNoIncomingIngredients(days int) ([]recommendations.Recommendation, error) {
var results []recommendations.Recommendation
dateFrom := time.Now().AddDate(0, 0, -days)
query := `
SELECT
p.id as product_id,
p.name as product_name,
'Нет закупок (' || ? || ' дн). Входит в: ' || STRING_AGG(DISTINCT parent.name, ', ') as reason,
? as type
FROM recipe_items ri
JOIN recipes r ON ri.recipe_id = r.id
JOIN products p ON ri.product_id = p.id
JOIN products parent ON r.product_id = parent.id
WHERE
(r.date_to IS NULL OR r.date_to >= CURRENT_DATE)
AND p.type = 'GOODS'
AND p.is_deleted = false -- Сам ингредиент не удален
AND parent.is_deleted = false -- Блюдо, в которое он входит, не удалено
AND p.id NOT IN (
SELECT product_id
FROM store_operations
WHERE op_type = ?
AND period_from >= ?
)
GROUP BY p.id, p.name
ORDER BY p.name ASC
`
if err := r.db.Raw(query, strconv.Itoa(days), recommendations.TypeNoIncoming, operations.OpTypePurchase, dateFrom).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
// 4. Товары, которые закупаем, но не расходуем ("Висяки")
func (r *pgRepository) FindStaleGoods(days int) ([]recommendations.Recommendation, error) {
var results []recommendations.Recommendation
dateFrom := time.Now().AddDate(0, 0, -days)
query := `
SELECT DISTINCT
p.id as product_id,
p.name as product_name,
? as reason,
? as type
FROM store_operations so
JOIN products p ON so.product_id = p.id
WHERE
so.op_type = ?
AND so.period_from >= ?
AND p.is_deleted = false -- Проверка на удаление
AND p.id NOT IN (
SELECT product_id
FROM store_operations
WHERE op_type = ?
AND period_from >= ?
)
ORDER BY p.name ASC
`
reason := fmt.Sprintf("Были закупки, но нет расхода за %d дн.", days)
if err := r.db.Raw(query, reason, recommendations.TypeStale, operations.OpTypePurchase, dateFrom, operations.OpTypeUsage, dateFrom).
Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
// 5. Блюдо используется в техкарте другого блюда
func (r *pgRepository) FindDishesInRecipes() ([]recommendations.Recommendation, error) {
var results []recommendations.Recommendation
query := `
SELECT DISTINCT
child.id as product_id,
child.name as product_name,
'Является Блюдом (DISH), но указан ингредиентом в: ' || parent.name as reason,
? as type
FROM recipe_items ri
JOIN products child ON ri.product_id = child.id
JOIN recipes r ON ri.recipe_id = r.id
JOIN products parent ON r.product_id = parent.id
WHERE
child.type = 'DISH'
AND child.is_deleted = false -- Вложенное блюдо не удалено
AND parent.is_deleted = false -- Родительское блюдо не удалено
AND (r.date_to IS NULL OR r.date_to >= CURRENT_DATE)
ORDER BY child.name ASC
`
if err := r.db.Raw(query, recommendations.TypeDishInRecipe).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
// 6. Есть расход (Usage), но нет прихода (Purchase)
func (r *pgRepository) FindUsageWithoutPurchase(days int) ([]recommendations.Recommendation, error) {
var results []recommendations.Recommendation
dateFrom := time.Now().AddDate(0, 0, -days)
query := `
SELECT DISTINCT
p.id as product_id,
p.name as product_name,
? as reason,
? as type
FROM store_operations so
JOIN products p ON so.product_id = p.id
WHERE
so.op_type = ? -- Есть расход (продажа/списание)
AND so.period_from >= ?
AND p.type = 'GOODS' -- Только для товаров
AND p.is_deleted = false -- Товар жив
AND p.id NOT IN ( -- Но не было закупок
SELECT product_id
FROM store_operations
WHERE op_type = ?
AND period_from >= ?
)
ORDER BY p.name ASC
`
reason := fmt.Sprintf("Товар расходуется (продажи/списания), но не закупался последние %d дн.", days)
// Аргументы: reason, type, OpUsage, date, OpPurchase, date
if err := r.db.Raw(query,
reason,
recommendations.TypeUsageNoIncoming,
operations.OpTypeUsage,
dateFrom,
operations.OpTypePurchase,
dateFrom,
).Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}