# 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 │ ├── priceupdater/main.go # Cron job for price updates & alerts │ └── 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, vendor VARCHAR(50), -- Parsed from lot_name: CPU_AMD_9654 → "AMD" 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), notes TEXT, is_template BOOLEAN DEFAULT FALSE, 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, vendor, model from lot_name: ```go // "CPU_AMD_9654" → category="CPU", vendor="AMD", model="9654" // "MB_INTEL_4.Sapphire_2S_32xDDR5" → category="MB", vendor="INTEL", model="4.Sapphire_2S_32xDDR5" // "MEM_DDR5_64G_5600" → category="MEM", vendor="DDR5", model="64G_5600" // "GPU_NV_RTX_4090_PCIe" → category="GPU", vendor="NV", model="RTX_4090_PCIe" func ParsePartNumber(lotName string) (category, vendor, model string) { parts := strings.SplitN(lotName, "_", 3) if len(parts) >= 1 { category = parts[0] } if len(parts) >= 2 { vendor = parts[1] } if len(parts) >= 3 { model = parts[2] } 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 - **price_spike** (MEDIUM): price increased > 20% from previous period - **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 DELETE /api/configs/:uuid → delete GET /api/configs/:uuid/export → export as JSON POST /api/configs/import → import from 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 price updater (cron job) go run ./cmd/priceupdater # Run importer (one-time setup) go run ./cmd/importer # Build for production CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/quoteforge ./cmd/server # Run tests go test ./... ``` ## Dependencies (go.mod) ```go module github.com/mchus/quoteforge go 1.22 require ( github.com/gin-gonic/gin v1.9.1 github.com/go-sql-driver/mysql v1.7.1 gorm.io/gorm v1.25.5 gorm.io/driver/mysql v1.5.2 github.com/xuri/excelize/v2 v2.8.0 github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/uuid v1.5.0 golang.org/x/crypto v0.17.0 gopkg.in/yaml.v3 v3.0.1 ) ``` ## Development Priorities 1. **Phase 1 (MVP):** Project setup, models, component API, basic UI, CSV export 2. **Phase 2:** JWT auth with roles, pricing admin UI, all price methods 3. **Phase 3:** Save/load configs, JSON import/export, XLSX export, cron jobs 4. **Phase 4:** Usage stats, alerts system, dashboard 5. **Phase 5:** Polish, tests, Docker, documentation ## 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