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>
149 lines
4.8 KiB
Go
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)
|
|
}
|
|
}
|