spread

https://git.tonybtw.com/spread.git git://git.tonybtw.com/spread.git
3,904 bytes raw
1
package app
2
3
import (
4
	"context"
5
6
	"spread/internal/spread"
7
)
8
9
// BOMSummary is one saved bill of materials, for listings.
10
type BOMSummary struct {
11
	ID              int64
12
	Ref             string
13
	SubscriberID    int64
14
	SubscriberName  string
15
	UploadedByEmail string
16
	Filename        string
17
	TotalSavings    float64
18
	OurCut          float64
19
	NetSavings      float64
20
	Lines           int
21
	CreatedAt       string
22
}
23
24
// BOMLine is one re-quoted line of a saved BOM.
25
type BOMLine struct {
26
	Line        int
27
	PartNumber  string
28
	Description string
29
	Quantity    float64
30
	UnitPrice   float64
31
	SpreadPrice float64
32
	LineSaving  float64
33
	Matched     bool
34
}
35
36
// BOMDetail is a saved BOM plus its lines, for the detail view.
37
type BOMDetail struct {
38
	BOMSummary
39
	Items []BOMLine
40
}
41
42
// saveBOM persists an analyzed report under a subscriber.
43
func (a *App) saveBOM(ctx context.Context, subscriberID, uploadedBy int64, rep spread.Report, filename string) (int64, error) {
44
	tx, err := a.pool.Begin(ctx)
45
	if err != nil {
46
		return 0, err
47
	}
48
	defer tx.Rollback(ctx)
49
50
	var id int64
51
	err = tx.QueryRow(ctx,
52
		`INSERT INTO boms (subscriber_id, uploaded_by, ref, filename, total_savings, our_cut, net_savings)
53
		 VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
54
		subscriberID, uploadedBy, rep.BOMRef, filename,
55
		rep.Totals.Savings, rep.Totals.OurCut, rep.Totals.NetSavings,
56
	).Scan(&id)
57
	if err != nil {
58
		return 0, err
59
	}
60
61
	for _, l := range rep.Lines {
62
		if _, err := tx.Exec(ctx,
63
			`INSERT INTO bom_lines
64
			   (bom_id, line, part_number, description, quantity, unit_price, spread_price, line_saving, matched)
65
			 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
66
			id, l.Line, l.PartNumber, l.Description, l.Quantity, l.UnitPrice, l.SpreadPrice, l.LineSaving, l.Matched,
67
		); err != nil {
68
			return 0, err
69
		}
70
	}
71
	return id, tx.Commit(ctx)
72
}
73
74
const bomSelect = `
75
	SELECT b.id, b.ref, b.subscriber_id, s.name, coalesce(u.email, ''), b.filename,
76
	       b.total_savings, b.our_cut, b.net_savings,
77
	       (SELECT count(*) FROM bom_lines l WHERE l.bom_id = b.id),
78
	       to_char(b.created_at, 'YYYY-MM-DD HH24:MI')
79
	  FROM boms b
80
	  JOIN subscribers s ON s.id = b.subscriber_id
81
	  LEFT JOIN users u ON u.id = b.uploaded_by`
82
83
func scanSummary(row interface {
84
	Scan(...any) error
85
}) (BOMSummary, error) {
86
	var b BOMSummary
87
	err := row.Scan(&b.ID, &b.Ref, &b.SubscriberID, &b.SubscriberName, &b.UploadedByEmail,
88
		&b.Filename, &b.TotalSavings, &b.OurCut, &b.NetSavings, &b.Lines, &b.CreatedAt)
89
	return b, err
90
}
91
92
// listBOMs returns saved BOMs, newest first. A nil subscriberID lists every
93
// subscriber's BOMs (admin review); otherwise only that subscriber's.
94
func (a *App) listBOMs(ctx context.Context, subscriberID *int64) ([]BOMSummary, error) {
95
	q := bomSelect
96
	args := []any{}
97
	if subscriberID != nil {
98
		q += ` WHERE b.subscriber_id = $1`
99
		args = append(args, *subscriberID)
100
	}
101
	q += ` ORDER BY b.created_at DESC, b.id DESC`
102
103
	rows, err := a.pool.Query(ctx, q, args...)
104
	if err != nil {
105
		return nil, err
106
	}
107
	defer rows.Close()
108
109
	var out []BOMSummary
110
	for rows.Next() {
111
		b, err := scanSummary(rows)
112
		if err != nil {
113
			return nil, err
114
		}
115
		out = append(out, b)
116
	}
117
	return out, rows.Err()
118
}
119
120
func (a *App) getBOM(ctx context.Context, id int64) (BOMDetail, error) {
121
	var d BOMDetail
122
	sum, err := scanSummary(a.pool.QueryRow(ctx, bomSelect+` WHERE b.id = $1`, id))
123
	if err != nil {
124
		return d, err
125
	}
126
	d.BOMSummary = sum
127
128
	rows, err := a.pool.Query(ctx,
129
		`SELECT line, part_number, description, quantity, unit_price, spread_price, line_saving, matched
130
		   FROM bom_lines WHERE bom_id = $1 ORDER BY line`, id)
131
	if err != nil {
132
		return d, err
133
	}
134
	defer rows.Close()
135
	for rows.Next() {
136
		var l BOMLine
137
		if err := rows.Scan(&l.Line, &l.PartNumber, &l.Description, &l.Quantity,
138
			&l.UnitPrice, &l.SpreadPrice, &l.LineSaving, &l.Matched); err != nil {
139
			return d, err
140
		}
141
		d.Items = append(d.Items, l)
142
	}
143
	return d, rows.Err()
144
}