Apply remaining pricelist and local-first updates

This commit is contained in:
Mikhail Chusavitin
2026-02-06 13:01:40 +03:00
parent 1b48401828
commit e9307c4bad
19 changed files with 767 additions and 30 deletions

View File

@@ -28,6 +28,7 @@ func ConfigurationToLocal(cfg *models.Configuration) *LocalConfiguration {
Notes: cfg.Notes,
IsTemplate: cfg.IsTemplate,
ServerCount: cfg.ServerCount,
PricelistID: cfg.PricelistID,
PriceUpdatedAt: cfg.PriceUpdatedAt,
CreatedAt: cfg.CreatedAt,
UpdatedAt: time.Now(),
@@ -70,6 +71,7 @@ func LocalToConfiguration(local *LocalConfiguration) *models.Configuration {
Notes: local.Notes,
IsTemplate: local.IsTemplate,
ServerCount: local.ServerCount,
PricelistID: local.PricelistID,
PriceUpdatedAt: local.PriceUpdatedAt,
CreatedAt: local.CreatedAt,
}

View File

@@ -530,6 +530,15 @@ func (l *LocalDB) GetLocalPricelistByServerID(serverID uint) (*LocalPricelist, e
return &pricelist, nil
}
// GetLocalPricelistByVersion returns a local pricelist by version string.
func (l *LocalDB) GetLocalPricelistByVersion(version string) (*LocalPricelist, error) {
var pricelist LocalPricelist
if err := l.db.Where("version = ?", version).First(&pricelist).Error; err != nil {
return nil, err
}
return &pricelist, nil
}
// GetLocalPricelistByID returns a local pricelist by its local ID
func (l *LocalDB) GetLocalPricelistByID(id uint) (*LocalPricelist, error) {
var pricelist LocalPricelist
@@ -605,6 +614,25 @@ func (l *LocalDB) MarkPricelistAsUsed(pricelistID uint, isUsed bool) error {
Update("is_used", isUsed).Error
}
// RecalculateAllLocalPricelistUsage refreshes local_pricelists.is_used based on active configurations.
func (l *LocalDB) RecalculateAllLocalPricelistUsage() error {
return l.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&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
})
}
// DeleteLocalPricelist deletes a pricelist and its items
func (l *LocalDB) DeleteLocalPricelist(id uint) error {
// Delete items first

View File

@@ -42,6 +42,11 @@ var localMigrations = []localMigration{
name: "Create default projects and attach existing configurations",
run: backfillProjectsForConfigurations,
},
{
id: "2026_02_06_pricelist_backfill",
name: "Attach existing configurations to latest local pricelist and recalc usage",
run: backfillConfigurationPricelists,
},
}
func runLocalMigrations(db *gorm.DB) error {
@@ -192,6 +197,40 @@ func ensureDefaultProjectTx(tx *gorm.DB, ownerUsername string) (*LocalProject, e
return &project, nil
}
func backfillConfigurationPricelists(tx *gorm.DB) error {
var latest LocalPricelist
if err := tx.Order("created_at DESC").First(&latest).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return fmt.Errorf("load latest local pricelist: %w", err)
}
if err := tx.Model(&LocalConfiguration{}).
Where("pricelist_id IS NULL").
Update("pricelist_id", latest.ServerID).Error; err != nil {
return fmt.Errorf("backfill configuration pricelist_id: %w", err)
}
if err := tx.Model(&LocalPricelist{}).Where("1 = 1").Update("is_used", false).Error; err != nil {
return fmt.Errorf("reset local pricelist usage flags: %w", err)
}
if err := 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; err != nil {
return fmt.Errorf("recalculate local pricelist usage flags: %w", err)
}
return nil
}
func chooseNonZeroTime(candidate time.Time, fallback time.Time) time.Time {
if candidate.IsZero() {
return fallback

View File

@@ -72,6 +72,7 @@ type LocalConfiguration struct {
Notes string `json:"notes"`
IsTemplate bool `gorm:"default:false" json:"is_template"`
ServerCount int `gorm:"default:1" json:"server_count"`
PricelistID *uint `gorm:"index" json:"pricelist_id,omitempty"`
PriceUpdatedAt *time.Time `gorm:"type:timestamp" json:"price_updated_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`

View File

@@ -22,6 +22,7 @@ func BuildConfigurationSnapshot(localCfg *LocalConfiguration) (string, error) {
"notes": localCfg.Notes,
"is_template": localCfg.IsTemplate,
"server_count": localCfg.ServerCount,
"pricelist_id": localCfg.PricelistID,
"price_updated_at": localCfg.PriceUpdatedAt,
"created_at": localCfg.CreatedAt,
"updated_at": localCfg.UpdatedAt,
@@ -50,6 +51,7 @@ func DecodeConfigurationSnapshot(data string) (*LocalConfiguration, error) {
Notes string `json:"notes"`
IsTemplate bool `json:"is_template"`
ServerCount int `json:"server_count"`
PricelistID *uint `json:"pricelist_id"`
PriceUpdatedAt *time.Time `json:"price_updated_at"`
OriginalUserID uint `json:"original_user_id"`
OriginalUsername string `json:"original_username"`
@@ -74,6 +76,7 @@ func DecodeConfigurationSnapshot(data string) (*LocalConfiguration, error) {
Notes: snapshot.Notes,
IsTemplate: snapshot.IsTemplate,
ServerCount: snapshot.ServerCount,
PricelistID: snapshot.PricelistID,
PriceUpdatedAt: snapshot.PriceUpdatedAt,
OriginalUserID: snapshot.OriginalUserID,
OriginalUsername: snapshot.OriginalUsername,