feat: 14 new endpoints closing jupiter->Doormile API gaps, plus two tenant-scoping bug fixes

New endpoints:
- Admin: partner CRUD (GET/POST /admin/partners, GET/PUT/DELETE
  /admin/partners/:id), bulk express booking create
  (POST /admin/expressbooking/bulk), bulk cancel
  (POST /admin/bookings/bulk-cancel), reports (GET /admin/reports),
  password change (PUT /admin/profile/password), miler notify
  (POST /admin/milers/:id/notify)
- Miler: PIN reset (POST /miler/reset-pin), cancel assignment
  (POST /miler/bookings/:bookingid/cancel), skip delivery
  (POST /miler/consignments/:id/skip)
- Hub: batch assign (POST /hub/bookings/batch-assign) - greedy
  nearest-rider queue clearing, capped per rider

Bug fixes:
- BookingPickupComplete now sets Consignment.Tenantid from the
  booking's tenant instead of the completing miler's own tenant
  (fixes cross-tenant shipment mis-attribution)
- GetHubUnassignedBookings/GetHubBookingsRange now scoped via
  scopeBookingsToOwnTenant (fixes partner hub staff seeing other
  tenants' bookings)

Also: CRM booking routes renamed to expressbooking to end the naming
collision with the separate CRM clients feature; PickupBooking gains
nullable Tenantid; adds CLAUDE.md project memory.

Verified: go build ./... and go vet ./... both clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 12:22:39 +05:30
parent 77a723e047
commit c272a33fa6
10 changed files with 1404 additions and 43 deletions

View File

@@ -376,6 +376,91 @@ func MilerDeliverConsignment(c *fiber.Ctx) error {
})
}
// MilerSkipDelivery records a failed/incomplete final-mile delivery attempt
// (customer unavailable, gate locked, wrong address, etc.) without closing
// out the consignment — the miler still holds the parcel, the consignment
// stays Out_for_Delivery, and the attempt is logged for a retry. This is the
// "skipped" half of the old system's 8-in-1 status endpoint; MilerDeliverConsignment
// above is the "delivered" half, and MilerCancelAssignment (milerController.go)
// is the pre-pickup "cancelled"/"rejected" half.
func MilerSkipDelivery(c *fiber.Ctx) error {
milerUserID := c.Locals("userid").(int)
consignmentID, err := strconv.Atoi(c.Params("id"))
if err != nil {
return utils.BadRequest(c, "invalid consignment ID")
}
var req struct {
Reason string `json:"reason"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
}
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "invalid request body")
}
if req.Reason == "" {
return utils.BadRequest(c, "reason is required")
}
var consignment models.Consignment
if err := db.DB.First(&consignment, consignmentID).Error; err != nil {
return utils.NotFound(c, "consignment not found")
}
var booking models.PickupBooking
if err := db.DB.Where("consignmentid = ? AND assignedmileruserid = ?", consignment.Consignmentid, milerUserID).
First(&booking).Error; err != nil {
return utils.NotFound(c, "assigned consignment not found")
}
if consignment.Status != constants.ConsignmentOutForDelivery {
return utils.BadRequest(c, "consignment is not out for delivery")
}
tx := db.DB.Begin()
consignment.Attemptcount += 1
consignment.Updatedat = time.Now()
if err := tx.Save(&consignment).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to record skipped attempt")
}
history := models.ConsignmentHistory{
Consignmentid: consignment.Consignmentid,
Userid: &milerUserID,
Eventstatus: "Delivery_Skipped",
Remarks: fmt.Sprintf("Attempt %d skipped at (%.5f, %.5f): %s", consignment.Attemptcount, req.Lat, req.Lon, req.Reason),
}
if err := tx.Create(&history).Error; err != nil {
tx.Rollback()
return utils.Internal(c, "failed to log skipped attempt")
}
if err := tx.Commit().Error; err != nil {
return utils.Internal(c, "failed to commit skipped attempt")
}
// After 3 failed attempts, flag it for hub attention rather than leaving
// it silently retrying forever.
if consignment.Attemptcount >= 3 {
exception := models.ConsignmentException{
Consignmentid: consignment.Consignmentid,
Reportedbyuserid: &milerUserID,
Exceptiontype: constants.ExceptionUndeliverable,
Severity: "Medium",
Description: fmt.Sprintf("3 delivery attempts failed. Last reason: %s", req.Reason),
}
db.DB.Create(&exception)
}
return utils.OK(c, fiber.Map{
"consignmentid": consignment.Consignmentid,
"attemptcount": consignment.Attemptcount,
"status": consignment.Status,
})
}
// --------------------
// EARNINGS
// --------------------