Files
PriceForge/bible-local/pricelist.md
Mikhail Chusavitin f48615e8a9 Modularize Go files, extract JS to static, implement competitor pricelists
Go refactoring:
- Split handlers/pricing.go (2446→291 lines) into 5 focused files
- Split services/stock_import.go (1334→~400 lines) into stock_mappings.go + stock_parse.go
- Split services/sync/service.go (1290→~250 lines) into 3 files

JS extraction:
- Move all inline <script> blocks to web/static/js/ (6 files)
- Templates reduced: admin_pricing 2873→521, lot 1531→304, vendor_mappings 1063→169, etc.

Competitor pricelists (migrations 033-039):
- qt_competitors + partnumber_log_competitors tables
- Excel import with column mapping, dedup, bulk insert
- p/n→lot resolution via weighted_median, discount applied
- Unmapped p/ns written to qt_vendor_partnumber_seen
- Quote counts (unique/total) shown on /admin/competitors
- price_method="weighted_median", price_period_days=0 stored explicitly

Fix price_method/price_period_days for warehouse items:
- warehouse: weighted_avg, period=0
- competitor: weighted_median, period=0
- Removes misleading DB defaults (was: median/90)

Update bible: architecture.md, pricelist.md, history.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 07:44:10 +03:00

4.5 KiB
Raw Blame History

Pricelists

Types

PriceForge supports three pricelist types (source field):

Type Data source Status
estimate qt_lot_metadata — snapshot of current prices Active
warehouse stock_log — stock import data Active
competitor Excel exports from competitor B2B portals Active

Estimate

Source: snapshot of current component prices from qt_lot_metadata.

Filter:

  • Include only rows with current_price > 0.
  • Exclude hidden rows (is_hidden = 1).

Stores:

  • price_period_days — price calculation window
  • price_coefficient — markup coefficient
  • manual_price — manually set price
  • meta_prices — historical prices

Purpose: primary pricelist for calculations and estimates.

Categories: loaded from lot.lot_category for each component.


Warehouse

Source: stock_log (stock import records).

Rules (CRITICAL):

  1. Only mapped partnumbers — only positions with a record in qt_partnumber_book_items are included. Unmapped partnumbers must not appear in any form.
  2. No pricing settings — do not load from lot_metadata: no price_period_days, price_coefficient, manual_price, meta_prices.
  3. Price method: always weighted_avg (quantity-weighted average).
  4. Field price_method always contains "weighted_avg".
  5. These are raw stock prices without additional processing.

Categories: loaded from lot.lot_category.

Implementation: internal/warehouse/snapshot.go


Competitor

Source: Excel exports from competitor B2B portals, uploaded manually via /admin/competitors.

Rules (CRITICAL):

  1. Only mapped partnumbers — only positions with a record in qt_partnumber_book_items are included. Unmapped p/ns are counted and written to qt_vendor_partnumber_seen (source_type = "competitor:<code>").
  2. No pricing settings — do not load from lot_metadata: no price_period_days, price_coefficient, manual_price, meta_prices.
  3. Price method: always weighted_median (quantity-weighted median across all matched p/ns for a lot).
  4. Field price_method always contains "weighted_median", price_period_days always 0.
  5. Discount: effective price = weighted_median × (1 expected_discount_pct / 100).
  6. Deduplication: if price and qty for a p/n are unchanged since last import — skip insert into partnumber_log_competitors.

Key tables:

  • qt_competitors — competitor profiles (name, code, discount %, column mapping JSON, currency)
  • partnumber_log_competitors — historical quote log (competitor_id, partnumber, price USD, price local, qty, date)

Implementation: internal/services/competitor_import.go, internal/repository/competitor.go


Data Model

type Pricelist struct {
    ID      uint   `gorm:"primaryKey"`
    Version string
    Source  string // "estimate" | "warehouse" | "competitor"
    // ...
}

type PricelistItem struct {
    ID          uint    `gorm:"primaryKey"`
    PricelistID uint
    LotName     string  `gorm:"size:255"`
    Price       float64 `gorm:"type:decimal(12,2)"`
    LotCategory *string `gorm:"column:lot_category;size:50" json:"category,omitempty"`

    // Virtual fields (via JOIN or programmatically)
    LotDescription string   `gorm:"-:migration" json:"lot_description,omitempty"`
    AvailableQty   *float64 `gorm:"-" json:"available_qty,omitempty"`
    Partnumbers    []string `gorm:"-" json:"partnumbers,omitempty"`
}

gorm:"-:migration" — no DB column created, but mapped on SELECT. gorm:"-" — fully ignored in all DB operations.


Categories (lot_category)

  • Source: lot.lot_category column in table lot.
  • NOT from qt_lot_metadata.
  • NOT derived from LOT name.
  • Persisted into qt_pricelist_items.lot_category when pricelist is created.
  • JSON field name: "category".
  • JOIN with lot is only needed for lot_description; category is already in qt_pricelist_items.
  • Default value when category is missing: PART_.

Pricelist Creation (background task)

Creation runs via Task Manager. Handler returns task_id; frontend polls.

POST /api/pricelists/create
→ { "task_id": "uuid" }
→ polling GET /api/tasks/:id
→ { "status": "completed", "result": { "pricelist_id": 42 } }

Task type: TaskTypePricelistCreate.

Implementation:

  • Service: internal/services/pricelist/service.go
  • Warehouse calc: internal/warehouse/snapshot.go
  • Handler: internal/handlers/pricelist.go