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:
@@ -134,6 +134,48 @@ func VerifyMilerPin(cfg *config.Config) fiber.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// ResetMilerPin lets a miler who forgot their PIN set a new one from just
|
||||
// their phone number, matching ResetCustomerPin's flow exactly (protected
|
||||
// only by authThrottle at the route level, same as the customer version —
|
||||
// no OTP verification wired in here either, consistent with the existing
|
||||
// pattern rather than a change to it).
|
||||
func ResetMilerPin(c *fiber.Ctx) error {
|
||||
req := new(dto.MilerResetPinRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Phone == "" || req.NewPin == "" {
|
||||
return utils.BadRequest(c, "phone and new_pin are required")
|
||||
}
|
||||
|
||||
configID := req.Configid
|
||||
if configID == 0 {
|
||||
configID = 1001
|
||||
}
|
||||
|
||||
var user models.AppUser
|
||||
if err := db.DB.Where("contactno = ? AND configid = ?", req.Phone, configID).First(&user).Error; err != nil {
|
||||
return utils.NotFound(c, "no miler account found for this phone number")
|
||||
}
|
||||
|
||||
if user.Roleid != 5 {
|
||||
return utils.Forbidden(c, "this endpoint is restricted to miler accounts")
|
||||
}
|
||||
|
||||
pinHash, err := utils.HashPassword(req.NewPin)
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to process PIN reset")
|
||||
}
|
||||
|
||||
user.Password = pinHash
|
||||
if err := db.DB.Save(&user).Error; err != nil {
|
||||
return utils.Internal(c, "failed to reset PIN")
|
||||
}
|
||||
|
||||
return utils.Message(c, "PIN reset successfully")
|
||||
}
|
||||
|
||||
func GetMilerProfile(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
|
||||
@@ -443,6 +485,84 @@ func RejectMilerAssignment(c *fiber.Ctx) error {
|
||||
return utils.Message(c, "assignment rejected")
|
||||
}
|
||||
|
||||
// MilerCancelAssignment lets a miler back out of a booking they've already
|
||||
// accepted but not yet picked up (vehicle breakdown, can't reach the
|
||||
// address, etc.). Distinct from RejectMilerAssignment, which only applies
|
||||
// before acceptance — once the parcel is picked up the booking has become a
|
||||
// consignment and this no longer applies (use MilerSkipDelivery instead for
|
||||
// a failed delivery attempt on an in-flight consignment). Releases the
|
||||
// booking for reassignment the same way RejectMilerAssignment does.
|
||||
func MilerCancelAssignment(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid booking ID")
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
if req.Reason == "" {
|
||||
req.Reason = "Cancelled by miler"
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
|
||||
var booking models.PickupBooking
|
||||
if err := tx.Where("bookingid = ? AND assignedmileruserid = ?", bookingID, milerUserID).First(&booking).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.NotFound(c, "assigned booking not found")
|
||||
}
|
||||
|
||||
if booking.Status == constants.BookingPickedUp || booking.Status == constants.BookingConvertedConsignment {
|
||||
tx.Rollback()
|
||||
return utils.BadRequest(c, "booking cannot be cancelled after pickup — the parcel is already in the network")
|
||||
}
|
||||
|
||||
var ba models.BookingAssignment
|
||||
if err := tx.Where("bookingid = ? AND mileruserid = ? AND assignmentstatus = ?",
|
||||
bookingID, milerUserID, constants.AssignmentAccepted).First(&ba).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.BadRequest(c, "no accepted assignment found for this booking")
|
||||
}
|
||||
|
||||
ba.Assignmentstatus = constants.AssignmentCancelled
|
||||
ba.Remarks = req.Reason
|
||||
if err := tx.Save(&ba).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to cancel assignment")
|
||||
}
|
||||
|
||||
booking.Status = constants.BookingCreated
|
||||
booking.Assignedmileruserid = nil
|
||||
booking.Updatedat = time.Now()
|
||||
if err := tx.Save(&booking).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to release booking")
|
||||
}
|
||||
|
||||
if err := tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).
|
||||
Update("availabilitystatus", constants.MilerAvailable).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to update miler availability")
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return utils.Internal(c, "failed to commit cancellation")
|
||||
}
|
||||
|
||||
if booking.Bookingsource == "CRM_Console" {
|
||||
go assignment.AssignCRMMiler(booking.Bookingid)
|
||||
} else {
|
||||
go assignment.AssignCustomerMiler(booking.Bookingid)
|
||||
}
|
||||
|
||||
return utils.Message(c, "assignment cancelled and released for reassignment")
|
||||
}
|
||||
|
||||
func BookingReachedCustomer(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
||||
@@ -665,9 +785,20 @@ func BookingPickupComplete(c *fiber.Ctx) error {
|
||||
consignmentStatus = constants.ConsignmentOutForDelivery
|
||||
}
|
||||
|
||||
// The consignment's tenant is the booking's own tenant (set explicitly at
|
||||
// CreateExpressBooking time), not the completing miler's tenantid claim — a
|
||||
// miler can carry parcels for tenants other than their own, and using
|
||||
// their JWT tenantid here mis-attributed every such consignment. Falls
|
||||
// back to the miler's own tenantid only for B2C bookings that don't carry
|
||||
// one yet, matching the previous behavior for that case.
|
||||
consignmentTenantID := c.Locals("tenantid").(int)
|
||||
if booking.Tenantid != nil {
|
||||
consignmentTenantID = *booking.Tenantid
|
||||
}
|
||||
|
||||
consignment := models.Consignment{
|
||||
Trackingno: trackingNo,
|
||||
Tenantid: c.Locals("tenantid").(int),
|
||||
Tenantid: consignmentTenantID,
|
||||
Pickuplatitude: booking.Pickuplatitude,
|
||||
Pickuplongitude: booking.Pickuplongitude,
|
||||
Deliverylatitude: booking.Deliverylatitude,
|
||||
|
||||
Reference in New Issue
Block a user