package repository import ( "fmt" "time" "git.mchus.pro/mchus/priceforge/internal/models" "gorm.io/gorm" "gorm.io/gorm/clause" ) // PartsLogRepository provides data access for the unified quote journal. type PartsLogRepository struct { db *gorm.DB } func NewPartsLogRepository(db *gorm.DB) *PartsLogRepository { return &PartsLogRepository{db: db} } // InsertBatch inserts rows into parts_log using INSERT IGNORE (dedup by supplier_code+partnumber+quote_date). func (r *PartsLogRepository) InsertBatch(rows []models.PartsLog) (int, error) { if len(rows) == 0 { return 0, nil } result := r.db.Clauses(clause.OnConflict{DoNothing: true}).CreateInBatches(rows, 500) if result.Error != nil { return 0, fmt.Errorf("insert parts_log batch: %w", result.Error) } return int(result.RowsAffected), nil } // CountBySupplier returns the number of rows per supplier_code. type SupplierCount struct { SupplierCode string `gorm:"column:supplier_code"` Count int64 `gorm:"column:cnt"` } func (r *PartsLogRepository) CountBySupplier() ([]SupplierCount, error) { var result []SupplierCount err := r.db.Model(&models.PartsLog{}). Select("supplier_code, COUNT(*) AS cnt"). Group("supplier_code"). Scan(&result).Error if err != nil { return nil, fmt.Errorf("count parts_log by supplier: %w", err) } return result, nil } // FindUnresolved returns rows where lot_name is NULL and partnumber is not a known lot_name. // limit caps the batch size for incremental backfill. func (r *PartsLogRepository) FindUnresolved(limit int) ([]models.PartsLog, error) { var rows []models.PartsLog err := r.db.Where("lot_name IS NULL"). Limit(limit). Find(&rows).Error if err != nil { return nil, fmt.Errorf("find unresolved parts_log: %w", err) } return rows, nil } // UpdateLotResolution sets lot_name and lot_category for a given row ID. func (r *PartsLogRepository) UpdateLotResolution(id uint64, lotName, lotCategory string) error { return r.db.Model(&models.PartsLog{}). Where("id = ?", id). Updates(map[string]any{ "lot_name": lotName, "lot_category": lotCategory, }).Error } // UpdateLotResolutionBatch sets lot_name and lot_category for multiple rows in one query per lot. type LotResolutionUpdate struct { ID uint64 LotName string LotCategory string } func (r *PartsLogRepository) UpdateLotResolutionBatch(updates []LotResolutionUpdate) error { if len(updates) == 0 { return nil } // Execute individual updates — GORM batch update by IDs with different values // requires per-row updates; use a transaction for atomicity. return r.db.Transaction(func(tx *gorm.DB) error { for _, u := range updates { if err := tx.Model(&models.PartsLog{}). Where("id = ?", u.ID). Updates(map[string]any{ "lot_name": u.LotName, "lot_category": u.LotCategory, }).Error; err != nil { return err } } return nil }) } // QuotesByLot returns price/qty rows for a lot within the given date window and supplier types. // Used by the pricelist generator. type QuoteRow struct { LotName string `gorm:"column:lot_name"` Price float64 `gorm:"column:price"` Qty *float64 `gorm:"column:qty"` LeadTimeWeeks *int `gorm:"column:lead_time_weeks"` SupplierType string `gorm:"column:supplier_type"` } func (r *PartsLogRepository) QuotesByLot( lotNames []string, supplierTypes []string, since time.Time, ) ([]QuoteRow, error) { if len(lotNames) == 0 || len(supplierTypes) == 0 { return nil, nil } var rows []QuoteRow err := r.db.Table("parts_log pl"). Select("pl.lot_name, pl.price, pl.qty, pl.lead_time_weeks, s.supplier_type"). Joins("INNER JOIN supplier s ON s.supplier_code = pl.supplier_code"). Where("pl.lot_name IN ?", lotNames). Where("s.supplier_type IN ?", supplierTypes). Where("pl.quote_date >= ?", since). Scan(&rows).Error if err != nil { return nil, fmt.Errorf("quotes by lot: %w", err) } return rows, nil } // LastKnownPricesByLot returns the most recent price per lot regardless of date. // Used for on_missing_quotes='keep' fallback. func (r *PartsLogRepository) LastKnownPricesByLot( lotNames []string, supplierTypes []string, ) (map[string]float64, error) { if len(lotNames) == 0 { return map[string]float64{}, nil } type row struct { LotName string `gorm:"column:lot_name"` Price float64 `gorm:"column:price"` } var rows []row query := r.db.Table("parts_log pl"). Select("pl.lot_name, pl.price"). Joins("INNER JOIN supplier s ON s.supplier_code = pl.supplier_code"). Where("pl.lot_name IN ?", lotNames). Where("pl.id IN (SELECT MAX(id) FROM parts_log WHERE lot_name IN ? GROUP BY lot_name)", lotNames) if len(supplierTypes) > 0 { query = query.Where("s.supplier_type IN ?", supplierTypes) } if err := query.Scan(&rows).Error; err != nil { return nil, fmt.Errorf("last known prices by lot: %w", err) } result := make(map[string]float64, len(rows)) for _, r := range rows { result[r.LotName] = r.Price } return result, nil }