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

13 KiB
Raw Blame History

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:

-- 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,
    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:

// "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

// 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
  • 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

# 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:

# 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