Files
backend_fiesta/facade/container.go
Suriyakumarvijayanayagam 72907dae74 Scan-to-order: label from the customer's camera to "buy it here"
POST /v1/mob/scan/lookup   label + customer → catalogue match, sizes, and
                           every registered store that sells it with live
                           stock, in-stock first / nearest first, one
                           recommended
POST /v1/mob/scan/confirm  chosen store + size + qty → re-read the ledger;
                           ok, or the next-nearest store with enough of the
                           same product
GET  /v1/mob/scan/stores   registered stores nearest first

Recognition is pgvector cosine search over every brand_* table (each
with its own index, merged) plus a word match that settles near-ties
and works alone when no model is configured. The embedder is chosen by
EMBEDDING_PROVIDER (OpenAI-compatible or Gemini) and must be the model
that indexed the catalogue: verified 2026-09-15 as all-MiniLM-L6-v2 over
search_query, served by the cluster's Ollama as `all-minilm`; the first
search refuses a width mismatch by name.

Customer, stores and catalogue are read concurrently under a 5 s cap; a
slow model degrades to a text answer. Vectors and ranked hits are cached
in Redis and in-process; live stock never is. Availability uses the same
rules as the customer catalogue (approve, publishedat, ledger balance,
outlet price else retail). No stock reservation: confirm re-reads.

scratch/cataloguedims reports the catalogue's embedding width and fill.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 17:04:34 +05:30

145 lines
6.4 KiB
Go

package facade
import (
"nearle/controllers"
"nearle/repositories"
"nearle/services"
"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
// 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) *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)
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,
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 }