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>
66 lines
1.7 KiB
Go
66 lines
1.7 KiB
Go
package utils
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestParseLatLng(t *testing.T) {
|
|
if _, _, ok := ParseLatLng("", ""); ok {
|
|
t.Error("blank must not parse")
|
|
}
|
|
if _, _, ok := ParseLatLng("0", "0"); ok {
|
|
t.Error("0,0 is an empty map picker, not a place")
|
|
}
|
|
if _, _, ok := ParseLatLng("91", "10"); ok {
|
|
t.Error("out of range")
|
|
}
|
|
lat, lng, ok := ParseLatLng(" 11.0168 ", "76.9558")
|
|
if !ok || lat != 11.0168 || lng != 76.9558 {
|
|
t.Errorf("got %v %v %v", lat, lng, ok)
|
|
}
|
|
}
|
|
|
|
func TestHaversineKm(t *testing.T) {
|
|
// Coimbatore railway station to Peelamedu, roughly 8 km.
|
|
d := HaversineKm(11.0018, 76.9660, 11.0290, 77.0290)
|
|
if d < 7 || d > 9 {
|
|
t.Errorf("got %.2f km", d)
|
|
}
|
|
if HaversineKm(1, 1, 1, 1) != 0 {
|
|
t.Error("same point")
|
|
}
|
|
}
|
|
|
|
func TestOpenNow(t *testing.T) {
|
|
at := func(h, m int) time.Time { return time.Date(2026, 9, 15, h, m, 0, 0, time.UTC) }
|
|
if !OpenNow("09:00", "21:00", at(12, 0)) || OpenNow("09:00", "21:00", at(22, 0)) {
|
|
t.Error("plain hours")
|
|
}
|
|
if !OpenNow("20:00", "02:00", at(1, 0)) || OpenNow("20:00", "02:00", at(12, 0)) {
|
|
t.Error("past midnight")
|
|
}
|
|
if !OpenNow("9:00 AM", "9:30 PM", at(21, 0)) || OpenNow("9:00 AM", "9:30 PM", at(21, 45)) {
|
|
t.Error("12-hour clock")
|
|
}
|
|
if !OpenNow("", "", at(3, 0)) || !OpenNow("always", "", at(3, 0)) {
|
|
t.Error("unknown hours mean open")
|
|
}
|
|
}
|
|
|
|
func TestSearchTokens(t *testing.T) {
|
|
got := SearchTokens("Milk Bikis 100g, Britannia (2 x 50gm)")
|
|
want := []string{"milk", "bikis", "britannia"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("got %v want %v", got, want)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("got %v want %v", got, want)
|
|
}
|
|
}
|
|
if len(SearchTokens("500ml 1kg 2")) != 0 {
|
|
t.Error("pack sizes alone are not searchable")
|
|
}
|
|
}
|