- Add GetLatestQuotesAllCompetitors() repo method: latest quote per (competitor_id, partnumber) across all active competitors - Add RebuildPricelist() service method: loads all quotes, applies each competitor's discount, aggregates with weighted_median per lot, creates single combined competitor pricelist - Add POST /api/competitors/pricelist handler + route - JS: "Создать прайслист" on competitor tab calls new endpoint instead of the generic one that required explicit items This allows recreating the competitor pricelist after new lot mappings are added, without requiring a new file upload. 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 = ?
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|