fix in the catlogue
This commit is contained in:
@@ -3,6 +3,7 @@ package repositories
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"nearle/db"
|
"nearle/db"
|
||||||
"nearle/models"
|
"nearle/models"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -118,15 +119,29 @@ func (r *catalogueRepository) GetBrands() ([]models.CatalogueBrand, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var brands []models.CatalogueBrand
|
var brands []models.CatalogueBrand
|
||||||
|
var failures int
|
||||||
|
|
||||||
for brand, table := range catalogueBrandTables {
|
for brand, table := range catalogueBrandTables {
|
||||||
var count int64
|
var count int64
|
||||||
if err := r.db.Table(table).Count(&count).Error; err != nil {
|
if err := r.db.Table(table).Count(&count).Error; err != nil {
|
||||||
return nil, fmt.Errorf("counting %s: %w", table, err)
|
// One brand's table being absent or unreadable must not hide the
|
||||||
|
// others. It used to abort the whole call, so a single missing
|
||||||
|
// table emptied the brand filter and — through
|
||||||
|
// getProductsAllBrands, which had the same flaw — the entire
|
||||||
|
// import screen, for every brand.
|
||||||
|
log.Printf("catalogue: skipping brand %q (%s): %v", brand, table, err)
|
||||||
|
failures++
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
brands = append(brands, models.CatalogueBrand{Brand: brand, ProductCount: count})
|
brands = append(brands, models.CatalogueBrand{Brand: brand, ProductCount: count})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every table failing is a different thing from every table being empty:
|
||||||
|
// the first is an outage and must be reported, the second is a fact.
|
||||||
|
if failures == len(catalogueBrandTables) {
|
||||||
|
return nil, fmt.Errorf("no catalogue brand table could be read (%d configured)", failures)
|
||||||
|
}
|
||||||
|
|
||||||
return brands, nil
|
return brands, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,19 +235,32 @@ func (r *catalogueRepository) getProductsAllBrands(category, keyword string, pag
|
|||||||
sort.Strings(brands)
|
sort.Strings(brands)
|
||||||
|
|
||||||
var all []models.CatalogueProduct
|
var all []models.CatalogueProduct
|
||||||
|
var failures int
|
||||||
|
|
||||||
for _, brand := range brands {
|
for _, brand := range brands {
|
||||||
table := catalogueBrandTables[brand]
|
table := catalogueBrandTables[brand]
|
||||||
|
|
||||||
var rows []catalogueProductRow
|
var rows []catalogueProductRow
|
||||||
dataQuery := fmt.Sprintf(`SELECT %s FROM %s WHERE %s ORDER BY id`, catalogueProductColumns, table, whereClause)
|
dataQuery := fmt.Sprintf(`SELECT %s FROM %s WHERE %s ORDER BY id`, catalogueProductColumns, table, whereClause)
|
||||||
if err := r.db.Raw(dataQuery, args...).Scan(&rows).Error; err != nil {
|
if err := r.db.Raw(dataQuery, args...).Scan(&rows).Error; err != nil {
|
||||||
return nil, 0, fmt.Errorf("querying %s: %w", table, err)
|
// Skip the brand rather than abandoning the merge. This aborted on
|
||||||
|
// the first failure, so one absent table returned 500 for a browse
|
||||||
|
// across all brands — which is what the import screen asks for by
|
||||||
|
// default, leaving it permanently empty while five of six brands
|
||||||
|
// were perfectly readable.
|
||||||
|
log.Printf("catalogue: skipping brand %q (%s) in all-brands browse: %v", brand, table, err)
|
||||||
|
failures++
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
all = append(all, row.toModel(brand))
|
all = append(all, row.toModel(brand))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if failures == len(brands) {
|
||||||
|
return nil, 0, fmt.Errorf("no catalogue brand table could be read (%d configured)", failures)
|
||||||
|
}
|
||||||
|
|
||||||
sort.Slice(all, func(i, j int) bool {
|
sort.Slice(all, func(i, j int) bool {
|
||||||
return strings.ToLower(all[i].ProductName) < strings.ToLower(all[j].ProductName)
|
return strings.ToLower(all[i].ProductName) < strings.ToLower(all[j].ProductName)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -75,6 +75,48 @@ func RegisterPosRoutes(api fiber.Router, f *facade.Facade) {
|
|||||||
pos.Get("/health/location", f.PosController.LocationHealth)
|
pos.Get("/health/location", f.PosController.LocationHealth)
|
||||||
|
|
||||||
registerPosStaffConsoleRoutes(api, f)
|
registerPosStaffConsoleRoutes(api, f)
|
||||||
|
registerPosReadConsoleRoutes(api, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same counter-sales reads, for callers that are not a terminal.
|
||||||
|
//
|
||||||
|
// `PosAuth` pins a request to the outlet inside a terminal's token. The web
|
||||||
|
// console has no such token and cannot obtain one — `/pos/login` refuses an
|
||||||
|
// account that is not a till account, which is the separation working as
|
||||||
|
// intended. So the moment `POS_AUTH_REQUIRED=true` is set, every POS screen in
|
||||||
|
// the back office goes dark: the two were mutually exclusive.
|
||||||
|
//
|
||||||
|
// Rather than weaken the terminal guard or hand the console a terminal
|
||||||
|
// identity, the reads are offered again outside the group. A browser and a till
|
||||||
|
// are different callers and belong on different doors.
|
||||||
|
//
|
||||||
|
// Only the five the console actually reads, and only reads. Specifically NOT
|
||||||
|
// `/health/terminal`: it takes a terminal code and no outlet, so it resolves
|
||||||
|
// the shop from the heartbeat and checks that against the caller's token. Off
|
||||||
|
// this group there is no token to check, and mirroring it would undo that.
|
||||||
|
// Nothing in the console calls it — `/health/location` answers the same
|
||||||
|
// question with an outlet to scope by.
|
||||||
|
//
|
||||||
|
// These inherit the `/web` surface's authentication, which is none. That is not
|
||||||
|
// a new hole opened here — `getposusers` on the neighbouring group already
|
||||||
|
// answers unauthenticated and returns PINs — but it is the reason this whole
|
||||||
|
// surface wants a session guard, which is tracked separately.
|
||||||
|
func registerPosReadConsoleRoutes(api fiber.Router, f *facade.Facade) {
|
||||||
|
for _, group := range []string{"/v1/web/pos", "/v1/mob/pos"} {
|
||||||
|
g := api.Group(group)
|
||||||
|
|
||||||
|
g.Get("/sales", f.PosController.GetSales)
|
||||||
|
g.Get("/sales/detail", f.PosController.GetSaleDetail)
|
||||||
|
g.Get("/sales/summary", f.PosController.GetSalesSummary)
|
||||||
|
|
||||||
|
// The fleet board. Scoped by the outlet named in the query, exactly as
|
||||||
|
// on the terminal group — the handler reads nothing from a session.
|
||||||
|
g.Get("/health/location", f.PosController.LocationHealth)
|
||||||
|
|
||||||
|
// What a till at this shop can sell. Read-only and already scoped by
|
||||||
|
// store id; the console shows it to explain why a product will not ring.
|
||||||
|
g.Get("/catalogue", f.PosController.Catalogue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Till staff, managed from the web console rather than from a counter.
|
// Till staff, managed from the web console rather than from a counter.
|
||||||
|
|||||||
Reference in New Issue
Block a user