From 3531c656d44b997bf73c143ccbc36eeac2106589 Mon Sep 17 00:00:00 2001 From: abhishek Date: Mon, 10 Aug 2026 15:36:39 +0530 Subject: [PATCH] fix in the catlogue --- repositories/catalogueRepository.go | 32 ++++++++++++++++++++-- routes/posroutes.go | 42 +++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/repositories/catalogueRepository.go b/repositories/catalogueRepository.go index 3bca09c..d576676 100644 --- a/repositories/catalogueRepository.go +++ b/repositories/catalogueRepository.go @@ -3,6 +3,7 @@ package repositories import ( "errors" "fmt" + "log" "nearle/db" "nearle/models" "sort" @@ -118,15 +119,29 @@ func (r *catalogueRepository) GetBrands() ([]models.CatalogueBrand, error) { } var brands []models.CatalogueBrand + var failures int for brand, table := range catalogueBrandTables { var count int64 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}) } + // 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 } @@ -220,19 +235,32 @@ func (r *catalogueRepository) getProductsAllBrands(category, keyword string, pag sort.Strings(brands) var all []models.CatalogueProduct + var failures int + for _, brand := range brands { table := catalogueBrandTables[brand] var rows []catalogueProductRow 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 { - 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 { 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 { return strings.ToLower(all[i].ProductName) < strings.ToLower(all[j].ProductName) }) diff --git a/routes/posroutes.go b/routes/posroutes.go index 8f4517c..bdf9ec7 100644 --- a/routes/posroutes.go +++ b/routes/posroutes.go @@ -75,6 +75,48 @@ func RegisterPosRoutes(api fiber.Router, f *facade.Facade) { pos.Get("/health/location", f.PosController.LocationHealth) 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.