Files
backend_fiesta/scratch/dbinspect/cleanup.go
Suriya e3459a0f1c Ingest counter sales from the POS terminals, over MQTT and HTTP
A till holds every bill in its own SQLite database and keeps it for
seven days after we acknowledge it, marking one synced only when its id
comes back in an ack. Everything here follows from that.

Silence is not acceptance, so a failing ingest publishes nothing at all
and the terminal simply sends again. A duplicate is a success, because
at-least-once delivery means a lost ack legitimately re-delivers bills
we already hold, and calling those failures would strand a day of
takings on the till. Deduplication is a unique index on the terminal's
UUID plus an advisory lock held for the transaction.

Bills land in pos_orders / pos_order_items rather than orders: a counter
bill carries a cashier, a terminal, a rounding adjustment, promos,
loyalty movement and a payment split that orders has nowhere to put, and
forcing one into the other loses whatever does not fit. Stock is *not*
split — a counter sale writes the same productstocks rows an app order
does, through helpers extracted from createOrderTx so the rule that
prevents overselling has one implementation rather than two.
GetRevenueSummary and GetSalesSummary were extended to union the new
table in; any new report has to remember the same.

Terminal health goes to Redis under a 90-second TTL, sharing the
instance the express backend uses. A heartbeat is a fact with an expiry
date: a till that loses power stops refreshing and ages off the board by
itself, where a Postgres row would need ~288k writes a day and a reaper.

Proven end to end against the live estate before commit: a bill over
HTTP and one over the real Mosquitto broker, the same bill three times
producing one row and one stock movement, and a heartbeat arriving on
the health endpoint. All probe data was removed afterwards.

Four things that only surfaced against real data. An unset jsonb column
failed the very first bill. Product SKUs are unusable as barcodes — 6,245
products share 93 SKUs and "1" covers 5,794 of them — against the till's
unique index, so barcodes fall back to the product id. A taxpercent of
-1 exists and would have put negative GST in a filed slab. And a product
with id 0 exists, which can never be billed and is now skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:48:00 +05:30

117 lines
4.1 KiB
Go

package main
import (
"fmt"
"gorm.io/gorm"
)
// cleanup removes everything the end-to-end proof wrote to the live database.
//
// Three things, in an order that leaves nothing half-undone: the stock the test
// bills consumed is returned, the bills themselves are deleted, and the price
// that was set to make a product sellable goes back to what it was.
//
// The two test bills are named explicitly rather than deleted by date or by
// "everything in pos_orders" — a table that will hold real takings tomorrow is
// not one to run an unbounded DELETE against.
const probeMobile = "9840012345"
var testOrderIDs = []string{
"11111111-2222-4333-8444-555555555555", // the HTTP probe
"99999999-8888-4777-8666-555555555555", // the MQTT probe
}
func cleanup(db *gorm.DB) {
var billIDs []int
db.Raw(`SELECT posorderid FROM pos_orders WHERE terminalorderid IN ?`,
testOrderIDs).Scan(&billIDs)
if len(billIDs) == 0 {
fmt.Println("no test bills found — nothing to undo")
}
// Stock first. Deleting the bills before returning what they consumed would
// leave the ledger short with nothing left to explain why.
var consumed []struct {
Productid int
Quantity float64
}
db.Raw(`SELECT productid, SUM(quantity) AS quantity
FROM pos_order_items WHERE posorderid IN ?
GROUP BY productid`, billIDs).Scan(&consumed)
for _, c := range consumed {
qty := int(c.Quantity)
if float64(qty) < c.Quantity {
qty++ // the ingest rounded up, so the reversal must too
}
if err := db.Exec(`
INSERT INTO productstocks (tenantid, stockdate, locationid, productid,
quantity, stocktype, status)
VALUES (?, NOW(), ?, ?, ?, 'in', 'Active')`,
tenantID, locationID, c.Productid, qty).Error; err != nil {
fmt.Println(" return stock:", err)
continue
}
fmt.Printf(" returned %d units of product %d\n", qty, c.Productid)
}
if len(billIDs) > 0 {
db.Exec(`DELETE FROM pos_order_items WHERE posorderid IN ?`, billIDs)
db.Exec(`DELETE FROM pos_orders WHERE posorderid IN ?`, billIDs)
fmt.Printf(" deleted %d test bill(s)\n", len(billIDs))
}
db.Exec(`UPDATE productlocations SET price = 0
WHERE tenantid=? AND locationid=? AND productid=?`,
tenantID, locationID, productID)
fmt.Printf(" product %d price restored to 0\n", productID)
// The shopper the probes created. Removed only when nothing references it —
// a customer row attached to a real order is not test data any more.
var referenced int
db.Raw(`SELECT COUNT(*) FROM orders WHERE customerid =
(SELECT MIN(customerid) FROM customers WHERE contactno = ?)`,
probeMobile).Scan(&referenced)
if referenced > 0 {
fmt.Printf(" customer %s left in place — %d order(s) reference it\n",
probeMobile, referenced)
} else {
res := db.Exec(`DELETE FROM customers WHERE contactno = ?`, probeMobile)
fmt.Printf(" removed %d probe customer row(s)\n", res.RowsAffected)
}
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(" product %d balance back to %.0f\n", productID, balance)
}
// showCustomer reports whether the registration probe reached the customers
// table, and under which id — the question an ack alone cannot answer.
func showCustomer(db *gorm.DB, mobile string) {
var rows []struct {
Customerid int
Firstname string
Contactno string
Applocationid int
}
db.Raw(`SELECT customerid, COALESCE(firstname,'') AS firstname,
COALESCE(contactno,'') AS contactno,
COALESCE(applocationid,0) AS applocationid
FROM customers WHERE contactno = ?`, mobile).Scan(&rows)
if len(rows) == 0 {
fmt.Printf(" no customer with contactno %s\n", mobile)
return
}
for _, r := range rows {
fmt.Printf(" customerid=%d %q contactno=%s applocid=%d\n",
r.Customerid, r.Firstname, r.Contactno, r.Applocationid)
}
}