updates on the api endpoints on the customer page and more

This commit is contained in:
2026-09-02 16:32:53 +05:30
parent 8e2c484bcb
commit d12629a1e4
31 changed files with 3541 additions and 121 deletions

View File

@@ -2142,7 +2142,23 @@ type AdminBookingRequest struct {
// callers written against the earlier docs. It is never stored as-is: the
// column of that name foreign-keys to appcustomerlocations, not to a
// client's sites.
Pickuplocationid *int `json:"pickuplocationid"`
Pickuplocationid *int `json:"pickuplocationid"`
// PickupSourceType says what kind of place this booking is collected from —
// one of constants.PickupSource*. Optional: left blank it is classified from
// what the payload carries (a base id, a client site id, or neither), so
// existing console callers keep working unchanged. Send it explicitly to
// create a Base/Hub-origin booking.
PickupSourceType string `json:"pickup_source_type"`
// Pickuphubid names the base a Base → Customer booking is collected FROM.
// Required when pickup_source_type is "hub"; supplying it is also enough on
// its own, since a booking that names a base is a base-origin booking. The
// base's own address, pincode and coordinates fill in whatever the caller
// left blank, so the dispatch board never has to retype a gate address.
Pickuphubid *int `json:"pickuphubid"`
// Sourceid is accepted as an alias for whichever id the source type implies —
// the app and the console have both used this spelling. With
// pickup_source_type "hub" it is a base id; otherwise a client-site id.
Sourceid *int `json:"sourceid"`
Pickupaddress string `json:"pickupaddress"`
Pickuppincode string `json:"pickuppincode"`
Pickuplatitude float64 `json:"pickuplatitude"`
@@ -2202,7 +2218,38 @@ func createExpressBooking(req AdminBookingRequest, autoAssign bool) (*models.Pic
// against the earlier documentation. It is the wrong column: it foreign-keys
// to appcustomerlocations, so a tenantlocations id in it fails the insert.
// Both names resolve to Tenantlocationid.
// A base-origin pickup (Base/Hub → Customer). The base is the actual place the
// rider collects from, so its address and coordinates become the booking's
// pickup point and the row records both the type and the base id — that pair
// is what the rider app reads to title the stop as a Base rather than as the
// rider's own office, and what the dispatch board reads back on the row.
baseID := req.Pickuphubid
if baseID == nil && strings.EqualFold(req.PickupSourceType, constants.PickupSourceHub) {
baseID = req.Sourceid
}
if baseID != nil {
var hub models.Hub
if err := db.DB.Where("hubid = ? AND deletedat IS NULL", *baseID).First(&hub).Error; err != nil {
return nil, &expressBookingValidationError{"pickuphubid does not match a known base"}
}
req.PickupSourceType = constants.PickupSourceHub
req.Pickuphubid = &hub.Hubid
if req.Pickupaddress == "" {
req.Pickupaddress = hub.Address
}
if req.Pickuppincode == "" {
req.Pickuppincode = hub.Pincode
}
if req.Pickuplatitude == 0 && req.Pickuplongitude == 0 {
req.Pickuplatitude, req.Pickuplongitude = hub.Latitude, hub.Longitude
}
}
siteID := req.Tenantlocationid
// sourceid doubles as the client-site id when the source is not a base.
if siteID == nil && baseID == nil {
siteID = req.Sourceid
}
if siteID == nil {
siteID = req.Pickuplocationid
}
@@ -2238,10 +2285,34 @@ func createExpressBooking(req AdminBookingRequest, autoAssign bool) (*models.Pic
// pickup actually is. Without this the field stays null — as it did on every
// booking in the system — and per-site reporting has nothing to group by,
// because the console sends a kitchen's address rather than its id.
if req.Tenantlocationid == nil {
if req.Tenantlocationid == nil && req.Pickuphubid == nil {
req.Tenantlocationid = matchTenantLocation(req.Tenantid, req.Pickupaddress, req.Pickuplatitude, req.Pickuplongitude)
}
// Classify the source once, here, rather than leaving every reader to guess.
// A caller-supplied type wins as long as it is one we know; an unknown word is
// dropped rather than stored, so the column never holds something the app has
// no meaning for. A door pickup is recorded as "customer" explicitly — the
// whole point of the column is that a blank cannot be told apart from an
// address nobody filled in.
switch {
case req.Pickuphubid != nil:
req.PickupSourceType = constants.PickupSourceHub
case strings.EqualFold(req.PickupSourceType, constants.PickupSourceStore):
req.PickupSourceType = constants.PickupSourceStore
case strings.EqualFold(req.PickupSourceType, constants.PickupSourceCustomer):
req.PickupSourceType = constants.PickupSourceCustomer
case strings.EqualFold(req.PickupSourceType, constants.PickupSourceMerchant):
// Honoured even with no site id attached. A merchant collection with no
// configured location is still a shop, and telling the rider "customer
// door" would send them looking for a person who is not there.
req.PickupSourceType = constants.PickupSourceMerchant
case req.Tenantlocationid != nil:
req.PickupSourceType = constants.PickupSourceMerchant
default:
req.PickupSourceType = constants.PickupSourceCustomer
}
tx := db.DB.Begin()
customerID := req.Appcustomerid
@@ -2276,6 +2347,8 @@ func createExpressBooking(req AdminBookingRequest, autoAssign bool) (*models.Pic
Appcustomerid: customerID,
Pickuplocationid: req.Pickuplocationid,
Tenantlocationid: req.Tenantlocationid,
Pickupsourcetype: req.PickupSourceType,
Pickuphubid: req.Pickuphubid,
Pickupaddress: req.Pickupaddress,
Pickuppincode: req.Pickuppincode,
Pickuplatitude: req.Pickuplatitude,
@@ -2525,7 +2598,65 @@ func GetAdminBookingDetails(c *fiber.Ctx) error {
if err := q.First(&booking, id).Error; err != nil {
return utils.NotFound(c, "booking not found")
}
return utils.OK(c, booking)
// The routing decision and the inputs it was made from, so a support call
// about "why does this say handover instead of delivery" is a lookup rather
// than a reconstruction. Everything here is derived from stored state — no
// new columns, and it stays right if the routing rule changes, because it
// reads the same helpers the pivot does.
var customer models.AppCustomer
db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer)
sourceType, sourceID, sourceName, sourceAddress := pickupSource(&booking,
customer.Firstname+" "+customer.Lastname)
routing := fiber.Map{
"pickup_source_type": sourceType,
"pickup_source_id": sourceID,
"pickup_source_name": sourceName,
"from_address": sourceAddress,
"from_pincode": booking.Pickuppincode,
"to_address": booking.Deliveryaddress,
"destination_pincode": booking.Deliverypincode,
// hyperlocal: same postal area, so no base leg — the collecting rider
// carries it to the receiver. Otherwise it goes through a base. This is the
// decision pickup-complete makes, shown with the inputs it makes it from.
"is_hyperlocal": isHyperlocalBooking(booking.Pickuppincode, booking.Deliverypincode,
booking.Pickuplatitude, booking.Pickuplongitude,
booking.Deliverylatitude, booking.Deliverylongitude),
}
// Before pickup the decision has not been taken yet, so the routing result is
// a projection; after pickup it is fact, read off the consignment.
if booking.Consignmentid != nil {
var cn models.Consignment
if db.DB.First(&cn, *booking.Consignmentid).Error == nil {
booking.Consignmentstatus = cn.Status
routing["consignment_state"] = cn.Status
routing["next_action"] = nextActionForConsignment(cn.Status)
routing["next_hub"] = renderBase(loadHub(cn.Currenthubid))
routing["inwardedat"] = cn.Inwardedat
routing["decided"] = true
}
} else {
routing["consignment_state"] = ""
routing["next_action"] = constants.NextActionPickup
routing["next_hub"] = nil
routing["decided"] = false
}
// routing rides alongside the booking's own fields rather than nesting them
// under a new key — the console reads this response as a booking object today,
// and moving those fields would break every screen that does.
raw, err := json.Marshal(booking)
if err != nil {
return utils.OK(c, booking)
}
payload := map[string]interface{}{}
if err := json.Unmarshal(raw, &payload); err != nil {
return utils.OK(c, booking)
}
payload["routing"] = routing
return utils.OK(c, payload)
}
func AdminAssignMiler(c *fiber.Ctx) error {