Files
backend_fiesta/scratch/dbinspect/main.go
Suriya fc81df14e4 Answer the catalogue as a delta when the terminal sends a revision
Every pull was a full snapshot, so a shop with a thousand products
re-sent all of them to correct one price. The response now carries a
revision the terminal stores and hands back, and a pull that supplies
one gets only what moved: the product row, its row at that outlet, or
its stock ledger. Stock is included because a shop's count drifts from a
till's on every sale rung at another counter, and a delta that ignored
it would let that drift persist until someone forced a full pull.

The dangerous part is the flag, not the filter. A response marked
is_delta:false tells the terminal to withdraw every product it does not
mention — so a filtered result carrying that label empties the shelf.
Both are now derived from one value, and there is no path through the
function that filters without also setting the flag.

Everything ambiguous resolves toward the snapshot. A revision that is
malformed, empty, or issued to another outlet yields a zero cutoff and a
complete response; the opposite would leave a terminal permanently
missing changes with nothing to show for it. The revision advances only
on the final page, so a terminal that abandons a paginated pull cannot
end up holding one that claims it saw pages it never received. And the
stamp is taken a second in the past, because a product written during
the same second the query ran would otherwise fall on the wrong side of
the next cutoff and be skipped for good.

A delta still cannot withdraw a deleted product — removing a row from
productlocations leaves no tombstone — so a periodic pull without a
revision is what collects those.

Verified against the live outlet: a full pull of 12, a delta returning
only the one product whose price had changed, and pagination that stays
exact now that productid <= 0 is excluded in SQL rather than after the
LIMIT.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 19:26:01 +05:30

184 lines
5.7 KiB
Go

