package main import ( "fmt" "log" "nearle/db" "nearle/facade" "nearle/messaging" "nearle/models" "nearle/routes" "os" "os/signal" "strings" "syscall" "time" _ "time/tzdata" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/joho/godotenv" "gorm.io/gorm" ) func init() { godotenv.Load() } func main() { app := fiber.New() app.Use(cors.New(cors.Config{ AllowHeaders: "Origin,Content-Type,Accept,Content-Length,Accept-Language,Accept-Encoding,Connection,Access-Control-Allow-Origin", AllowOrigins: "*", AllowCredentials: true, AllowMethods: "GET,POST,HEAD,PUT,DELETE,PATCH,OPTIONS", })) fmt.Println("🌐 Connecting to databases...") db.Connect() fmt.Println("✅ Database connections established!") // Shared with the express backend. POS terminal presence lives here under a // TTL; optional, because losing the health board is an inconvenience and // losing a sale is not. db.InitRedis() // Ensure schema is updated db.DB.AutoMigrate(&models.StockRequest{}) // Counter sales from the in-store terminals. Separate tables from `orders` // because a bill carries a cashier, a terminal, rounding, promos, loyalty // and a payment split that `orders` has nowhere to put. if err := db.DB.AutoMigrate(&models.PosOrders{}, &models.PosOrderItems{}); err != nil { log.Fatal("POS schema migration failed:", err) } f := facade.NewFacade(db.DB, db.CatalogueDB) routes.RegisterRoutes(app, f) // POS terminals reach the ingest over MQTT when MQTT_URL is set, and over // HTTP otherwise. Both land on the same service, so a bill cannot behave // differently depending on how it arrived. // // A broker that is configured but unreachable is fatal on purpose: coming // up healthy while every till quietly queues is the worse failure. posMqtt, err := messaging.StartPosMqttConsumer(f.PosService()) if err != nil { log.Fatal("POS MQTT consumer failed to start:", err) } // Start server go func() { if err := app.Listen(":1122"); err != nil { log.Fatal("Server failed to start:", err) } }() gracefulShutdown(posMqtt) } func selectDBMiddleware(c *fiber.Ctx) error { path := c.Path() result := strings.Split(path, "/") var flavour string if len(result) > 1 { flavour = result[1] } var currentDB *gorm.DB switch flavour { case "dev", "live": currentDB = db.DB } if currentDB != nil { c.Locals("DB", currentDB) } return c.Next() } func gracefulShutdown(posMqtt *messaging.PosMqttConsumer) { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) <-c fmt.Println("\nShutting down gracefully...") // Drained before anything else: a bill mid-commit still gets its ack, and // without one the terminal would hold it and send it again on restart. posMqtt.Close() db.CloseRedis() // Normally: close db.DB_DEV and db.DB_LIVE // Example: // closeDB(db.DB_DEV) // closeDB(db.DB_LIVE) time.Sleep(2 * time.Second) fmt.Println("Shutdown complete.") os.Exit(0) }