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>
This commit is contained in:
2026-09-15 17:04:34 +05:30
parent 1633617dc4
commit 72907dae74
14 changed files with 2841 additions and 1 deletions

View File

@@ -21,4 +21,5 @@ func RegisterRoutes(app *fiber.App, f *facade.Facade) {
RegisterCatalogueRoutes(api, f)
RegisterPosRoutes(api, f)
RegisterUploadRoutes(api, f)
RegisterScanRoutes(api, f)
}

17
routes/scanroutes.go Normal file
View File

@@ -0,0 +1,17 @@
package routes
import (
"nearle/facade"
"github.com/gofiber/fiber/v2"
)
// Scan-to-order, customer app only. See controllers/scanController.go for
// the three calls and services/scanService.go for the pipeline behind them.
func RegisterScanRoutes(api fiber.Router, f *facade.Facade) {
scan := api.Group("/v1/mob/scan")
scan.Post("/lookup", f.ScanController.Lookup)
scan.Post("/confirm", f.ScanController.Confirm)
scan.Get("/stores", f.ScanController.Stores)
}