refactor: remove CurrentPrice from local_components and transition to pricelist-based pricing
## Overview Removed the CurrentPrice and SyncedAt fields from local_components, transitioning to a pricelist-based pricing model where all prices are sourced from local_pricelist_items based on the configuration's selected pricelist. ## Changes ### Data Model Updates - **LocalComponent**: Now stores only metadata (LotName, LotDescription, Category, Model) - Removed: CurrentPrice, SyncedAt (both redundant) - Pricing is now exclusively sourced from local_pricelist_items - **LocalConfiguration**: Added pricelist selection fields - Added: WarehousePricelistID, CompetitorPricelistID - These complement the existing PricelistID (Estimate) ### Migrations - Added migration "drop_component_unused_fields" to remove CurrentPrice and SyncedAt columns - Added migration "add_warehouse_competitor_pricelists" to add new pricelist fields ### Component Sync - Removed current_price from MariaDB query - Removed CurrentPrice assignment in component creation - SyncComponentPrices now exclusively updates based on pricelist_items via quote calculation ### Quote Calculation - Added PricelistID field to QuoteRequest - Updated local-first path to use pricelist_items instead of component.CurrentPrice - Falls back to latest estimate pricelist if PricelistID not specified - Maintains offline-first behavior: local queries work without MariaDB ### Configuration Refresh - Removed fallback on component.CurrentPrice - Prices are only refreshed from local_pricelist_items - If price not found in pricelist, original price is preserved ### API Changes - Removed CurrentPrice from ComponentView - Components API no longer returns pricing information - Pricing is accessed via QuoteService or PricelistService ### Code Cleanup - Removed UpdateComponentPricesFromPricelist() method - Removed EnsureComponentPricesFromPricelists() method - Updated UnifiedRepository to remove offline pricing logic - Updated converters to remove CurrentPrice mapping ## Architecture Impact - Components = metadata store only - Prices = managed by pricelist system - Quote calculation = owns all pricing logic - Local-first behavior preserved: SQLite queries work offline, no MariaDB dependency ## Testing - Build successful - All code compiles without errors - Ready for migration testing with existing databases Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,7 +53,6 @@ type ComponentView struct {
|
||||
Category string `json:"category"`
|
||||
CategoryName string `json:"category_name"`
|
||||
Model string `json:"model"`
|
||||
CurrentPrice *float64 `json:"current_price"`
|
||||
PriceFreshness models.PriceFreshness `json:"price_freshness"`
|
||||
PopularityScore float64 `json:"popularity_score"`
|
||||
Specs models.Specs `json:"specs,omitempty"`
|
||||
@@ -92,7 +91,6 @@ func (s *ComponentService) List(filter repository.ComponentFilter, page, perPage
|
||||
view := ComponentView{
|
||||
LotName: c.LotName,
|
||||
Model: c.Model,
|
||||
CurrentPrice: c.CurrentPrice,
|
||||
PriceFreshness: c.GetPriceFreshness(30, 60, 90, 3),
|
||||
PopularityScore: c.PopularityScore,
|
||||
Specs: c.Specs,
|
||||
@@ -134,7 +132,6 @@ func (s *ComponentService) GetByLotName(lotName string) (*ComponentView, error)
|
||||
view := &ComponentView{
|
||||
LotName: c.LotName,
|
||||
Model: c.Model,
|
||||
CurrentPrice: c.CurrentPrice,
|
||||
PriceFreshness: c.GetPriceFreshness(30, 60, 90, 3),
|
||||
PopularityScore: c.PopularityScore,
|
||||
Specs: c.Specs,
|
||||
|
||||
@@ -347,7 +347,7 @@ func (s *LocalConfigurationService) RefreshPrices(uuid string, ownerUsername str
|
||||
}
|
||||
latestPricelist, latestErr := s.localDB.GetLatestLocalPricelist()
|
||||
|
||||
// Update prices for all items
|
||||
// Update prices for all items from pricelist
|
||||
updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items))
|
||||
for i, item := range localCfg.Items {
|
||||
if latestErr == nil && latestPricelist != nil {
|
||||
@@ -362,20 +362,8 @@ func (s *LocalConfigurationService) RefreshPrices(uuid string, ownerUsername str
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
updatedItems[i] = item
|
||||
continue
|
||||
}
|
||||
|
||||
// Update item with current price from local cache
|
||||
updatedItems[i] = localdb.LocalConfigItem{
|
||||
LotName: item.LotName,
|
||||
Quantity: item.Quantity,
|
||||
UnitPrice: *component.CurrentPrice,
|
||||
}
|
||||
// Keep original item if price not found in pricelist
|
||||
updatedItems[i] = item
|
||||
}
|
||||
|
||||
// Update configuration
|
||||
@@ -672,7 +660,7 @@ func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Co
|
||||
}
|
||||
latestPricelist, latestErr := s.localDB.GetLatestLocalPricelist()
|
||||
|
||||
// Update prices for all items
|
||||
// Update prices for all items from pricelist
|
||||
updatedItems := make(localdb.LocalConfigItems, len(localCfg.Items))
|
||||
for i, item := range localCfg.Items {
|
||||
if latestErr == nil && latestPricelist != nil {
|
||||
@@ -687,20 +675,8 @@ func (s *LocalConfigurationService) RefreshPricesNoAuth(uuid string) (*models.Co
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
updatedItems[i] = item
|
||||
continue
|
||||
}
|
||||
|
||||
// Update item with current price from local cache
|
||||
updatedItems[i] = localdb.LocalConfigItem{
|
||||
LotName: item.LotName,
|
||||
Quantity: item.Quantity,
|
||||
UnitPrice: *component.CurrentPrice,
|
||||
}
|
||||
// Keep original item if price not found in pricelist
|
||||
updatedItems[i] = item
|
||||
}
|
||||
|
||||
// Update configuration
|
||||
|
||||
@@ -78,6 +78,7 @@ type QuoteRequest struct {
|
||||
LotName string `json:"lot_name"`
|
||||
Quantity int `json:"quantity"`
|
||||
} `json:"items"`
|
||||
PricelistID *uint `json:"pricelist_id,omitempty"` // Optional: use specific pricelist for pricing
|
||||
}
|
||||
|
||||
type PriceLevelsRequest struct {
|
||||
@@ -123,6 +124,16 @@ func (s *QuoteService) ValidateAndCalculate(req *QuoteRequest) (*QuoteValidation
|
||||
Warnings: make([]string, 0),
|
||||
}
|
||||
|
||||
// Determine which pricelist to use for pricing
|
||||
pricelistID := req.PricelistID
|
||||
if pricelistID == nil || *pricelistID == 0 {
|
||||
// By default, use latest estimate pricelist
|
||||
latestPricelist, err := s.localDB.GetLatestLocalPricelistBySource("estimate")
|
||||
if err == nil && latestPricelist != nil {
|
||||
pricelistID = &latestPricelist.ServerID
|
||||
}
|
||||
}
|
||||
|
||||
var total float64
|
||||
for _, reqItem := range req.Items {
|
||||
localComp, err := s.localDB.GetLocalComponent(reqItem.LotName)
|
||||
@@ -142,13 +153,19 @@ func (s *QuoteService) ValidateAndCalculate(req *QuoteRequest) (*QuoteValidation
|
||||
TotalPrice: 0,
|
||||
}
|
||||
|
||||
if localComp.CurrentPrice != nil && *localComp.CurrentPrice > 0 {
|
||||
item.UnitPrice = *localComp.CurrentPrice
|
||||
item.TotalPrice = *localComp.CurrentPrice * float64(reqItem.Quantity)
|
||||
item.HasPrice = true
|
||||
total += item.TotalPrice
|
||||
// Get price from pricelist_items
|
||||
if pricelistID != nil {
|
||||
price, found := s.lookupPriceByPricelistID(*pricelistID, reqItem.LotName)
|
||||
if found && price > 0 {
|
||||
item.UnitPrice = price
|
||||
item.TotalPrice = price * float64(reqItem.Quantity)
|
||||
item.HasPrice = true
|
||||
total += item.TotalPrice
|
||||
} else {
|
||||
result.Warnings = append(result.Warnings, "No price available for: "+reqItem.LotName)
|
||||
}
|
||||
} else {
|
||||
result.Warnings = append(result.Warnings, "No price available for: "+reqItem.LotName)
|
||||
result.Warnings = append(result.Warnings, "No pricelist available for: "+reqItem.LotName)
|
||||
}
|
||||
|
||||
result.Items = append(result.Items, item)
|
||||
|
||||
@@ -346,17 +346,10 @@ func (s *Service) SyncPricelists() (int, error) {
|
||||
}
|
||||
|
||||
synced := 0
|
||||
var latestEstimateLocalID uint
|
||||
var latestEstimateCreatedAt time.Time
|
||||
for _, pl := range serverPricelists {
|
||||
// Check if pricelist already exists locally
|
||||
existing, _ := s.localDB.GetLocalPricelistByServerID(pl.ID)
|
||||
if existing != nil {
|
||||
// Track latest estimate pricelist by created_at for component refresh.
|
||||
if pl.Source == string(models.PricelistSourceEstimate) && (latestEstimateCreatedAt.IsZero() || pl.CreatedAt.After(latestEstimateCreatedAt)) {
|
||||
latestEstimateCreatedAt = pl.CreatedAt
|
||||
latestEstimateLocalID = existing.ID
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -385,10 +378,6 @@ func (s *Service) SyncPricelists() (int, error) {
|
||||
slog.Debug("synced pricelist with items", "version", pl.Version, "items", itemCount)
|
||||
}
|
||||
|
||||
if pl.Source == string(models.PricelistSourceEstimate) && (latestEstimateCreatedAt.IsZero() || pl.CreatedAt.After(latestEstimateCreatedAt)) {
|
||||
latestEstimateCreatedAt = pl.CreatedAt
|
||||
latestEstimateLocalID = localPL.ID
|
||||
}
|
||||
synced++
|
||||
}
|
||||
|
||||
@@ -399,16 +388,6 @@ func (s *Service) SyncPricelists() (int, error) {
|
||||
slog.Info("deleted stale local pricelists", "deleted", removed)
|
||||
}
|
||||
|
||||
// Update component prices from latest estimate pricelist only.
|
||||
if latestEstimateLocalID > 0 {
|
||||
updated, err := s.localDB.UpdateComponentPricesFromPricelist(latestEstimateLocalID)
|
||||
if err != nil {
|
||||
slog.Warn("failed to update component prices from pricelist", "error", err)
|
||||
} else {
|
||||
slog.Info("updated component prices from latest pricelist", "updated", updated)
|
||||
}
|
||||
}
|
||||
|
||||
// Update last sync time
|
||||
s.localDB.SetLastSyncTime(time.Now())
|
||||
s.RecordSyncHeartbeat()
|
||||
|
||||
Reference in New Issue
Block a user