Files
Suriya 5c04d40f0e Scope /health/terminal to the caller's tenant, and backfill the blank codes
The outlet check could not go in the middleware with the others: it scopes a
request by the location it names, and this route names only a terminal code —
free text minted at the till. The outlet is not known until after the lookup,
so the check happens in the handler, reading the outlet off the heartbeat
itself rather than off the request.

Guessing "T4A9" now answers 403 instead of another shop's pending bills,
takings so far today, and app version. Silent when no token is presented, in
step with middleware.PosAuth: while POS_AUTH_REQUIRED is off, real tills are
still calling this unauthenticated and refusing them would blank the fleet
board for exactly the terminals it watches.

scratch/termbackfill repairs the rows left behind by the posTerminalFor bug.
The code was never lost — it is the third segment of the invoice number the
till printed in the same transaction — so this derives rather than guesses,
and refuses to write a code that outlet has never filed a bill under. 39 of
40 recovered; the holdout is a synthetic proof bill whose only sibling is
itself, and unverifiable codes stay blank rather than becoming plausible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 11:22:15 +05:30

133 lines
4.5 KiB
Go

// Backfills terminalid on bills that arrived before the posTerminalFor fix.
//
// Between the till and v1.3.96 the ingest wrote the fallback terminal code onto
// the batch while the row was built from the order, so bills landed with an
// empty terminalid and `byterminal` grouped them all under "". The code was
// never lost — it is the third segment of the invoice number the till printed,
// INV-2608-T5EDD-00116, minted at the same terminal in the same transaction.
// So this derives rather than guesses.
//
// Only rows whose terminalid is blank AND whose invoice number matches the
// exact shape are touched; anything else is left alone and reported.
//
// go run ./scratch/termbackfill # dry run — shows every change
// go run ./scratch/termbackfill apply # writes, then prints the undo
package main
import (
"fmt"
"log"
"os"
"github.com/joho/godotenv"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// Anchored, and the terminal segment is required to be non-empty. A loose
// pattern here would write a blank over a blank and report it as a fix.
const invoiceShape = `^INV-[0-9]+-[A-Za-z0-9]+-[0-9]+$`
type row struct {
Posorderid int
Invoicenumber string
Locationid int
Derived string
Businessdate string
}
func main() {
apply := len(os.Args) > 1 && os.Args[1] == "apply"
_ = 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)
}
var blank int
db.Raw(`SELECT COUNT(*) FROM pos_orders WHERE COALESCE(terminalid,'') = ''`).Scan(&blank)
var rows []row
db.Raw(`SELECT posorderid, invoicenumber, locationid, businessdate,
split_part(invoicenumber, '-', 3) AS derived
FROM pos_orders
WHERE COALESCE(terminalid,'') = '' AND invoicenumber ~ ?
ORDER BY posorderid`, invoiceShape).Scan(&rows)
fmt.Printf("bills with a blank terminalid: %d\n", blank)
fmt.Printf("of those, recoverable from the invoice number: %d\n\n", len(rows))
if len(rows) != blank {
fmt.Printf("!! %d bill(s) cannot be recovered and will be left blank\n\n", blank-len(rows))
}
// A derived code must already be in use at the same outlet, or it is not a
// terminal — it is a segment that happened to parse. A code that fails is
// skipped, not fatal: it means one till's bills are *all* blank, which no
// amount of cross-referencing can confirm from this table alone. Writing it
// anyway would launder a guess into the ledger as though it were recovered.
counts := map[string]int{}
for _, r := range rows {
counts[fmt.Sprintf("%d/%s", r.Locationid, r.Derived)]++
}
trusted := map[string]bool{}
seen := map[string]bool{}
fmt.Printf("%-10s %-9s %-6s %-12s %s\n", "location", "terminal", "bills", "date", "known at this outlet?")
for _, r := range rows {
key := fmt.Sprintf("%d/%s", r.Locationid, r.Derived)
if seen[key] {
continue
}
seen[key] = true
var known int
db.Raw(`SELECT COUNT(*) FROM pos_orders
WHERE locationid = ? AND terminalid = ?`, r.Locationid, r.Derived).Scan(&known)
trusted[key] = known > 0
verdict := fmt.Sprintf("yes — %d bill(s) already filed under it", known)
if known == 0 {
verdict = "NO — never seen here, SKIPPED"
}
fmt.Printf("%-10d %-9s %-6d %-12s %s\n", r.Locationid, r.Derived, counts[key], r.Businessdate, verdict)
}
ids := make([]int, 0, len(rows))
skipped := 0
for _, r := range rows {
if trusted[fmt.Sprintf("%d/%s", r.Locationid, r.Derived)] {
ids = append(ids, r.Posorderid)
continue
}
skipped++
}
fmt.Printf("\nwill update %d bill(s); leaving %d blank\n", len(ids), skipped)
if len(ids) == 0 {
return
}
if !apply {
fmt.Println("dry run — nothing written. re-run with `apply` to write.")
return
}
res := db.Exec(`UPDATE pos_orders
SET terminalid = split_part(invoicenumber, '-', 3)
WHERE posorderid IN ? AND COALESCE(terminalid,'') = ''`, ids)
if res.Error != nil {
log.Fatal(res.Error)
}
fmt.Printf("\nupdated %d bill(s)\n", res.RowsAffected)
var stillBlank int
db.Raw(`SELECT COUNT(*) FROM pos_orders WHERE COALESCE(terminalid,'') = ''`).Scan(&stillBlank)
fmt.Printf("bills still blank: %d\n", stillBlank)
fmt.Printf("\nundo:\n UPDATE pos_orders SET terminalid = '' WHERE posorderid IN (%v);\n", ids)
}