# QuoteForge - Claude Code Instructions ## Project Overview QuoteForge — корпоративный инструмент для конфигурирования серверов и формирования коммерческих предложений (КП). Приложение интегрируется с существующей базой данных RFQ_LOG. ## Tech Stack - **Language:** Go 1.22+ - **Web Framework:** Gin (github.com/gin-gonic/gin) - **ORM:** GORM (gorm.io/gorm) - **Database:** MariaDB 11 (existing database RFQ_LOG) - **Frontend:** HTML templates + htmx + Tailwind CSS (CDN) - **Excel Export:** excelize (github.com/xuri/excelize/v2) - **Auth:** JWT (github.com/golang-jwt/jwt/v5) ## Project Structure ``` quoteforge/ ├── cmd/ │ ├── server/main.go # Main HTTP server │ └── importer/main.go # Import metadata from lot table ├── internal/ │ ├── config/config.go # YAML config loading │ ├── models/ # GORM models │ ├── handlers/ # Gin HTTP handlers │ ├── services/ # Business logic │ ├── middleware/ # Auth, CORS, roles │ └── repository/ # Database queries ├── web/ │ ├── templates/ # Go HTML templates │ └── static/ # CSS, JS ├── migrations/ # SQL migration files ├── config.yaml └── go.mod ``` ## Existing Database Tables (READ-ONLY - DO NOT MODIFY) These tables are used by other systems. Our app only reads from them: ```sql -- Component catalog CREATE TABLE lot ( lot_name CHAR(255) PRIMARY KEY, -- e.g., "CPU_AMD_9654", "MB_INTEL_4.Sapphire_2S" lot_description VARCHAR(10000) ); -- Price history from suppliers CREATE TABLE lot_log ( lot_log_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, lot CHAR(255) NOT NULL, -- FK → lot.lot_name supplier CHAR(255) NOT NULL, -- FK → supplier.supplier_name date DATE NOT NULL, price DOUBLE NOT NULL, quality CHAR(255), comments VARCHAR(15000), FOREIGN KEY (lot) REFERENCES lot(lot_name), FOREIGN KEY (supplier) REFERENCES supplier(supplier_name) ); -- Supplier catalog CREATE TABLE supplier ( supplier_name CHAR(255) PRIMARY KEY, supplier_comment VARCHAR(10000) ); ``` ## New Tables (prefix qt_) QuoteForge creates these tables: ```sql -- Users CREATE TABLE qt_users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(100) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, role ENUM('viewer', 'editor', 'pricing_admin', 'admin') DEFAULT 'viewer', is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- Component metadata (extends lot table) CREATE TABLE qt_lot_metadata ( lot_name CHAR(255) PRIMARY KEY, category_id INT, model VARCHAR(100), -- Parsed: CPU_AMD_9654 → "9654" specs JSON, current_price DECIMAL(12,2), price_method ENUM('manual', 'median', 'average', 'weighted_median') DEFAULT 'median', price_period_days INT DEFAULT 90, price_updated_at TIMESTAMP, request_count INT DEFAULT 0, last_request_date DATE, popularity_score DECIMAL(10,4), FOREIGN KEY (lot_name) REFERENCES lot(lot_name) ); -- Categories CREATE TABLE qt_categories ( id INT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(20) UNIQUE NOT NULL, -- MB, CPU, MEM, GPU, SSD, HDD, RAID, NIC, HCA, HBA, DPU, PS name VARCHAR(100) NOT NULL, name_ru VARCHAR(100), display_order INT DEFAULT 0, is_required BOOLEAN DEFAULT FALSE ); -- Saved configurations CREATE TABLE qt_configurations ( id INT AUTO_INCREMENT PRIMARY KEY, uuid VARCHAR(36) UNIQUE NOT NULL, user_id INT NOT NULL, name VARCHAR(200) NOT NULL, items JSON NOT NULL, -- [{"lot_name": "CPU_AMD_9654", "quantity": 2, "unit_price": 11500}] total_price DECIMAL(12,2), custom_price DECIMAL(12,2), -- User-defined target price (for discounts) notes TEXT, is_template BOOLEAN DEFAULT FALSE, server_count INT DEFAULT 1, -- Number of servers in configuration price_updated_at TIMESTAMP, -- Last time prices were refreshed created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES qt_users(id) ); -- Price overrides CREATE TABLE qt_price_overrides ( id INT AUTO_INCREMENT PRIMARY KEY, lot_name CHAR(255) NOT NULL, price DECIMAL(12,2) NOT NULL, valid_from DATE NOT NULL, valid_until DATE, reason TEXT, created_by INT NOT NULL, FOREIGN KEY (lot_name) REFERENCES lot(lot_name) ); -- Alerts for pricing admins CREATE TABLE qt_pricing_alerts ( id INT AUTO_INCREMENT PRIMARY KEY, lot_name CHAR(255) NOT NULL, alert_type ENUM('high_demand_stale_price', 'price_spike', 'price_drop', 'no_recent_quotes', 'trending_no_price') NOT NULL, severity ENUM('low', 'medium', 'high', 'critical') DEFAULT 'medium', message TEXT NOT NULL, details JSON, status ENUM('new', 'acknowledged', 'resolved', 'ignored') DEFAULT 'new', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Usage statistics CREATE TABLE qt_component_usage_stats ( lot_name CHAR(255) PRIMARY KEY, quotes_total INT DEFAULT 0, quotes_last_30d INT DEFAULT 0, quotes_last_7d INT DEFAULT 0, total_quantity INT DEFAULT 0, total_revenue DECIMAL(14,2) DEFAULT 0, trend_direction ENUM('up', 'stable', 'down') DEFAULT 'stable', trend_percent DECIMAL(5,2) DEFAULT 0, last_used_at TIMESTAMP ); ``` ## Key Business Logic ### 1. Part Number Parsing Extract category and model from lot_name: ```go // "CPU_AMD_9654" → category="CPU", model="AMD_9654" // "MB_INTEL_4.Sapphire_2S" → category="MB", model="INTEL_4.Sapphire_2S" // "MEM_DDR5_64G_5600" → category="MEM", model="DDR5_64G_5600" // "GPU_NV_RTX_4090_PCIe" → category="GPU", model="NV_RTX_4090_PCIe" func ParsePartNumber(lotName string) (category, model string) { parts := strings.SplitN(lotName, "_", 2) if len(parts) >= 1 { category = parts[0] } if len(parts) >= 2 { model = parts[1] } return } ``` ### 2. Price Calculation Methods ```go // Median - simple median of prices in period func CalculateMedian(prices []float64) float64 // Average - arithmetic mean func CalculateAverage(prices []float64) float64 // Weighted Median - recent prices have higher weight (exponential decay) // weight = e^(-days_since_quote / decay_days) func CalculateWeightedMedian(prices []PricePoint, decayDays int) float64 ``` ### 3. Price Freshness (color coding) ```go // Green: < 30 days AND >= 3 quotes // Yellow: 30-60 days OR 1-2 quotes // Orange: 60-90 days // Red: > 90 days OR no price func GetPriceFreshness(daysSinceUpdate int, quoteCount int) string { if daysSinceUpdate < 30 && quoteCount >= 3 { return "fresh" // green } else if daysSinceUpdate < 60 { return "normal" // yellow } else if daysSinceUpdate < 90 { return "stale" // orange } return "critical" // red } ``` ### 4. Component Sorting Sort by: popularity + price freshness. Components without prices go to the bottom. ```go // Sort score = popularity_score * 10 + freshness_bonus - no_price_penalty // freshness_bonus: fresh=100, normal=50, stale=10, critical=0 // no_price_penalty: -1000 if current_price is NULL or 0 ``` ### 5. Alert Generation Generate alerts when: - **high_demand_stale_price** (CRITICAL): >= 5 quotes/month AND price > 60 days old - **trending_no_price** (HIGH): trend_percent > 50% AND no price set - **no_recent_quotes** (MEDIUM): popular component, no supplier quotes > 90 days ## API Endpoints ### Auth ``` POST /api/auth/login → {"username", "password"} → {"token", "refresh_token"} POST /api/auth/logout POST /api/auth/refresh GET /api/auth/me → current user info ``` ### Components ``` GET /api/components → list with pagination GET /api/components?category=CPU&vendor=AMD → filtered GET /api/components/:lot_name → single component details GET /api/categories → category list ``` ### Quote Builder ``` POST /api/quote/validate → {"items": [...]} → {"valid": bool, "errors": [], "warnings": []} POST /api/quote/calculate → {"items": [...]} → {"items": [...], "total": 45000.00} ``` ### Export ``` POST /api/export/csv → {"items": [...], "name": "Config 1"} → CSV file POST /api/export/xlsx → {"items": [...], "name": "Config 1"} → XLSX file ``` ### Configurations ``` GET /api/configs → list user's configurations POST /api/configs → save new configuration GET /api/configs/:uuid → get by UUID PUT /api/configs/:uuid → update POST /api/configs/:uuid/refresh-prices → refresh prices for all components DELETE /api/configs/:uuid → delete GET /api/configs/:uuid/export → export as JSON ``` ### Pricing Admin (requires role: pricing_admin or admin) ``` GET /admin/pricing/stats → dashboard stats GET /admin/pricing/components → components with pricing info GET /admin/pricing/components/:lot_name → component pricing details POST /admin/pricing/update → update price method/value POST /admin/pricing/recalculate-all → recalculate all prices GET /admin/pricing/alerts → list alerts POST /admin/pricing/alerts/:id/acknowledge → mark as seen POST /admin/pricing/alerts/:id/resolve → mark as resolved POST /admin/pricing/alerts/:id/ignore → dismiss alert ``` ### htmx Partials ``` GET /partials/components?category=CPU&vendor=AMD → HTML fragment GET /partials/cart → cart HTML GET /partials/summary → price summary HTML ``` ## User Roles | Role | Permissions | |------|-------------| | viewer | View components, create quotes, export | | editor | + save/load configurations | | pricing_admin | + manage prices, view alerts | | admin | + manage users | ## Frontend Guidelines - **Mobile-first** design - Use **htmx** for interactivity (hx-get, hx-post, hx-target, hx-swap) - Use **Tailwind CSS** via CDN - Minimal custom JavaScript - Color scheme for price freshness: - `text-green-600 bg-green-50` - fresh - `text-yellow-600 bg-yellow-50` - normal - `text-orange-600 bg-orange-50` - stale - `text-red-600 bg-red-50` - critical ## Commands ```bash # Run development server go run ./cmd/server # Run importer (one-time setup) go run ./cmd/importer # Run cron jobs manually go run ./cmd/cron -job=alerts # Check and generate alerts go run ./cmd/cron -job=update-prices # Recalculate all prices go run ./cmd/cron -job=reset-counters # Reset usage counters go run ./cmd/cron -job=update-popularity # Update popularity scores # Build for production CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/quoteforge ./cmd/server CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/quoteforge-cron ./cmd/cron # Run tests go test ./... ```` ## Cron Jobs QuoteForge now includes automated cron jobs for maintenance tasks. These can be run using the built-in cron functionality in the Docker container. ### Docker Compose Setup The Docker setup includes a dedicated cron service that runs the following jobs: - **Alerts check**: Every hour (0 * * * *) - **Price updates**: Daily at 2 AM (0 2 * * *) - **Usage counter reset**: Weekly on Sunday at 1 AM (0 1 * * 0) - **Popularity score updates**: Daily at 3 AM (0 3 * * *) ### Manual Cron Job Execution You can also run cron jobs manually using the quoteforge-cron binary: ```bash # Check and generate alerts go run ./cmd/cron -job=alerts # Recalculate all prices go run ./cmd/cron -job=update-prices # Reset usage counters go run ./cmd/cron -job=reset-counters # Update popularity scores go run ./cmd/cron -job=update-popularity ``` ### Cron Job Details - **Alerts check**: Generates alerts for components with high demand and stale prices, trending components without prices, and components with no recent quotes - **Price updates**: Recalculates prices for all components using configured methods (median, weighted median, average) - **Usage counter reset**: Resets weekly and monthly usage counters for components - **Popularity score updates**: Recalculates popularity scores based on supplier quote activity ## Code Style - Use standard Go formatting (gofmt) - Error handling: always check errors, wrap with context - Logging: use structured logging (slog or zerolog) - Comments: in Russian or English, be consistent - File naming: snake_case for files, PascalCase for types