176 lines
7.3 KiB
Go
176 lines
7.3 KiB
Go
package assignment
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"doormile/constants"
|
|
"doormile/models"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Balanced assignment, Phase B, against a real Postgres. Skipped unless
|
|
// REGISTRY_TEST_DSN is set — see eligibility_pg_test.go.
|
|
|
|
// autoOrder is order() for an assignment made by auto-assignment (labelled),
|
|
// the only kind the release touches.
|
|
func autoOrder(t *testing.T, gdb *gorm.DB, rider int, bookingStatus, asgStatus string, at time.Time) int {
|
|
t.Helper()
|
|
id := order(t, gdb, rider, bookingStatus, asgStatus, at)
|
|
mustDo(t, gdb.Model(&models.BookingAssignment{}).Where("bookingid = ?", id).Update("remarks", autoAssignedRemark).Error)
|
|
return id
|
|
}
|
|
|
|
func bookingState(t *testing.T, gdb *gorm.DB, id int) (string, *int) {
|
|
t.Helper()
|
|
var b models.PickupBooking
|
|
mustDo(t, gdb.First(&b, id).Error)
|
|
return b.Status, b.Assignedmileruserid
|
|
}
|
|
|
|
func assignmentState(t *testing.T, gdb *gorm.DB, bookingID int) string {
|
|
t.Helper()
|
|
var a models.BookingAssignment
|
|
mustDo(t, gdb.Where("bookingid = ?", bookingID).Order("bookingassignmentid DESC").First(&a).Error)
|
|
return a.Assignmentstatus
|
|
}
|
|
|
|
// An order the rider never accepted goes back to the pool after the timeout;
|
|
// one they accepted, or one still within the timeout, stays with them.
|
|
func TestReleaseUnacceptedOrders(t *testing.T) {
|
|
gdb := balanceSetup(t)
|
|
t.Setenv("ASSIGNMENT_ACCEPT_TIMEOUT_MINUTES", "10")
|
|
onDuty(t, gdb, 1, time.Now().Add(-time.Hour))
|
|
mustDo(t, gdb.Model(&models.MilerProfile{}).Where("userid = 1").Update("availabilitystatus", constants.MilerAssigned).Error)
|
|
|
|
stale := autoOrder(t, gdb, 1, constants.BookingMilerAssigned, constants.AssignmentAssigned, time.Now().Add(-15*time.Minute))
|
|
fresh := autoOrder(t, gdb, 1, constants.BookingMilerAssigned, constants.AssignmentAssigned, time.Now().Add(-3*time.Minute))
|
|
accepted := autoOrder(t, gdb, 1, constants.BookingPickupScheduled, constants.AssignmentAccepted, time.Now().Add(-40*time.Minute))
|
|
|
|
if n := releaseUnaccepted(); n != 1 {
|
|
t.Fatalf("released %d, want 1", n)
|
|
}
|
|
if st, r := bookingState(t, gdb, stale); st != constants.BookingPendingPickup || r != nil {
|
|
t.Fatalf("stale order: status %s rider %v, want Pending_Pickup with no rider", st, r)
|
|
}
|
|
if s := assignmentState(t, gdb, stale); s != constants.AssignmentReassigned {
|
|
t.Fatalf("stale assignment = %s, want Reassigned", s)
|
|
}
|
|
if st, r := bookingState(t, gdb, fresh); st != constants.BookingMilerAssigned || r == nil {
|
|
t.Fatal("an order still inside the timeout must stay with the rider")
|
|
}
|
|
if st, r := bookingState(t, gdb, accepted); st != constants.BookingPickupScheduled || r == nil {
|
|
t.Fatal("an accepted order must never be released")
|
|
}
|
|
// The rider still holds two orders, so stays Assigned.
|
|
var p models.MilerProfile
|
|
mustDo(t, gdb.Where("userid = 1").First(&p).Error)
|
|
if p.Availabilitystatus != constants.MilerAssigned {
|
|
t.Fatalf("rider status %s, want Assigned (still holding orders)", p.Availabilitystatus)
|
|
}
|
|
|
|
// A rider hand-picked by staff (no auto label) is never taken away.
|
|
manual := order(t, gdb, 1, constants.BookingMilerAssigned, constants.AssignmentAssigned, time.Now().Add(-2*time.Hour))
|
|
if n := releaseUnaccepted(); n != 0 {
|
|
t.Fatal("a manually assigned order must never be released")
|
|
}
|
|
if st, r := bookingState(t, gdb, manual); st != constants.BookingMilerAssigned || r == nil {
|
|
t.Fatal("manual order changed")
|
|
}
|
|
|
|
t.Setenv("ASSIGNMENT_ACCEPT_TIMEOUT_MINUTES", "0")
|
|
autoOrder(t, gdb, 1, constants.BookingMilerAssigned, constants.AssignmentAssigned, time.Now().Add(-2*time.Hour))
|
|
if n := releaseUnaccepted(); n != 0 {
|
|
t.Fatal("timeout 0 must switch the release off")
|
|
}
|
|
}
|
|
|
|
// A rider freed of their only order goes back to Available.
|
|
func TestReleaseFreesARiderWithNothingElse(t *testing.T) {
|
|
gdb := balanceSetup(t)
|
|
onDuty(t, gdb, 1, time.Now().Add(-time.Hour))
|
|
mustDo(t, gdb.Model(&models.MilerProfile{}).Where("userid = 1").Update("availabilitystatus", constants.MilerAssigned).Error)
|
|
autoOrder(t, gdb, 1, constants.BookingMilerAssigned, constants.AssignmentAssigned, time.Now().Add(-30*time.Minute))
|
|
if n := releaseUnaccepted(); n != 1 {
|
|
t.Fatalf("released %d, want 1", n)
|
|
}
|
|
var p models.MilerProfile
|
|
mustDo(t, gdb.Where("userid = 1").First(&p).Error)
|
|
if p.Availabilitystatus != constants.MilerAvailable {
|
|
t.Fatalf("rider status %s, want Available", p.Availabilitystatus)
|
|
}
|
|
}
|
|
|
|
// A rider accepting at the same moment wins: nothing is released.
|
|
func TestReleaseLosesToAConcurrentAccept(t *testing.T) {
|
|
gdb := balanceSetup(t)
|
|
onDuty(t, gdb, 1, time.Now().Add(-time.Hour))
|
|
b := order(t, gdb, 1, constants.BookingMilerAssigned, constants.AssignmentAssigned, time.Now().Add(-30*time.Minute))
|
|
var a models.BookingAssignment
|
|
mustDo(t, gdb.Where("bookingid = ?", b).First(&a).Error)
|
|
// The rider accepts between the release listing and its write.
|
|
mustDo(t, gdb.Model(&models.PickupBooking{}).Where("bookingid = ?", b).Update("status", constants.BookingPickupScheduled).Error)
|
|
if releaseOne(a.Bookingassignmentid, b, 1, 10) {
|
|
t.Fatal("must not release an order the rider has just accepted")
|
|
}
|
|
if s := assignmentState(t, gdb, b); s != constants.AssignmentAssigned {
|
|
t.Fatalf("assignment changed to %s", s)
|
|
}
|
|
}
|
|
|
|
// A released (or rejected) order is never offered back to that rider: it goes
|
|
// to someone else even if the first rider is now the least loaded.
|
|
func TestReleasedOrderGoesToAnotherRider(t *testing.T) {
|
|
gdb := balanceSetup(t)
|
|
onDuty(t, gdb, 1, time.Now().Add(-time.Hour))
|
|
onDuty(t, gdb, 2, time.Now().Add(-time.Hour))
|
|
// Rider 2 is busier, so a plain balance would pick rider 1.
|
|
order(t, gdb, 2, constants.BookingMilerAssigned, constants.AssignmentAccepted, time.Now())
|
|
b := autoOrder(t, gdb, 1, constants.BookingMilerAssigned, constants.AssignmentAssigned, time.Now().Add(-30*time.Minute))
|
|
if n := releaseUnaccepted(); n != 1 {
|
|
t.Fatalf("released %d, want 1", n)
|
|
}
|
|
id, err := assignOne(t, gdb, b, near(1, 2))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if id != 2 {
|
|
t.Fatalf("released order went to rider %d, want 2 (never back to rider 1)", id)
|
|
}
|
|
// Same rule for a rider who rejected it.
|
|
r := order(t, gdb, 0, constants.BookingPendingPickup, "", time.Time{})
|
|
mustDo(t, gdb.Create(&models.BookingAssignment{Bookingid: r, Mileruserid: 2, Assignmentstatus: constants.AssignmentRejected, Assignedat: time.Now()}).Error)
|
|
if id, _ := assignOne(t, gdb, r, near(1, 2)); id != 1 {
|
|
t.Fatalf("rejected order went to rider %d, want 1", id)
|
|
}
|
|
}
|
|
|
|
// When a rider starts duty, only the pending orders within reach are swept.
|
|
func TestPendingNearOnlyPicksNearbyOrders(t *testing.T) {
|
|
gdb := balanceSetup(t)
|
|
now := time.Now()
|
|
mk := func(no string, lat, lon float64) {
|
|
nextBookingID++
|
|
mustDo(t, gdb.Create(&models.PickupBooking{Bookingid: nextBookingID, Bookingno: no, Status: constants.BookingPendingPickup,
|
|
Bookingsource: constants.BookingSourceExpress, Pickuplatitude: lat, Pickuplongitude: lon,
|
|
Createdat: now.Add(-10 * time.Minute)}).Error)
|
|
}
|
|
mk("DM-NEAR1", 11.0090, 76.9500) // at the rider
|
|
mk("DM-NEAR2", 11.0500, 76.9800) // ~5.5 km
|
|
mk("DM-FAR", 11.3000, 77.2000) // ~40 km
|
|
mk("DM-CHENNAI", 13.0827, 80.2707) // another city
|
|
|
|
got, err := pendingNear(11.0090, 76.9500, now)
|
|
mustDo(t, err)
|
|
names := map[string]bool{}
|
|
for _, b := range got {
|
|
var full models.PickupBooking
|
|
mustDo(t, gdb.First(&full, b.Bookingid).Error)
|
|
names[full.Bookingno] = true
|
|
}
|
|
if len(got) != 2 || !names["DM-NEAR1"] || !names["DM-NEAR2"] {
|
|
t.Fatalf("swept %v, want only the two nearby orders", names)
|
|
}
|
|
}
|