- Add price_updated_at field to qt_configurations table to track when prices were last updated - Add RefreshPrices() method in configuration service to update all component prices with current values from database - Add POST /api/configs/:uuid/refresh-prices API endpoint for price updates - Add "Refresh Prices" button in configurator UI next to Save button - Display last price update timestamp in human-readable format (e.g., "5 min ago", "2 hours ago") - Create migration 004_add_price_updated_at.sql for database schema update - Update CLAUDE.md documentation with new API endpoint and schema changes - Add MIGRATION_PRICE_REFRESH.md with detailed migration instructions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
393 lines
13 KiB
Markdown
393 lines
13 KiB
Markdown
# 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
|