| package main |
|
|
| import ( |
| "encoding/json" |
| "log" |
| "os" |
| "os/signal" |
| "path/filepath" |
| "syscall" |
|
|
| "restaurant-pos/internal/config" |
| "restaurant-pos/internal/database" |
| "restaurant-pos/internal/handlers" |
| "restaurant-pos/internal/middleware" |
| "restaurant-pos/internal/models" |
| "restaurant-pos/internal/services" |
| ws "restaurant-pos/internal/websocket" |
|
|
| "github.com/gofiber/contrib/websocket" |
| "github.com/gofiber/fiber/v2" |
| ) |
|
|
| func main() { |
| log.Println("=== HSS POS System ===") |
| log.Println("Starting server...") |
|
|
| |
| cfg := config.Load() |
|
|
| |
| db, err := database.Initialize(cfg.DBPath) |
| if err != nil { |
| log.Fatalf("Failed to initialize database: %v", err) |
| } |
|
|
| |
| database.Seed(db) |
|
|
| |
| hub := ws.NewHub() |
| go hub.Run() |
|
|
| |
| app := fiber.New(fiber.Config{ |
| AppName: "HSS POS", |
| BodyLimit: 10 * 1024 * 1024, |
| ErrorHandler: customErrorHandler, |
| }) |
|
|
| |
| middleware.Setup(app, cfg) |
|
|
| |
| app.Get("/health", func(c *fiber.Ctx) error { |
| return c.JSON(fiber.Map{ |
| "status": "ok", |
| "service": "restaurant-pos", |
| "ws_clients": hub.ClientCount(), |
| }) |
| }) |
|
|
| |
| app.Get("/api/v1/internal/checkpoint", func(c *fiber.Ctx) error { |
| sqlDB, err := db.DB() |
| if err != nil { |
| return c.Status(500).JSON(fiber.Map{"error": "failed to get DB handle"}) |
| } |
| var busyLog, checkpointed, total int |
| err = sqlDB.QueryRow("PRAGMA wal_checkpoint(TRUNCATE)").Scan(&busyLog, &checkpointed, &total) |
| if err != nil { |
| return c.Status(500).JSON(fiber.Map{"error": err.Error()}) |
| } |
| log.Printf("[DB] WAL checkpoint: busy=%d, checkpointed=%d, total=%d", busyLog, checkpointed, total) |
| return c.JSON(fiber.Map{ |
| "status": "ok", |
| "busy": busyLog, |
| "checkpointed": checkpointed, |
| "total": total, |
| }) |
| }) |
|
|
| |
| |
| |
| authHandler := handlers.NewAuthHandler(db, cfg) |
| productHandler := handlers.NewProductHandler(db) |
| orderHandler := handlers.NewOrderHandler(db, hub) |
| kotHandler := handlers.NewKOTHandler(db, hub) |
| tableHandler := handlers.NewTableHandler(db, hub) |
| inventoryHandler := handlers.NewInventoryHandler(db) |
| vendorHandler := handlers.NewVendorHandler(db) |
| expenseHandler := handlers.NewExpenseHandler(db) |
| customerHandler := handlers.NewCustomerHandler(db) |
| reportHandler := handlers.NewReportHandler(db) |
| settingsHandler := handlers.NewSettingsHandler(db) |
| whatsappHandler := handlers.NewWhatsAppHandler(db, orderHandler) |
| externalHandler := handlers.NewExternalOrderHandler(db, orderHandler) |
| printerHandler := handlers.NewPrinterHandler(db) |
| edcHandler := handlers.NewEDCHandler(db) |
| edcHandler.AutoMigrate() |
|
|
| |
| |
| |
| syncService := services.NewSyncService(db, cfg) |
| orderHandler.SyncService = syncService |
|
|
| syncHandler := handlers.NewSyncHandler(db, cfg, syncService) |
|
|
| |
| relayService := services.NewOrderRelayService(db, cfg) |
| relayService.OrderProcessor = func(platform string, payload json.RawMessage) error { |
| |
| |
| return externalHandler.ProcessRelayedOrder(platform, payload) |
| } |
|
|
| |
| syncService.Start() |
| relayService.Start() |
|
|
| log.Printf("[MODE] Deployment: %s | Location: %s (%s)", cfg.DeploymentMode, cfg.LocationName, cfg.LocationID) |
|
|
| |
| |
| |
| api := app.Group("/api/v1") |
|
|
| |
| auth := api.Group("/auth") |
| auth.Post("/login", authHandler.Login) |
| auth.Post("/pin-login", authHandler.PinLogin) |
|
|
| |
| api.Get("/menu", productHandler.GetMenu) |
|
|
| |
| api.Get("/customers/search", customerHandler.SearchCustomer) |
|
|
| |
| app.Get("/ws", websocket.New(func(c *websocket.Conn) { |
| userID := c.Query("user_id", "anonymous") |
| role := c.Query("role", "") |
| station := c.Query("station", "") |
|
|
| client := &ws.Client{ |
| Conn: c, |
| UserID: userID, |
| Role: role, |
| Station: station, |
| Channels: make(map[string]bool), |
| } |
|
|
| |
| switch role { |
| case "kitchen": |
| if station != "" { |
| client.Channels[station] = true |
| } |
| case "captain": |
| client.Channels["orders"] = true |
| client.Channels["tables"] = true |
| case "cashier": |
| client.Channels["orders"] = true |
| client.Channels["payments"] = true |
| } |
|
|
| hub.Register(client) |
| defer hub.Unregister(client) |
|
|
| |
| for { |
| _, msg, err := c.ReadMessage() |
| if err != nil { |
| break |
| } |
| |
| _ = msg |
| } |
| })) |
|
|
| |
| |
| |
| |
| webhooks := api.Group("/webhooks") |
| webhooks.Get("/whatsapp", whatsappHandler.VerifyWebhook) |
| webhooks.Post("/whatsapp", whatsappHandler.ReceiveMessage) |
| webhooks.Post("/swiggy", externalHandler.SwiggyWebhook) |
| webhooks.Post("/zomato", externalHandler.ZomatoWebhook) |
| webhooks.Post("/external-order", externalHandler.ReceiveExternalOrder) |
| webhooks.Post("/edc/callback", edcHandler.Callback) |
|
|
| |
| |
| |
| sync := api.Group("/sync") |
| sync.Get("/status", syncHandler.Status) |
| sync.Post("/trigger", syncHandler.TriggerSync) |
| sync.Post("/ingest", syncHandler.Ingest) |
| sync.Get("/menu", syncHandler.GetMenu) |
| sync.Get("/locations", syncHandler.ListLocations) |
| sync.Get("/dashboard", syncHandler.CloudDashboard) |
|
|
| |
| relay := sync.Group("/relay") |
| relay.Post("/webhook/:platform", syncHandler.RelayWebhook) |
| relay.Get("/pending", syncHandler.RelayPending) |
| relay.Post("/ack", syncHandler.RelayAck) |
| relay.Get("/stats", syncHandler.RelayStats) |
|
|
| |
| |
| |
| protected := api.Group("", middleware.AuthRequired(cfg, db)) |
|
|
| |
| protected.Get("/auth/me", authHandler.Me) |
| protected.Post("/auth/change-password", authHandler.ChangePassword) |
|
|
| |
| users := protected.Group("/users", middleware.RequireRoles(models.RoleAdmin, models.RoleManager)) |
| users.Get("/", authHandler.ListUsers) |
| users.Post("/", authHandler.CreateUser) |
| users.Put("/:id", authHandler.UpdateUser) |
| users.Patch("/:id/toggle", authHandler.ToggleUserStatus) |
| protected.Get("/roles", authHandler.ListRoles) |
|
|
| |
| categories := protected.Group("/categories") |
| categories.Get("/", productHandler.ListCategories) |
| categories.Get("/:id", productHandler.GetCategory) |
| categories.Post("/", middleware.RequireRoles(models.RoleAdmin, models.RoleManager), productHandler.CreateCategory) |
| categories.Put("/:id", middleware.RequireRoles(models.RoleAdmin, models.RoleManager), productHandler.UpdateCategory) |
| categories.Delete("/:id", middleware.RequireRoles(models.RoleAdmin, models.RoleManager), productHandler.DeleteCategory) |
|
|
| |
| products := protected.Group("/products") |
| products.Get("/", productHandler.ListProducts) |
| products.Get("/:id", productHandler.GetProduct) |
| products.Post("/", middleware.RequireRoles(models.RoleAdmin, models.RoleManager), productHandler.CreateProduct) |
| products.Put("/:id", middleware.RequireRoles(models.RoleAdmin, models.RoleManager), productHandler.UpdateProduct) |
| products.Patch("/:id/availability", productHandler.ToggleAvailability) |
| products.Delete("/:id", middleware.RequireRoles(models.RoleAdmin, models.RoleManager), productHandler.DeleteProduct) |
|
|
| |
| modifiers := protected.Group("/modifier-groups") |
| modifiers.Get("/", productHandler.ListModifierGroups) |
| modifiers.Post("/", middleware.RequireRoles(models.RoleAdmin, models.RoleManager), productHandler.CreateModifierGroup) |
|
|
| |
| orders := protected.Group("/orders") |
| orders.Get("/", orderHandler.ListOrders) |
| orders.Get("/active", orderHandler.ActiveOrders) |
| orders.Get("/:id", orderHandler.GetOrder) |
| orders.Post("/", orderHandler.CreateOrder) |
| orders.Post("/:id/items", orderHandler.AddItems) |
| orders.Post("/:id/discount", orderHandler.ApplyDiscount) |
| orders.Post("/:id/payment", orderHandler.ProcessPayment) |
| orders.Post("/:id/void", middleware.RequireRoles(models.RoleAdmin, models.RoleManager, models.RoleCashier), orderHandler.VoidOrder) |
| orders.Post("/:id/reprint", orderHandler.ReprintBill) |
| orders.Patch("/:id/status", orderHandler.UpdateOrderStatus) |
| orders.Patch("/:id/items/:itemId/cancel", orderHandler.CancelOrderItem) |
| orders.Post("/:id/split", orderHandler.SplitBill) |
| orders.Post("/merge-tables", orderHandler.MergeTables) |
| orders.Post("/transfer-table", orderHandler.TransferTable) |
|
|
| |
| kots := protected.Group("/kots") |
| kots.Get("/", kotHandler.ListKOTs) |
| kots.Get("/stats", kotHandler.KOTStats) |
| kots.Get("/kds", kotHandler.KDSView) |
| kots.Get("/history", kotHandler.KOTHistory) |
| kots.Patch("/:id/status", kotHandler.UpdateKOTStatus) |
| kots.Patch("/:id/items/:itemId/ready", kotHandler.MarkItemReady) |
| kots.Post("/:id/cancel-item", kotHandler.CancelKOTItem) |
|
|
| |
| tables := protected.Group("/tables") |
| tables.Get("/", tableHandler.ListTables) |
| tables.Get("/overview", tableHandler.TableOverview) |
| tables.Get("/sections", tableHandler.ListSections) |
| tables.Post("/sections", middleware.RequireRoles(models.RoleAdmin, models.RoleManager), tableHandler.CreateSection) |
| tables.Post("/", middleware.RequireRoles(models.RoleAdmin, models.RoleManager), tableHandler.CreateTable) |
| tables.Put("/:id", middleware.RequireRoles(models.RoleAdmin, models.RoleManager), tableHandler.UpdateTable) |
| tables.Patch("/:id/status", tableHandler.UpdateTableStatus) |
| tables.Delete("/:id", middleware.RequireRoles(models.RoleAdmin, models.RoleManager), tableHandler.DeleteTable) |
|
|
| |
| inventory := protected.Group("/inventory") |
| inventory.Get("/", inventoryHandler.ListItems) |
| inventory.Get("/low-stock", inventoryHandler.LowStockAlerts) |
| inventory.Get("/:id", inventoryHandler.GetItem) |
| inventory.Post("/", inventoryHandler.CreateItem) |
| inventory.Put("/:id", inventoryHandler.UpdateItem) |
| inventory.Post("/movements", inventoryHandler.AddStockMovement) |
| inventory.Get("/:id/movements", inventoryHandler.GetMovements) |
|
|
| |
| recipes := protected.Group("/recipes") |
| recipes.Get("/", inventoryHandler.ListRecipes) |
| recipes.Get("/:id", inventoryHandler.GetRecipe) |
| recipes.Post("/", inventoryHandler.CreateRecipe) |
| recipes.Put("/:id", inventoryHandler.UpdateRecipe) |
| recipes.Delete("/:id", inventoryHandler.DeleteRecipe) |
|
|
| |
| vendors := protected.Group("/vendors") |
| vendors.Get("/", vendorHandler.ListVendors) |
| vendors.Get("/:id", vendorHandler.GetVendor) |
| vendors.Post("/", vendorHandler.CreateVendor) |
| vendors.Put("/:id", vendorHandler.UpdateVendor) |
|
|
| |
| purchases := protected.Group("/purchases") |
| purchases.Get("/", vendorHandler.ListPurchaseOrders) |
| purchases.Post("/", vendorHandler.CreatePurchaseOrder) |
| purchases.Post("/:id/receive", vendorHandler.ReceivePurchaseOrder) |
|
|
| |
| expenses := protected.Group("/expenses") |
| expenses.Get("/", expenseHandler.ListExpenses) |
| expenses.Get("/summary", expenseHandler.ExpenseSummary) |
| expenses.Post("/", expenseHandler.CreateExpense) |
| expenses.Put("/:id", expenseHandler.UpdateExpense) |
| expenses.Delete("/:id", middleware.RequireRoles(models.RoleAdmin, models.RoleManager), expenseHandler.DeleteExpense) |
|
|
| |
| customers := protected.Group("/customers") |
| customers.Get("/", customerHandler.ListCustomers) |
| customers.Get("/:id", customerHandler.GetCustomer) |
| customers.Post("/", customerHandler.CreateCustomer) |
| customers.Put("/:id", customerHandler.UpdateCustomer) |
| customers.Get("/:id/orders", customerHandler.CustomerOrders) |
| customers.Post("/:id/loyalty", customerHandler.ManageLoyalty) |
|
|
| |
| crm := protected.Group("/crm") |
| crm.Get("/campaigns", customerHandler.ListCampaigns) |
| crm.Post("/campaigns", customerHandler.CreateCampaign) |
|
|
| |
| reports := protected.Group("/reports", middleware.RequireRoles(models.RoleAdmin, models.RoleManager, models.RoleAccountant)) |
| reports.Get("/sales/dashboard", reportHandler.SalesDashboard) |
| reports.Get("/sales", reportHandler.SalesReport) |
| reports.Get("/gst", reportHandler.GSTReport) |
| reports.Get("/items", reportHandler.ItemSalesReport) |
| reports.Get("/categories", reportHandler.CategorySalesReport) |
| reports.Get("/captain", reportHandler.CaptainReport) |
| reports.Get("/kitchen", reportHandler.KitchenReport) |
| reports.Get("/revenue-leakage", reportHandler.RevenueLeakage) |
| reports.Get("/kot", reportHandler.KOTReport) |
| reports.Get("/cancelled-items", reportHandler.CancelledItemsReport) |
| reports.Get("/expenses", reportHandler.ExpenseReport) |
| reports.Get("/inventory", reportHandler.InventoryReport) |
|
|
| |
| settings := protected.Group("/settings", middleware.RequireRoles(models.RoleAdmin)) |
| settings.Get("/config", settingsHandler.GetConfig) |
| settings.Put("/config", settingsHandler.UpdateConfig) |
| settings.Get("/audit-logs", settingsHandler.GetAuditLogs) |
|
|
| |
| printers := protected.Group("/printers", middleware.RequireRoles(models.RoleAdmin, models.RoleManager)) |
| printers.Get("/", printerHandler.ListPrinters) |
| printers.Post("/", printerHandler.CreatePrinter) |
| printers.Get("/:id", printerHandler.GetPrinter) |
| printers.Put("/:id", printerHandler.UpdatePrinter) |
| printers.Delete("/:id", printerHandler.DeletePrinter) |
| printers.Post("/:id/test", printerHandler.TestPrinter) |
|
|
| |
| printRoutes := protected.Group("/print-routes", middleware.RequireRoles(models.RoleAdmin, models.RoleManager)) |
| printRoutes.Get("/", printerHandler.ListRoutes) |
| printRoutes.Post("/", printerHandler.CreateRoute) |
| printRoutes.Delete("/:id", printerHandler.DeleteRoute) |
|
|
| |
| protected.Get("/print-jobs", printerHandler.ListJobs) |
|
|
| |
| protected.Post("/orders/:id/print-bill", printerHandler.PrintBill) |
| protected.Post("/kots/:id/print", printerHandler.PrintKOT) |
|
|
| |
| platforms := protected.Group("/platforms", middleware.RequireRoles(models.RoleAdmin)) |
| platforms.Get("/", externalHandler.ListPlatforms) |
| platforms.Post("/", externalHandler.CreatePlatform) |
| platforms.Get("/:id", externalHandler.GetPlatform) |
| platforms.Put("/:id", externalHandler.UpdatePlatform) |
| platforms.Delete("/:id", externalHandler.DeletePlatform) |
|
|
| |
| onlineOrders := protected.Group("/online-orders") |
| onlineOrders.Get("/", externalHandler.ListOnlineOrders) |
| onlineOrders.Get("/counts", externalHandler.OnlineOrderCounts) |
| onlineOrders.Post("/manual", externalHandler.CreateManualOnlineOrder) |
| onlineOrders.Put("/:id/accept", externalHandler.AcceptOrder) |
| onlineOrders.Put("/:id/reject", externalHandler.RejectOrder) |
| onlineOrders.Put("/:id/ready", externalHandler.MarkReady) |
| onlineOrders.Put("/:id/picked-up", externalHandler.MarkPickedUp) |
| onlineOrders.Put("/:id/delivered", externalHandler.MarkDelivered) |
|
|
| |
| edcRoutes := protected.Group("/edc") |
| edcRoutes.Post("/initiate", edcHandler.Initiate) |
| edcRoutes.Get("/status/:id", edcHandler.Status) |
| edcRoutes.Post("/cancel/:id", edcHandler.Cancel) |
| edcRoutes.Get("/config", edcHandler.GetConfig) |
|
|
| |
| protected.Get("/webhook-logs", middleware.RequireRoles(models.RoleAdmin), externalHandler.ListWebhookLogs) |
|
|
| |
| |
| |
| staticDir := os.Getenv("STATIC_DIR") |
| if staticDir != "" { |
| if info, stErr := os.Stat(staticDir); stErr == nil && info.IsDir() { |
| log.Printf("Serving static frontend from: %s", staticDir) |
| indexFile := filepath.Join(staticDir, "index.html") |
|
|
| app.Get("/*", func(c *fiber.Ctx) error { |
| |
| filePath := filepath.Join(staticDir, c.Path()) |
| if fInfo, fErr := os.Stat(filePath); fErr == nil && !fInfo.IsDir() { |
| return c.SendFile(filePath) |
| } |
| |
| return c.SendFile(indexFile) |
| }) |
| } |
| } |
|
|
| |
| |
| |
| go func() { |
| port := ":" + cfg.Port |
| log.Printf("Server starting on http://localhost%s", port) |
| log.Printf("API docs: http://localhost%s/api/v1", port) |
| log.Printf("WebSocket: ws://localhost%s/ws", port) |
| log.Printf("Environment: %s", cfg.Environment) |
|
|
| if err := app.Listen(port); err != nil { |
| log.Fatalf("Server failed to start: %v", err) |
| } |
| }() |
|
|
| |
| quit := make(chan os.Signal, 1) |
| signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) |
| <-quit |
|
|
| log.Println("Shutting down server...") |
| app.Shutdown() |
| log.Println("Server stopped") |
| } |
|
|
| func customErrorHandler(c *fiber.Ctx, err error) error { |
| code := fiber.StatusInternalServerError |
| if e, ok := err.(*fiber.Error); ok { |
| code = e.Code |
| } |
|
|
| return c.Status(code).JSON(fiber.Map{ |
| "success": false, |
| "error": err.Error(), |
| }) |
| } |
|
|