Files
backend_fiesta/models/scan.go
2026-09-30 16:13:25 +05:30

207 lines
9.0 KiB
Go

package models
// Scan-to-order.
//
// A customer points the app at a packet, Google Lens (on the phone) reads a
// label off it, and the app asks: "which of MY shops has this, in what sizes,
// and which one should I buy from?" These are the shapes on both sides of
// that conversation.
// ScanLookupRequest is what the app sends once Lens has produced a label.
type ScanLookupRequest struct {
Customerid int `json:"customerid"`
// What Lens read: "Milk Bikis", "Dabur Honey 500g". Free text, trimmed
// and capped by the service. Not required when Brand and Catalogueid
// name a product outright.
Label string `json:"label"`
// A product the customer has already chosen, by its catalogue key —
// which is how the app resolves a `candidates` list from an earlier
// ambiguous lookup, and how a deep link or a re-order skips recognition
// altogether. When both are set the label is ignored and no catalogue
// search runs.
Brand string `json:"brand"`
Catalogueid int64 `json:"catalogueid"`
// Where the customer is right now. Optional: without it the customer's
// saved primary address is used, and without that stores are listed in
// registration order with no distance.
Latitude FlexibleString `json:"latitude"`
Longitude FlexibleString `json:"longitude"`
// The tenants the app believes the customer has scanned into. Optional and
// never trusted on its own: the server intersects it with the
// tenantcustomers table and reports anything it dropped.
Tenantids []int `json:"tenantids"`
// How many stores to return. 0 = all registered stores that stock it.
Limit int `json:"limit"`
}
// ScanStore is one of the customer's registered outlets.
type ScanStore struct {
Tenantid int `json:"tenantid"`
Tenantname string `json:"tenantname"`
Locationid int `json:"locationid"`
Locationname string `json:"locationname"`
Address string `json:"address,omitempty"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
// Kilometres from the customer, or -1 when either side has no usable
// coordinates. Never omitted: a missing number is easy to misread as 0.
DistanceKm float64 `json:"distance_km"`
// Delivery reach in the outlet's own units, straight from tenantlocations.
Deliveryradius int `json:"deliveryradius"`
Deliverymins int `json:"deliverymins"`
Open bool `json:"open"`
}
// ScanOption is one thing the customer can actually put in the basket at one
// store: the matched product itself, or one of its sizes. Each is a real
// product row with its own price and stock, which is why they are flat.
type ScanOption struct {
// Where this is sold.
//
// On the option and not only on the enclosing store, because an order line
// carries both and the app would otherwise have to reach back up the
// response to build one. `Locationid` is the real outlet and never 0.
Tenantid int `json:"tenantid"`
Locationid int `json:"locationid"`
Productid int `json:"productid"`
Productname string `json:"productname"`
// The pack size, both ways round.
//
// `Size` is the printable "500 g" the app has always shown. The two parts
// are sent beside it because an order line needs the unit on its own, and
// pulling it back out of the label means parsing a string a shop typed.
Size string `json:"size"`
Unitvalue string `json:"unitvalue"`
Productunit string `json:"productunit"`
// What the customer pays: the outlet's own price, or the product's retail
// price where the outlet has not set one.
Price float64 `json:"price"`
// What the shop paid. NOT a price to charge — it is `products.productcost`,
// the same field the product screens return, and billing against it would
// sell at cost.
Productcost float64 `json:"productcost"`
// Carried on the order header, so the app has them without a second read.
Categoryid int `json:"categoryid"`
Subcategoryid int `json:"subcategoryid"`
Stock int `json:"stock"`
Available bool `json:"available"`
// The same URL under both names: `image` is what this endpoint has always
// sent, `productimage` is what every other product response calls it and
// what an order line is built from.
Image string `json:"image,omitempty"`
Productimage string `json:"productimage,omitempty"`
// Is this the product that matched, or a size hanging under it?
IsVariant bool `json:"is_variant"`
Variantname string `json:"variantname,omitempty"`
// How the row was tied back to the catalogue: "imageid",
// "brand+catalogueid", "name" or "variant-of:<productid>".
MatchedBy string `json:"matched_by"`
}
// ScanStoreOffer is one store and what it can sell.
type ScanStoreOffer struct {
ScanStore
// Nearest store with at least one option in stock. Exactly one offer
// carries this, and only when something is in stock somewhere.
Recommended bool `json:"recommended"`
// Any option in stock here.
Available bool `json:"available"`
Options []ScanOption `json:"options"`
}
// ScanCatalogueMatch is what the catalogue search settled on.
type ScanCatalogueMatch struct {
Brand string `json:"brand"`
Catalogueid int64 `json:"catalogueid"`
Imageid string `json:"imageid,omitempty"`
ProductName string `json:"product_name"`
Title string `json:"title,omitempty"`
Category string `json:"category,omitempty"`
Size string `json:"size,omitempty"`
VariantKey string `json:"variant_key,omitempty"`
Image string `json:"image,omitempty"`
Score float64 `json:"score"`
// "vector+text", "text" or "direct" — how the score was produced. The app
// can be more cautious with a text-only match; "direct" means the caller
// named the product by its catalogue key and nothing was recognised.
Method string `json:"method"`
// Set only on entries of `candidates`: at least one of the customer's
// registered stores has this product in stock right now. Candidates are
// ordered with the available ones first, so a "did you mean?" list can
// show what is actually buyable before what is not.
Available bool `json:"available,omitempty"`
}
// ScanLookupResponse is the answer to a scan.
type ScanLookupResponse struct {
Label string `json:"label"`
// The best catalogue product for the label, and the sizes of it the
// catalogue knows about (each a separate catalogue row).
//
// Match is nil when nothing was recognised, and also when several
// products matched equally well — see Ambiguous.
Match *ScanCatalogueMatch `json:"match"`
Variants []ScanCatalogueMatch `json:"catalogue_variants"`
// Several products fit the label and no one of them is a clear winner —
// which is what a bare brand name ("britannia") or a generic word
// ("biscuits") produces, and Lens returns those often because a
// wordmark is the most legible thing on a packet.
//
// When true: Match is nil, Stores is empty, and Candidates holds the
// products to offer as "did you mean?". Picking one means calling
// /lookup again with that candidate's `brand` and `catalogueid`.
//
// Guessing instead would mean showing a confident price for a product
// the customer did not photograph.
Ambiguous bool `json:"ambiguous"`
Candidates []ScanCatalogueMatch `json:"candidates"`
// 0..1. Below ~0.5 the app should confirm with the customer before
// showing prices. With Ambiguous set this is the leader's score, which
// by definition the runner-up nearly equals.
Confidence float64 `json:"confidence"`
// Registered stores that stock the product, nearest first, in-stock
// first. Empty with Available=false when none does.
Stores []ScanStoreOffer `json:"stores"`
Available bool `json:"available"`
// The locationid of the store marked Recommended, or 0.
RecommendedLocationid int `json:"recommended_locationid"`
// Tenant ids the app sent that the customer is not actually registered
// with. Empty normally; non-empty means the app's local list is stale.
UnregisteredTenantids []int `json:"unregistered_tenantids,omitempty"`
Message string `json:"message"`
}
// ScanConfirmRequest is sent when the customer taps a store and a size.
type ScanConfirmRequest struct {
Customerid int `json:"customerid"`
Tenantid int `json:"tenantid"`
Locationid int `json:"locationid"`
Productid int `json:"productid"`
Quantity int `json:"quantity"`
Latitude FlexibleString `json:"latitude"`
Longitude FlexibleString `json:"longitude"`
}
// ScanConfirmResponse says whether the pick still holds, and where to go if
// it does not.
type ScanConfirmResponse struct {
Ok bool `json:"ok"`
// "in_stock", "insufficient_stock", "out_of_stock", "not_sold_here",
// "store_not_registered".
Reason string `json:"reason"`
Store *ScanStore `json:"store,omitempty"`
Option *ScanOption `json:"option,omitempty"`
Requested int `json:"requested"`
// The next-nearest registered store with enough of the same product, when
// the chosen one has run out. Nil when there is none.
Alternative *ScanStoreOffer `json:"alternative,omitempty"`
Message string `json:"message"`
}