Files
apskel-pos-backend/internal/router/router.go
T
efrilmandClaude Opus 5.5 798a36bd6c feat(enakgame): game sessions, rewards, vouchers, budgets and events
EnakGame phases 1-8 of docs/tasks-enakgame.md (EG-101 to EG-803), built on the
existing EnakPoint/EnakCoin wallet (docs/rfc-enakgame.md).

Foundation (phase 1)
- Migrations 000103-000106: games extended with organization, slug, status,
  entry cost and result rules, old games archived (not deleted); budgets,
  versioned reward configs, sessions and session rewards; the ledger types
  GAME_SPEND_REFUND, GAME_REWARD and REWARD_REDEEM_REFUND; audit_logs.
- AuditLogger writes in the caller's transaction only.
- enakgame.limit.user_daily and global_daily organization settings.

Games and sessions (phases 2-4)
- Admin /marketing/enakgame: games, reward config versions (immutable but for
  status, one ACTIVE per game), budgets with non-overlapping global periods and
  a daily job opening the next month.
- Customer /customer/enakgame: start (Idempotency-Key, entry cost and config
  frozen on the session), complete (result validation, reward engine, max_reward
  cap, daily limits via game_reward_counters, one GAME_REWARD per budget),
  automatic refunds for system errors and deactivated games, and a session job.
- Reward engine: FIXED, SCORE_BASED, OUTCOME_BASED, PROBABILITY (crypto/rand),
  rounded down.

Vouchers and budgets (phases 5-6)
- Migration 000108 and 000107: vouchers, codes, redemptions, cost attribution;
  Economy Guard counters.
- STATIC and CODE_POOL redemption in one transaction with the REDEEM PIN action;
  realized cost traced through the lots to the budget that paid the reward.
- Budget metrics: realized cost, forecast, exposure and status. Migrations
  000109-000110 add the wallet_lots indexes they need, built CONCURRENTLY.

Events (phase 7)
- Migration 000111: game events, each with its own EVENT budget. Event extras
  stack per PRD §16 defaults, with event and per-customer limits.

External vouchers (phase 8)
- VoucherProvider contract, two-step PENDING redemption and a recovery job,
  tested with a fake provider. No provider adapter is registered yet, so
  EXTERNAL vouchers stay out of the catalog.

Not yet decided before release: reward rounding, event stacking, budget
exhaustion policy and thresholds (RFC §19.2). Migrations 000103-000111 have
not been run on any shared database.

Also fixes a leftover PAYMENT filter in a wallet test and a data race in a
test PIN fake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:53:14 +07:00

760 lines
44 KiB
Go

