This commit is contained in:
2026-09-22 15:27:13 +05:30
parent 29e189b7e0
commit ee79c80338
8 changed files with 1042 additions and 4 deletions

180
scratch/fix_miler_hubs.go Normal file
View File

@@ -0,0 +1,180 @@
//go:build ignore
// Give every Available miler the hub it plainly belongs to.
//
// Why this is needed: assignment everywhere filters
// `hubid = ? AND availabilitystatus = 'Available'`, and that intersection was
// empty — all 15 Available milers had hubid NULL, while all 15 milers that
// had a hub were Offline or already busy. Two seed runs populated disjoint
// sets, so no rider was reachable by HubBatchAssign, HubAutoAssign, or the
// auto-assign retry loop.
//
// The hub is not guessed. Each miler is matched to the NEAREST ACTIVE HUB
// WITHIN ITS OWN applocationid — a rider is never handed a hub in another
// city, and within a city the closest one wins. The seeded GPS sits almost
// exactly on a hub in every case, so this reproduces the intended pairing
// rather than inventing one.
//
// Run: go run scratch/fix_miler_hubs.go (plan only, writes nothing)
// go run scratch/fix_miler_hubs.go --apply (writes, in one transaction)
package main
import (
"fmt"
"math"
"os"
"strings"
"doormile/config"
"doormile/db"
"github.com/joho/godotenv"
)
type hub struct {
Hubid int
Hubname string
Applocationid int
Latitude float64
Longitude float64
}
type miler struct {
Userid int
Displayname string
Applocationid int
Currentlatitude float64
Currentlongitude float64
}
func haversineKM(lat1, lon1, lat2, lon2 float64) float64 {
const R = 6371
toRad := func(d float64) float64 { return d * math.Pi / 180 }
dLat, dLon := toRad(lat2-lat1), toRad(lon2-lon1)
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(toRad(lat1))*math.Cos(toRad(lat2))*math.Sin(dLon/2)*math.Sin(dLon/2)
return R * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
}
func main() {
apply := false
for _, a := range os.Args[1:] {
if a == "--apply" {
apply = true
}
}
_ = godotenv.Load()
db.Connect(config.Load())
if db.DB == nil {
fmt.Println("no database connection")
os.Exit(1)
}
var hubs []hub
db.DB.Raw(`SELECT hubid, COALESCE(hubname,'') AS hubname,
COALESCE(applocationid,0) AS applocationid,
COALESCE(latitude,0) AS latitude, COALESCE(longitude,0) AS longitude
FROM hubs
WHERE deletedat IS NULL AND status = 'Active'
AND latitude IS NOT NULL AND longitude IS NOT NULL
AND latitude <> 0 AND longitude <> 0`).Scan(&hubs)
var milers []miler
db.DB.Raw(`SELECT userid, COALESCE(displayname,'') AS displayname,
COALESCE(applocationid,0) AS applocationid,
COALESCE(currentlatitude,0) AS currentlatitude,
COALESCE(currentlongitude,0) AS currentlongitude
FROM milerprofiles
WHERE availabilitystatus = 'Available' AND hubid IS NULL
ORDER BY applocationid, userid`).Scan(&milers)
fmt.Printf("candidate hubs: %d milers to fix: %d\n\n", len(hubs), len(milers))
type change struct {
userid int
name string
hubid int
hubname string
km float64
}
var changes []change
var skipped []string
for _, m := range milers {
best := -1
bestKM := math.MaxFloat64
for _, h := range hubs {
// Never cross a city boundary, whatever the distance says.
if h.Applocationid != m.Applocationid {
continue
}
d := haversineKM(m.Currentlatitude, m.Currentlongitude, h.Latitude, h.Longitude)
if d < bestKM {
bestKM, best = d, h.Hubid
}
}
if best == -1 {
skipped = append(skipped, fmt.Sprintf("user=%d %s (no active hub at applocationid=%d)",
m.Userid, m.Displayname, m.Applocationid))
continue
}
name := ""
for _, h := range hubs {
if h.Hubid == best {
name = h.Hubname
}
}
changes = append(changes, change{m.Userid, m.Displayname, best, name, bestKM})
}
fmt.Println("PLAN — nearest active hub in the miler's own city:")
for _, c := range changes {
fmt.Printf(" user=%-5d %-28s -> hub %-4d %-30s (%.2f km)\n",
c.userid, c.name, c.hubid, c.hubname, c.km)
}
if len(skipped) > 0 {
fmt.Println("\nSKIPPED (left untouched):")
for _, s := range skipped {
fmt.Println(" " + s)
}
}
// Rollback is trivial and worth printing either way: every row being
// written currently holds NULL, so undoing this is one statement.
ids := make([]string, 0, len(changes))
for _, c := range changes {
ids = append(ids, fmt.Sprint(c.userid))
}
fmt.Printf("\nROLLBACK:\n UPDATE milerprofiles SET hubid = NULL WHERE userid IN (%s);\n",
strings.Join(ids, ","))
if !apply {
fmt.Println("\n(plan only — nothing written. Re-run with --apply to commit.)")
return
}
tx := db.DB.Begin()
for _, c := range changes {
// The hubid IS NULL guard makes this idempotent and means a concurrent
// write cannot be clobbered: if someone set a hub in the meantime,
// theirs stands.
res := tx.Exec(`UPDATE milerprofiles SET hubid = ?, updatedat = NOW()
WHERE userid = ? AND hubid IS NULL`, c.hubid, c.userid)
if res.Error != nil {
tx.Rollback()
fmt.Println("FAILED, rolled back:", res.Error)
os.Exit(1)
}
}
if err := tx.Commit().Error; err != nil {
fmt.Println("commit failed:", err)
os.Exit(1)
}
fmt.Printf("\nAPPLIED: %d milers updated.\n", len(changes))
var eligible int64
db.DB.Raw(`SELECT COUNT(*) FROM milerprofiles
WHERE availabilitystatus = 'Available' AND hubid IS NOT NULL`).Scan(&eligible)
fmt.Println("Available AND hubid set is now:", eligible)
}