spread

https://git.tonybtw.com/spread.git git://git.tonybtw.com/spread.git
3,296 bytes raw
1
package app
2
3
import (
4
	"context"
5
	"time"
6
7
	"spread/internal/spread"
8
)
9
10
// CatalogEntry is one admin-managed supplier price.
11
type CatalogEntry struct {
12
	PartNumber  string
13
	Description string
14
	SpreadPrice float64
15
	UpdatedAt   time.Time
16
}
17
18
func (a *App) listCatalog(ctx context.Context) ([]CatalogEntry, error) {
19
	rows, err := a.pool.Query(ctx,
20
		`SELECT part_number, description, spread_price, updated_at
21
		   FROM catalog ORDER BY part_number`)
22
	if err != nil {
23
		return nil, err
24
	}
25
	defer rows.Close()
26
27
	var out []CatalogEntry
28
	for rows.Next() {
29
		var e CatalogEntry
30
		if err := rows.Scan(&e.PartNumber, &e.Description, &e.SpreadPrice, &e.UpdatedAt); err != nil {
31
			return nil, err
32
		}
33
		out = append(out, e)
34
	}
35
	return out, rows.Err()
36
}
37
38
func (a *App) getCatalogEntry(ctx context.Context, pn string) (CatalogEntry, error) {
39
	var e CatalogEntry
40
	err := a.pool.QueryRow(ctx,
41
		`SELECT part_number, description, spread_price, updated_at
42
		   FROM catalog WHERE part_number = $1`, spread.NormalizePN(pn),
43
	).Scan(&e.PartNumber, &e.Description, &e.SpreadPrice, &e.UpdatedAt)
44
	return e, err
45
}
46
47
func (a *App) upsertCatalog(ctx context.Context, pn, desc string, price float64) error {
48
	_, err := a.pool.Exec(ctx,
49
		`INSERT INTO catalog (part_number, description, spread_price, updated_at)
50
		 VALUES ($1, $2, $3, now())
51
		 ON CONFLICT (part_number)
52
		 DO UPDATE SET description = EXCLUDED.description,
53
		               spread_price = EXCLUDED.spread_price,
54
		               updated_at = now()`,
55
		spread.NormalizePN(pn), desc, price)
56
	return err
57
}
58
59
func (a *App) deleteCatalog(ctx context.Context, pn string) error {
60
	_, err := a.pool.Exec(ctx, `DELETE FROM catalog WHERE part_number = $1`, spread.NormalizePN(pn))
61
	return err
62
}
63
64
func (a *App) catalogCount(ctx context.Context) (int, error) {
65
	var n int
66
	err := a.pool.QueryRow(ctx, `SELECT count(*) FROM catalog`).Scan(&n)
67
	return n, err
68
}
69
70
// catalogSource loads the whole catalog into memory as a spread.Catalog so a
71
// BOM can be re-quoted with a single query instead of one per line.
72
func (a *App) catalogSource(ctx context.Context) (spread.Catalog, error) {
73
	rows, err := a.pool.Query(ctx, `SELECT part_number, spread_price FROM catalog`)
74
	if err != nil {
75
		return nil, err
76
	}
77
	defer rows.Close()
78
79
	m := mapCatalog{}
80
	for rows.Next() {
81
		var pn string
82
		var price float64
83
		if err := rows.Scan(&pn, &price); err != nil {
84
			return nil, err
85
		}
86
		m[pn] = price
87
	}
88
	return m, rows.Err()
89
}
90
91
type mapCatalog map[string]float64
92
93
func (m mapCatalog) Lookup(pn string) (float64, bool) {
94
	p, ok := m[spread.NormalizePN(pn)]
95
	return p, ok
96
}
97
98
// demoCatalog mirrors the five parts on the landing page so a fresh admin can
99
// seed a working catalog with one click.
100
var demoCatalog = []CatalogEntry{
101
	{"GRM188R71H104KA93D", "0603 0.1µF Capacitor", 0.0142, time.Time{}},
102
	{"DIN 912", "M3×10 Hex Cap Screw, A2 SS", 0.0335, time.Time{}},
103
	{"UL1007", "22 AWG Hookup Wire, 3m", 0.9450, time.Time{}},
104
	{"STM32F103C8T6", "STM32F103C8T6 MCU LQFP-48", 2.9200, time.Time{}},
105
	{"RC0805FR-0710KL", "0805 10kΩ Resistor, 1%", 0.0086, time.Time{}},
106
}
107
108
func (a *App) seedDemoCatalog(ctx context.Context) error {
109
	for _, e := range demoCatalog {
110
		if err := a.upsertCatalog(ctx, e.PartNumber, e.Description, e.SpreadPrice); err != nil {
111
			return err
112
		}
113
	}
114
	return nil
115
}