package router
import (
"apskel-pos-be/config"
"apskel-pos-be/internal/handler"
"apskel-pos-be/internal/middleware"
"apskel-pos-be/internal/service"
"apskel-pos-be/internal/transformer"
"apskel-pos-be/internal/validator"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
)
type Router struct {
config *config.Config
healthHandler *handler.HealthHandler
authHandler *handler.AuthHandler
userHandler *handler.UserHandler
organizationHandler *handler.OrganizationHandler
outletHandler *handler.OutletHandler
outletSettingHandler *handler.OutletSettingHandlerImpl
categoryHandler *handler.CategoryHandler
productHandler *handler.ProductHandler
productVariantHandler *handler.ProductVariantHandler
inventoryHandler *handler.InventoryHandler
orderHandler *handler.OrderHandler
fileHandler *handler.FileHandler
customerHandler *handler.CustomerHandler
paymentMethodHandler *handler.PaymentMethodHandler
analyticsHandler *handler.AnalyticsHandler
reportHandler *handler.ReportHandler
tableHandler *handler.TableHandler
unitHandler *handler.UnitHandler
ingredientHandler *handler.IngredientHandler
productRecipeHandler *handler.ProductRecipeHandler
vendorHandler *handler.VendorHandler
purchaseOrderHandler *handler.PurchaseOrderHandler
purchaseCategoryHandler *handler.PurchaseCategoryHandler
unitConverterHandler *handler.IngredientUnitConverterHandler
chartOfAccountTypeHandler *handler.ChartOfAccountTypeHandler
chartOfAccountHandler *handler.ChartOfAccountHandler
accountHandler *handler.AccountHandler
orderIngredientTransactionHandler *handler.OrderIngredientTransactionHandler
gamificationHandler *handler.GamificationHandler
rewardHandler *handler.RewardHandler
campaignHandler *handler.CampaignHandler
customerAuthHandler *handler.CustomerAuthHandler
customerPointsHandler *handler.CustomerPointsHandler
spinGameHandler *handler.SpinGameHandler
userDeviceHandler *handler.UserDeviceHandler
notificationHandler *handler.NotificationHandler
selfOrderHandler *handler.SelfOrderHandler
productOutletPriceHandler *handler.ProductOutletPriceHandler
expenseHandler *handler.ExpenseHandler
cashAdvanceHandler *handler.CashAdvanceHandler
walletAdminHandler *handler.WalletAdminHandler
loyaltySettingsHandler *handler.LoyaltySettingsHandler
customerPinHandler *handler.CustomerPinHandler
customerWalletHandler *handler.CustomerWalletHandler
customerDeviceHandler *handler.CustomerDeviceHandler
customerOutletHandler *handler.CustomerOutletHandler
customerOrderHandler *handler.CustomerOrderHandler
enakGameAdminHandler *handler.EnakGameAdminHandler
enakGameCustomerHandler *handler.EnakGameCustomerHandler
authMiddleware *middleware.AuthMiddleware
customerAuthMiddleware *middleware.CustomerAuthMiddleware
redisClient *redis.Client
}
func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, purchaseCategoryService service.PurchaseCategoryService, purchaseCategoryValidator validator.PurchaseCategoryValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware, userDeviceService service.UserDeviceService, userDeviceValidator validator.UserDeviceValidator, notificationService service.NotificationService, notificationValidator validator.NotificationValidator, productOutletPriceService service.ProductOutletPriceService, productOutletPriceValidator validator.ProductOutletPriceValidator, selfOrderHandler *handler.SelfOrderHandler, expenseService *service.ExpenseServiceImpl, expenseValidator *validator.ExpenseValidatorImpl, cashAdvanceService service.CashAdvanceService, cashAdvanceValidator validator.CashAdvanceValidator, walletAdminService service.WalletAdminService, walletValidator validator.WalletValidator, loyaltySettingsService service.LoyaltySettingsService, customerPinService service.CustomerPinService, customerWalletService service.CustomerWalletService, customerDeviceService service.CustomerDeviceService, customerOutletService service.CustomerOutletService, customerOrderService service.CustomerOrderService, enakGameAdminService service.EnakGameAdminService, enakGameCustomerService service.EnakGameCustomerService, redisClient *redis.Client) *Router {
return &Router{
config: cfg,
healthHandler: healthHandler,
authHandler: handler.NewAuthHandler(authService),
userHandler: handler.NewUserHandler(userService, userValidator),
organizationHandler: handler.NewOrganizationHandler(organizationService, organizationValidator),
outletHandler: handler.NewOutletHandler(outletService, outletValidator),
outletSettingHandler: handler.NewOutletSettingHandlerImpl(outletSettingService),
categoryHandler: handler.NewCategoryHandler(categoryService, categoryValidator),
productHandler: handler.NewProductHandler(productService, productValidator),
inventoryHandler: handler.NewInventoryHandler(inventoryService, inventoryValidator),
orderHandler: handler.NewOrderHandler(orderService, orderValidator, transformer.NewTransformer()),
fileHandler: handler.NewFileHandler(fileService, fileValidator, transformer.NewTransformer()),
customerHandler: handler.NewCustomerHandler(customerService, customerValidator),
paymentMethodHandler: handler.NewPaymentMethodHandler(paymentMethodService, paymentMethodValidator),
analyticsHandler: handler.NewAnalyticsHandler(analyticsService, transformer.NewTransformer()),
reportHandler: handler.NewReportHandler(reportService, userService),
tableHandler: handler.NewTableHandler(tableService, tableValidator, cfg.Server.SelfOrderUrl),
unitHandler: handler.NewUnitHandler(unitService),
ingredientHandler: handler.NewIngredientHandler(ingredientService),
productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService),
vendorHandler: handler.NewVendorHandler(vendorService, vendorValidator),
purchaseOrderHandler: handler.NewPurchaseOrderHandler(purchaseOrderService, purchaseOrderValidator),
purchaseCategoryHandler: handler.NewPurchaseCategoryHandler(purchaseCategoryService, purchaseCategoryValidator),
unitConverterHandler: handler.NewIngredientUnitConverterHandler(unitConverterService, unitConverterValidator),
chartOfAccountTypeHandler: handler.NewChartOfAccountTypeHandler(chartOfAccountTypeService, chartOfAccountTypeValidator),
chartOfAccountHandler: handler.NewChartOfAccountHandler(chartOfAccountService, chartOfAccountValidator),
accountHandler: handler.NewAccountHandler(accountService, accountValidator),
orderIngredientTransactionHandler: handler.NewOrderIngredientTransactionHandler(&orderIngredientTransactionService, orderIngredientTransactionValidator),
gamificationHandler: handler.NewGamificationHandler(gamificationService, gamificationValidator),
rewardHandler: handler.NewRewardHandler(rewardService, rewardValidator),
campaignHandler: handler.NewCampaignHandler(campaignService, campaignValidator),
customerAuthHandler: handler.NewCustomerAuthHandler(customerAuthService, customerAuthValidator),
customerPointsHandler: handler.NewCustomerPointsHandler(customerPointsService),
spinGameHandler: handler.NewSpinGameHandler(spinGameService),
authMiddleware: authMiddleware,
customerAuthMiddleware: customerAuthMiddleware,
productVariantHandler: handler.NewProductVariantHandler(productVariantService, productVariantValidator),
userDeviceHandler: handler.NewUserDeviceHandler(userDeviceService, userDeviceValidator),
notificationHandler: handler.NewNotificationHandler(notificationService, notificationValidator),
selfOrderHandler: selfOrderHandler,
productOutletPriceHandler: handler.NewProductOutletPriceHandler(productOutletPriceService, productOutletPriceValidator),
expenseHandler: handler.NewExpenseHandler(expenseService, expenseValidator),
cashAdvanceHandler: handler.NewCashAdvanceHandler(cashAdvanceService, cashAdvanceValidator),
walletAdminHandler: handler.NewWalletAdminHandler(walletAdminService, walletValidator),
loyaltySettingsHandler: handler.NewLoyaltySettingsHandler(loyaltySettingsService),
customerPinHandler: handler.NewCustomerPinHandler(customerPinService),
customerWalletHandler: handler.NewCustomerWalletHandler(customerWalletService),
customerDeviceHandler: handler.NewCustomerDeviceHandler(customerDeviceService),
customerOutletHandler: handler.NewCustomerOutletHandler(customerOutletService),
customerOrderHandler: handler.NewCustomerOrderHandler(customerOrderService),
enakGameAdminHandler: handler.NewEnakGameAdminHandler(enakGameAdminService),
enakGameCustomerHandler: handler.NewEnakGameCustomerHandler(enakGameCustomerService),
redisClient: redisClient,
}
}
func (r *Router) Init() *gin.Engine {
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
engine.Use(
middleware.JsonAPI(),
middleware.CORS(),
middleware.CorrelationID(),
middleware.Recover(),
middleware.HTTPStatLogger(),
middleware.PopulateContext(),
)
r.addAppRoutes(engine)
return engine
}
func (r *Router) addAppRoutes(rg *gin.Engine) {
rg.GET("/health", r.healthHandler.HealthCheck)
v1 := rg.Group("/api/v1")
{
auth := v1.Group("/auth")
{
auth.POST("/login", r.authHandler.Login)
auth.POST("/logout", r.authHandler.Logout)
auth.POST("/refresh", r.authHandler.RefreshToken)
auth.GET("/validate", r.authHandler.ValidateToken)
auth.GET("/profile", r.authHandler.GetProfile)
}
// Customer authentication routes
customerAuth := v1.Group("/customer-auth")
{
customerAuth.POST("/check-phone", r.customerAuthHandler.CheckPhone)
customerAuth.POST("/register/start", r.customerAuthHandler.RegisterStart)
customerAuth.POST("/register/verify-otp", r.customerAuthHandler.RegisterVerifyOtp)
customerAuth.POST("/register/set-password", r.customerAuthHandler.RegisterSetPassword)
customerAuth.POST("/login", r.customerAuthHandler.Login)
customerAuth.POST("/resend-otp", r.customerAuthHandler.ResendOtp)
}
// Customer authenticated routes
customer := v1.Group("/customer")
customer.Use(r.customerAuthMiddleware.ValidateCustomerToken())
{
customer.GET("/points", r.customerPointsHandler.GetCustomerPoints)
customer.GET("/wallet", r.customerPointsHandler.GetCustomerWallet)
customer.GET("/wallet/transactions", r.customerPointsHandler.GetCustomerWalletTransactions)
customer.GET("/wallet/expiring", r.customerPointsHandler.GetCustomerWalletExpiring)
customer.GET("/wallet/exchange/preview", r.customerWalletHandler.PreviewExchange)
customer.POST("/wallet/exchange", r.customerWalletHandler.Exchange)
customer.GET("/wallet/transfer/recipient", r.customerWalletHandler.TransferRecipient)
customer.POST("/wallet/transfer", r.customerWalletHandler.Transfer)
customer.PUT("/devices", r.customerDeviceHandler.Register)
customer.DELETE("/devices/:device_id", r.customerDeviceHandler.Unregister)
customer.GET("/outlets", r.customerOutletHandler.List)
customer.GET("/orders", r.customerOrderHandler.List)
customer.GET("/orders/:id", r.customerOrderHandler.Detail)
// PIN that approves moving EnakPoint and EnakCoin (docs/prd-point-coin.md F11)
customer.GET("/pin/status", r.customerPinHandler.Status)
customer.POST("/pin/otp", r.customerPinHandler.RequestOtp)
customer.POST("/pin", r.customerPinHandler.CreatePin)
customer.PUT("/pin", r.customerPinHandler.ChangePin)
customer.POST("/pin/reset", r.customerPinHandler.ResetPin)
customer.GET("/games", r.customerPointsHandler.GetCustomerGames)
customer.GET("/ferris-wheel", r.customerPointsHandler.GetFerrisWheelGame)
customer.POST("/spin", r.spinGameHandler.PlaySpinGame)
// EnakGame (docs/rfc-enakgame.md §11). /enakgame because /customer/games is
// the old spin.
enakGame := customer.Group("/enakgame")
{
enakGame.GET("/games", r.enakGameCustomerHandler.ListGames)
enakGame.POST("/sessions", r.enakGameCustomerHandler.StartSession)
enakGame.GET("/sessions", r.enakGameCustomerHandler.ListSessions)
enakGame.GET("/sessions/:id", r.enakGameCustomerHandler.GetSession)
enakGame.POST("/sessions/:id/complete", r.enakGameCustomerHandler.CompleteSession)
enakGame.GET("/vouchers", r.enakGameCustomerHandler.ListVouchers)
enakGame.POST("/vouchers/:id/redeem", r.enakGameCustomerHandler.RedeemVoucher)
enakGame.GET("/redemptions", r.enakGameCustomerHandler.ListRedemptions)
}
}
selfOrder := v1.Group("/self-order")
{
selfOrder.GET("/table/:token", r.selfOrderHandler.ValidateToken)
selfOrder.GET("/categories", r.selfOrderHandler.ListCategories)
selfOrder.GET("/menu", r.selfOrderHandler.GetMenu)
selfOrder.POST("/orders", r.selfOrderHandler.CreateOrder)
selfOrder.GET("/orders/:session_id", r.selfOrderHandler.GetOrdersBySession)
}
organizations := v1.Group("/organizations")
{
organizations.POST("", r.organizationHandler.CreateOrganization)
}
protected := v1.Group("")
protected.Use(r.authMiddleware.RequireAuth())
{
users := protected.Group("/users")
{
adminUsers := users.Group("")
adminUsers.Use(r.authMiddleware.RequireAdminOrManager())
{
adminUsers.POST("", r.userHandler.CreateUser)
adminUsers.GET("", r.userHandler.ListUsers)
adminUsers.GET("/:id", r.userHandler.GetUser)
adminUsers.PUT("/:id", r.userHandler.UpdateUser)
adminUsers.DELETE("/:id", r.userHandler.DeleteUser)
adminUsers.PUT("/:id/activate", r.userHandler.ActivateUser)
adminUsers.PUT("/:id/deactivate", r.userHandler.DeactivateUser)
adminUsers.POST("/select-outlet", r.userHandler.UpdateUserOutlet)
}
users.PUT("/:id/password", r.userHandler.ChangePassword)
}
protectedOrganizations := protected.Group("/organizations")
{
adminOrgRoutes := protectedOrganizations.Group("")
adminOrgRoutes.Use(r.authMiddleware.RequireSuperAdmin())
{
adminOrgRoutes.GET("", r.organizationHandler.ListOrganizations)
adminOrgRoutes.GET("/:id", r.organizationHandler.GetOrganization)
adminOrgRoutes.PUT("/:id", r.organizationHandler.UpdateOrganization)
adminOrgRoutes.DELETE("/:id", r.organizationHandler.DeleteOrganization)
}
}
categories := protected.Group("/categories")
categories.Use(r.authMiddleware.RequireAdminOrManager())
{
categories.POST("", r.categoryHandler.CreateCategory)
categories.GET("", r.categoryHandler.ListCategories)
categories.GET("/:id", r.categoryHandler.GetCategory)
categories.PUT("/:id", r.categoryHandler.UpdateCategory)
categories.DELETE("/:id", r.categoryHandler.DeleteCategory)
}
units := protected.Group("/units")
units.Use(r.authMiddleware.RequireAdminOrManager())
{
units.POST("", r.unitHandler.Create)
units.GET("", r.unitHandler.GetAll)
units.GET("/:id", r.unitHandler.GetByID)
units.PUT("/:id", r.unitHandler.Update)
units.DELETE("/:id", r.unitHandler.Delete)
}
products := protected.Group("/products")
products.Use(r.authMiddleware.RequireAdminOrManager())
{
products.POST("", r.productHandler.CreateProduct)
products.GET("", r.productHandler.ListProducts)
products.GET("/all", r.productHandler.ListProductAll)
products.GET("/:id", r.productHandler.GetProduct)
products.PUT("/:id", r.productHandler.UpdateProduct)
products.DELETE("/:id", r.productHandler.DeleteProduct)
}
productOutletPrices := protected.Group("/product-outlet-prices")
productOutletPrices.Use(r.authMiddleware.RequireAdminOrManager())
{
productOutletPrices.POST("", r.productOutletPriceHandler.Upsert)
productOutletPrices.POST("/bulk", r.productOutletPriceHandler.BulkUpsert)
productOutletPrices.GET("/product/:product_id", r.productOutletPriceHandler.GetByProduct)
productOutletPrices.GET("/outlet/:outlet_id", r.productOutletPriceHandler.GetByOutlet)
productOutletPrices.GET("/product/:product_id/outlet/:outlet_id", r.productOutletPriceHandler.GetByProductAndOutlet)
productOutletPrices.DELETE("/:id", r.productOutletPriceHandler.Delete)
}
productVariants := protected.Group("/product-variants")
{
productVariants.POST("", r.productVariantHandler.CreateProductVariant)
productVariants.PUT("/:id", r.productVariantHandler.UpdateProductVariant)
productVariants.DELETE("/:id", r.productVariantHandler.DeleteProductVariant)
productVariants.GET("/:id", r.productVariantHandler.GetProductVariant)
}
inventory := protected.Group("/inventory")
inventory.Use(r.authMiddleware.RequireAdminOrManager())
{
inventory.POST("", r.inventoryHandler.CreateInventory)
inventory.GET("", r.inventoryHandler.ListInventory)
inventory.GET("/:id", r.inventoryHandler.GetInventory)
inventory.PUT("/:id", r.inventoryHandler.UpdateInventory)
inventory.DELETE("/:id", r.inventoryHandler.DeleteInventory)
inventory.POST("/adjust", r.inventoryHandler.AdjustInventory)
inventory.POST("/restock", r.inventoryHandler.RestockInventory)
inventory.GET("/low-stock/:outlet_id", r.inventoryHandler.GetLowStockItems)
inventory.GET("/zero-stock/:outlet_id", r.inventoryHandler.GetZeroStockItems)
inventory.GET("/report/summary/:outlet_id", r.inventoryHandler.GetInventoryReportSummary)
inventory.GET("/report/details/:outlet_id", r.inventoryHandler.GetInventoryReportDetails)
}
orders := protected.Group("/orders")
orders.Use(r.authMiddleware.RequireAdminOrManager())
{
orders.GET("", r.orderHandler.ListOrders)
orders.GET("/:id", r.orderHandler.GetOrderByID)
orders.POST("", r.orderHandler.CreateOrder)
orders.POST("/:id/add-items", middleware.IdempotencyMiddleware(r.redisClient), r.orderHandler.AddToOrder)
orders.PUT("/:id", r.orderHandler.UpdateOrder)
orders.PUT("/:id/customer", r.orderHandler.SetOrderCustomer)
orders.POST("/void", middleware.IdempotencyMiddleware(r.redisClient), r.orderHandler.VoidOrder)
orders.POST("/:id/refund", middleware.IdempotencyMiddleware(r.redisClient), r.orderHandler.RefundOrder)
orders.POST("/split-bill", r.orderHandler.SplitBill)
}
payments := protected.Group("/payments")
payments.Use(r.authMiddleware.RequireAdminOrManager())
{
payments.POST("", middleware.IdempotencyMiddleware(r.redisClient), r.orderHandler.CreatePayment)
payments.POST("/:id/refund", middleware.IdempotencyMiddleware(r.redisClient), r.orderHandler.RefundPayment)
}
paymentMethods := protected.Group("/payment-methods")
paymentMethods.Use(r.authMiddleware.RequireAdminOrManager())
{
paymentMethods.POST("", r.paymentMethodHandler.CreatePaymentMethod)
paymentMethods.GET("", r.paymentMethodHandler.ListPaymentMethods)
paymentMethods.GET("/:id", r.paymentMethodHandler.GetPaymentMethod)
paymentMethods.PUT("/:id", r.paymentMethodHandler.UpdatePaymentMethod)
paymentMethods.DELETE("/:id", r.paymentMethodHandler.DeletePaymentMethod)
paymentMethods.GET("/organization/:organization_id/active", r.paymentMethodHandler.GetActivePaymentMethodsByOrganization)
}
files := protected.Group("/files")
files.Use(r.authMiddleware.RequireAdminOrManager())
{
files.GET("/organization", r.fileHandler.GetFilesByOrganization)
files.GET("/user", r.fileHandler.GetFilesByUser)
files.GET("/:id", r.fileHandler.GetFileByID)
files.POST("/upload", r.fileHandler.UploadFile)
files.PUT("/:id", r.fileHandler.UpdateFile)
}
customers := protected.Group("/customers")
customers.Use(r.authMiddleware.RequireAdminOrManager())
{
customers.POST("", r.customerHandler.CreateCustomer)
customers.GET("", r.customerHandler.ListCustomers)
customers.GET("/:id", r.customerHandler.GetCustomer)
customers.PUT("/:id", r.customerHandler.UpdateCustomer)
customers.DELETE("/:id", r.customerHandler.DeleteCustomer)
customers.POST("/set-default", r.customerHandler.SetDefaultCustomer)
customers.GET("/default", r.customerHandler.GetDefaultCustomer)
}
analytics := protected.Group("/analytics")
analytics.Use(r.authMiddleware.RequireAdminOrManager())
{
analytics.GET("/payment-methods", r.analyticsHandler.GetPaymentMethodAnalytics)
analytics.GET("/sales", r.analyticsHandler.GetSalesAnalytics)
analytics.GET("/purchasing", r.analyticsHandler.GetPurchasingAnalytics)
analytics.GET("/products", r.analyticsHandler.GetProductAnalytics)
analytics.GET("/categories", r.analyticsHandler.GetProductAnalyticsPerCategory)
analytics.GET("/profit-sharing", r.analyticsHandler.GetProductAnalyticsPerParentCategory)
analytics.GET("/profit-sharing/:parent_category_id", r.analyticsHandler.GetParentCategoryAnalyticsDetail)
analytics.GET("/dashboard", r.analyticsHandler.GetDashboardAnalytics)
analytics.GET("/profit-loss", r.analyticsHandler.GetProfitLossAnalytics)
analytics.GET("/exclusive-summary/period", r.analyticsHandler.GetExclusiveSummaryPeriod)
analytics.GET("/exclusive-summary/monthly", r.analyticsHandler.GetExclusiveSummaryMonthly)
analytics.GET("/exclusive-summary/mtd", r.analyticsHandler.GetExclusiveSummaryMTD)
}
tables := protected.Group("/tables")
tables.Use(r.authMiddleware.RequireAdminOrManager())
{
tables.POST("", r.tableHandler.Create)
tables.GET("", r.tableHandler.List)
tables.GET("/:id", r.tableHandler.GetByID)
tables.PUT("/:id", r.tableHandler.Update)
tables.DELETE("/:id", r.tableHandler.Delete)
tables.POST("/:id/occupy", r.tableHandler.OccupyTable)
tables.POST("/:id/release", r.tableHandler.ReleaseTable)
tables.GET("/:id/qr", r.tableHandler.GenerateQRCode)
}
ingredients := protected.Group("/ingredients")
ingredients.Use(r.authMiddleware.RequireAdminOrManagerOrPurchasing())
{
ingredients.POST("", r.ingredientHandler.Create)
ingredients.GET("", r.ingredientHandler.GetAll)
ingredients.GET("/:id", r.ingredientHandler.GetByID)
ingredients.PUT("/:id", r.ingredientHandler.Update)
ingredients.DELETE("/:id", r.ingredientHandler.Delete)
ingredients.POST("/:id/compositions", r.ingredientHandler.AddCompositions)
ingredients.PUT("/compositions/:composition_id", r.ingredientHandler.UpdateComposition)
ingredients.DELETE("/compositions/:composition_id", r.ingredientHandler.DeleteComposition)
}
vendors := protected.Group("/vendors")
vendors.Use(r.authMiddleware.RequireAdminOrManagerOrPurchasing())
{
vendors.POST("", r.vendorHandler.CreateVendor)
vendors.GET("", r.vendorHandler.ListVendors)
vendors.GET("/active", r.vendorHandler.GetActiveVendors)
vendors.GET("/:id", r.vendorHandler.GetVendor)
vendors.PUT("/:id", r.vendorHandler.UpdateVendor)
vendors.DELETE("/:id", r.vendorHandler.DeleteVendor)
}
purchaseOrders := protected.Group("/purchase-orders")
purchaseOrders.Use(r.authMiddleware.RequireAdminOrManagerOrPurchasing())
{
purchaseOrders.POST("", r.purchaseOrderHandler.CreatePurchaseOrder)
purchaseOrders.GET("", r.purchaseOrderHandler.ListPurchaseOrders)
purchaseOrders.GET("/status/:status", r.purchaseOrderHandler.GetPurchaseOrdersByStatus)
purchaseOrders.GET("/overdue", r.purchaseOrderHandler.GetOverduePurchaseOrders)
purchaseOrders.GET("/teams", r.purchaseOrderHandler.ListPurchaseTeams)
purchaseOrders.GET("/:id", r.purchaseOrderHandler.GetPurchaseOrder)
purchaseOrders.PUT("/:id", r.purchaseOrderHandler.UpdatePurchaseOrder)
purchaseOrders.PUT("/:id/status/:status", r.purchaseOrderHandler.UpdatePurchaseOrderStatus)
purchaseOrders.DELETE("/:id", r.purchaseOrderHandler.DeletePurchaseOrder)
}
purchaseCategories := protected.Group("/purchase-categories")
purchaseCategories.Use(r.authMiddleware.RequireAdminOrManagerOrPurchasing())
{
purchaseCategories.POST("", r.purchaseCategoryHandler.CreatePurchaseCategory)
purchaseCategories.GET("", r.purchaseCategoryHandler.ListPurchaseCategories)
purchaseCategories.GET("/:id", r.purchaseCategoryHandler.GetPurchaseCategory)
purchaseCategories.PUT("/:id", r.purchaseCategoryHandler.UpdatePurchaseCategory)
purchaseCategories.DELETE("/:id", r.purchaseCategoryHandler.DeletePurchaseCategory)
}
unitConverters := protected.Group("/unit-converters")
unitConverters.Use(r.authMiddleware.RequireAdminOrManagerOrPurchasing())
{
unitConverters.POST("", r.unitConverterHandler.CreateIngredientUnitConverter)
unitConverters.GET("", r.unitConverterHandler.ListIngredientUnitConverters)
unitConverters.GET("/ingredient/:ingredient_id", r.unitConverterHandler.GetConvertersForIngredient)
unitConverters.GET("/ingredient/:ingredient_id/units", r.unitConverterHandler.GetUnitsByIngredientID)
unitConverters.POST("/convert", r.unitConverterHandler.ConvertUnit)
unitConverters.GET("/:id", r.unitConverterHandler.GetIngredientUnitConverter)
unitConverters.PUT("/:id", r.unitConverterHandler.UpdateIngredientUnitConverter)
unitConverters.DELETE("/:id", r.unitConverterHandler.DeleteIngredientUnitConverter)
}
productRecipes := protected.Group("/product-recipes")
productRecipes.Use(r.authMiddleware.RequireAdminOrManager())
{
productRecipes.POST("", r.productRecipeHandler.Create)
productRecipes.POST("/bulk", r.productRecipeHandler.BulkCreate)
productRecipes.GET("/:id", r.productRecipeHandler.GetByID)
productRecipes.PUT("/:id", r.productRecipeHandler.Update)
productRecipes.DELETE("/:id", r.productRecipeHandler.Delete)
productRecipes.GET("/product/:product_id", r.productRecipeHandler.GetByProductID)
productRecipes.GET("/ingredient/:ingredient_id", r.productRecipeHandler.GetByIngredientID)
}
// Accounting routes
chartOfAccountTypes := protected.Group("/chart-of-account-types")
chartOfAccountTypes.Use(r.authMiddleware.RequireAdminOrManager())
{
chartOfAccountTypes.POST("", r.chartOfAccountTypeHandler.CreateChartOfAccountType)
chartOfAccountTypes.GET("", r.chartOfAccountTypeHandler.ListChartOfAccountTypes)
chartOfAccountTypes.GET("/:id", r.chartOfAccountTypeHandler.GetChartOfAccountTypeByID)
chartOfAccountTypes.PUT("/:id", r.chartOfAccountTypeHandler.UpdateChartOfAccountType)
chartOfAccountTypes.DELETE("/:id", r.chartOfAccountTypeHandler.DeleteChartOfAccountType)
}
chartOfAccounts := protected.Group("/chart-of-accounts")
chartOfAccounts.Use(r.authMiddleware.RequireAdminOrManager())
{
chartOfAccounts.POST("", r.chartOfAccountHandler.CreateChartOfAccount)
chartOfAccounts.GET("", r.chartOfAccountHandler.ListChartOfAccounts)
chartOfAccounts.GET("/:id", r.chartOfAccountHandler.GetChartOfAccountByID)
chartOfAccounts.PUT("/:id", r.chartOfAccountHandler.UpdateChartOfAccount)
chartOfAccounts.DELETE("/:id", r.chartOfAccountHandler.DeleteChartOfAccount)
chartOfAccounts.GET("/organization/:organization_id", r.chartOfAccountHandler.GetChartOfAccountsByOrganization)
chartOfAccounts.GET("/organization/:organization_id/type/:type_id", r.chartOfAccountHandler.GetChartOfAccountsByType)
}
accounts := protected.Group("/accounts")
accounts.Use(r.authMiddleware.RequireAdminOrManager())
{
accounts.POST("", r.accountHandler.CreateAccount)
accounts.GET("", r.accountHandler.ListAccounts)
accounts.GET("/:id", r.accountHandler.GetAccountByID)
accounts.PUT("/:id", r.accountHandler.UpdateAccount)
accounts.DELETE("/:id", r.accountHandler.DeleteAccount)
accounts.GET("/organization/:organization_id", r.accountHandler.GetAccountsByOrganization)
accounts.GET("/chart-of-account/:chart_of_account_id", r.accountHandler.GetAccountsByChartOfAccount)
accounts.PUT("/:id/balance", r.accountHandler.UpdateAccountBalance)
accounts.GET("/:id/balance", r.accountHandler.GetAccountBalance)
}
expenses := protected.Group("/expenses")
expenses.Use(r.authMiddleware.RequireAdminOrManagerOrPurchasing())
{
expenses.POST("", r.expenseHandler.CreateExpense)
expenses.GET("", r.expenseHandler.ListExpenses)
expenses.GET("/analytics", r.expenseHandler.GetExpenseAnalytics)
expenses.GET("/:id", r.expenseHandler.GetExpense)
expenses.PUT("/:id", r.expenseHandler.UpdateExpense)
expenses.DELETE("/:id", r.expenseHandler.DeleteExpense)
}
cashAdvances := protected.Group("/cash-advances")
cashAdvances.Use(r.authMiddleware.RequireAdminOrManagerOrPurchasing())
{
cashAdvances.POST("", r.cashAdvanceHandler.CreateCashAdvance)
cashAdvances.GET("", r.cashAdvanceHandler.ListCashAdvances)
// Registered ahead of /:id so the picker path is not read as an id.
cashAdvances.GET("/teams", r.cashAdvanceHandler.ListCashAdvanceTeams)
cashAdvances.GET("/:id", r.cashAdvanceHandler.GetCashAdvance)
cashAdvances.PUT("/:id", r.cashAdvanceHandler.UpdateCashAdvance)
cashAdvances.PUT("/:id/status/:status", r.cashAdvanceHandler.UpdateCashAdvanceStatus)
cashAdvances.DELETE("/:id", r.cashAdvanceHandler.DeleteCashAdvance)
}
orderIngredientTransactions := protected.Group("/order-ingredient-transactions")
orderIngredientTransactions.Use(r.authMiddleware.RequireAdminOrManager())
{
orderIngredientTransactions.POST("", r.orderIngredientTransactionHandler.CreateOrderIngredientTransaction)
orderIngredientTransactions.GET("", r.orderIngredientTransactionHandler.ListOrderIngredientTransactions)
orderIngredientTransactions.GET("/:id", r.orderIngredientTransactionHandler.GetOrderIngredientTransactionByID)
orderIngredientTransactions.PUT("/:id", r.orderIngredientTransactionHandler.UpdateOrderIngredientTransaction)
orderIngredientTransactions.DELETE("/:id", r.orderIngredientTransactionHandler.DeleteOrderIngredientTransaction)
orderIngredientTransactions.GET("/order/:order_id", r.orderIngredientTransactionHandler.GetOrderIngredientTransactionsByOrder)
orderIngredientTransactions.GET("/order-item/:order_item_id", r.orderIngredientTransactionHandler.GetOrderIngredientTransactionsByOrderItem)
orderIngredientTransactions.GET("/ingredient/:ingredient_id", r.orderIngredientTransactionHandler.GetOrderIngredientTransactionsByIngredient)
orderIngredientTransactions.GET("/summary", r.orderIngredientTransactionHandler.GetOrderIngredientTransactionSummary)
orderIngredientTransactions.POST("/bulk", r.orderIngredientTransactionHandler.BulkCreateOrderIngredientTransactions)
}
gamification := protected.Group("/marketing")
gamification.Use(r.authMiddleware.RequireAdminOrManager())
{
// Tiers
tiers := gamification.Group("/tiers")
{
tiers.POST("", r.gamificationHandler.CreateTier)
tiers.GET("", r.gamificationHandler.ListTiers)
tiers.GET("/:id", r.gamificationHandler.GetTier)
tiers.PUT("/:id", r.gamificationHandler.UpdateTier)
tiers.DELETE("/:id", r.gamificationHandler.DeleteTier)
tiers.GET("/by-points/:points", r.gamificationHandler.GetTierByPoints)
}
// Games
games := gamification.Group("/games")
{
games.POST("", r.gamificationHandler.CreateGame)
games.GET("", r.gamificationHandler.ListGames)
games.GET("/active", r.gamificationHandler.GetActiveGames)
games.GET("/:id", r.gamificationHandler.GetGame)
games.PUT("/:id", r.gamificationHandler.UpdateGame)
games.DELETE("/:id", r.gamificationHandler.DeleteGame)
}
// Game Prizes
gamePrizes := gamification.Group("/game-prizes")
{
gamePrizes.POST("", r.gamificationHandler.CreateGamePrize)
gamePrizes.GET("", r.gamificationHandler.ListGamePrizes)
gamePrizes.GET("/:id", r.gamificationHandler.GetGamePrize)
gamePrizes.PUT("/:id", r.gamificationHandler.UpdateGamePrize)
gamePrizes.DELETE("/:id", r.gamificationHandler.DeleteGamePrize)
gamePrizes.GET("/game/:game_id", r.gamificationHandler.GetGamePrizesByGameID)
gamePrizes.GET("/game/:game_id/available", r.gamificationHandler.GetAvailablePrizes)
}
//// Game Plays
//gamePlays := gamification.Group("/game-plays")
//{
// gamePlays.POST("", r.gamificationHandler.CreateGamePlay)
// gamePlays.GET("", r.gamificationHandler.ListGamePlays)
// gamePlays.GET("/:id", r.gamificationHandler.GetGamePlay)
// gamePlays.POST("/play", r.gamificationHandler.PlayGame)
//}
// Omset Tracker
//omsetTracker := gamification.Group("/omset-tracker")
//{
// omsetTracker.POST("", r.gamificationHandler.CreateOmsetTracker)
// omsetTracker.GET("", r.gamificationHandler.ListOmsetTrackers)
// omsetTracker.GET("/:id", r.gamificationHandler.GetOmsetTracker)
// omsetTracker.PUT("/:id", r.gamificationHandler.UpdateOmsetTracker)
// omsetTracker.DELETE("/:id", r.gamificationHandler.DeleteOmsetTracker)
//}
// Rewards
rewards := gamification.Group("/rewards")
{
rewards.POST("", r.rewardHandler.CreateReward)
rewards.GET("", r.rewardHandler.ListRewards)
rewards.GET("/:id", r.rewardHandler.GetReward)
rewards.PUT("/:id", r.rewardHandler.UpdateReward)
rewards.DELETE("/:id", r.rewardHandler.DeleteReward)
rewards.PUT("/:id/stock/:stock", r.rewardHandler.UpdateRewardStock)
rewards.GET("/type/:type", r.rewardHandler.GetRewardsByType)
}
// Campaigns
campaigns := gamification.Group("/campaigns")
{
campaigns.POST("", r.campaignHandler.CreateCampaign)
campaigns.GET("", r.campaignHandler.ListCampaigns)
campaigns.GET("/active", r.campaignHandler.GetActiveCampaigns)
campaigns.GET("/app", r.campaignHandler.GetCampaignsForApp)
campaigns.GET("/:id", r.campaignHandler.GetCampaign)
campaigns.PUT("/:id", r.campaignHandler.UpdateCampaign)
campaigns.DELETE("/:id", r.campaignHandler.DeleteCampaign)
}
// Campaign Rules
// EnakPoint & EnakCoin settings of the organization (docs/prd-point-coin.md F2)
gamification.GET("/loyalty-settings", r.loyaltySettingsHandler.GetOrganizationSettings)
gamification.PUT("/loyalty-settings", r.authMiddleware.RequireLoyaltyManager(), r.loyaltySettingsHandler.UpdateOrganizationSettings)
gamification.GET("/loyalty-settings/history", r.loyaltySettingsHandler.ListHistory)
// EnakPoint & EnakCoin wallet of one customer (docs/prd-point-coin.md F7)
marketingCustomers := gamification.Group("/customers")
{
marketingCustomers.GET("/:id/wallet", r.walletAdminHandler.GetCustomerWallet)
marketingCustomers.POST("/:id/wallet/adjust", r.authMiddleware.RequireLoyaltyManager(), r.walletAdminHandler.AdjustCustomerWallet)
marketingCustomers.DELETE("/:id/pin", r.authMiddleware.RequireLoyaltyManager(), r.customerPinHandler.RemovePin)
marketingCustomers.GET("/:id/security-events", r.customerPinHandler.ListSecurityEvents)
}
// Trace one ledger row lot by lot back to where its balance came from (F7, §8.1)
gamification.GET("/wallet-transactions/:id/trace", r.walletAdminHandler.TraceTransaction)
// EnakGame (docs/rfc-enakgame.md §11). Reward configurations and budgets
// change what rewards cost, so writing them takes a loyalty manager.
enakGame := gamification.Group("/enakgame")
{
enakGame.POST("/games", r.enakGameAdminHandler.CreateGame)
enakGame.GET("/games", r.enakGameAdminHandler.ListGames)
enakGame.GET("/games/:id", r.enakGameAdminHandler.GetGame)
enakGame.PUT("/games/:id", r.enakGameAdminHandler.UpdateGame)
enakGame.PUT("/games/:id/status", r.enakGameAdminHandler.SetGameStatus)
enakGame.GET("/games/:id/reward-configs", r.enakGameAdminHandler.ListRewardConfigs)
enakGame.POST("/games/:id/reward-configs", r.authMiddleware.RequireLoyaltyManager(), r.enakGameAdminHandler.CreateRewardConfig)
enakGame.POST("/reward-configs/:id/activate", r.authMiddleware.RequireLoyaltyManager(), r.enakGameAdminHandler.ActivateRewardConfig)
enakGame.GET("/budgets", r.enakGameAdminHandler.ListBudgets)
enakGame.GET("/budgets/:id", r.enakGameAdminHandler.GetBudget)
enakGame.GET("/budgets/:id/metrics", r.enakGameAdminHandler.BudgetMetrics)
enakGame.POST("/budgets", r.authMiddleware.RequireLoyaltyManager(), r.enakGameAdminHandler.CreateBudget)
enakGame.PUT("/budgets/:id", r.authMiddleware.RequireLoyaltyManager(), r.enakGameAdminHandler.UpdateBudget)
enakGame.DELETE("/budgets/:id", r.authMiddleware.RequireLoyaltyManager(), r.enakGameAdminHandler.DeleteBudget)
// Events change what rewards cost, like budgets.
enakGame.GET("/events", r.enakGameAdminHandler.ListEvents)
enakGame.GET("/events/:id", r.enakGameAdminHandler.GetEvent)
enakGame.POST("/events", r.authMiddleware.RequireLoyaltyManager(), r.enakGameAdminHandler.CreateEvent)
enakGame.PUT("/events/:id", r.authMiddleware.RequireLoyaltyManager(), r.enakGameAdminHandler.UpdateEvent)
enakGame.PUT("/events/:id/status", r.authMiddleware.RequireLoyaltyManager(), r.enakGameAdminHandler.SetEventStatus)
enakGame.GET("/vouchers", r.enakGameAdminHandler.ListVouchers)
enakGame.GET("/vouchers/:id", r.enakGameAdminHandler.GetVoucher)
enakGame.GET("/vouchers/:id/codes", r.enakGameAdminHandler.ListVoucherCodes)
enakGame.POST("/vouchers", r.authMiddleware.RequireLoyaltyManager(), r.enakGameAdminHandler.CreateVoucher)
enakGame.PUT("/vouchers/:id", r.authMiddleware.RequireLoyaltyManager(), r.enakGameAdminHandler.UpdateVoucher)
enakGame.PUT("/vouchers/:id/status", r.authMiddleware.RequireLoyaltyManager(), r.enakGameAdminHandler.SetVoucherStatus)
enakGame.POST("/vouchers/:id/codes", r.authMiddleware.RequireLoyaltyManager(), r.enakGameAdminHandler.ImportVoucherCodes)
}
campaignRules := gamification.Group("/campaign-rules")
{
campaignRules.POST("", r.campaignHandler.CreateCampaignRule)
campaignRules.GET("", r.campaignHandler.ListCampaignRules)
campaignRules.GET("/:id", r.campaignHandler.GetCampaignRule)
campaignRules.PUT("/:id", r.campaignHandler.UpdateCampaignRule)
campaignRules.DELETE("/:id", r.campaignHandler.DeleteCampaignRule)
campaignRules.GET("/campaign/:campaign_id", r.campaignHandler.GetCampaignRulesByCampaignID)
}
}
outlets := protected.Group("/outlets")
outlets.Use(r.authMiddleware.RequireAdminOrManager())
{
outlets.POST("", r.outletHandler.CreateOutlet)
outlets.GET("/list", r.outletHandler.ListOutlets)
outlets.GET("/detail/:id", r.outletHandler.GetOutlet)
outlets.PUT("/detail/:id", r.outletHandler.UpdateOutlet)
outlets.GET("/printer-setting/:outlet_id", r.outletSettingHandler.GetPrinterSettings)
outlets.PUT("/printer-setting/:outlet_id", r.outletSettingHandler.UpdatePrinterSettings)
// EnakPoint & EnakCoin settings of the outlet (docs/prd-point-coin.md F1)
outlets.GET("/:outlet_id/loyalty-settings", r.loyaltySettingsHandler.GetOutletSettings)
outlets.PUT("/:outlet_id/loyalty-settings", r.authMiddleware.RequireLoyaltyManager(), r.loyaltySettingsHandler.UpdateOutletSettings)
outlets.GET("/:outlet_id/tables/available", r.tableHandler.GetAvailableTables)
outlets.GET("/:outlet_id/tables/occupied", r.tableHandler.GetOccupiedTables)
// Reports
outlets.GET("/:outlet_id/reports/daily-transaction.pdf", r.reportHandler.GetDailyTransactionReportPDF)
outlets.GET("/:outlet_id/reports/profit-loss.pdf", r.reportHandler.GetProfitLossReportPDF)
}
// User device routes - accessible by authenticated users for their own devices
userDevices := protected.Group("/user-devices")
{
userDevices.POST("/register", r.userDeviceHandler.RegisterDevice)
userDevices.GET("/me", r.userDeviceHandler.GetMyDevices)
userDevices.GET("/:id", r.userDeviceHandler.GetDevice)
userDevices.PUT("/:id", r.userDeviceHandler.UpdateDevice)
userDevices.DELETE("/:id", r.userDeviceHandler.DeleteDevice)
}
// Admin-only user device routes
adminUserDevices := protected.Group("/user-devices")
adminUserDevices.Use(r.authMiddleware.RequireAdminOrManager())
{
adminUserDevices.GET("", r.userDeviceHandler.ListDevices)
adminUserDevices.GET("/user/:user_id", r.userDeviceHandler.GetDevicesByUser)
}
// Notification routes - authenticated users manage their own notifications
notifications := protected.Group("/notifications")
{
notifications.GET("", r.notificationHandler.List)
notifications.GET("/:id", r.notificationHandler.GetByID)
notifications.PUT("/:id/read", r.notificationHandler.MarkAsRead)
notifications.PUT("/read-all", r.notificationHandler.MarkAllAsRead)
notifications.DELETE("/:id", r.notificationHandler.Delete)
}
// Admin notification routes - send and broadcast
adminNotifications := protected.Group("/notifications")
adminNotifications.Use(r.authMiddleware.RequireAdminOrManager())
{
adminNotifications.POST("/send", r.notificationHandler.Send)
adminNotifications.POST("/broadcast", r.notificationHandler.Broadcast)
}
}
}
}