Files
QuoteForge/CLAUDE.md
2026-01-26 18:30:45 +03:00

12 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
│   ├── 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 - 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 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

  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