207 lines
9.0 KiB
Go
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"`
|
|
}
|