# Architecture ## Tech Stack | Component | Technology | |-----------|------------| | Backend | Go 1.22+ (go.mod: 1.24.0) | | Web framework | Gin | | ORM | GORM | | Database | MariaDB 11 | | Frontend | htmx + Tailwind CDN (no build step) | | Excel | excelize | | Go module | `git.mchus.pro/mchus/priceforge` | --- ## Directory Structure ``` PriceForge/ ├── bible/ # Architectural documentation (you are here) ├── bin/ # Build output ├── cmd/ │ ├── pfs/ # Main server (main.go ~749 lines) │ ├── cron/ # Cron job runner │ ├── importer/ # Stock import utility │ ├── migrate/ # Migration tool │ └── migrate_ops_projects/ # Legacy project migrator ├── data/ # Data files ├── internal/ # Application code │ ├── appmeta/ # Application metadata │ ├── appstate/ # Global state │ ├── config/ # YAML config loading │ ├── db/ # DB connection, connection pool │ ├── dbutil/ # DB utilities │ ├── handlers/ # HTTP handlers (12 files) │ ├── localdb/ # SQLite for local settings │ ├── lotmatch/ # LOT mapping resolver │ ├── middleware/ # HTTP middleware │ ├── models/ # GORM models and migrations │ ├── repository/ # Data access layer (11 files) │ ├── services/ # Business logic │ │ ├── alerts/ # Alert service │ │ ├── pricelist/ # Pricelist service │ │ ├── pricing/ # Price calculation │ │ ├── sync/ # Sync (4 files) │ │ ├── stock_import.go # Stock import: core, Import, replaceStockLogs │ │ ├── stock_mappings.go # Stock import: mappings, ignore rules, seen-index │ │ ├── stock_parse.go # Stock import: XLSX/MXL parsing, date detection │ │ ├── competitor_import.go # Competitor Excel import, pricelist build │ │ ├── vendor_mapping.go # Vendor mapping service │ │ └── component.go # Component service │ ├── tasks/ # Background Task Manager │ └── warehouse/ # Warehouse price calculation ├── migrations/ # SQL migrations (39 files) ├── scripts/ # Build and release scripts ├── web/ │ ├── static/ │ │ ├── app.css # Tailwind CSS │ │ └── js/ # Page JS (6 files, extracted from templates) │ └── templates/ # HTML templates (shell only, JS in static/js/) ├── CLAUDE.md # AI agent instructions ├── README.md # Quick start └── todo.md # Operational task list ``` --- ## Application Layers (3-tier) ``` HTTP Request │ ▼ ┌─────────────┐ │ Handlers │ internal/handlers/ — request parsing, validation, response └──────┬──────┘ │ ▼ ┌─────────────┐ │ Services │ internal/services/ — business logic, orchestration └──────┬──────┘ │ ▼ ┌─────────────┐ │ Repository │ internal/repository/ — SQL queries via GORM └──────┬──────┘ │ ▼ MariaDB 11 ``` --- ## Key Modules ### internal/handlers/ (12 files) | File | Purpose | |------|---------| | `web.go` | Page rendering | | `component.go` | Component list and detail endpoints | | `pricing.go` | PricingHandler struct, NewPricingHandler, stats, query helpers | | `pricing_components.go` | ListComponents, GetComponentPricing, UpdatePrice, RecalculateAll, PreviewPrice | | `pricing_alerts.go` | ListAlerts, AcknowledgeAlert, ResolveAlert, IgnoreAlert | | `pricing_stock.go` | ImportStockLog, stock mappings, ignore rules | | `pricing_vendor.go` | Vendor mappings CRUD, CSV import/export | | `pricing_lots.go` | ListLotsTable, CreateLot, ListLots, GetLotStats, PartnumberBooks | | `pricelist.go` | Pricelist CRUD and CSV export | | `competitor.go` | Competitor CRUD, import, ParseHeaders | | `setup.go` | DB connection setup and test | | `sync.go` | Sync diagnostic endpoints | ### internal/services/ | File/dir | Purpose | |----------|---------| | `pricelist/` | Create Estimate, Warehouse, Competitor pricelists | | `pricing/` | Price calc: median, weighted_median, coefficients | | `alerts/` | Alert generation (price staleness, demand) | | `stock_import.go` | Stock import core: Import, replaceStockLogs, buildWarehousePricelistItems | | `stock_mappings.go` | Mappings, ignore rules, seen-index (upsertSeenRows) | | `stock_parse.go` | XLSX/MXL parsing, date detection, XLSX helper functions | | `competitor_import.go` | Competitor Excel import, p/n→lot resolution, pricelist build | | `sync/service.go` | Sync service struct, status, heartbeat | | `sync/sync_pricelists.go` | Pricelist sync to local DB | | `sync/sync_changes.go` | Push pending changes (configurations, projects) | | `sync/sync_import.go` | Import configurations/projects to local DB | | `vendor_mapping.go` | Resolve partnumber → LOT, bundles, ignore | | `component.go` | Component listing and search | ### internal/warehouse/ `snapshot.go` — warehouse pricelist creation: weighted average, bundle expansion, allocation. ### internal/lotmatch/ `matcher.go` — vendor partnumber → LOT resolver: 1. Exact match `(vendor, partnumber)` 2. Fallback: `(vendor='', partnumber)` ### internal/tasks/ Task Manager: Submit, status polling, task types. See [background-tasks.md](background-tasks.md). --- ## HTML Templates (web/templates/) | Template | Route | Purpose | |----------|-------|---------| | `base.html` | — | Layout, navigation | | `admin_pricing.html` | `/admin/pricing` | Tabs: Estimate, Warehouse, Alerts | | `lot.html` | `/lot` | Tabs: LOT components, Mappings | | `pricelists.html` | `/pricelists` | Pricelist listing | | `pricelist_detail.html` | `/pricelists/:id` | Pricelist detail view | | `vendor_mappings.html` | `/vendor-mappings` | Vendor mapping UI | | `components_list.html` | `/partials/components` | Components partial | | `setup.html` | `/setup` | DB connection setup | | `sync_status.html` | partial | Online/offline indicator | --- ## Navigation (base.html) ``` LOT → /lot Pricing Admin → /admin/pricing Settings → /setup ``` Configurator, projects, and export links must **not** appear in the menu. --- ## Database - **Primary**: MariaDB 11 (all operational data) - **Local**: SQLite (only app settings and technical data; not used in `pfs` runtime) - **Migrations**: `migrations/` — SQL files applied automatically on startup via `RunSQLMigrations` ### Authoritative Table List (baseline 2026-03-21, before migrations 042–049) **Active — core:** | Table | Purpose | |-------|---------| | `lot` | Components / articles (primary catalog) | | `qt_lot_metadata` | Component price metadata. PK `(lot_name)` before migration 046, `(lot_name, pricelist_type)` after. All queries must filter `WHERE pricelist_type='estimate'` after 046. | | `qt_pricelists` | Pricelist headers (source, version, timestamps) | | `qt_pricelist_items` | Pricelist item rows. Gains `lead_time_weeks INT NULL` in migration 047. | | `qt_categories` | Category definitions (code, name, name_ru). FK from `qt_lot_metadata.category_id`. | | `qt_partnumber_book_items` | Canonical PN catalog; one row per `partnumber`, `lots_json` maps to lot names | | `qt_partnumber_books` | Versioned PN book snapshots with `partnumbers_json` | | `qt_vendor_partnumber_seen` | Seen registry (unique by `partnumber`). `is_ignored` + `is_pattern` flags. | | `stock_ignore_rules` | Active ignore patterns for stock import (`target`, `match_type`, `pattern`). Used in `ListStockIgnoreRules`, `UpsertStockIgnoreRule`, `DeleteStockIgnoreRule`. | | `qt_pricing_alerts` | Price alerts (staleness, spikes, drops) | | `qt_component_usage_stats` | Per-lot quote counters and trend. **Column names**: `quotes_last30d`, `quotes_last7d` (no underscore before digit). Go model has explicit `gorm:"column:..."` tags for these. | | `qt_configurations` | Client configurations (quote documents). Written by child app, read by PriceForge. | | `qt_projects` | Client projects. Written by child app. | | `qt_scheduler_runs` | Embedded scheduler job state | | `qt_schema_migrations` | Applied SQL migration log | | `supplier` | Supplier catalog. Extended in migration 042 with `supplier_code`, `supplier_type`, `price_uplift`, etc. Before 042: only `supplier_name` + `supplier_comment`. | **Active — competitor subsystem:** | Table | Purpose | |-------|---------| | `qt_competitors` | Competitor profiles (name, code, `price_uplift`, `column_mapping` JSON) | | `partnumber_log_competitors` | Legacy competitor quote log. Read-only archive after migration 048. FK to `qt_competitors.id`. | **Active — legacy quote archives (read-only after parts_log backfill):** | Table | Purpose | |-------|---------| | `lot_log` | Historical trader quotes (lot_name, supplier, price, date). FK to `lot` and `supplier`. | | `stock_log` | Historical warehouse stock records (partnumber, supplier, price, qty). No FK to supplier. | **Active — sync with child app:** | Table | Purpose | |-------|---------| | `qt_client_schema_state` | Per-client sync state and pricelist version tracking | | `qt_pricelist_sync_status` | Pricelist sync timestamps per user | **Kept but not referenced in Go code:** | Table | Notes | |-------|-------| | `machine` | Legacy machine catalog. Has FK from `machine_log`. No Go code references. | | `machine_log` | Legacy machine quote log. FK to `machine` and `supplier`. No Go code references. | **Dropped (2026-03-21):** | Table | Reason | |-------|--------| | `qt_client_local_migrations` | Used by old sync mechanism; removed | | `qt_price_overrides` | Model existed but feature was never implemented | | `qt_users` | Model existed but authentication was never implemented in PriceForge | --- ## Startup Flow 1. Parse flags (`--config`, `--migrate`, `--version`) 2. Load `config.yaml` 3. **Fail-fast**: connect to MariaDB; exit on failure 4. Apply SQL migrations 5. Start Gin router on `host:port` 6. Auto-open browser (if loopback address)