Files
Suriya 64a219e7da Stop tracking .env
It has been in the repository since the initial commit carrying the live
database host, user and password. Removing it from the index stops that
getting worse; the credentials are still in history and should be
rotated, which needs coordinating with everything that reads them.

Deployments should pass configuration as container environment rather
than shipping a file — a file on disk is one `git add -f` away from
being committed again.

Also closes the last untested path: a shopper registration published
over the broker rather than posted over HTTP. All three MQTT topics —
order, customer and health — have now been fired against the live
Mosquitto instance and acknowledged.

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

149 lines
4.8 KiB
Go

// Publishes a bill over the real broker and waits for the ack, standing in for
// a till until one is available to test with.
//
// go run ./scratch/mqttpub
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/joho/godotenv"
)
const (
locationID = "1135"
terminalID = "T4A9"
orderID = "99999999-8888-4777-8666-555555555555" // distinct from the HTTP test
)
func main() {
_ = godotenv.Load()
broker := os.Getenv("MQTT_URL")
if broker == "" {
broker = "tcp://66.116.225.226:1883"
}
opts := mqtt.NewClientOptions().
AddBroker(broker).
SetClientID("pos-e2e-probe").
SetUsername(os.Getenv("MQTT_USER")).
SetPassword(os.Getenv("MQTT_PASSWORD")).
SetCleanSession(true)
client := mqtt.NewClient(opts)
if t := client.Connect(); t.Wait() && t.Error() != nil {
log.Fatal("connect:", t.Error())
}
defer client.Disconnect(500)
fmt.Println("connected to", broker)
acks := make(chan []byte, 1)
ackTopic := fmt.Sprintf("nearle/pos/%s/%s/ack", locationID, terminalID)
if t := client.Subscribe(ackTopic, 1, func(_ mqtt.Client, m mqtt.Message) {
acks <- m.Payload()
}); t.Wait() && t.Error() != nil {
log.Fatal("subscribe:", t.Error())
}
fmt.Println("listening on", ackTopic)
batch := map[string]any{
"schema": 1,
"batch_id": "batch-mqtt-0001",
"store_id": locationID,
"terminal_id": terminalID,
"sent_at": time.Now().UTC().Format(time.RFC3339),
"orders": []map[string]any{{
"id": orderID,
"invoice_number": "INV-2608-T4A9-00002",
"created_at": time.Now().UTC().Format(time.RFC3339),
"terminal_id": terminalID,
"cashier": "Divya",
"customer": map[string]any{"id": "c-2", "mobile": "9840099999", "name": "Ravi"},
"subtotal": 60.0,
"discount": 0.0,
"promos": []any{},
"tax": 4.44,
"tax_breakdown": map[string]float64{"0.08": 4.44},
"round_off": 0.0,
"total": 60.0,
"points_earned": 0,
"points_redeemed": 0,
"payments": []map[string]any{{"method": "upi", "amount": 60.0, "reference": "TESTUPI"}},
"items": []map[string]any{{
"product_id": "6988", "barcode": "6988", "name": "Mysore Banana",
"quantity": 1, "unit_price": 60.0, "discount": 0.0,
"gst_rate": 0.08, "tax": 4.44, "line_total": 60.0,
}},
}},
}
body, _ := json.Marshal(batch)
orderTopic := fmt.Sprintf("nearle/pos/%s/%s/order", locationID, terminalID)
if t := client.Publish(orderTopic, 1, false, body); t.Wait() && t.Error() != nil {
log.Fatal("publish:", t.Error())
}
fmt.Println("published a bill to", orderTopic)
// The same 20 seconds a real till waits before giving up and re-sending.
select {
case payload := <-acks:
fmt.Println("\nACK RECEIVED:")
var pretty map[string]any
_ = json.Unmarshal(payload, &pretty)
out, _ := json.MarshalIndent(pretty, " ", " ")
fmt.Println(" " + string(out))
case <-time.After(20 * time.Second):
fmt.Println("\nNO ACK within 20s — a real till would keep the bill and send it again")
os.Exit(1)
}
// A heartbeat too, so the Redis presence path is exercised.
health, _ := json.Marshal(map[string]any{
"schema": 1, "status": "online",
"terminal_id": terminalID, "location_id": locationID,
"store_name": "Ragul stores Selvapuram", "app_version": "1.1.0",
"pending_bills": 0, "pending_registrations": 0,
"today_bills": 2, "today_amount": 170.0,
"printer_reachable": false,
"reported_at": time.Now().UTC().Format(time.RFC3339),
})
healthTopic := fmt.Sprintf("nearle/pos/%s/%s/health", locationID, terminalID)
if t := client.Publish(healthTopic, 1, false, health); t.Wait() && t.Error() != nil {
log.Fatal("publish health:", t.Error())
}
fmt.Println("\npublished a heartbeat to", healthTopic)
// The registration uplink. Tested over HTTP early on; this is the same
// service reached over the broker, which is the path a real till uses.
custBatch, _ := json.Marshal(map[string]any{
"schema": 1, "batch_id": "batch-cust-mqtt-0001",
"store_id": locationID, "terminal_id": terminalID,
"customers": []map[string]any{{
"id": "3d7a0000-0000-4000-8000-000000000001",
"mobile": "9840077777",
"name": "MQTT Probe Shopper",
"registered_at": time.Now().UTC().Format(time.RFC3339),
"registered_by_terminal": terminalID,
}},
})
custTopic := fmt.Sprintf("nearle/pos/%s/%s/customer", locationID, terminalID)
if t := client.Publish(custTopic, 1, false, custBatch); t.Wait() && t.Error() != nil {
log.Fatal("publish customer:", t.Error())
}
fmt.Println("published a registration to", custTopic)
select {
case payload := <-acks:
fmt.Println(" registration ACK:", string(payload))
case <-time.After(20 * time.Second):
fmt.Println(" NO ACK for the registration")
os.Exit(1)
}
}