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 }