feat: express-batch dispatch — manual trigger for the AI agent
Adds the backend half of the ExpressDispatchAgent flow. Express orders can now be created batch by batch (bulk create only accumulates them, unassigned), then an operator hits one endpoint to hand the whole pending set to the agent for tenant-scoped assignment + road sequencing. The normal B2C flow is untouched. - POST /admin/expressbooking/dispatch: manual trigger. Console-auth, tenant- scoped; gathers the tenant's pending unassigned express orders (or a chosen subset) and publishes express.dispatch_requested. - internal API for the agent: GET /internal/express/riders (tenant's available riders), GET /internal/express/bookings, POST /internal/express/assign (writes the agent's decided assignments with their sequence; re-checks the already- assigned guard so the agent can't double-assign). - booking_assignment_service.go: extracted a behavior-preserving assignMilerTx core; AssignMilerToBooking is unchanged in behavior. assignExpressStops writes a batch, one FCM per rider instead of one per stop. - EXPRESS JetStream stream / express.dispatch_requested subject. - Gated behind EXPRESS_AGENT_ENABLED (default off): deploying this changes nothing until the agent is confirmed running and the flag is flipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2150,7 +2150,12 @@ func (e *expressBookingValidationError) Error() string { return e.msg }
|
||||
// AdminBulkCreateBookings (many, used by CSV/bulk import). Takes no
|
||||
// *fiber.Ctx — the original function never touched c after BodyParser, so
|
||||
// both callers can use this identically.
|
||||
func createExpressBooking(req AdminBookingRequest) (*models.PickupBooking, error) {
|
||||
// createExpressBooking creates one console booking. autoAssign controls whether
|
||||
// it kicks off the per-booking assignment engine (AssignCRMMiler) inline. Single
|
||||
// bookings assign inline; a bulk batch passes false, because the whole batch is
|
||||
// handed to the ExpressDispatchAgent as one unit instead — assigning inline as
|
||||
// well would assign every booking twice by two different logics.
|
||||
func createExpressBooking(req AdminBookingRequest, autoAssign bool) (*models.PickupBooking, error) {
|
||||
if len(req.Parcels) == 0 {
|
||||
return nil, &expressBookingValidationError{"at least one parcel is required"}
|
||||
}
|
||||
@@ -2386,7 +2391,9 @@ func createExpressBooking(req AdminBookingRequest) (*models.PickupBooking, error
|
||||
return nil, fmt.Errorf("failed to create booking")
|
||||
}
|
||||
|
||||
go assignment.AssignCRMMiler(booking.Bookingid)
|
||||
if autoAssign {
|
||||
go assignment.AssignCRMMiler(booking.Bookingid)
|
||||
}
|
||||
|
||||
if db.Js != nil {
|
||||
payload := map[string]interface{}{
|
||||
@@ -2421,7 +2428,7 @@ func CreateExpressBooking(c *fiber.Ctx) error {
|
||||
req.Tenantid = own
|
||||
}
|
||||
|
||||
booking, err := createExpressBooking(*req)
|
||||
booking, err := createExpressBooking(*req, true)
|
||||
if err != nil {
|
||||
if _, ok := err.(*expressBookingValidationError); ok {
|
||||
return utils.BadRequest(c, err.Error())
|
||||
@@ -2461,13 +2468,20 @@ func AdminBulkCreateBookings(c *fiber.Ctx) error {
|
||||
|
||||
ownTenant := consoleTenantID(c)
|
||||
|
||||
// When the express agent is enabled, a bulk create only *accumulates* pending
|
||||
// bookings — inline per-booking assignment is suppressed so orders can pile up
|
||||
// batch by batch, and the operator later fires the agent over the whole lot
|
||||
// with POST /admin/expressbooking/dispatch. When disabled (default), nothing
|
||||
// changes from the old behavior: each booking auto-assigns inline.
|
||||
agentEnabled := expressAgentEnabled()
|
||||
|
||||
for i, item := range req.Bookings {
|
||||
// Same tenant pin as the single-booking path — a bulk import must not
|
||||
// be a way around it.
|
||||
if ownTenant != 0 {
|
||||
item.Tenantid = ownTenant
|
||||
}
|
||||
booking, err := createExpressBooking(item)
|
||||
booking, err := createExpressBooking(item, !agentEnabled)
|
||||
if err != nil {
|
||||
results = append(results, result{Index: i, Success: false, Error: err.Error()})
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user