Apply remaining pricelist and local-first updates
This commit is contained in:
@@ -24,6 +24,7 @@ type ConfigurationService struct {
|
||||
configRepo *repository.ConfigurationRepository
|
||||
projectRepo *repository.ProjectRepository
|
||||
componentRepo *repository.ComponentRepository
|
||||
pricelistRepo *repository.PricelistRepository
|
||||
quoteService *QuoteService
|
||||
}
|
||||
|
||||
@@ -31,12 +32,14 @@ func NewConfigurationService(
|
||||
configRepo *repository.ConfigurationRepository,
|
||||
projectRepo *repository.ProjectRepository,
|
||||
componentRepo *repository.ComponentRepository,
|
||||
pricelistRepo *repository.PricelistRepository,
|
||||
quoteService *QuoteService,
|
||||
) *ConfigurationService {
|
||||
return &ConfigurationService{
|
||||
configRepo: configRepo,
|
||||
projectRepo: projectRepo,
|
||||
componentRepo: componentRepo,
|
||||
pricelistRepo: pricelistRepo,
|
||||
quoteService: quoteService,
|
||||
}
|
||||
}
|
||||
@@ -49,6 +52,7 @@ type CreateConfigRequest struct {
|
||||
Notes string `json:"notes"`
|
||||
IsTemplate bool `json:"is_template"`
|
||||
ServerCount int `json:"server_count"`
|
||||
PricelistID *uint `json:"pricelist_id,omitempty"`
|
||||
}
|
||||
|
||||
func (s *ConfigurationService) Create(ownerUsername string, req *CreateConfigRequest) (*models.Configuration, error) {
|
||||
@@ -56,6 +60,10 @@ func (s *ConfigurationService) Create(ownerUsername string, req *CreateConfigReq
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pricelistID, err := s.resolvePricelistID(req.PricelistID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
total := req.Items.Total()
|
||||
|
||||
@@ -75,6 +83,7 @@ func (s *ConfigurationService) Create(ownerUsername string, req *CreateConfigReq
|
||||
Notes: req.Notes,
|
||||
IsTemplate: req.IsTemplate,
|
||||
ServerCount: req.ServerCount,
|
||||
PricelistID: pricelistID,
|
||||
}
|
||||
|
||||
if err := s.configRepo.Create(config); err != nil {
|
||||
@@ -115,6 +124,10 @@ func (s *ConfigurationService) Update(uuid string, ownerUsername string, req *Cr
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pricelistID, err := s.resolvePricelistID(req.PricelistID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
total := req.Items.Total()
|
||||
|
||||
@@ -131,6 +144,7 @@ func (s *ConfigurationService) Update(uuid string, ownerUsername string, req *Cr
|
||||
config.Notes = req.Notes
|
||||
config.IsTemplate = req.IsTemplate
|
||||
config.ServerCount = req.ServerCount
|
||||
config.PricelistID = pricelistID
|
||||
|
||||
if err := s.configRepo.Update(config); err != nil {
|
||||
return nil, err
|
||||
@@ -207,6 +221,7 @@ func (s *ConfigurationService) CloneToProject(configUUID string, ownerUsername s
|
||||
Notes: original.Notes,
|
||||
IsTemplate: false, // Clone is never a template
|
||||
ServerCount: original.ServerCount,
|
||||
PricelistID: original.PricelistID,
|
||||
}
|
||||
|
||||
if err := s.configRepo.Create(clone); err != nil {
|
||||
@@ -261,6 +276,10 @@ func (s *ConfigurationService) UpdateNoAuth(uuid string, req *CreateConfigReques
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pricelistID, err := s.resolvePricelistID(req.PricelistID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
total := req.Items.Total()
|
||||
if req.ServerCount > 1 {
|
||||
@@ -275,6 +294,7 @@ func (s *ConfigurationService) UpdateNoAuth(uuid string, req *CreateConfigReques
|
||||
config.Notes = req.Notes
|
||||
config.IsTemplate = req.IsTemplate
|
||||
config.ServerCount = req.ServerCount
|
||||
config.PricelistID = pricelistID
|
||||
|
||||
if err := s.configRepo.Update(config); err != nil {
|
||||
return nil, err
|
||||
@@ -341,6 +361,7 @@ func (s *ConfigurationService) CloneNoAuthToProject(configUUID string, newName s
|
||||
Notes: original.Notes,
|
||||
IsTemplate: false,
|
||||
ServerCount: original.ServerCount,
|
||||
PricelistID: original.PricelistID,
|
||||
}
|
||||
|
||||
if err := s.configRepo.Create(clone); err != nil {
|
||||
@@ -370,6 +391,23 @@ func (s *ConfigurationService) resolveProjectUUID(ownerUsername string, projectU
|
||||
return &project.UUID, nil
|
||||
}
|
||||
|
||||
func (s *ConfigurationService) resolvePricelistID(pricelistID *uint) (*uint, error) {
|
||||
if s.pricelistRepo == nil {
|
||||
return pricelistID, nil
|
||||
}
|
||||
if pricelistID != nil && *pricelistID > 0 {
|
||||
if _, err := s.pricelistRepo.GetByID(*pricelistID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pricelistID, nil
|
||||
}
|
||||
latest, err := s.pricelistRepo.GetLatestActive()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &latest.ID, nil
|
||||
}
|
||||
|
||||
// RefreshPricesNoAuth refreshes prices without ownership check
|
||||
func (s *ConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Configuration, error) {
|
||||
config, err := s.configRepo.GetByUUID(uuid)
|
||||
@@ -377,8 +415,30 @@ func (s *ConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Configu
|
||||
return nil, ErrConfigNotFound
|
||||
}
|
||||
|
||||
var latestPricelistID *uint
|
||||
if s.pricelistRepo != nil {
|
||||
if pl, err := s.pricelistRepo.GetLatestActive(); err == nil {
|
||||
latestPricelistID = &pl.ID
|
||||
}
|
||||
}
|
||||
|
||||
updatedItems := make(models.ConfigItems, len(config.Items))
|
||||
for i, item := range config.Items {
|
||||
if latestPricelistID != nil {
|
||||
if price, err := s.pricelistRepo.GetPriceForLot(*latestPricelistID, item.LotName); err == nil && price > 0 {
|
||||
updatedItems[i] = models.ConfigItem{
|
||||
LotName: item.LotName,
|
||||
Quantity: item.Quantity,
|
||||
UnitPrice: price,
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if s.componentRepo == nil {
|
||||
updatedItems[i] = item
|
||||
continue
|
||||
}
|
||||
metadata, err := s.componentRepo.GetByLotName(item.LotName)
|
||||
if err != nil || metadata.CurrentPrice == nil {
|
||||
updatedItems[i] = item
|
||||
@@ -399,6 +459,9 @@ func (s *ConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Configu
|
||||
}
|
||||
|
||||
config.TotalPrice = &total
|
||||
if latestPricelistID != nil {
|
||||
config.PricelistID = latestPricelistID
|
||||
}
|
||||
now := time.Now()
|
||||
config.PriceUpdatedAt = &now
|
||||
|
||||
@@ -432,10 +495,32 @@ func (s *ConfigurationService) RefreshPrices(uuid string, ownerUsername string)
|
||||
return nil, ErrConfigForbidden
|
||||
}
|
||||
|
||||
var latestPricelistID *uint
|
||||
if s.pricelistRepo != nil {
|
||||
if pl, err := s.pricelistRepo.GetLatestActive(); err == nil {
|
||||
latestPricelistID = &pl.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Update prices for all items
|
||||
updatedItems := make(models.ConfigItems, len(config.Items))
|
||||
for i, item := range config.Items {
|
||||
if latestPricelistID != nil {
|
||||
if price, err := s.pricelistRepo.GetPriceForLot(*latestPricelistID, item.LotName); err == nil && price > 0 {
|
||||
updatedItems[i] = models.ConfigItem{
|
||||
LotName: item.LotName,
|
||||
Quantity: item.Quantity,
|
||||
UnitPrice: price,
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Get current component price
|
||||
if s.componentRepo == nil {
|
||||
updatedItems[i] = item
|
||||
continue
|
||||
}
|
||||
metadata, err := s.componentRepo.GetByLotName(item.LotName)
|
||||
if err != nil || metadata.CurrentPrice == nil {
|
||||
// Keep original item if component not found or no price available
|
||||
@@ -461,6 +546,9 @@ func (s *ConfigurationService) RefreshPrices(uuid string, ownerUsername string)
|
||||
}
|
||||
|
||||
config.TotalPrice = &total
|
||||
if latestPricelistID != nil {
|
||||
config.PricelistID = latestPricelistID
|
||||
}
|
||||
|
||||
// Set price update timestamp
|
||||
now := time.Now()
|
||||
|
||||
@@ -59,6 +59,10 @@ func (s *LocalConfigurationService) Create(ownerUsername string, req *CreateConf
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pricelistID, err := s.resolvePricelistID(req.PricelistID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
total := req.Items.Total()
|
||||
if req.ServerCount > 1 {
|
||||
@@ -76,6 +80,7 @@ func (s *LocalConfigurationService) Create(ownerUsername string, req *CreateConf
|
||||
Notes: req.Notes,
|
||||
IsTemplate: req.IsTemplate,
|
||||
ServerCount: req.ServerCount,
|
||||
PricelistID: pricelistID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
@@ -128,6 +133,10 @@ func (s *LocalConfigurationService) Update(uuid string, ownerUsername string, re
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pricelistID, err := s.resolvePricelistID(req.PricelistID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
total := req.Items.Total()
|
||||
if req.ServerCount > 1 {
|
||||
@@ -150,6 +159,7 @@ func (s *LocalConfigurationService) Update(uuid string, ownerUsername string, re
|
||||
localCfg.Notes = req.Notes
|
||||
localCfg.IsTemplate = req.IsTemplate
|
||||
localCfg.ServerCount = req.ServerCount
|
||||
localCfg.PricelistID = pricelistID
|
||||
localCfg.UpdatedAt = time.Now()
|
||||
localCfg.SyncStatus = "pending"
|
||||
|
||||
@@ -254,6 +264,7 @@ func (s *LocalConfigurationService) CloneToProject(configUUID string, ownerUsern
|
||||
Notes: original.Notes,
|
||||
IsTemplate: false,
|
||||
ServerCount: original.ServerCount,
|
||||
PricelistID: original.PricelistID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
@@ -324,10 +335,28 @@ func (s *LocalConfigurationService) RefreshPrices(uuid string, ownerUsername str
|
||||
return nil, ErrConfigForbidden
|
||||
}
|
||||
|
||||
// Refresh local pricelists when online and use latest active/local pricelist for recalculation.
|
||||
if s.isOnline() {
|
||||
_ = s.syncService.SyncPricelistsIfNeeded()
|
||||
}
|
||||
latestPricelist, latestErr := s.localDB.GetLatestLocalPricelist()
|
||||
|
||||
// Update prices for all items
|
||||
updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items))
|
||||
for i, item := range localCfg.Items {
|
||||
// Get current component price from local cache
|
||||
if latestErr == nil && latestPricelist != nil {
|
||||
price, err := s.localDB.GetLocalPriceForLot(latestPricelist.ID, item.LotName)
|
||||
if err == nil && price > 0 {
|
||||
updatedItems[i] = localdb.LocalConfigItem{
|
||||
LotName: item.LotName,
|
||||
Quantity: item.Quantity,
|
||||
UnitPrice: price,
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to current component price from local cache
|
||||
component, err := s.localDB.GetLocalComponent(item.LotName)
|
||||
if err != nil || component.CurrentPrice == nil {
|
||||
// Keep original item if component not found or no price available
|
||||
@@ -353,6 +382,9 @@ func (s *LocalConfigurationService) RefreshPrices(uuid string, ownerUsername str
|
||||
}
|
||||
|
||||
localCfg.TotalPrice = &total
|
||||
if latestErr == nil && latestPricelist != nil {
|
||||
localCfg.PricelistID = &latestPricelist.ServerID
|
||||
}
|
||||
|
||||
// Set price update timestamp and mark for sync
|
||||
now := time.Now()
|
||||
@@ -390,6 +422,10 @@ func (s *LocalConfigurationService) UpdateNoAuth(uuid string, req *CreateConfigR
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pricelistID, err := s.resolvePricelistID(req.PricelistID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
total := req.Items.Total()
|
||||
if req.ServerCount > 1 {
|
||||
@@ -411,6 +447,7 @@ func (s *LocalConfigurationService) UpdateNoAuth(uuid string, req *CreateConfigR
|
||||
localCfg.Notes = req.Notes
|
||||
localCfg.IsTemplate = req.IsTemplate
|
||||
localCfg.ServerCount = req.ServerCount
|
||||
localCfg.PricelistID = pricelistID
|
||||
localCfg.UpdatedAt = time.Now()
|
||||
localCfg.SyncStatus = "pending"
|
||||
|
||||
@@ -502,6 +539,7 @@ func (s *LocalConfigurationService) CloneNoAuthToProject(configUUID string, newN
|
||||
Notes: original.Notes,
|
||||
IsTemplate: false,
|
||||
ServerCount: original.ServerCount,
|
||||
PricelistID: original.PricelistID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
@@ -640,10 +678,27 @@ func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Co
|
||||
return nil, ErrConfigNotFound
|
||||
}
|
||||
|
||||
if s.isOnline() {
|
||||
_ = s.syncService.SyncPricelistsIfNeeded()
|
||||
}
|
||||
latestPricelist, latestErr := s.localDB.GetLatestLocalPricelist()
|
||||
|
||||
// Update prices for all items
|
||||
updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items))
|
||||
for i, item := range localCfg.Items {
|
||||
// Get current component price from local cache
|
||||
if latestErr == nil && latestPricelist != nil {
|
||||
price, err := s.localDB.GetLocalPriceForLot(latestPricelist.ID, item.LotName)
|
||||
if err == nil && price > 0 {
|
||||
updatedItems[i] = localdb.LocalConfigItem{
|
||||
LotName: item.LotName,
|
||||
Quantity: item.Quantity,
|
||||
UnitPrice: price,
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to current component price from local cache
|
||||
component, err := s.localDB.GetLocalComponent(item.LotName)
|
||||
if err != nil || component.CurrentPrice == nil {
|
||||
// Keep original item if component not found or no price available
|
||||
@@ -669,6 +724,9 @@ func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Co
|
||||
}
|
||||
|
||||
localCfg.TotalPrice = &total
|
||||
if latestErr == nil && latestPricelist != nil {
|
||||
localCfg.PricelistID = &latestPricelist.ServerID
|
||||
}
|
||||
|
||||
// Set price update timestamp and mark for sync
|
||||
now := time.Now()
|
||||
@@ -815,6 +873,9 @@ func (s *LocalConfigurationService) createWithVersion(localCfg *localdb.LocalCon
|
||||
if err := s.enqueueConfigurationPendingChangeTx(tx, localCfg, "create", version, createdBy); err != nil {
|
||||
return fmt.Errorf("enqueue create pending change: %w", err)
|
||||
}
|
||||
if err := s.recalculateLocalPricelistUsageTx(tx); err != nil {
|
||||
return fmt.Errorf("recalculate local pricelist usage: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -854,6 +915,9 @@ func (s *LocalConfigurationService) saveWithVersionAndPending(localCfg *localdb.
|
||||
if err := s.enqueueConfigurationPendingChangeTx(tx, localCfg, operation, version, createdBy); err != nil {
|
||||
return fmt.Errorf("enqueue %s pending change: %w", operation, err)
|
||||
}
|
||||
if err := s.recalculateLocalPricelistUsageTx(tx); err != nil {
|
||||
return fmt.Errorf("recalculate local pricelist usage: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -958,6 +1022,7 @@ func (s *LocalConfigurationService) rollbackToVersion(configurationUUID string,
|
||||
current.Notes = rollbackData.Notes
|
||||
current.IsTemplate = rollbackData.IsTemplate
|
||||
current.ServerCount = rollbackData.ServerCount
|
||||
current.PricelistID = rollbackData.PricelistID
|
||||
current.PriceUpdatedAt = rollbackData.PriceUpdatedAt
|
||||
current.UpdatedAt = time.Now()
|
||||
current.SyncStatus = "pending"
|
||||
@@ -1015,6 +1080,9 @@ func (s *LocalConfigurationService) rollbackToVersion(configurationUUID string,
|
||||
if err := s.enqueueConfigurationPendingChangeTx(tx, ¤t, "rollback", version, userID); err != nil {
|
||||
return fmt.Errorf("enqueue rollback pending change: %w", err)
|
||||
}
|
||||
if err := s.recalculateLocalPricelistUsageTx(tx); err != nil {
|
||||
return fmt.Errorf("recalculate local pricelist usage: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -1038,6 +1106,7 @@ func (s *LocalConfigurationService) enqueueConfigurationPendingChangeTx(
|
||||
IdempotencyKey: fmt.Sprintf("%s:v%d:%s", localCfg.UUID, version.VersionNo, operation),
|
||||
ConfigurationUUID: localCfg.UUID,
|
||||
ProjectUUID: localCfg.ProjectUUID,
|
||||
PricelistID: localCfg.PricelistID,
|
||||
Operation: operation,
|
||||
CurrentVersionID: version.ID,
|
||||
CurrentVersionNo: version.VersionNo,
|
||||
@@ -1071,6 +1140,21 @@ func (s *LocalConfigurationService) decodeConfigurationSnapshot(data string) (*l
|
||||
return localdb.DecodeConfigurationSnapshot(data)
|
||||
}
|
||||
|
||||
func (s *LocalConfigurationService) recalculateLocalPricelistUsageTx(tx *gorm.DB) error {
|
||||
if err := tx.Model(&localdb.LocalPricelist{}).Where("1 = 1").Update("is_used", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Exec(`
|
||||
UPDATE local_pricelists
|
||||
SET is_used = 1
|
||||
WHERE server_id IN (
|
||||
SELECT DISTINCT pricelist_id
|
||||
FROM local_configurations
|
||||
WHERE pricelist_id IS NOT NULL AND is_active = 1
|
||||
)
|
||||
`).Error
|
||||
}
|
||||
|
||||
func stringPtrOrNil(value string) *string {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
@@ -1116,3 +1200,25 @@ func (s *LocalConfigurationService) resolveProjectUUID(ownerUsername string, pro
|
||||
|
||||
return &project.UUID, nil
|
||||
}
|
||||
|
||||
func (s *LocalConfigurationService) resolvePricelistID(pricelistID *uint) (*uint, error) {
|
||||
if pricelistID != nil && *pricelistID > 0 {
|
||||
if _, err := s.localDB.GetLocalPricelistByServerID(*pricelistID); err == nil {
|
||||
return pricelistID, nil
|
||||
}
|
||||
if s.isOnline() {
|
||||
if _, err := s.syncService.SyncPricelists(); err == nil {
|
||||
if _, err := s.localDB.GetLocalPricelistByServerID(*pricelistID); err == nil {
|
||||
return pricelistID, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("pricelist %d not available locally", *pricelistID)
|
||||
}
|
||||
|
||||
latest, err := s.localDB.GetLatestLocalPricelist()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &latest.ServerID, nil
|
||||
}
|
||||
|
||||
@@ -9,29 +9,79 @@ import (
|
||||
|
||||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/repository"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/services/pricing"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *repository.PricelistRepository
|
||||
componentRepo *repository.ComponentRepository
|
||||
pricingSvc *pricing.Service
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewService(db *gorm.DB, repo *repository.PricelistRepository, componentRepo *repository.ComponentRepository) *Service {
|
||||
type CreateProgress struct {
|
||||
Current int
|
||||
Total int
|
||||
Status string
|
||||
Message string
|
||||
Updated int
|
||||
Errors int
|
||||
LotName string
|
||||
}
|
||||
|
||||
func NewService(db *gorm.DB, repo *repository.PricelistRepository, componentRepo *repository.ComponentRepository, pricingSvc *pricing.Service) *Service {
|
||||
return &Service{
|
||||
repo: repo,
|
||||
componentRepo: componentRepo,
|
||||
pricingSvc: pricingSvc,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateFromCurrentPrices creates a new pricelist by taking a snapshot of current prices
|
||||
func (s *Service) CreateFromCurrentPrices(createdBy string) (*models.Pricelist, error) {
|
||||
return s.CreateFromCurrentPricesWithProgress(createdBy, nil)
|
||||
}
|
||||
|
||||
// CreateFromCurrentPricesWithProgress creates a pricelist and reports coarse-grained progress.
|
||||
func (s *Service) CreateFromCurrentPricesWithProgress(createdBy string, onProgress func(CreateProgress)) (*models.Pricelist, error) {
|
||||
if s.repo == nil || s.db == nil {
|
||||
return nil, fmt.Errorf("offline mode: cannot create pricelists")
|
||||
}
|
||||
|
||||
report := func(p CreateProgress) {
|
||||
if onProgress != nil {
|
||||
onProgress(p)
|
||||
}
|
||||
}
|
||||
report(CreateProgress{Current: 0, Total: 100, Status: "starting", Message: "Подготовка"})
|
||||
|
||||
updated, errs := 0, 0
|
||||
if s.pricingSvc != nil {
|
||||
report(CreateProgress{Current: 1, Total: 100, Status: "recalculating", Message: "Обновление цен компонентов"})
|
||||
updated, errs = s.pricingSvc.RecalculateAllPricesWithProgress(func(p pricing.RecalculateProgress) {
|
||||
if p.Total <= 0 {
|
||||
return
|
||||
}
|
||||
phaseCurrent := 1 + int(float64(p.Current)/float64(p.Total)*90.0)
|
||||
if phaseCurrent > 91 {
|
||||
phaseCurrent = 91
|
||||
}
|
||||
report(CreateProgress{
|
||||
Current: phaseCurrent,
|
||||
Total: 100,
|
||||
Status: "recalculating",
|
||||
Message: "Обновление цен компонентов",
|
||||
Updated: p.Updated,
|
||||
Errors: p.Errors,
|
||||
LotName: p.LotName,
|
||||
})
|
||||
})
|
||||
}
|
||||
report(CreateProgress{Current: 92, Total: 100, Status: "recalculated", Message: "Цены обновлены", Updated: updated, Errors: errs})
|
||||
|
||||
report(CreateProgress{Current: 95, Total: 100, Status: "snapshot", Message: "Создание снимка прайслиста"})
|
||||
expiresAt := time.Now().AddDate(1, 0, 0) // +1 year
|
||||
const maxCreateAttempts = 5
|
||||
var pricelist *models.Pricelist
|
||||
@@ -101,6 +151,7 @@ func (s *Service) CreateFromCurrentPrices(createdBy string) (*models.Pricelist,
|
||||
"items", len(items),
|
||||
"created_by", createdBy,
|
||||
)
|
||||
report(CreateProgress{Current: 100, Total: 100, Status: "completed", Message: "Прайслист создан", Updated: updated, Errors: errs})
|
||||
|
||||
return pricelist, nil
|
||||
}
|
||||
@@ -130,6 +181,21 @@ func (s *Service) List(page, perPage int) ([]models.PricelistSummary, int64, err
|
||||
return s.repo.List(offset, perPage)
|
||||
}
|
||||
|
||||
// ListActive returns active pricelists with pagination.
|
||||
func (s *Service) ListActive(page, perPage int) ([]models.PricelistSummary, int64, error) {
|
||||
if s.repo == nil {
|
||||
return []models.PricelistSummary{}, 0, nil
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if perPage < 1 {
|
||||
perPage = 20
|
||||
}
|
||||
offset := (page - 1) * perPage
|
||||
return s.repo.ListActive(offset, perPage)
|
||||
}
|
||||
|
||||
// GetByID returns a pricelist by ID
|
||||
func (s *Service) GetByID(id uint) (*models.Pricelist, error) {
|
||||
if s.repo == nil {
|
||||
@@ -161,6 +227,22 @@ func (s *Service) Delete(id uint) error {
|
||||
return s.repo.Delete(id)
|
||||
}
|
||||
|
||||
// SetActive toggles active state for a pricelist.
|
||||
func (s *Service) SetActive(id uint, isActive bool) error {
|
||||
if s.repo == nil {
|
||||
return fmt.Errorf("offline mode: cannot update pricelists")
|
||||
}
|
||||
return s.repo.SetActive(id, isActive)
|
||||
}
|
||||
|
||||
// GetPriceForLot returns price by pricelist/lot.
|
||||
func (s *Service) GetPriceForLot(pricelistID uint, lotName string) (float64, error) {
|
||||
if s.repo == nil {
|
||||
return 0, fmt.Errorf("offline mode: pricelist service not available")
|
||||
}
|
||||
return s.repo.GetPriceForLot(pricelistID, lotName)
|
||||
}
|
||||
|
||||
// CanWrite returns true if the user can create pricelists
|
||||
func (s *Service) CanWrite() bool {
|
||||
if s.repo == nil {
|
||||
|
||||
@@ -63,6 +63,7 @@ type ConfigurationChangePayload struct {
|
||||
IdempotencyKey string `json:"idempotency_key"`
|
||||
ConfigurationUUID string `json:"configuration_uuid"`
|
||||
ProjectUUID *string `json:"project_uuid,omitempty"`
|
||||
PricelistID *uint `json:"pricelist_id,omitempty"`
|
||||
Operation string `json:"operation"` // create/update/rollback/deactivate/reactivate/delete
|
||||
CurrentVersionID string `json:"current_version_id,omitempty"`
|
||||
CurrentVersionNo int `json:"current_version_no,omitempty"`
|
||||
@@ -610,6 +611,9 @@ func (s *Service) pushConfigurationCreate(change *localdb.PendingChange) error {
|
||||
if err := s.ensureConfigurationProject(mariaDB, &cfg); err != nil {
|
||||
return fmt.Errorf("resolve configuration project: %w", err)
|
||||
}
|
||||
if err := s.ensureConfigurationPricelist(mariaDB, &cfg); err != nil {
|
||||
return fmt.Errorf("resolve configuration pricelist: %w", err)
|
||||
}
|
||||
|
||||
// Create on server
|
||||
if err := configRepo.Create(&cfg); err != nil {
|
||||
@@ -668,6 +672,9 @@ func (s *Service) pushConfigurationUpdate(change *localdb.PendingChange) error {
|
||||
if err := s.ensureConfigurationProject(mariaDB, &cfg); err != nil {
|
||||
return fmt.Errorf("resolve configuration project: %w", err)
|
||||
}
|
||||
if err := s.ensureConfigurationPricelist(mariaDB, &cfg); err != nil {
|
||||
return fmt.Errorf("resolve configuration pricelist: %w", err)
|
||||
}
|
||||
|
||||
// Ensure we have a server ID before updating
|
||||
// If the payload doesn't have ID, get it from local configuration
|
||||
@@ -801,6 +808,29 @@ func (s *Service) ensureConfigurationProject(mariaDB *gorm.DB, cfg *models.Confi
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ensureConfigurationPricelist(mariaDB *gorm.DB, cfg *models.Configuration) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("configuration is nil")
|
||||
}
|
||||
|
||||
pricelistRepo := repository.NewPricelistRepository(mariaDB)
|
||||
|
||||
if cfg.PricelistID != nil && *cfg.PricelistID > 0 {
|
||||
if _, err := pricelistRepo.GetByID(*cfg.PricelistID); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
latest, err := pricelistRepo.GetLatestActive()
|
||||
if err != nil {
|
||||
cfg.PricelistID = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg.PricelistID = &latest.ID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) pushConfigurationRollback(change *localdb.PendingChange) error {
|
||||
// Last-write-wins for now: rollback is pushed as an update with rollback metadata.
|
||||
return s.pushConfigurationUpdate(change)
|
||||
@@ -848,6 +878,7 @@ func (s *Service) resolveConfigurationPayloadForPush(change *localdb.PendingChan
|
||||
if currentVersionNo > 0 {
|
||||
payload.CurrentVersionNo = currentVersionNo
|
||||
}
|
||||
payload.PricelistID = currentCfg.PricelistID
|
||||
}
|
||||
|
||||
isStale := false
|
||||
@@ -885,6 +916,7 @@ func decodeConfigurationChangePayload(change *localdb.PendingChange) (Configurat
|
||||
IdempotencyKey: fmt.Sprintf("%s:%s:legacy", cfg.UUID, change.Operation),
|
||||
ConfigurationUUID: cfg.UUID,
|
||||
ProjectUUID: cfg.ProjectUUID,
|
||||
PricelistID: cfg.PricelistID,
|
||||
Operation: change.Operation,
|
||||
ConflictPolicy: "last_write_wins",
|
||||
Snapshot: cfg,
|
||||
|
||||
@@ -248,6 +248,7 @@ CREATE TABLE qt_configurations (
|
||||
notes TEXT NULL,
|
||||
is_template INTEGER NOT NULL DEFAULT 0,
|
||||
server_count INTEGER NOT NULL DEFAULT 1,
|
||||
pricelist_id INTEGER NULL,
|
||||
price_updated_at DATETIME NULL,
|
||||
created_at DATETIME
|
||||
);`).Error; err != nil {
|
||||
|
||||
Reference in New Issue
Block a user