updates on the admincontroller and the hubcity fix and queue orders

This commit is contained in:
2026-09-25 16:30:38 +05:30
parent ee79c80338
commit 44ba33eda2
7 changed files with 441 additions and 31 deletions

View File

@@ -1623,9 +1623,57 @@ func GetHubs(c *fiber.Ctx) error {
if err := query.Find(&hubs).Error; err != nil {
return utils.Internal(c, "failed to fetch hubs")
}
attachHubCities(hubs)
return utils.List(c, hubs, int64(len(hubs)))
}
// attachHubCities fills Hub.City from applocations, in ONE query for the whole
// page rather than one per hub — this list is read on every console page load
// through ZoneContext, so a per-row lookup would be twenty round trips for a
// field that comes from a five-row table.
//
// A hub whose applocationid matches nothing keeps an empty City. That is the
// honest answer, and callers already treat "" as "unknown": ZoneContext skips
// its city comparison rather than matching everything.
func attachHubCities(hubs []models.Hub) {
if len(hubs) == 0 {
return
}
ids := make([]int, 0, len(hubs))
seen := map[int]bool{}
for _, h := range hubs {
if h.Applocationid != 0 && !seen[h.Applocationid] {
seen[h.Applocationid] = true
ids = append(ids, h.Applocationid)
}
}
if len(ids) == 0 {
return
}
var locs []models.AppLocation
if err := db.DB.Where("applocationid IN ?", ids).Find(&locs).Error; err != nil {
// A failed lookup leaves every City empty, which is the same state the
// response had before this existed. Refusing the whole hub list because
// one derived label could not be resolved would be worse.
return
}
byID := make(map[int]string, len(locs))
for _, l := range locs {
byID[l.Applocationid] = l.Applocationname
}
applyHubCities(hubs, byID)
}
// applyHubCities writes the resolved city onto each hub. Split from the query so
// the mapping — including what happens to a hub whose applocation is missing —
// can be tested without a database.
func applyHubCities(hubs []models.Hub, byID map[int]string) {
for i := range hubs {
hubs[i].City = byID[hubs[i].Applocationid]
}
}
func CreateHub(c *fiber.Ctx) error {
req := new(dto.HubCreateRequest)
if err := c.BodyParser(req); err != nil {
@@ -1659,7 +1707,12 @@ func GetHubDetails(c *fiber.Ctx) error {
if err := db.DB.Where("hubid = ? AND deletedat IS NULL", id).First(&hub).Error; err != nil {
return utils.NotFound(c, "hub not found")
}
return utils.OK(c, hub)
// A one-element slice, because attachHubCities writes THROUGH the slice —
// handing it `[]models.Hub{hub}` would fill a copy and return the original
// with City still empty.
one := []models.Hub{hub}
attachHubCities(one)
return utils.OK(c, one[0])
}
func UpdateHub(c *fiber.Ctx) error {

View File

@@ -0,0 +1,84 @@
package controllers
import (
"encoding/json"
"testing"
"doormile/models"
)
// A hub's city is derived, not stored. `hubs` carries only `applocationid`, and
// for as long as the response carried that and nothing else, every console
// screen that asked a hub what city it was in got undefined:
//
// - ZoneContext.matchesZone compares an order's address text against the
// hub's city. An empty city makes that comparison unreachable, so the only
// matcher left was a 35km radius — and an order stored without coordinates
// then belonged to no zone at all.
// - fetchAppLocations names each city `hub.city || hub.hubname`, so the
// Pricing and report pickers offered "Coimbatore Jupiter Hub" where they
// meant "Coimbatore".
func TestApplyHubCities(t *testing.T) {
locations := map[int]string{1: "Coimbatore", 3: "Bangalore"}
t.Run("resolves each hub against its applocation", func(t *testing.T) {
hubs := []models.Hub{
{Hubid: 1, Hubname: "Coimbatore Jupiter Hub", Applocationid: 1},
{Hubid: 5, Hubname: "Bangalore Earth Hub", Applocationid: 3},
}
applyHubCities(hubs, locations)
if hubs[0].City != "Coimbatore" || hubs[1].City != "Bangalore" {
t.Fatalf("got %q and %q", hubs[0].City, hubs[1].City)
}
})
t.Run("an unknown applocation leaves the city empty, not guessed", func(t *testing.T) {
// Empty is the honest answer and callers already read it as "unknown":
// ZoneContext skips its city comparison rather than matching everything.
hubs := []models.Hub{{Hubid: 99, Hubname: "Somewhere Hub", Applocationid: 42}}
applyHubCities(hubs, locations)
if hubs[0].City != "" {
t.Fatalf("expected empty city, got %q", hubs[0].City)
}
})
t.Run("a hub with no applocation at all is left alone", func(t *testing.T) {
hubs := []models.Hub{{Hubid: 98, Hubname: "Orphan Hub"}}
applyHubCities(hubs, locations)
if hubs[0].City != "" {
t.Fatalf("expected empty city, got %q", hubs[0].City)
}
})
t.Run("the city follows the applocation, never the name", func(t *testing.T) {
// Hub 22 is a live example of why this is worth asserting: it is named
// "Chennai Comet Hub" but sits on applocationid 1, because CreateCityHub
// copies the creating staff member's location.
hubs := []models.Hub{{Hubid: 22, Hubname: "Chennai Comet Hub", Applocationid: 1}}
applyHubCities(hubs, locations)
if hubs[0].City != "Coimbatore" {
t.Fatalf("city must come from applocationid, got %q", hubs[0].City)
}
})
t.Run("an empty list is not an error", func(t *testing.T) {
applyHubCities(nil, locations)
applyHubCities([]models.Hub{}, locations)
})
}
// The field has to survive JSON encoding, since the console reads `hub.city`.
// `gorm:"-"` keeps it out of the SQL; it must not also keep it out of the body.
func TestHubCityIsSerialised(t *testing.T) {
b, err := json.Marshal(models.Hub{Hubid: 5, Hubname: "Bangalore Earth Hub", City: "Bangalore"})
if err != nil {
t.Fatal(err)
}
var back map[string]any
if err := json.Unmarshal(b, &back); err != nil {
t.Fatal(err)
}
if back["city"] != "Bangalore" {
t.Fatalf("city missing from the hub response: %s", b)
}
}

View File

@@ -61,6 +61,11 @@ func MilerStartDuty(c *fiber.Ctx) error {
})
db.DB.Model(&models.AppUser{}).Where("userid = ?", milerUserID).Update("onduty", 1)
// Make the rider findable by auto-assignment straight away, from the
// position they started duty at, instead of only after the app's first
// location ping. Skipped when the app sent no coordinates.
indexMilerLocation(milerUserID, req.Lat, req.Lon)
return utils.OK(c, fiber.Map{
"dutylogid": dutyLog.Dutylogid,
"loginat": dutyLog.Loginat,

View File

@@ -352,6 +352,32 @@ func UpdateMilerProfile(c *fiber.Ctx) error {
return utils.OK(c, profile)
}
// indexMilerLocation records a rider's position where auto-assignment looks
// for riders: the `miler:gps:{userid}` key and the `milers:locations` GEO set
// that internal/assignment GEOSEARCHes around a pickup.
//
// Used by UpdateMilerLocation and MilerStartDuty. Before, only the location
// ping wrote here, so a rider who had tapped "Start duty" was Available in the
// database but invisible to assignment until their app sent its first GPS
// ping — orders created in that gap were never offered to them.
func indexMilerLocation(milerUserID int, lat, lon float64) {
if db.Rdb == nil || lat == 0 || lon == 0 {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
redisKey := fmt.Sprintf("miler:gps:%d", milerUserID)
val := fmt.Sprintf("%f,%f", lat, lon)
db.Rdb.Set(ctx, redisKey, val, 30*time.Minute)
db.Rdb.GeoAdd(ctx, "milers:locations", &redis.GeoLocation{
Name: strconv.Itoa(milerUserID),
Latitude: lat,
Longitude: lon,
})
}
func UpdateMilerLocation(c *fiber.Ctx) error {
milerUserID := c.Locals("userid").(int)
@@ -380,20 +406,7 @@ func UpdateMilerLocation(c *fiber.Ctx) error {
return utils.Internal(c, "failed to update location")
}
if db.Rdb != nil {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
redisKey := fmt.Sprintf("miler:gps:%d", milerUserID)
val := fmt.Sprintf("%f,%f", req.Latitude, req.Longitude)
db.Rdb.Set(ctx, redisKey, val, 30*time.Minute)
db.Rdb.GeoAdd(ctx, "milers:locations", &redis.GeoLocation{
Name: strconv.Itoa(milerUserID),
Latitude: req.Latitude,
Longitude: req.Longitude,
})
}
indexMilerLocation(milerUserID, req.Latitude, req.Longitude)
return utils.OK(c, fiber.Map{
"latitude": req.Latitude,