- Drop `expected_discount_pct`, add `price_uplift DECIMAL(8,4) DEFAULT 1.3` to `qt_competitors` (migration 040); formula: effective_price = price / uplift - Extend `LoadLotMetrics` to return per-PN qty map (`pnQtysByLot`) - Add virtual fields `CompetitorNames`, `PriceSpreadPct`, `PartnumberQtys` to `PricelistItem`; populate via `enrichWarehouseItems` / `enrichCompetitorItems` - Competitor quotes filtered to qty > 0 before lot resolution - New "stock layout" on pricelist detail page for warehouse/competitor: Partnumbers column (PN + qty, only qty>0), Поставщик column, no Настройки/Доступно - Spread badge ±N% shown next to price for competitor rows - Bible updated: pricelist.md, history.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
153 lines
4.9 KiB
Go
153 lines
4.9 KiB
Go
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 = ? AND qty > 0
|
|
GROUP BY partnumber
|
|
) latest ON plc.partnumber = latest.partnumber
|
|
AND plc.date = latest.max_date
|
|
WHERE plc.competitor_id = ? AND plc.qty > 0
|
|
ORDER BY plc.partnumber ASC
|
|
`, competitorID, competitorID).Scan("es).Error
|
|
return quotes, err
|
|
}
|
|
|
|
// GetLatestQuotesAllCompetitors returns the most recent quote per (competitor_id, partnumber)
|
|
// across ALL active competitors. Used when building the combined competitor pricelist.
|
|
func (r *CompetitorRepository) GetLatestQuotesAllCompetitors() ([]models.CompetitorQuote, error) {
|
|
var quotes []models.CompetitorQuote
|
|
err := r.db.Raw(`
|
|
SELECT plc.*
|
|
FROM partnumber_log_competitors plc
|
|
INNER JOIN (
|
|
SELECT competitor_id, partnumber, MAX(date) AS max_date
|
|
FROM partnumber_log_competitors
|
|
GROUP BY competitor_id, partnumber
|
|
) latest ON plc.competitor_id = latest.competitor_id
|
|
AND plc.partnumber = latest.partnumber
|
|
AND plc.date = latest.max_date
|
|
INNER JOIN qt_competitors c ON c.id = plc.competitor_id AND c.is_active = 1
|
|
ORDER BY plc.competitor_id, plc.partnumber ASC
|
|
`).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
|
|
}
|