Skip to content

Commit a7f39be

Browse files
committed
feat(overview): thread premium_usd through model.Lot + report read path
Add PremiumUSD to model.Lot and copy it in Resolve(), mirroring how the Category/Subcategory/Trophy taxonomy fields are threaded. Also add premium_usd to the ResolveDataset lots SELECT/Scan so the resolved dataset (and thus the /api/summary report the Overview stack table consumes) actually carries it — the scout's model-only prediction was necessary but insufficient; the read query that feeds the report had to carry premium too. Premium is a display memo (a component of basis), never summed into any melt/market total. Extends TestResolve with a case asserting PremiumUSD threads through and does not alter basis. Sell/split path (splitLot) untouched. Refs: om-0pqe
1 parent b802990 commit a7f39be

3 files changed

Lines changed: 28 additions & 2 deletions

File tree

internal/model/model.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ type Lot struct {
8787
FaceValueUSD float64 `json:"face_value_usd"`
8888
Acquired string `json:"acquired"`
8989
Source string `json:"source"`
90+
PremiumUSD float64 `json:"premium_usd,omitempty"` // paid over melt at acquisition; a component of basis, display-only
9091
Category string `json:"category,omitempty"` // CRH find taxonomy (ADR-006)
9192
Subcategory string `json:"subcategory,omitempty"` // CRH find taxonomy (ADR-006)
9293
Trophy bool `json:"trophy,omitempty"`
@@ -117,6 +118,7 @@ func Resolve(h Holding, t ItemType) Lot {
117118
FaceValueUSD: h.FaceValueUSD,
118119
Acquired: h.Acquired,
119120
Source: h.Source,
121+
PremiumUSD: h.PremiumUSD,
120122
Category: h.Category,
121123
Subcategory: h.Subcategory,
122124
Trophy: h.Trophy,

internal/model/model_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,27 @@ func TestResolveUsesGrossWeightPurityAndUnit(t *testing.T) {
4646
t.Fatalf("FineOzEach = %v, want %v", got, want)
4747
}
4848
}
49+
50+
// PremiumUSD is a display memo (paid over melt at acquisition), a component of
51+
// basis. It must thread from Holding through Resolve into the flat Lot so calc/UI
52+
// (the Overview stack table) can surface it — mirroring the taxonomy fields.
53+
func TestResolveThreadsPremiumUSD(t *testing.T) {
54+
h := Holding{
55+
ID: 1,
56+
ItemTypeID: 1,
57+
Qty: 1,
58+
BasisUSD: 1512,
59+
PremiumUSD: 62,
60+
Acquired: "2026-07-02",
61+
Source: "test",
62+
}
63+
itemType := ItemType{ID: 1, Name: "1 oz Gold Eagle", Metal: "gold", Fineness: ".9167", FineOzEach: 1}
64+
lot := Resolve(h, itemType)
65+
if got, want := lot.PremiumUSD, 62.0; got != want {
66+
t.Fatalf("PremiumUSD = %v, want %v", got, want)
67+
}
68+
// Sanity: premium is a memo, not folded into basis by Resolve.
69+
if got, want := lot.BasisUSD, 1512.0; got != want {
70+
t.Fatalf("BasisUSD = %v, want %v (premium must not alter basis)", got, want)
71+
}
72+
}

internal/store/data.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ func (s *Store) ResolveDataset() (model.Dataset, error) {
284284
// holdings -> resolved lots
285285
rows, err = s.db.Query(
286286
`SELECT id, item_type_id, roll_txn_id, activity, qty, gross_weight, purity, weight_unit, basis_usd,
287-
face_value_usd, acquired, source, category, subcategory, trophy
287+
premium_usd, face_value_usd, acquired, source, category, subcategory, trophy
288288
FROM lots WHERE disposed IS NULL OR disposed = '' ORDER BY id`)
289289
if err != nil {
290290
return d, fmt.Errorf("load lots: %w", err)
@@ -295,7 +295,7 @@ func (s *Store) ResolveDataset() (model.Dataset, error) {
295295
var source, cat, subcat, wu sql.NullString
296296
var trophy int64
297297
if err := rows.Scan(&h.ID, &h.ItemTypeID, &rtid, &h.Activity, &h.Qty, &h.GrossWeight,
298-
&h.Purity, &wu, &h.BasisUSD, &h.FaceValueUSD, &h.Acquired, &source, &cat, &subcat, &trophy); err != nil {
298+
&h.Purity, &wu, &h.BasisUSD, &h.PremiumUSD, &h.FaceValueUSD, &h.Acquired, &source, &cat, &subcat, &trophy); err != nil {
299299
rows.Close()
300300
return d, err
301301
}

0 commit comments

Comments
 (0)