seedprices is the only account of what the 15 seeded retail prices were and how to put the zeros back — products at 1135 and 1185 were all at 0, so nothing was sellable and the terminal could not be exercised at all. It refuses to overwrite a price a human already set, so re-running it is safe. termfixcleanup removes the one bill posted to prove the terminalid fix against production. Named by its own terminalorderid rather than by date or by "the newest row": pos_orders holds real takings and is not a table to run an unbounded DELETE against. healthproof carries a warning it did not have when it was written. MQTT_USER in .env is pos_ingest, which the ACL denies publish on the health topic, and Mosquitto answers an ACL-denied QoS 1 publish with a PUBACK before discarding it. So the tool connects, reports success, and nothing arrives — indistinguishable from a dead consumer, which is how it read for an hour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
195 lines
5.8 KiB
Go
195 lines
5.8 KiB
Go
// Seed retail prices so the POS has something sellable.
|
|
//
|
|
// These are plausible Coimbatore figures, not authoritative ones. They exist so
|
|
// the terminal can ring a real bill; the owner corrects them afterwards.
|
|
//
|
|
// go run ./scratch/seedprices plan # show every change and the undo SQL
|
|
// go run ./scratch/seedprices apply # write them
|
|
// go run ./scratch/seedprices verify # read back what the catalogue now serves
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
|
|
"github.com/joho/godotenv"
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
type priced struct {
|
|
locationID int
|
|
productID int
|
|
name string
|
|
unit string
|
|
price float64
|
|
note string
|
|
}
|
|
|
|
// Prices are per the product's own unit — per kilogram where the unit is
|
|
// kilogram, per pack where it is piece. Getting that backwards is the easiest
|
|
// way to make a till look broken, so the unit is carried through and printed.
|
|
var seed = []priced{
|
|
// 1135 — fresh produce
|
|
{1135, 6988, "Mysore Banana", "kilogram", 60, ""},
|
|
{1135, 6989, "Jammu Apple", "piece", 30, "per fruit, not per kg"},
|
|
{1135, 6990, "Small orange", "kilogram", 90, ""},
|
|
{1135, 6991, "Red Guava", "kilogram", 80, ""},
|
|
{1135, 6992, "Pomegrante", "kilogram", 180, ""},
|
|
{1135, 6993, "Salem Mango", "kilogram", 90, "seasonal, swings 80-120"},
|
|
{1135, 6994, "Pineapple", "kilogram", 60, ""},
|
|
{1135, 6995, "Strawberries", "piece", 150, "priced as a punnet"},
|
|
{1135, 6996, "Maceral", "kilogram", 220, "READ AS MACKEREL - correct if wrong"},
|
|
{1135, 6997, "Tuna", "kilogram", 280, ""},
|
|
{1135, 6998, "Hatsun curd", "piece", 30, "500g pouch"},
|
|
{1135, 7014, "Apple", "kilogram", 200, ""},
|
|
|
|
// 1185 — packaged
|
|
{1185, 7074, "Amla Dabur Oral Care Chewing Gum 10g", "piece", 10, ""},
|
|
{1185, 7075, "Cheetos Chips 100g", "piece", 40, ""},
|
|
{1185, 7076, "Cheerios Breakfast Cereal 100g", "piece", 120, ""},
|
|
// 7077 Hot Heads is already at 50 — someone set it deliberately, leave it.
|
|
}
|
|
|
|
func main() {
|
|
mode := "plan"
|
|
if len(os.Args) > 1 {
|
|
mode = os.Args[1]
|
|
}
|
|
|
|
_ = godotenv.Load()
|
|
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
|
os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_USER"),
|
|
os.Getenv("DB_PASSWORD"), os.Getenv("DB_NAME"))
|
|
|
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
switch mode {
|
|
case "plan":
|
|
plan(db, false)
|
|
case "apply":
|
|
plan(db, true)
|
|
case "verify":
|
|
verify(db)
|
|
default:
|
|
log.Fatalf("unknown mode %q — use plan, apply or verify", mode)
|
|
}
|
|
}
|
|
|
|
func plan(db *gorm.DB, write bool) {
|
|
fmt.Printf("%-6s %-38s %-9s %8s -> %8s\n", "id", "product", "unit", "now", "new")
|
|
fmt.Println("--------------------------------------------------------------------------------")
|
|
|
|
undo := []string{}
|
|
changes := 0
|
|
|
|
for _, p := range seed {
|
|
var current struct {
|
|
Price float64
|
|
Tenantid int
|
|
Found bool
|
|
}
|
|
row := db.Raw(`SELECT COALESCE(price, 0) AS price, tenantid, true AS found
|
|
FROM productlocations
|
|
WHERE productid = ? AND locationid = ?
|
|
LIMIT 1`, p.productID, p.locationID).Scan(¤t)
|
|
if row.Error != nil {
|
|
log.Fatalf("reading %d: %v", p.productID, row.Error)
|
|
}
|
|
if !current.Found {
|
|
fmt.Printf("%-6d %-38s NO productlocations ROW - skipped\n", p.productID, p.name)
|
|
continue
|
|
}
|
|
|
|
// Never overwrite a price a human already set. A seed value is a
|
|
// placeholder; a real one is a decision, and losing it silently would
|
|
// be worse than leaving a gap.
|
|
if current.Price > 0 {
|
|
fmt.Printf("%-6d %-38s %-9s %8.2f already priced, left alone\n",
|
|
p.productID, p.name, p.unit, current.Price)
|
|
continue
|
|
}
|
|
|
|
note := ""
|
|
if p.note != "" {
|
|
note = " <- " + p.note
|
|
}
|
|
fmt.Printf("%-6d %-38s %-9s %8.2f -> %8.2f%s\n",
|
|
p.productID, p.name, p.unit, current.Price, p.price, note)
|
|
|
|
undo = append(undo, fmt.Sprintf(
|
|
"UPDATE productlocations SET price = %.2f WHERE productid = %d AND locationid = %d;",
|
|
current.Price, p.productID, p.locationID))
|
|
changes++
|
|
|
|
if write {
|
|
// updated is bumped so the catalogue delta carries the new price to
|
|
// terminals that already hold a revision, rather than waiting for
|
|
// someone to force a full pull.
|
|
err := db.Exec(`UPDATE productlocations
|
|
SET price = ?, updated = NOW()
|
|
WHERE productid = ? AND locationid = ?`,
|
|
p.price, p.productID, p.locationID).Error
|
|
if err != nil {
|
|
log.Fatalf("writing %d: %v", p.productID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Println("--------------------------------------------------------------------------------")
|
|
if write {
|
|
fmt.Printf("APPLIED %d price(s).\n\n", changes)
|
|
} else {
|
|
fmt.Printf("%d price(s) would change. Nothing written — run `apply` to commit.\n\n", changes)
|
|
}
|
|
|
|
fmt.Println("-- undo, if you want the zeros back:")
|
|
for _, u := range undo {
|
|
fmt.Println(u)
|
|
}
|
|
}
|
|
|
|
func verify(db *gorm.DB) {
|
|
type row struct {
|
|
Locationid int
|
|
Productid int
|
|
Productname string
|
|
Price float64
|
|
Taxpercent float64
|
|
}
|
|
var rows []row
|
|
db.Raw(`SELECT b.locationid, a.productid, a.productname,
|
|
COALESCE(b.price, 0) AS price, COALESCE(a.taxpercent, 0) AS taxpercent
|
|
FROM products a
|
|
INNER JOIN productlocations b ON a.productid = b.productid AND a.tenantid = b.tenantid
|
|
WHERE b.locationid IN (1135, 1185) AND a.productid > 0
|
|
ORDER BY b.locationid, a.productid`).Scan(&rows)
|
|
|
|
sellable := 0
|
|
for _, r := range rows {
|
|
flag := ""
|
|
if r.Price > 0 {
|
|
sellable++
|
|
} else {
|
|
flag = " <- still zero, not sellable"
|
|
}
|
|
fmt.Printf("loc %d %-6d %-38s %8.2f tax=%.0f%s\n",
|
|
r.Locationid, r.Productid, r.Productname[:min(38, len(r.Productname))], r.Price, r.Taxpercent, flag)
|
|
}
|
|
fmt.Printf("\n%d of %d rows are sellable.\n", sellable, len(rows))
|
|
}
|
|
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|