179 lines
5.8 KiB
Go
179 lines
5.8 KiB
Go
package playground
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// The read tools the backend serves itself. Every other read tool in the
|
|
// registry runs inside AI_engine (decide_*), calls an external service
|
|
// (sequence_stops, simulate_pricing_quote) or an engine-only internal route
|
|
// (list_express_*), and answers "unavailable" in the playground.
|
|
//
|
|
// Each executor selects named, non-personal columns only — never addresses,
|
|
// names, phones or notes — and Redact runs over the result as a backstop.
|
|
// Coordinates are rounded to 2 decimals (about 1 km).
|
|
|
|
const (
|
|
scanDefaultLimit = 20
|
|
scanMaxLimit = 50
|
|
nearbyMaxKm = 10.0
|
|
nearbyMaxCount = 20
|
|
)
|
|
|
|
// bookingRow is the only projection of a booking the playground exposes.
|
|
type bookingRow struct {
|
|
Bookingid int `json:"bookingid"`
|
|
Bookingno string `json:"bookingno"`
|
|
Tenantid *int `json:"tenantid"`
|
|
Status string `json:"status"`
|
|
Pickuppincode string `json:"pickuppincode"`
|
|
Deliverypincode string `json:"deliverypincode"`
|
|
Deliverycity string `json:"deliverycity"`
|
|
Pickuplatitude float64 `json:"pickuplat"`
|
|
Pickuplongitude float64 `json:"pickuplon"`
|
|
Deliverylatitude float64 `json:"deliverylat"`
|
|
Deliverylongitude float64 `json:"deliverylon"`
|
|
Assignedmileruserid *int `json:"assignedmileruserid"`
|
|
Routekm *float64 `json:"routekm"`
|
|
Createdat *time.Time `json:"-"`
|
|
Createdatist string `json:"createdat_ist,omitempty" gorm:"-"`
|
|
}
|
|
|
|
const bookingColumns = "bookingid, bookingno, tenantid, status, pickuppincode, deliverypincode, deliverycity, " +
|
|
"pickuplatitude, pickuplongitude, deliverylatitude, deliverylongitude, assignedmileruserid, routekm, createdat"
|
|
|
|
func round2(f float64) float64 { return math.Round(f*100) / 100 }
|
|
|
|
func (r *bookingRow) tidy() {
|
|
r.Pickuplatitude, r.Pickuplongitude = round2(r.Pickuplatitude), round2(r.Pickuplongitude)
|
|
r.Deliverylatitude, r.Deliverylongitude = round2(r.Deliverylatitude), round2(r.Deliverylongitude)
|
|
if r.Createdat != nil {
|
|
// pickupbookings.createdat is timestamp WITHOUT zone holding IST digits.
|
|
r.Createdatist = r.Createdat.Format("2006-01-02 15:04")
|
|
}
|
|
}
|
|
|
|
// Executors returns the read tools this backend can serve. A nil db or rdb
|
|
// just leaves the tools that need it out (they then answer "unavailable").
|
|
func Executors(db *gorm.DB, rdb *redis.Client) map[string]Executor {
|
|
execs := map[string]Executor{}
|
|
if db != nil {
|
|
execs["get_booking_cache"] = getBooking(db)
|
|
execs["scan_bookings"] = scanBookings(db)
|
|
}
|
|
if rdb != nil {
|
|
execs["nearby_milers"] = nearbyMilers(rdb)
|
|
}
|
|
return execs
|
|
}
|
|
|
|
func decode(input json.RawMessage, into any) error {
|
|
if len(input) == 0 {
|
|
return nil
|
|
}
|
|
if err := json.Unmarshal(input, into); err != nil {
|
|
return fmt.Errorf("invalid input: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getBooking(db *gorm.DB) Executor {
|
|
return func(ctx context.Context, input json.RawMessage) (any, error) {
|
|
var in struct {
|
|
BookingID int `json:"booking_id"`
|
|
}
|
|
if err := decode(input, &in); err != nil {
|
|
return nil, err
|
|
}
|
|
if in.BookingID <= 0 {
|
|
return nil, errors.New("booking_id (a positive integer) is required")
|
|
}
|
|
var row bookingRow
|
|
res := db.WithContext(ctx).Table("pickupbookings").Select(bookingColumns).
|
|
Where("bookingid = ?", in.BookingID).Limit(1).Scan(&row)
|
|
if res.Error != nil {
|
|
return nil, errors.New("booking lookup failed")
|
|
}
|
|
if res.RowsAffected == 0 {
|
|
return map[string]any{"found": false, "booking_id": in.BookingID}, nil
|
|
}
|
|
row.tidy()
|
|
return map[string]any{"found": true, "booking": row, "source": "pickupbookings table"}, nil
|
|
}
|
|
}
|
|
|
|
func scanBookings(db *gorm.DB) Executor {
|
|
return func(ctx context.Context, input json.RawMessage) (any, error) {
|
|
var in struct {
|
|
Status string `json:"status"`
|
|
Limit int `json:"limit"`
|
|
}
|
|
if err := decode(input, &in); err != nil {
|
|
return nil, err
|
|
}
|
|
if in.Limit <= 0 {
|
|
in.Limit = scanDefaultLimit
|
|
}
|
|
if in.Limit > scanMaxLimit {
|
|
in.Limit = scanMaxLimit
|
|
}
|
|
q := db.WithContext(ctx).Table("pickupbookings").Select(bookingColumns)
|
|
if s := strings.TrimSpace(in.Status); s != "" {
|
|
q = q.Where("status = ?", s)
|
|
}
|
|
var rows []bookingRow
|
|
if err := q.Order("bookingid DESC").Limit(in.Limit).Scan(&rows).Error; err != nil {
|
|
return nil, errors.New("booking scan failed")
|
|
}
|
|
byStatus := map[string]int{}
|
|
for i := range rows {
|
|
rows[i].tidy()
|
|
byStatus[rows[i].Status]++
|
|
}
|
|
return map[string]any{"count": len(rows), "bystatus": byStatus, "bookings": rows, "order": "newest first"}, nil
|
|
}
|
|
}
|
|
|
|
func nearbyMilers(rdb *redis.Client) Executor {
|
|
return func(ctx context.Context, input json.RawMessage) (any, error) {
|
|
var in struct {
|
|
Lat *float64 `json:"lat"`
|
|
Lon *float64 `json:"lon"`
|
|
RadiusKm float64 `json:"radius_km"`
|
|
}
|
|
if err := decode(input, &in); err != nil {
|
|
return nil, err
|
|
}
|
|
if in.Lat == nil || in.Lon == nil || math.Abs(*in.Lat) > 90 || math.Abs(*in.Lon) > 180 {
|
|
return nil, errors.New("lat and lon (decimal degrees) are required")
|
|
}
|
|
if in.RadiusKm <= 0 || in.RadiusKm > nearbyMaxKm {
|
|
in.RadiusKm = 5
|
|
}
|
|
locs, err := rdb.GeoSearchLocation(ctx, "milers:locations", &redis.GeoSearchLocationQuery{
|
|
GeoSearchQuery: redis.GeoSearchQuery{
|
|
Longitude: *in.Lon, Latitude: *in.Lat,
|
|
Radius: in.RadiusKm, RadiusUnit: "km", Sort: "ASC", Count: nearbyMaxCount,
|
|
},
|
|
WithDist: true,
|
|
}).Result()
|
|
if err != nil {
|
|
return nil, errors.New("live rider positions are unavailable")
|
|
}
|
|
riders := make([]map[string]any, 0, len(locs))
|
|
for _, l := range locs {
|
|
riders = append(riders, map[string]any{"miler": l.Name, "distancekm": math.Round(l.Dist*100) / 100})
|
|
}
|
|
return map[string]any{"count": len(riders), "radiuskm": in.RadiusKm, "riders": riders}, nil
|
|
}
|
|
}
|