updates on the ai agents and time series prediction and updates on the api to

This commit is contained in:
2026-10-09 13:50:30 +05:30
parent 29690c56f2
commit 153be40e5c
42 changed files with 4889 additions and 186 deletions

View File

@@ -14,7 +14,6 @@ import (
"doormile/dto"
"doormile/internal/assignment"
"doormile/internal/legs"
"doormile/internal/routing"
"doormile/models"
"doormile/utils"
@@ -1972,122 +1971,33 @@ func HubBatchAssign(c *fiber.Ctx) error {
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "invalid request body")
}
capPerRider := req.MaxPerRider
if capPerRider <= 0 {
capPerRider = defaultBatchAssignCapPerRider
}
bookingQuery := db.DB.Where("assignedmileruserid IS NULL AND status = ?", constants.BookingPendingPickup)
if len(req.Bookingids) > 0 {
bookingQuery = bookingQuery.Where("bookingid IN ?", req.Bookingids)
} else if prefix != "" {
bookingQuery = bookingQuery.Where("pickuppincode LIKE ?", prefix+"%")
}
bookingQuery = scopeBookingsToOwnTenant(c, bookingQuery)
var bookings []models.PickupBooking
if err := bookingQuery.Order("createdat ASC").Find(&bookings).Error; err != nil {
return utils.Internal(c, "failed to fetch pending bookings")
}
if len(bookings) == 0 {
return utils.OK(c, fiber.Map{"assigned": 0, "skipped": 0, "results": []fiber.Map{}})
}
// Every rider on duty at this hub, not only the idle ones. capPerRider is
// what limits a round; requiring Available made that limit unreachable,
// because a rider stopped being Available the moment they took the first
// booking of the very batch being built.
var riderProfiles []models.MilerProfile
if err := db.DB.Where("hubid = ? AND availabilitystatus IN ?", hubID, constants.MilerWorkingStatuses).
Find(&riderProfiles).Error; err != nil {
return utils.Internal(c, "failed to fetch available riders")
}
candidates := make([]*batchRiderCandidate, 0, len(riderProfiles))
for _, mp := range riderProfiles {
// Seed the count with what the rider is ALREADY holding. capPerRider
// has to mean "stops in hand", not "stops added by this call" — now
// that busy riders are eligible, counting only this call's additions
// would hand five more to someone already carrying five.
var openStops int64
db.DB.Model(&models.BookingAssignment{}).
Where("mileruserid = ? AND assignmentstatus IN ?", mp.Userid, []string{
constants.AssignmentAssigned,
constants.AssignmentAccepted,
}).
Count(&openStops)
candidates = append(candidates, &batchRiderCandidate{
userid: mp.Userid, lat: mp.Currentlatitude, lon: mp.Currentlongitude,
assigned: int(openStops),
})
}
results := make([]fiber.Map, 0, len(bookings))
assignedCount, skippedCount := 0, 0
for _, b := range bookings {
var nearest *batchRiderCandidate
nearestDist := math.MaxFloat64
for _, cand := range candidates {
if cand.assigned >= capPerRider {
continue
// The solver itself now lives in batchAssignService.go, shared with
// AdminBatchAssign. Only the scope differs: this route's riders are the
// ones on duty AT THIS HUB, and with no explicit bookingids it clears this
// hub's own pincode-prefixed queue.
result, err := RunBatchAssign(req.Bookingids, req.MaxPerRider, batchRiderScope{
ActorID: staffID,
Bookings: func(q *gorm.DB) *gorm.DB {
if len(req.Bookingids) == 0 && prefix != "" {
q = q.Where("pickuppincode LIKE ?", prefix+"%")
}
d := haversineKM(b.Pickuplatitude, b.Pickuplongitude, cand.lat, cand.lon)
if d < nearestDist {
nearestDist = d
nearest = cand
}
}
if nearest == nil {
results = append(results, fiber.Map{
"bookingid": b.Bookingid, "bookingno": b.Bookingno,
"assigned": false, "reason": "no available rider under capacity",
})
skippedCount++
continue
}
if _, err := AssignMilerToBooking(b.Bookingid, nearest.userid, &staffID); err != nil {
results = append(results, fiber.Map{
"bookingid": b.Bookingid, "bookingno": b.Bookingno,
"assigned": false, "reason": err.Error(),
})
skippedCount++
continue
}
nearest.assigned++
results = append(results, fiber.Map{
"bookingid": b.Bookingid, "bookingno": b.Bookingno,
"assigned": true, "mileruserid": nearest.userid, "distance_km": nearestDist,
})
assignedCount++
}
// Batch assignment is exactly the case stop-ordering exists for: a rider
// walks out of here with several bookings and, until now, no indication of
// what order to run them in. Sequence each rider that actually got work.
//
// Best-effort and deliberately after the assignments are committed: the
// optimizer is a separate service over the network, and it failing must
// leave the bookings assigned rather than undoing the batch.
sequenced := 0
for _, r := range ridersAssigned(results) {
if _, err := routing.SequenceMilerStops(r); err != nil {
utils.Warn("HubBatchAssign: stop sequencing failed",
"miler_userid", r, "error", err)
continue
}
sequenced++
return scopeBookingsToOwnTenant(c, q)
},
Riders: func(q *gorm.DB) *gorm.DB {
return q.Where("hubid = ?", hubID)
},
})
if err != nil {
utils.Error("HubBatchAssign: failed", "error", err)
return utils.Internal(c, "failed to assign bookings")
}
return utils.OK(c, fiber.Map{
"assigned": assignedCount,
"skipped": skippedCount,
"riderssequenced": sequenced,
"results": results,
"assigned": result.Assigned,
"skipped": result.Skipped,
"riderssequenced": result.Riderssequenced,
"results": result.Results,
})
}