Files
PriceForge/internal/repository/component.go
Mikhail Chusavitin 5f8aec456b Unified Quote Journal (parts_log) v3
- New unified append-only quote log table parts_log replaces three
  separate log tables (stock_log, partnumber_log_competitors, lot_log)
- Migrations 042-049: extend supplier, create parts_log/import_formats/
  ignore_rules, rework qt_lot_metadata composite PK, add lead_time_weeks
  to pricelist_items, backfill data, migrate ignore rules
- New services: PartsLogBackfillService, ImportFormatService,
  UnifiedImportService; new world pricelist type (all supplier types)
- qt_lot_metadata PK changed to (lot_name, pricelist_type); all queries
  now filter WHERE pricelist_type='estimate'
- Fix pre-existing bug: qt_component_usage_stats column names
  quotes_last30d/quotes_last7d (no underscore) — added explicit gorm tags
- Bible: full table inventory, baseline schema snapshot, updated pricelist/
  data-rules/api/history/architecture docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 17:25:54 +03:00

182 lines
5.0 KiB
Go

package repository
import (
"time"
"git.mchus.pro/mchus/priceforge/internal/models"
"gorm.io/gorm"
)
type ComponentRepository struct {
db *gorm.DB
}
func NewComponentRepository(db *gorm.DB) *ComponentRepository {
return &ComponentRepository{db: db}
}
type ComponentFilter struct {
Category string
Search string
HasPrice bool
ExcludeHidden bool
SortField string
SortDir string
}
func (r *ComponentRepository) List(filter ComponentFilter, offset, limit int) ([]models.LotMetadata, int64, error) {
var total int64
baseQuery := r.db.Table("qt_lot_metadata AS m").
Joins("LEFT JOIN lot AS l ON l.lot_name = m.lot_name").
Joins("LEFT JOIN qt_categories AS c ON c.id = m.category_id").
Where("m.pricelist_type = 'estimate'")
if filter.Category != "" {
baseQuery = baseQuery.Where("c.code = ?", filter.Category)
}
if filter.Search != "" {
search := "%" + filter.Search + "%"
baseQuery = baseQuery.Where("m.lot_name LIKE ? OR m.model LIKE ?", search, search)
}
if filter.HasPrice {
baseQuery = baseQuery.Where("m.current_price IS NOT NULL AND m.current_price > 0")
}
if filter.ExcludeHidden {
baseQuery = baseQuery.Where("m.is_hidden = ? OR m.is_hidden IS NULL", false)
}
if err := baseQuery.Count(&total).Error; err != nil {
return nil, 0, err
}
// Apply sorting
sortDir := "ASC"
if filter.SortDir == "desc" {
sortDir = "DESC"
}
query := baseQuery.Session(&gorm.Session{})
switch filter.SortField {
case "popularity_score":
query = query.Order("m.popularity_score " + sortDir)
case "current_price":
query = query.Order("CASE WHEN m.current_price IS NULL OR m.current_price = 0 THEN 1 ELSE 0 END").
Order("m.current_price " + sortDir)
case "lot_name":
query = query.Order("m.lot_name " + sortDir)
case "quote_count":
query = query.
Select("m.*, l.lot_description AS lot_description, c.id AS category_join_id, c.code AS category_code, (SELECT COUNT(*) FROM lot_log WHERE lot_log.lot = m.lot_name) as quote_count_sort").
Order("quote_count_sort " + sortDir)
default:
query = query.
Order("CASE WHEN m.current_price IS NULL OR m.current_price = 0 THEN 1 ELSE 0 END").
Order("m.popularity_score DESC")
}
type componentRow struct {
models.LotMetadata
LotDescription string `gorm:"column:lot_description"`
CategoryJoinID *uint `gorm:"column:category_join_id"`
CategoryCode *string `gorm:"column:category_code"`
}
var rows []componentRow
selectQuery := query
if filter.SortField != "quote_count" {
selectQuery = selectQuery.Select("m.*, l.lot_description AS lot_description, c.id AS category_join_id, c.code AS category_code")
}
err := selectQuery.
Offset(offset).
Limit(limit).
Scan(&rows).Error
if err != nil {
return nil, total, err
}
components := make([]models.LotMetadata, len(rows))
for i, row := range rows {
comp := row.LotMetadata
comp.Lot = &models.Lot{
LotName: comp.LotName,
LotDescription: row.LotDescription,
}
if row.CategoryCode != nil || row.CategoryJoinID != nil {
comp.Category = &models.Category{
ID: 0,
Code: "",
}
if row.CategoryJoinID != nil {
comp.Category.ID = *row.CategoryJoinID
}
if row.CategoryCode != nil {
comp.Category.Code = *row.CategoryCode
}
}
components[i] = comp
}
return components, total, err
}
func (r *ComponentRepository) GetByLotName(lotName string) (*models.LotMetadata, error) {
var component models.LotMetadata
err := r.db.
Preload("Lot").
Preload("Category").
Where("lot_name = ? AND pricelist_type = 'estimate'", lotName).
First(&component).Error
if err != nil {
return nil, err
}
return &component, nil
}
func (r *ComponentRepository) GetMultiple(lotNames []string) ([]models.LotMetadata, error) {
var components []models.LotMetadata
err := r.db.
Preload("Lot").
Preload("Category").
Where("lot_name IN ? AND pricelist_type = 'estimate'", lotNames).
Find(&components).Error
return components, err
}
func (r *ComponentRepository) Update(component *models.LotMetadata) error {
return r.db.Save(component).Error
}
func (r *ComponentRepository) DB() *gorm.DB {
return r.db
}
func (r *ComponentRepository) Create(component *models.LotMetadata) error {
return r.db.Create(component).Error
}
func (r *ComponentRepository) IncrementRequestCount(lotName string) error {
now := time.Now()
return r.db.Model(&models.LotMetadata{}).
Where("lot_name = ? AND pricelist_type = 'estimate'", lotName).
Updates(map[string]interface{}{
"request_count": gorm.Expr("request_count + 1"),
"last_request_date": now,
}).Error
}
// GetAllLots returns all lots from the existing lot table
func (r *ComponentRepository) GetAllLots() ([]models.Lot, error) {
var lots []models.Lot
err := r.db.Find(&lots).Error
return lots, err
}
// GetLotsWithoutMetadata returns lots that don't have qt_lot_metadata entries (estimate type)
func (r *ComponentRepository) GetLotsWithoutMetadata() ([]models.Lot, error) {
var lots []models.Lot
err := r.db.
Where("lot_name NOT IN (SELECT lot_name FROM qt_lot_metadata WHERE pricelist_type = 'estimate')").
Find(&lots).Error
return lots, err
}