diff --git a/controllers/adminController.go b/controllers/adminController.go index 609ba1f..b47fbb4 100644 --- a/controllers/adminController.go +++ b/controllers/adminController.go @@ -2116,8 +2116,26 @@ func GetAdminBookings(c *fiber.Ctx) error { return utils.Internal(c, "failed to count bookings") } + // Newest first, and deterministically so. + // + // Without an ORDER BY the row order is unspecified — Postgres returns heap + // order, which in practice is oldest first. Two things follow, and both bit: + // + // 1. The newest booking sits on the LAST page. The console drains a + // bounded window, so once pickupbookings outgrows that window a + // just-created customer-app booking can never reach the Orders page at + // all. It is written correctly and is simply never fetched. + // 2. OFFSET pagination over an unordered result is not stable: the same + // page can return different rows across two requests, so draining pages + // can duplicate and skip rows well before that threshold. + // + // bookingid rather than createdat: it is the primary key and unique, so the + // sort needs no tiebreaker and the paging cannot wobble between equal + // timestamps. It also matches the order the console already sorts into + // client-side, so page 1 is the newest page by both definitions. var bookings []models.PickupBooking if err := query.Preload("Parcels").Preload("ServiceOptions"). + Order("bookingid DESC"). Offset(offset).Limit(pagesize).Find(&bookings).Error; err != nil { return utils.Internal(c, "failed to fetch bookings") } diff --git a/docs/express-console-api.md b/docs/express-console-api.md index 10e4e69..152540c 100644 --- a/docs/express-console-api.md +++ b/docs/express-console-api.md @@ -251,10 +251,25 @@ delivered, riderkms, ridercharges, dutyminutes }`. | Method | Path | Notes | |---|---|---| -| GET | `/admin/bookings` | tenant-scoped list | +| GET | `/admin/bookings` | tenant-scoped list, **newest first** (`bookingid DESC`) | | POST | `/admin/expressbooking` | create one — passes CityGate | | POST | `/admin/expressbooking/bulk` | `{ "bookings": [ ... ] }`, max 200, per-row results | | GET | `/admin/bookings/:id` | 404 if outside your tenant | + +`GET /admin/bookings` is ordered `bookingid DESC` — page 1 is always the newest +page. The order is guaranteed, not incidental: `bookingid` is the primary key +and unique, so `OFFSET` paging over it is stable and two requests for the same +page return the same rows. Before this was explicit the result order was +unspecified (Postgres heap order, in practice oldest first), which put the +newest booking on the LAST page — a client draining a bounded number of pages +could never reach a just-created booking. + +It also accepts `?status=` as an EXACT, case-sensitive, single-value match +against the stored enum. The stored values are capitalised +(`Pending_Pickup`, `Converted_To_Consignment`); `?status=pending_pickup` +matches nothing and returns an empty list rather than an error. There is no +multi-status or date filter, so a client grouping several statuses into one tab +still has to filter its own rows. | GET | `/admin/bookings/:id/track` | **the tracking screen** — see below | | POST | `/admin/bookings/:id/assign-miler` | | | POST | `/admin/bookings/:id/assign-vehicle` | | @@ -393,7 +408,12 @@ rather than making the console reconcile two stores. - **Envelope**: `{ "success": true, "data": ... }` on success, `{ "success": false, "message": "..." }` on failure. Lists add `total`, paginated lists add `page`. -- **Pagination**: `?pageno=1&pagesize=100`. Default 500, cap 1000. +- **Pagination**: `?pageno=1&pagesize=100`. Default 20, cap 100. Both numbers + are enforced server-side (`min(100, max(1, pagesize))`); a request for a + larger page silently receives 100 rows and the response's own `pagesize` + field reports 100. This entry previously read "default 500, cap 1000", which + was wrong on both counts — clients sizing a page budget off it fetched a + twelfth of what they expected. - **Rate limits**: 300/min per IP globally, 10/min shared across all credential endpoints. Behind the ingress this keys on the proxy IP unless `TRUSTED_PROXIES` is set.