// End-to-end proof for the POS ingest, against the live database.
//
// Sets a temporary price on ONE product so a bill can be rung, and prints the
// exact SQL to undo it. Everything else is read-only.
//
// go run ./scratch/dbinspect price # set a test price, print the undo
// go run ./scratch/dbinspect verify # show the bill and the stock it moved
// go run ./scratch/dbinspect restore # put the price back
package main
import (
"fmt"
"log"
"os"
"github.com/joho/godotenv"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
const (
tenantID = 1087
locationID = 1135
productID = 6988 // Mysore Banana — 750 units in stock, 8% tax
testPrice = 60.00
)
func main() {
_ = godotenv.Load()
dsn := fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Kolkata",
os.Getenv("DB_HOST"), os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"),
os.Getenv("DB_NAME"), os.Getenv("DB_PORT"),
)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
log.Fatal("connect:", err)
}
mode := "verify"
if len(os.Args) > 1 {
mode = os.Args[1]
}
switch mode {
case "price":
var before float64
db.Raw(`SELECT COALESCE(price,0) FROM productlocations
WHERE tenantid=? AND locationid=? AND productid=?`,
tenantID, locationID, productID).Scan(&before)
if err := db.Exec(`UPDATE productlocations SET price = ?
WHERE tenantid=? AND locationid=? AND productid=?`,
testPrice, tenantID, locationID, productID).Error; err != nil {
log.Fatal("price:", err)
}
fmt.Printf("product %d priced at %.2f (was %.2f)\n", productID, testPrice, before)
fmt.Printf("\nUNDO:\n UPDATE productlocations SET price = %.2f\n"+
" WHERE tenantid=%d AND locationid=%d AND productid=%d;\n",
before, tenantID, locationID, productID)
case "restore":
if err := db.Exec(`UPDATE productlocations SET price = 0
WHERE tenantid=? AND locationid=? AND productid=?`,
tenantID, locationID, productID).Error; err != nil {
log.Fatal("restore:", err)
}
fmt.Printf("product %d price restored to 0\n", productID)
case "verify":
fmt.Println("=== pos_orders ===")
var bills []struct {
Posorderid int
Terminalorderid string
Invoicenumber string
Terminalid string
Cashiername string
Businessdate string
Subtotal float64
Taxamount float64
Roundoff float64
Total float64
Itemcount int
Paymentmode string
}
db.Raw(`SELECT posorderid, terminalorderid, invoicenumber, terminalid,
cashiername, businessdate, subtotal, taxamount, roundoff,
total, itemcount, paymentmode
FROM pos_orders ORDER BY posorderid DESC LIMIT 5`).Scan(&bills)
if len(bills) == 0 {
fmt.Println(" (none yet)")
}
for _, b := range bills {
fmt.Printf(" #%d %s till=%s cashier=%s date=%s\n",
b.Posorderid, b.Invoicenumber, b.Terminalid, b.Cashiername, b.Businessdate)
fmt.Printf(" uuid=%s\n", b.Terminalorderid)
fmt.Printf(" subtotal=%.2f tax=%.2f roundoff=%.2f total=%.2f items=%d paid=%s\n",
b.Subtotal, b.Taxamount, b.Roundoff, b.Total, b.Itemcount, b.Paymentmode)
}
fmt.Println("\n=== pos_order_items ===")
var items []struct {
Posorderid int
Productid int
Productname string
Quantity float64
Unitprice float64
Gstrate float64
Taxamount float64
Linetotal float64
}
db.Raw(`SELECT posorderid, productid, productname, quantity, unitprice,
gstrate, taxamount, linetotal
FROM pos_order_items ORDER BY posorderitemid DESC LIMIT 10`).Scan(&items)
for _, i := range items {
fmt.Printf(" bill#%d %-18.18s qty=%-6.2f @%-8.2f gst=%-6.2f tax=%-7.2f line=%.2f\n",
i.Posorderid, i.Productname, i.Quantity, i.Unitprice,
i.Gstrate, i.Taxamount, i.Linetotal)
}
fmt.Println("\n=== stock ledger for the test product ===")
var moves []struct {
Productstockid int
Quantity int
Stocktype string
Stockdate string
}
db.Raw(`SELECT productstockid, quantity, stocktype,
TO_CHAR(stockdate,'YYYY-MM-DD HH24:MI:SS') AS stockdate
FROM productstocks
WHERE tenantid=? AND locationid=? AND productid=?
ORDER BY productstockid DESC LIMIT 5`,
tenantID, locationID, productID).Scan(&moves)
for _, m := range moves {
fmt.Printf(" #%d %-4s qty=%-6d %s\n",
m.Productstockid, m.Stocktype, m.Quantity, m.Stockdate)
}
var balance float64
db.Raw(`SELECT COALESCE(
SUM(CASE WHEN LOWER(stocktype)='in' THEN quantity ELSE 0 END) -
SUM(CASE WHEN LOWER(stocktype)='out' THEN quantity ELSE 0 END), 0)
FROM productstocks WHERE tenantid=? AND locationid=? AND productid=?`,
tenantID, locationID, productID).Scan(&balance)
fmt.Printf(" balance now: %.0f\n", balance)
case "cleanup":
cleanup(db)
case "columns":
for _, t := range []string{"products", "productlocations", "productstocks"} {
fmt.Printf("=== %s ===\n", t)
var cols []struct {
ColumnName string
DataType string
}
db.Raw(`SELECT column_name, data_type FROM information_schema.columns
WHERE table_name = ? ORDER BY ordinal_position`, t).Scan(&cols)
for _, c := range cols {
marker := ""
n := c.ColumnName
if n == "created" || n == "updated" || n == "updated_at" ||
n == "stockdate" || n == "modified" {
marker = " <-- timestamp"
}
fmt.Printf(" %-24s %s%s\n", c.ColumnName, c.DataType, marker)
}
}
case "customer":
fmt.Println("=== customers matching the uplink probe ===")
showCustomer(db, "9840012345")
default:
log.Fatalf("unknown mode %q — use price, verify, restore or cleanup", mode)
}
}