222 lines
9.9 KiB
Go
222 lines
9.9 KiB
Go
package facade
|
|
|
|
import (
|
|
"log"
|
|
|
|
"nearle/controllers"
|
|
"nearle/repositories"
|
|
"nearle/services"
|
|
"nearle/services/tools"
|
|
"nearle/utils"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type Facade struct {
|
|
UserController *controllers.UserController
|
|
ProductController *controllers.ProductController
|
|
OrderController *controllers.OrderController
|
|
DeliveriesController *controllers.DeliveriesController
|
|
UtilsController *controllers.UtilsController
|
|
TenantController *controllers.TenantController
|
|
PartnerController *controllers.PartnerController
|
|
CustomerController *controllers.CustomerController
|
|
StockRequestController *controllers.StockRequestController
|
|
CatalogueController *controllers.CatalogueController
|
|
PosController *controllers.PosController
|
|
LiveController *controllers.LiveController
|
|
CatalogueUploadController *controllers.CatalogueUploadController
|
|
ScanController *controllers.ScanController
|
|
AssistantController *controllers.AssistantController
|
|
MCPController *controllers.MCPController
|
|
|
|
// Tools is what Nearle Buddy is allowed to do.
|
|
//
|
|
// Held on the facade because the assistant is not a module with a
|
|
// repository of its own — it is a door onto the services already built
|
|
// here, and every tool handler calls one of them rather than the database.
|
|
Tools *tools.Registry
|
|
|
|
// Held so the NATS consumer can reach the ingest without going through
|
|
// HTTP. Unexported: everything else should use the controller.
|
|
posService services.PosService
|
|
}
|
|
|
|
// NewFacade wires up modules against the main (nearledb) connection.
|
|
// catalogueDB is a separate connection to the pgvector catalogue database;
|
|
// it may be nil if catalogue env vars are not configured, in which case
|
|
// catalogue endpoints will error at query time rather than at startup.
|
|
// embedder may be nil too: scan-to-order then matches on words alone.
|
|
func NewFacade(db *gorm.DB, catalogueDB *gorm.DB, embedder utils.Embedder, chat utils.Chat, agentsDir, assistantWhy string) *Facade {
|
|
|
|
// User Module
|
|
userRepo := repositories.NewUserRepository(db)
|
|
userService := services.NewUserService(userRepo)
|
|
userController := controllers.NewUserController(userService)
|
|
|
|
// Catalogue Module (separate pgvector DB — never the main `db`). Built
|
|
// before the Product Module because ProductService depends on it to
|
|
// bridge catalogue imports into a tenant's own product catalogue.
|
|
catalogueRepo := repositories.NewCatalogueRepository(catalogueDB)
|
|
catalogueService := services.NewCatalogueService(catalogueRepo)
|
|
catalogueController := controllers.NewCatalogueController(catalogueService)
|
|
|
|
// Product Module
|
|
productRepo := repositories.NewProductRepository(db)
|
|
productService := services.NewProductService(productRepo, catalogueService)
|
|
productController := controllers.NewProductController(productService)
|
|
|
|
// Order Module
|
|
orderRepo := repositories.NewOrderRepository(db)
|
|
orderService := services.NewOrderService(orderRepo)
|
|
orderController := controllers.NewOrderController(orderService)
|
|
|
|
// Deliveries Module
|
|
deliveriesRepo := repositories.NewDeliveriesRepository(db)
|
|
deliveriesService := services.NewDeliveriesService(deliveriesRepo)
|
|
deliveriesController := controllers.NewDeliveriesController(deliveriesService)
|
|
|
|
// Utils Module
|
|
utilsRepo := repositories.NewUtilsRepository(db)
|
|
utilsService := services.NewUtilsService(utilsRepo)
|
|
utilsController := controllers.NewUtilsController(utilsService)
|
|
|
|
//Tenant Module
|
|
tenantRepo := repositories.NewTenantRepository(db)
|
|
tenantService := services.NewTenantService(tenantRepo)
|
|
tenantController := controllers.NewTenantController(tenantService)
|
|
|
|
//Partner Module
|
|
partnerRepo := repositories.NewPartnerRepository(db)
|
|
partnerService := services.NewPartnerService(partnerRepo)
|
|
partnerController := controllers.NewPartnerController(partnerService)
|
|
|
|
//Customer Module
|
|
customerRepo := repositories.NewCustomerRepository(db)
|
|
customerService := services.NewCustomerService(customerRepo)
|
|
customerController := controllers.NewCustomerController(customerService)
|
|
|
|
// Stock Request Module
|
|
stockRequestRepo := repositories.NewStockRequestRepository(db)
|
|
stockRequestService := services.NewStockRequestService(stockRequestRepo, productService)
|
|
stockRequestController := controllers.NewStockRequestController(stockRequestService)
|
|
|
|
// POS Module — ingest from the in-store terminals.
|
|
//
|
|
// Presence has no *gorm.DB: terminal health lives in Redis under a TTL, so
|
|
// a till that loses power ages out of the board by itself instead of
|
|
// leaving a Postgres row claiming it is online.
|
|
posRepo := repositories.NewPosRepository(db)
|
|
posPresence := repositories.NewPosPresenceRepository()
|
|
posService := services.NewPosService(posRepo, posPresence)
|
|
posController := controllers.NewPosController(posService)
|
|
|
|
// Shares the POS service purely for its outlet-ownership check — the
|
|
// stream itself reads no database and holds no state beyond its
|
|
// subscribers.
|
|
liveController := controllers.NewLiveController(posService)
|
|
|
|
// Catalogue Upload Module — our own receipt for every spreadsheet sent to
|
|
// the ingest service. Their host deletes an unreviewed drop after seven
|
|
// days and the batch id is the only credential for reading the result
|
|
// back, so the id has to be kept somewhere that outlives a browser tab.
|
|
catalogueUploadRepo := repositories.NewCatalogueUploadRepository(db)
|
|
catalogueUploadService := services.NewCatalogueUploadService(catalogueUploadRepo)
|
|
catalogueUploadController := controllers.NewCatalogueUploadController(catalogueUploadService)
|
|
|
|
// Scan Module — a label from the customer's camera to "buy it here".
|
|
// Reads both databases: the catalogue to recognise the product, nearledb
|
|
// for who the customer is and what their outlets have on the shelf.
|
|
scanRepo := repositories.NewScanRepository(db, catalogueDB)
|
|
scanService := services.NewScanService(scanRepo, embedder)
|
|
scanController := controllers.NewScanController(scanService)
|
|
|
|
// The assistant registry. Built last, because every tool it holds is a thin
|
|
// wrapper over a service constructed above.
|
|
//
|
|
// A registration error panics rather than being logged. A duplicate name or
|
|
// a tool with no description is a programming mistake, and a server that
|
|
// starts with a tool silently absent answers real questions with "I cannot
|
|
// do that" for a reason nobody can see from the outside.
|
|
// The help corpus, checked before it is registered. A passage carrying one
|
|
// shop's figures stops the server rather than reaching another shop's screen.
|
|
helpCorpus, err := tools.LoadHelp()
|
|
if err != nil {
|
|
panic("assistant help: " + err.Error())
|
|
}
|
|
|
|
// The audit trail goes to the database and to the log. See
|
|
// services/assistantAudit.go for why both.
|
|
auditRepo := repositories.NewAssistantAuditRepository(db)
|
|
toolRegistry := tools.New(services.NewDBAudit(auditRepo))
|
|
for _, tool := range []tools.Tool{
|
|
tools.StuckOrders(deliveriesService, nil),
|
|
tools.DeliveryProgress(deliveriesService),
|
|
tools.BranchPerformance(orderService),
|
|
tools.PendingApprovals(stockRequestService, nil),
|
|
tools.LowStock(productService),
|
|
tools.TillsNotSyncing(posService),
|
|
tools.SalesByChannel(orderService, posService, nil),
|
|
tools.Help(helpCorpus),
|
|
tools.ApproveStockRequest(stockRequestService, stockRequestService),
|
|
} {
|
|
if err := toolRegistry.Register(tool); err != nil {
|
|
panic("assistant tools: " + err.Error())
|
|
}
|
|
}
|
|
|
|
// Nearle Buddy. `chat` may be nil — a deployment with no model configured
|
|
// still gets the registry and the endpoint, and the endpoint answers "not
|
|
// switched on here" rather than a 500. The tools themselves are ordinary
|
|
// Go functions and work either way; only turning a sentence into a tool
|
|
// call needs a model.
|
|
// Agent definitions, validated against the registry above. A typo in a tool
|
|
// name stops the server rather than producing an agent that quietly cannot
|
|
// do one of the things it claims — which is invisible at runtime, because the
|
|
// model simply reports it could not look something up.
|
|
agents, err := services.LoadAgents(agentsDir, toolRegistry.Has)
|
|
if err != nil {
|
|
panic("assistant agents: " + err.Error())
|
|
}
|
|
|
|
log.Printf("assistant: %d agents loaded %v", len(agents), services.AgentNames(agents))
|
|
|
|
assistantService := services.NewAssistantService(toolRegistry, chat, agents)
|
|
// Why there is no model, if there is not. Passed through so /assistant/status
|
|
// can name the missing variable instead of just saying no.
|
|
if setter, ok := assistantService.(interface{ SetUnavailableReason(string) }); ok && chat == nil {
|
|
setter.SetUnavailableReason(assistantWhy)
|
|
}
|
|
assistantController := controllers.NewAssistantController(assistantService)
|
|
|
|
// The second door. Same registry, same agents, same session — see
|
|
// controllers/mcpController.go for why it is a door rather than a service.
|
|
mcpController := controllers.NewMCPController(toolRegistry, agents)
|
|
|
|
return &Facade{
|
|
UserController: userController,
|
|
ProductController: productController,
|
|
OrderController: orderController,
|
|
DeliveriesController: deliveriesController,
|
|
UtilsController: utilsController,
|
|
TenantController: tenantController,
|
|
PartnerController: partnerController,
|
|
CustomerController: customerController,
|
|
StockRequestController: stockRequestController,
|
|
CatalogueController: catalogueController,
|
|
PosController: posController,
|
|
LiveController: liveController,
|
|
CatalogueUploadController: catalogueUploadController,
|
|
ScanController: scanController,
|
|
AssistantController: assistantController,
|
|
MCPController: mcpController,
|
|
Tools: toolRegistry,
|
|
posService: posService,
|
|
}
|
|
}
|
|
|
|
// PosService exposes the ingest to callers outside the HTTP layer — the NATS
|
|
// consumer runs the same code path a POST does, so a bill arriving over MQTT
|
|
// and one arriving over HTTP cannot diverge.
|
|
func (f *Facade) PosService() services.PosService { return f.posService }
|