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>
This commit is contained in:
132
internal/repository/competitor.go
Normal file
132
internal/repository/competitor.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/priceforge/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CompetitorRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCompetitorRepository(db *gorm.DB) *CompetitorRepository {
|
||||
return &CompetitorRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *CompetitorRepository) List() ([]models.Competitor, error) {
|
||||
var competitors []models.Competitor
|
||||
if err := r.db.Order("name ASC").Find(&competitors).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return competitors, nil
|
||||
}
|
||||
|
||||
func (r *CompetitorRepository) GetByID(id uint64) (*models.Competitor, error) {
|
||||
var c models.Competitor
|
||||
if err := r.db.First(&c, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (r *CompetitorRepository) Create(c *models.Competitor) error {
|
||||
return r.db.Create(c).Error
|
||||
}
|
||||
|
||||
func (r *CompetitorRepository) Update(c *models.Competitor) error {
|
||||
return r.db.Save(c).Error
|
||||
}
|
||||
|
||||
func (r *CompetitorRepository) Delete(id uint64) error {
|
||||
return r.db.Delete(&models.Competitor{}, id).Error
|
||||
}
|
||||
|
||||
func (r *CompetitorRepository) SetActive(id uint64, active bool) error {
|
||||
return r.db.Model(&models.Competitor{}).Where("id = ?", id).Update("is_active", active).Error
|
||||
}
|
||||
|
||||
// GetLastQuoteForPN returns the most recent quote for (competitor_id, partnumber).
|
||||
// Used for deduplication at import time: skip if price and qty unchanged.
|
||||
func (r *CompetitorRepository) GetLastQuoteForPN(competitorID uint64, partnumber string) (price float64, qty float64, found bool, err error) {
|
||||
var quote models.CompetitorQuote
|
||||
dbErr := r.db.
|
||||
Where("competitor_id = ? AND partnumber = ?", competitorID, partnumber).
|
||||
Order("date DESC, id DESC").
|
||||
First("e).Error
|
||||
if errors.Is(dbErr, gorm.ErrRecordNotFound) {
|
||||
return 0, 0, false, nil
|
||||
}
|
||||
if dbErr != nil {
|
||||
return 0, 0, false, dbErr
|
||||
}
|
||||
return quote.Price, quote.Qty, true, nil
|
||||
}
|
||||
|
||||
// InsertQuote inserts a new competitor quote row.
|
||||
func (r *CompetitorRepository) InsertQuote(q *models.CompetitorQuote) error {
|
||||
return r.db.Create(q).Error
|
||||
}
|
||||
|
||||
// BulkInsertQuotes inserts multiple quotes in batches of 500.
|
||||
func (r *CompetitorRepository) BulkInsertQuotes(quotes []*models.CompetitorQuote) error {
|
||||
if len(quotes) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.CreateInBatches(quotes, 500).Error
|
||||
}
|
||||
|
||||
// GetLatestQuotesByPN returns the most recent (price, qty) per partnumber for a competitor.
|
||||
// Used when building the pricelist; p/n → lot resolution happens in the service layer.
|
||||
func (r *CompetitorRepository) GetLatestQuotesByPN(competitorID uint64) ([]models.CompetitorQuote, error) {
|
||||
var quotes []models.CompetitorQuote
|
||||
err := r.db.Raw(`
|
||||
SELECT plc.*
|
||||
FROM partnumber_log_competitors plc
|
||||
INNER JOIN (
|
||||
SELECT partnumber, MAX(date) AS max_date
|
||||
FROM partnumber_log_competitors
|
||||
WHERE competitor_id = ?
|
||||
GROUP BY partnumber
|
||||
) latest ON plc.partnumber = latest.partnumber
|
||||
AND plc.date = latest.max_date
|
||||
WHERE plc.competitor_id = ?
|
||||
ORDER BY plc.partnumber ASC
|
||||
`, competitorID, competitorID).Scan("es).Error
|
||||
return quotes, err
|
||||
}
|
||||
|
||||
// CompetitorQuoteCounts holds aggregate quote statistics for one competitor.
|
||||
type CompetitorQuoteCounts struct {
|
||||
CompetitorID uint64 `gorm:"column:competitor_id"`
|
||||
UniquePN int64 `gorm:"column:unique_pn"`
|
||||
TotalQuotes int64 `gorm:"column:total_quotes"`
|
||||
}
|
||||
|
||||
// GetQuoteCountsByCompetitor returns unique p/n count and total quote count per competitor.
|
||||
func (r *CompetitorRepository) GetQuoteCountsByCompetitor() ([]CompetitorQuoteCounts, error) {
|
||||
var results []CompetitorQuoteCounts
|
||||
err := r.db.Raw(`
|
||||
SELECT competitor_id,
|
||||
COUNT(DISTINCT partnumber) AS unique_pn,
|
||||
COUNT(*) AS total_quotes
|
||||
FROM partnumber_log_competitors
|
||||
GROUP BY competitor_id
|
||||
`).Scan(&results).Error
|
||||
return results, err
|
||||
}
|
||||
|
||||
// ListQuotes returns all quotes for a competitor since a given date.
|
||||
func (r *CompetitorRepository) ListQuotes(competitorID uint64, since time.Time) ([]models.CompetitorQuote, error) {
|
||||
var quotes []models.CompetitorQuote
|
||||
q := r.db.Where("competitor_id = ?", competitorID)
|
||||
if !since.IsZero() {
|
||||
q = q.Where("date >= ?", since)
|
||||
}
|
||||
if err := q.Order("date DESC, partnumber ASC").Find("es).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return quotes, nil
|
||||
}
|
||||
Reference in New Issue
Block a user