Files
QuoteForge/CLAUDE.md
Michael Chus f31ae69233 Add price refresh functionality to configurator
- 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>
2026-01-31 10:31:00 +03:00

393 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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