This commit is contained in:
2026-09-02 15:37:59 +05:30
parent 68871cb043
commit 09efc5403f
6 changed files with 289 additions and 46 deletions

26
controllers/appRequest.go Normal file
View File

@@ -0,0 +1,26 @@
package controllers
import (
"strings"
"github.com/gofiber/fiber/v2"
)
// isAppRequest reports whether a request arrived on the customer app's base.
//
// Several handlers are registered on both `/v1/web/...` and `/v1/mob/...`, and
// a few of them owe the two callers different answers. The clearest case is
// stock: the console lists a product with an empty shelf so a merchant can
// refill it, and the app must not list the same product at all, because a
// shopper can only choose it and be refused at checkout.
//
// The base is read from the path rather than passed down through the service,
// so the difference stays where it belongs — at the edge, in the one place that
// knows which audience asked. Services keep answering the same question the
// same way for everyone.
//
// Matched with the surrounding slashes so a tenant, product or keyword
// containing "mob" cannot make a console request look like an app one.
func isAppRequest(c *fiber.Ctx) bool {
return strings.Contains(c.Path(), "/v1/mob/")
}

View File

@@ -0,0 +1,50 @@
package controllers
import (
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
)
/*
The console and the customer app share handlers, and a few of those handlers owe
the two callers different answers. This is the switch that tells them apart, so
it is worth a test of its own: get it wrong in the permissive direction and
shoppers are offered empty shelves again; get it wrong in the strict direction
and a merchant's restocking screen goes blank, hiding the work they came to do.
*/
func pathSays(t *testing.T, path string, want bool) {
t.Helper()
app := fiber.New()
got := false
app.Get("/*", func(c *fiber.Ctx) error {
got = isAppRequest(c)
return nil
})
if _, err := app.Test(httptest.NewRequest("GET", path, nil)); err != nil {
t.Fatalf("Test(%q): %v", path, err)
}
if got != want {
t.Errorf("isAppRequest(%q) = %v, want %v", path, got, want)
}
}
func TestTheAppBaseIsRecognised(t *testing.T) {
pathSays(t, "/live/api/v1/mob/products/getallproducts", true)
}
func TestTheConsoleBaseIsNotTreatedAsTheApp(t *testing.T) {
// The console must keep seeing empty shelves — restocking them is the whole
// point of that screen.
pathSays(t, "/live/api/v1/web/products/getallproducts", false)
}
func TestAKeywordContainingMobDoesNotImpersonateTheApp(t *testing.T) {
// Matched with its slashes, so a search for "mobil" or a shop called
// "Mobius" cannot flip a console request into an app one and empty the
// merchant's screen.
pathSays(t, "/live/api/v1/web/products/getallproducts?keyword=mobile", false)
pathSays(t, "/live/api/v1/web/tenants/search?keyword=mob", false)
}

View File

@@ -385,6 +385,23 @@ func (ctl *ProductController) GetAllProducts(c *fiber.Ctx) error {
categoryID, subcategoryID, productID, applocationID, tenantID,
locationID, keyword, productStatus, approve, pageno, pagesize,
)
// The customer app is not shown what the shop cannot sell.
//
// Scoped to the /v1/mob base rather than applied in the service, because
// this one handler answers on BOTH bases and the two callers want opposite
// things. The console reads it to restock — an empty line is exactly what a
// merchant needs to see and act on, so filtering there would hide the work.
// A shopper reading the same list can only be misled by it: measured
// 2026-09-02 on R mart, four products were on offer in the app with a zero
// balance, including Cadbury Bournvita 500g and two Amul packs. Ordering one
// gets a 409 at checkout, after the shopper has chosen it.
//
// It filters on Productstock, the same number the response carries, so the
// list and the figure beside it cannot disagree.
if err == nil && isAppRequest(c) {
details = services.InStockOnlyGrouped(details)
}
if err != nil {
return c.JSON(fiber.Map{
"status": false,