feat: world-прайслист как заглушка для отсутствующих цен

Там, где для LOT нет цены в estimate/warehouse/competitor, подставляется
цена из world-прайслиста. Такие ячейки в таблицах «Цена покупки»/«Цена продажи»
подсвечиваются (amber), участвуют в «Итого» и убирают красную «*».
В CSV-экспорте добавлена колонка «Заглушка (world)» с перечнем столбцов,
где сработал фолбэк.

Добавлены Tx-версии GetLatestLocalPricelistBySource/GetLocalPricesForLots,
чтобы резолв прайслистов внутри транзакции не дедлочил single-connection пул.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-07-10 10:25:46 +03:00
co-authored by Claude Opus 4.8
parent 498cbf5490
commit 70a3ff255f
10 changed files with 586 additions and 52 deletions
@@ -82,6 +82,51 @@ func TestCalculatePriceLevels_UsesExplicitPricelistIDs(t *testing.T) {
}
}
func TestCalculatePriceLevels_WorldFallback(t *testing.T) {
db := newPriceLevelsTestDB(t)
repo := repository.NewPricelistRepository(db)
service := NewQuoteService(repo, nil)
seedPricelistWithItem(t, repo, "estimate", "CPU_Z", 100)
seedPricelistWithItem(t, repo, "world", "CPU_Z", 150)
result, err := service.CalculatePriceLevels(&PriceLevelsRequest{
Items: []struct {
LotName string `json:"lot_name"`
Quantity int `json:"quantity"`
}{
{LotName: "CPU_Z", Quantity: 1},
},
})
if err != nil {
t.Fatalf("CalculatePriceLevels returned error: %v", err)
}
item := result.Items[0]
if item.EstimatePrice == nil || *item.EstimatePrice != 100 {
t.Fatalf("expected native estimate 100, got %#v", item.EstimatePrice)
}
if item.EstimateFromWorld {
t.Fatalf("expected estimate_from_world false when native price exists")
}
if item.WarehousePrice == nil || *item.WarehousePrice != 150 {
t.Fatalf("expected world-fallback warehouse 150, got %#v", item.WarehousePrice)
}
if !item.WarehouseFromWorld {
t.Fatalf("expected warehouse_from_world true")
}
if item.CompetitorPrice == nil || *item.CompetitorPrice != 150 {
t.Fatalf("expected world-fallback competitor 150, got %#v", item.CompetitorPrice)
}
if !item.CompetitorFromWorld {
t.Fatalf("expected competitor_from_world true")
}
if len(item.PriceMissing) != 0 {
t.Fatalf("expected no price_missing after world fallback, got %#v", item.PriceMissing)
}
}
func newPriceLevelsTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})