New Quotator and some major changes to pricing admin

This commit is contained in:
Mikhail Chusavitin
2026-01-26 18:30:45 +03:00
parent a93644131c
commit d7d6e9d62c
24 changed files with 565 additions and 112 deletions

View File

@@ -4,9 +4,9 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/mchus/quoteforge/internal/middleware"
"github.com/mchus/quoteforge/internal/repository"
"github.com/mchus/quoteforge/internal/services"
"git.mchus.pro/mchus/quoteforge/internal/middleware"
"git.mchus.pro/mchus/quoteforge/internal/repository"
"git.mchus.pro/mchus/quoteforge/internal/services"
)
type AuthHandler struct {

View File

@@ -5,8 +5,8 @@ import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/mchus/quoteforge/internal/repository"
"github.com/mchus/quoteforge/internal/services"
"git.mchus.pro/mchus/quoteforge/internal/repository"
"git.mchus.pro/mchus/quoteforge/internal/services"
)
type ComponentHandler struct {
@@ -23,7 +23,6 @@ func (h *ComponentHandler) List(c *gin.Context) {
filter := repository.ComponentFilter{
Category: c.Query("category"),
Vendor: c.Query("vendor"),
Search: c.Query("search"),
HasPrice: c.Query("has_price") == "true",
}
@@ -58,15 +57,3 @@ func (h *ComponentHandler) GetCategories(c *gin.Context) {
c.JSON(http.StatusOK, categories)
}
func (h *ComponentHandler) GetVendors(c *gin.Context) {
category := c.Query("category")
vendors, err := h.componentService.GetVendors(category)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, vendors)
}

View File

@@ -6,8 +6,8 @@ import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/mchus/quoteforge/internal/middleware"
"github.com/mchus/quoteforge/internal/services"
"git.mchus.pro/mchus/quoteforge/internal/middleware"
"git.mchus.pro/mchus/quoteforge/internal/services"
)
type ConfigurationHandler struct {

View File

@@ -3,32 +3,39 @@ package handlers
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/mchus/quoteforge/internal/middleware"
"github.com/mchus/quoteforge/internal/models"
"github.com/mchus/quoteforge/internal/repository"
"github.com/mchus/quoteforge/internal/services/alerts"
"github.com/mchus/quoteforge/internal/services/pricing"
"git.mchus.pro/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/repository"
"git.mchus.pro/mchus/quoteforge/internal/services/alerts"
"git.mchus.pro/mchus/quoteforge/internal/services/pricing"
"gorm.io/gorm"
)
type PricingHandler struct {
db *gorm.DB
pricingService *pricing.Service
alertService *alerts.Service
componentRepo *repository.ComponentRepository
priceRepo *repository.PriceRepository
statsRepo *repository.StatsRepository
}
func NewPricingHandler(
db *gorm.DB,
pricingService *pricing.Service,
alertService *alerts.Service,
componentRepo *repository.ComponentRepository,
priceRepo *repository.PriceRepository,
statsRepo *repository.StatsRepository,
) *PricingHandler {
return &PricingHandler{
db: db,
pricingService: pricingService,
alertService: alertService,
componentRepo: componentRepo,
priceRepo: priceRepo,
statsRepo: statsRepo,
}
}
@@ -39,19 +46,23 @@ func (h *PricingHandler) GetStats(c *gin.Context) {
trendingComponents, _ := h.statsRepo.GetTrendingComponents(10)
c.JSON(http.StatusOK, gin.H{
"new_alerts_count": newAlerts,
"top_components": topComponents,
"trending_components": trendingComponents,
"new_alerts_count": newAlerts,
"top_components": topComponents,
"trending_components": trendingComponents,
})
}
type ComponentWithCount struct {
models.LotMetadata
QuoteCount int64 `json:"quote_count"`
}
func (h *PricingHandler) ListComponents(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))
filter := repository.ComponentFilter{
Category: c.Query("category"),
Vendor: c.Query("vendor"),
Search: c.Query("search"),
}
@@ -69,8 +80,25 @@ func (h *PricingHandler) ListComponents(c *gin.Context) {
return
}
// Get quote counts
lotNames := make([]string, len(components))
for i, comp := range components {
lotNames[i] = comp.LotName
}
counts, _ := h.priceRepo.GetQuoteCounts(lotNames)
// Combine components with counts
result := make([]ComponentWithCount, len(components))
for i, comp := range components {
result[i] = ComponentWithCount{
LotMetadata: comp,
QuoteCount: counts[comp.LotName],
}
}
c.JSON(http.StatusOK, gin.H{
"components": components,
"components": result,
"total": total,
"page": page,
"per_page": perPage,
@@ -102,41 +130,287 @@ type UpdatePriceRequest struct {
LotName string `json:"lot_name" binding:"required"`
Method models.PriceMethod `json:"method"`
PeriodDays int `json:"period_days"`
Coefficient float64 `json:"coefficient"`
ManualPrice *float64 `json:"manual_price"`
Reason string `json:"reason"`
ClearManual bool `json:"clear_manual"`
}
func (h *PricingHandler) UpdatePrice(c *gin.Context) {
userID := middleware.GetUserID(c)
var req UpdatePriceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.ManualPrice != nil && *req.ManualPrice > 0 {
err := h.pricingService.SetManualPrice(req.LotName, *req.ManualPrice, req.Reason, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
updates := map[string]interface{}{}
// Update method if specified
if req.Method != "" {
err := h.pricingService.UpdatePriceMethod(req.LotName, req.Method, req.PeriodDays)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
updates["price_method"] = req.Method
}
c.JSON(http.StatusOK, gin.H{"message": "price updated"})
// Update period days
if req.PeriodDays >= 0 {
updates["price_period_days"] = req.PeriodDays
}
// Update coefficient
updates["price_coefficient"] = req.Coefficient
// Handle manual price
if req.ClearManual {
updates["manual_price"] = nil
} else if req.ManualPrice != nil {
updates["manual_price"] = *req.ManualPrice
// Also update current price immediately when setting manual
updates["current_price"] = *req.ManualPrice
updates["price_updated_at"] = time.Now()
}
err := h.db.Model(&models.LotMetadata{}).
Where("lot_name = ?", req.LotName).
Updates(updates).Error
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Recalculate price if not using manual price
if req.ManualPrice == nil {
h.recalculateSinglePrice(req.LotName)
}
// Get updated component to return new price
var comp models.LotMetadata
h.db.Where("lot_name = ?", req.LotName).First(&comp)
c.JSON(http.StatusOK, gin.H{
"message": "price updated",
"current_price": comp.CurrentPrice,
})
}
func (h *PricingHandler) recalculateSinglePrice(lotName string) {
var comp models.LotMetadata
if err := h.db.Where("lot_name = ?", lotName).First(&comp).Error; err != nil {
return
}
// Skip if manual price is set
if comp.ManualPrice != nil && *comp.ManualPrice > 0 {
return
}
periodDays := comp.PricePeriodDays
usedAllTime := false
var result struct {
Price *float64
}
// First try with configured period
if periodDays > 0 {
query := `SELECT AVG(price) as price FROM lot_log WHERE lot = ? AND date >= DATE_SUB(NOW(), INTERVAL ? DAY)`
h.db.Raw(query, lotName, periodDays).Scan(&result)
// If no prices found in period, fall back to all time
if result.Price == nil || *result.Price <= 0 {
query = `SELECT AVG(price) as price FROM lot_log WHERE lot = ?`
h.db.Raw(query, lotName).Scan(&result)
if result.Price != nil && *result.Price > 0 {
usedAllTime = true
}
}
} else {
query := `SELECT AVG(price) as price FROM lot_log WHERE lot = ?`
h.db.Raw(query, lotName).Scan(&result)
}
if result.Price == nil || *result.Price <= 0 {
return
}
finalPrice := *result.Price
if comp.PriceCoefficient != 0 {
finalPrice = finalPrice * (1 + comp.PriceCoefficient/100)
}
now := time.Now()
updates := map[string]interface{}{
"current_price": finalPrice,
"price_updated_at": now,
}
// If we fell back to all time, update the period setting
if usedAllTime {
updates["price_period_days"] = 0
}
h.db.Model(&models.LotMetadata{}).
Where("lot_name = ?", lotName).
Updates(updates)
}
func (h *PricingHandler) RecalculateAll(c *gin.Context) {
// This would be better as a background job
c.JSON(http.StatusAccepted, gin.H{"message": "recalculation started"})
// Set headers for SSE
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
// Get all components with their settings
var components []models.LotMetadata
h.db.Find(&components)
total := int64(len(components))
// Send initial progress
c.SSEvent("progress", gin.H{"current": 0, "total": total, "status": "starting"})
c.Writer.Flush()
lotNames := make([]string, 0, len(components))
for i := range components {
lotNames = append(lotNames, components[i].LotName)
}
// Batch query: Get average prices with date info for all lots
// This query returns both all-time average and days since last quote
type PriceResult struct {
Lot string `gorm:"column:lot"`
AvgPrice *float64 `gorm:"column:avg_price"`
AvgPrice90 *float64 `gorm:"column:avg_price_90"`
LastQuoteAge *int `gorm:"column:last_quote_age"`
}
var priceResults []PriceResult
h.db.Raw(`
SELECT
lot,
AVG(price) as avg_price,
AVG(CASE WHEN date >= DATE_SUB(NOW(), INTERVAL 90 DAY) THEN price END) as avg_price_90,
DATEDIFF(NOW(), MAX(date)) as last_quote_age
FROM lot_log
WHERE lot IN ?
GROUP BY lot
`, lotNames).Scan(&priceResults)
// Build price maps
priceMap := make(map[string]PriceResult, len(priceResults))
for _, p := range priceResults {
priceMap[p.Lot] = p
}
c.SSEvent("progress", gin.H{"current": 0, "total": total, "status": "processing", "updated": 0, "skipped": 0, "manual": 0, "errors": 0})
c.Writer.Flush()
// Process components and prepare batch updates
var updated, skipped, manual, errors int
now := time.Now()
batchSize := 200
for i := 0; i < len(components); i += batchSize {
end := i + batchSize
if end > len(components) {
end = len(components)
}
batch := components[i:end]
// Start transaction for batch updates
tx := h.db.Begin()
for _, comp := range batch {
// If manual price is set, use it
if comp.ManualPrice != nil && *comp.ManualPrice > 0 {
err := tx.Model(&models.LotMetadata{}).
Where("lot_name = ?", comp.LotName).
Updates(map[string]interface{}{
"current_price": *comp.ManualPrice,
"price_updated_at": now,
}).Error
if err != nil {
errors++
} else {
manual++
}
continue
}
// Get price data for this component
priceData, hasPrices := priceMap[comp.LotName]
if !hasPrices || priceData.AvgPrice == nil || *priceData.AvgPrice <= 0 {
skipped++
continue
}
// Determine which price to use
var finalPrice float64
usedAllTime := false
periodDays := comp.PricePeriodDays
if periodDays <= 0 {
// Already using all time
finalPrice = *priceData.AvgPrice
} else {
// Try period price first (using 90-day as proxy)
if priceData.AvgPrice90 != nil && *priceData.AvgPrice90 > 0 {
finalPrice = *priceData.AvgPrice90
} else {
// Fall back to all time
finalPrice = *priceData.AvgPrice
usedAllTime = true
}
}
// Apply coefficient
if comp.PriceCoefficient != 0 {
finalPrice = finalPrice * (1 + comp.PriceCoefficient/100)
}
updates := map[string]interface{}{
"current_price": finalPrice,
"price_updated_at": now,
}
if usedAllTime {
updates["price_period_days"] = 0
}
err := tx.Model(&models.LotMetadata{}).
Where("lot_name = ?", comp.LotName).
Updates(updates).Error
if err != nil {
errors++
} else {
updated++
}
}
// Commit batch
if err := tx.Commit().Error; err != nil {
errors += len(batch)
}
// Send progress update
c.SSEvent("progress", gin.H{
"current": updated + skipped + manual + errors,
"total": total,
"updated": updated,
"skipped": skipped,
"manual": manual,
"errors": errors,
"status": "processing",
})
c.Writer.Flush()
}
// Send completion
c.SSEvent("progress", gin.H{
"current": updated + skipped + manual + errors,
"total": total,
"updated": updated,
"skipped": skipped,
"manual": manual,
"errors": errors,
"status": "completed",
})
c.Writer.Flush()
}
func (h *PricingHandler) ListAlerts(c *gin.Context) {
@@ -208,3 +482,65 @@ func (h *PricingHandler) IgnoreAlert(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "ignored"})
}
type PreviewPriceRequest struct {
LotName string `json:"lot_name" binding:"required"`
Method string `json:"method"`
PeriodDays int `json:"period_days"`
Coefficient float64 `json:"coefficient"`
}
func (h *PricingHandler) PreviewPrice(c *gin.Context) {
var req PreviewPriceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Get component
var comp models.LotMetadata
if err := h.db.Where("lot_name = ?", req.LotName).First(&comp).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "component not found"})
return
}
// Get median for all time
var medianAllTime struct {
Price *float64
}
h.db.Raw(`SELECT AVG(price) as price FROM lot_log WHERE lot = ?`, req.LotName).Scan(&medianAllTime)
// Get quote count
var quoteCount int64
h.db.Model(&models.LotLog{}).Where("lot = ?", req.LotName).Count(&quoteCount)
// Calculate new price based on parameters
var basePrice *float64
if req.PeriodDays > 0 {
var result struct {
Price *float64
}
h.db.Raw(`SELECT AVG(price) as price FROM lot_log WHERE lot = ? AND date >= DATE_SUB(NOW(), INTERVAL ? DAY)`, req.LotName, req.PeriodDays).Scan(&result)
basePrice = result.Price
} else {
basePrice = medianAllTime.Price
}
var newPrice *float64
if basePrice != nil && *basePrice > 0 {
calculated := *basePrice
if req.Coefficient != 0 {
calculated = calculated * (1 + req.Coefficient/100)
}
newPrice = &calculated
}
c.JSON(http.StatusOK, gin.H{
"lot_name": req.LotName,
"current_price": comp.CurrentPrice,
"median_all_time": medianAllTime.Price,
"new_price": newPrice,
"quote_count": quoteCount,
"manual_price": comp.ManualPrice,
})
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/mchus/quoteforge/internal/services"
"git.mchus.pro/mchus/quoteforge/internal/services"
)
type QuoteHandler struct {

View File

@@ -5,8 +5,8 @@ import (
"strings"
"github.com/gin-gonic/gin"
"github.com/mchus/quoteforge/internal/models"
"github.com/mchus/quoteforge/internal/services"
"git.mchus.pro/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/services"
)
const (

View File

@@ -35,18 +35,19 @@ func (s *Specs) Scan(value interface{}) error {
}
type LotMetadata struct {
LotName string `gorm:"column:lot_name;primaryKey;size:255" json:"lot_name"`
CategoryID *uint `gorm:"column:category_id" json:"category_id"`
Vendor string `gorm:"size:50" json:"vendor"`
Model string `gorm:"size:100" json:"model"`
Specs Specs `gorm:"type:json" json:"specs"`
CurrentPrice *float64 `gorm:"type:decimal(12,2)" json:"current_price"`
PriceMethod PriceMethod `gorm:"type:enum('manual','median','average','weighted_median');default:'median'" json:"price_method"`
PricePeriodDays int `gorm:"default:90" json:"price_period_days"`
PriceUpdatedAt *time.Time `json:"price_updated_at"`
RequestCount int `gorm:"default:0" json:"request_count"`
LastRequestDate *time.Time `gorm:"type:date" json:"last_request_date"`
PopularityScore float64 `gorm:"type:decimal(10,4);default:0" json:"popularity_score"`
LotName string `gorm:"column:lot_name;primaryKey;size:255" json:"lot_name"`
CategoryID *uint `gorm:"column:category_id" json:"category_id"`
Model string `gorm:"size:100" json:"model"`
Specs Specs `gorm:"type:json" json:"specs"`
CurrentPrice *float64 `gorm:"type:decimal(12,2)" json:"current_price"`
PriceMethod PriceMethod `gorm:"type:enum('manual','median','average','weighted_median');default:'median'" json:"price_method"`
PricePeriodDays int `gorm:"default:90" json:"price_period_days"`
PriceCoefficient float64 `gorm:"type:decimal(5,2);default:0" json:"price_coefficient"`
ManualPrice *float64 `gorm:"type:decimal(12,2)" json:"manual_price"`
PriceUpdatedAt *time.Time `json:"price_updated_at"`
RequestCount int `gorm:"default:0" json:"request_count"`
LastRequestDate *time.Time `gorm:"type:date" json:"last_request_date"`
PopularityScore float64 `gorm:"type:decimal(10,4);default:0" json:"popularity_score"`
// Relations
Lot *Lot `gorm:"foreignKey:LotName;references:LotName" json:"lot,omitempty"`

View File

@@ -30,3 +30,22 @@ func SeedCategories(db *gorm.DB) error {
}
return nil
}
// SeedAdminUser creates default admin user if not exists
// Default credentials: admin / admin123
func SeedAdminUser(db *gorm.DB, passwordHash string) error {
var count int64
db.Model(&User{}).Where("username = ?", "admin").Count(&count)
if count > 0 {
return nil
}
admin := &User{
Username: "admin",
Email: "admin@example.com",
PasswordHash: passwordHash,
Role: RoleAdmin,
IsActive: true,
}
return db.Create(admin).Error
}

View File

@@ -1,7 +1,7 @@
package repository
import (
"github.com/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/models"
"gorm.io/gorm"
)

View File

@@ -1,7 +1,7 @@
package repository
import (
"github.com/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/models"
"gorm.io/gorm"
)

View File

@@ -3,7 +3,7 @@ package repository
import (
"time"
"github.com/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/models"
"gorm.io/gorm"
)
@@ -17,7 +17,6 @@ func NewComponentRepository(db *gorm.DB) *ComponentRepository {
type ComponentFilter struct {
Category string
Vendor string
Search string
HasPrice bool
}
@@ -34,9 +33,6 @@ func (r *ComponentRepository) List(filter ComponentFilter, offset, limit int) ([
query = query.Joins("JOIN qt_categories ON qt_lot_metadata.category_id = qt_categories.id").
Where("qt_categories.code = ?", filter.Category)
}
if filter.Vendor != "" {
query = query.Where("vendor = ?", filter.Vendor)
}
if filter.Search != "" {
search := "%" + filter.Search + "%"
query = query.Where("lot_name LIKE ? OR model LIKE ?", search, search)
@@ -89,17 +85,6 @@ func (r *ComponentRepository) Create(component *models.LotMetadata) error {
return r.db.Create(component).Error
}
func (r *ComponentRepository) GetVendors(category string) ([]string, error) {
var vendors []string
query := r.db.Model(&models.LotMetadata{}).Distinct("vendor")
if category != "" {
query = query.Joins("JOIN qt_categories ON qt_lot_metadata.category_id = qt_categories.id").
Where("qt_categories.code = ?", category)
}
err := query.Pluck("vendor", &vendors).Error
return vendors, err
}
func (r *ComponentRepository) IncrementRequestCount(lotName string) error {
now := time.Now()
return r.db.Model(&models.LotMetadata{}).

View File

@@ -1,7 +1,7 @@
package repository
import (
"github.com/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/models"
"gorm.io/gorm"
)

View File

@@ -3,7 +3,7 @@ package repository
import (
"time"
"github.com/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/models"
"gorm.io/gorm"
)
@@ -97,3 +97,28 @@ func (r *PriceRepository) GetQuoteCount(lotName string, periodDays int) (int64,
return count, err
}
// GetQuoteCounts returns quote counts for multiple lot names
func (r *PriceRepository) GetQuoteCounts(lotNames []string) (map[string]int64, error) {
type Result struct {
Lot string
Count int64
}
var results []Result
err := r.db.Model(&models.LotLog{}).
Select("lot, COUNT(*) as count").
Where("lot IN ?", lotNames).
Group("lot").
Scan(&results).Error
if err != nil {
return nil, err
}
counts := make(map[string]int64)
for _, r := range results {
counts[r.Lot] = r.Count
}
return counts, nil
}

View File

@@ -3,7 +3,7 @@ package repository
import (
"time"
"github.com/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/models"
"gorm.io/gorm"
)

View File

@@ -1,7 +1,7 @@
package repository
import (
"github.com/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/models"
"gorm.io/gorm"
)

View File

@@ -4,9 +4,9 @@ import (
"fmt"
"time"
"github.com/mchus/quoteforge/internal/config"
"github.com/mchus/quoteforge/internal/models"
"github.com/mchus/quoteforge/internal/repository"
"git.mchus.pro/mchus/quoteforge/internal/config"
"git.mchus.pro/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/repository"
)
type Service struct {

View File

@@ -5,9 +5,9 @@ import (
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/mchus/quoteforge/internal/config"
"github.com/mchus/quoteforge/internal/models"
"github.com/mchus/quoteforge/internal/repository"
"git.mchus.pro/mchus/quoteforge/internal/config"
"git.mchus.pro/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/repository"
"golang.org/x/crypto/bcrypt"
)

View File

@@ -5,8 +5,8 @@ import (
"errors"
"github.com/google/uuid"
"github.com/mchus/quoteforge/internal/models"
"github.com/mchus/quoteforge/internal/repository"
"git.mchus.pro/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/repository"
)
var (

View File

@@ -5,7 +5,7 @@ import (
"sort"
"time"
"github.com/mchus/quoteforge/internal/repository"
"git.mchus.pro/mchus/quoteforge/internal/repository"
)
// CalculateMedian returns the median of prices

View File

@@ -3,9 +3,9 @@ package pricing
import (
"time"
"github.com/mchus/quoteforge/internal/config"
"github.com/mchus/quoteforge/internal/models"
"github.com/mchus/quoteforge/internal/repository"
"git.mchus.pro/mchus/quoteforge/internal/config"
"git.mchus.pro/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/repository"
)
type Service struct {
@@ -176,3 +176,30 @@ type PriceStats struct {
Percentile25 float64 `json:"percentile_25"`
Percentile75 float64 `json:"percentile_75"`
}
// RecalculateAllPrices recalculates prices for all components
func (s *Service) RecalculateAllPrices() (updated int, errors int) {
// Get all components
filter := repository.ComponentFilter{}
offset := 0
limit := 100
for {
components, _, err := s.componentRepo.List(filter, offset, limit)
if err != nil || len(components) == 0 {
break
}
for _, comp := range components {
if err := s.UpdateComponentPrice(comp.LotName); err != nil {
errors++
} else {
updated++
}
}
offset += limit
}
return updated, errors
}

View File

@@ -3,9 +3,9 @@ package services
import (
"errors"
"github.com/mchus/quoteforge/internal/models"
"github.com/mchus/quoteforge/internal/repository"
"github.com/mchus/quoteforge/internal/services/pricing"
"git.mchus.pro/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/repository"
"git.mchus.pro/mchus/quoteforge/internal/services/pricing"
)
var (