package recommendations import ( "fmt" "strconv" "github.com/google/uuid" "gorm.io/gorm" "rmser/internal/domain/operations" "rmser/internal/domain/recommendations" ) type pgRepository struct { db *gorm.DB } func NewRepository(db *gorm.DB) recommendations.Repository { return &pgRepository{db: db} } // --- Методы Хранения --- func (r *pgRepository) SaveAll(serverID uuid.UUID, items []recommendations.Recommendation) error { return r.db.Transaction(func(tx *gorm.DB) error { // Удаляем только записи ЭТОГО сервера if err := tx.Where("rms_server_id = ?", serverID).Delete(&recommendations.Recommendation{}).Error; err != nil { return err } // Проставляем server_id для всех записей for i := range items { items[i].RMSServerID = serverID } if len(items) > 0 { if err := tx.CreateInBatches(items, 100).Error; err != nil { return err } } return nil }) } func (r *pgRepository) GetAll(serverID uuid.UUID) ([]recommendations.Recommendation, error) { var items []recommendations.Recommendation err := r.db.Where("rms_server_id = ?", serverID).Find(&items).Error return items, err } // --- Методы Аналитики --- // 1. Товары (GOODS/PREPARED), не используемые в техкартах func (r *pgRepository) FindUnusedGoods(serverID uuid.UUID) ([]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.rms_server_id = ? AND p.type IN ('GOODS', 'PREPARED') AND p.is_deleted = false AND p.id NOT IN ( SELECT DISTINCT ri.product_id FROM recipe_items ri JOIN recipes r ON ri.recipe_id = r.id WHERE r.rms_server_id = ? ) AND p.id NOT IN ( SELECT DISTINCT r.product_id FROM recipes r WHERE r.rms_server_id = ? ) ORDER BY p.name ASC ` if err := r.db.Raw(query, recommendations.TypeUnused, serverID, serverID, serverID).Scan(&results).Error; err != nil { return nil, err } return results, nil } // 2. Закупается, но нет в техкартах func (r *pgRepository) FindPurchasedButUnused(serverID uuid.UUID, days int) ([]recommendations.Recommendation, error) { var results []recommendations.Recommendation 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.rms_server_id = ? AND so.op_type = ? AND so.period_to >= CURRENT_DATE - INTERVAL '1 day' AND p.is_deleted = false AND p.id NOT IN ( SELECT DISTINCT ri.product_id FROM recipe_items ri JOIN recipes r ON ri.recipe_id = r.id WHERE r.rms_server_id = ? ) ORDER BY p.name ASC ` if err := r.db.Raw(query, recommendations.TypePurchasedButUnused, serverID, operations.OpTypePurchase, serverID, ).Scan(&results).Error; err != nil { return nil, err } return results, nil } // 3. Ингредиенты в актуальных техкартах без закупок func (r *pgRepository) FindNoIncomingIngredients(serverID uuid.UUID, days int) ([]recommendations.Recommendation, error) { var results []recommendations.Recommendation 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.rms_server_id = ? AND (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 so.product_id FROM store_operations so WHERE so.rms_server_id = ? AND so.op_type = ? AND so.period_to >= CURRENT_DATE - INTERVAL '1 day' ) GROUP BY p.id, p.name ORDER BY p.name ASC ` if err := r.db.Raw(query, strconv.Itoa(days), recommendations.TypeNoIncoming, serverID, serverID, operations.OpTypePurchase, ).Scan(&results).Error; err != nil { return nil, err } return results, nil } // 4. Товары, которые закупаем, но не расходуем ("Висяки") func (r *pgRepository) FindStaleGoods(serverID uuid.UUID, days int) ([]recommendations.Recommendation, error) { var results []recommendations.Recommendation 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.rms_server_id = ? AND so.op_type = ? AND so.period_to >= CURRENT_DATE - INTERVAL '1 day' AND p.is_deleted = false AND p.id NOT IN ( SELECT so2.product_id FROM store_operations so2 WHERE so2.rms_server_id = ? AND so2.op_type = ? AND so2.period_to >= CURRENT_DATE - INTERVAL '1 day' ) ORDER BY p.name ASC ` reason := fmt.Sprintf("Были закупки, но нет расхода за %d дн.", days) if err := r.db.Raw(query, reason, recommendations.TypeStale, serverID, operations.OpTypePurchase, serverID, operations.OpTypeUsage, ).Scan(&results).Error; err != nil { return nil, err } return results, nil } // 5. Блюдо используется в техкарте другого блюда func (r *pgRepository) FindDishesInRecipes(serverID uuid.UUID) ([]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 r.rms_server_id = ? AND 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, serverID).Scan(&results).Error; err != nil { return nil, err } return results, nil } // 6. Есть расход (Usage), но нет прихода (Purchase) func (r *pgRepository) FindUsageWithoutPurchase(serverID uuid.UUID, days int) ([]recommendations.Recommendation, error) { var results []recommendations.Recommendation 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.rms_server_id = ? AND so.op_type = ? AND so.period_to >= CURRENT_DATE - INTERVAL '1 day' AND p.type = 'GOODS' AND p.is_deleted = false AND p.id NOT IN ( SELECT so2.product_id FROM store_operations so2 WHERE so2.rms_server_id = ? AND so2.op_type = ? AND so2.period_to >= CURRENT_DATE - INTERVAL '1 day' ) ORDER BY p.name ASC ` reason := fmt.Sprintf("Товар расходуется (продажи/списания), но не закупался последние %d дн.", days) if err := r.db.Raw(query, reason, recommendations.TypeUsageNoIncoming, serverID, operations.OpTypeUsage, serverID, operations.OpTypePurchase, ).Scan(&results).Error; err != nil { return nil, err } return results, nil }