mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
244 lines
7.4 KiB
Go
244 lines
7.4 KiB
Go
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
|
||
}
|