12 KiB
12 KiB
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:
-- 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:
-- 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:
// "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
// 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)
// 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.
// 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- freshtext-yellow-600 bg-yellow-50- normaltext-orange-600 bg-orange-50- staletext-red-600 bg-red-50- critical
Commands
# 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)
module git.mchus.pro/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
- Phase 1 (MVP): Project setup, models, component API, basic UI, CSV export
- Phase 2: JWT auth with roles, pricing admin UI, all price methods
- Phase 3: Save/load configs, JSON import/export, XLSX export, cron jobs
- Phase 4: Usage stats, alerts system, dashboard
- 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