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:". 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"` }