feat: miler app contract gaps — stop type, COD, pre-pickup skip, profile
Close the gaps the miler-app dev flagged against the deployed contract. - GET /miler/bookings: return stoptype (pickup|delivery, from status), step + road-optimized sequence (cumulativekms/etaminutes/cumulativeeta), and codamount/paymentmode. List sorted by step, unsequenced last. Lookups batched to avoid N+1. - POST /miler/bookings/:bookingid/skip: pre-pickup skip that keeps the booking assigned and resumable — the "route back" the consignment-only delivery skip couldn't give a not-yet-picked-up booking. - GET /miler/earnings: add cancelled_stops + total_stops for success rate. - PUT /miler/profile: persist email (to appusers, 409 on unique clash) and a new nullable milerprofiles.address column. - POST /miler/assignments/:id/reject: accept reason from body OR ?reason=. Notifications read-state and bonuspoints deliberately left as-is — both need a product/business decision, not code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -206,6 +206,22 @@ func MilerEndBreak(c *fiber.Ctx) error {
|
|||||||
// MILER'S OWN BOOKINGS
|
// MILER'S OWN BOOKINGS
|
||||||
// --------------------
|
// --------------------
|
||||||
|
|
||||||
|
// milerStopType tells the app whether the actionable leg of a booking is a
|
||||||
|
// pickup or a delivery, so the mixed-route screen no longer has to infer it
|
||||||
|
// structurally and default everything to "pickup". It is derived from the
|
||||||
|
// booking's own status: before the parcel is in the rider's hands the next
|
||||||
|
// action is the pickup; once picked up (or converted to a consignment) the
|
||||||
|
// next action is the delivery.
|
||||||
|
func milerStopType(status string) string {
|
||||||
|
switch status {
|
||||||
|
case constants.BookingPickedUp, constants.BookingConvertedConsignment:
|
||||||
|
return "delivery"
|
||||||
|
default:
|
||||||
|
// Created, Miler_Assigned, Pickup_Scheduled and anything else pre-pickup.
|
||||||
|
return "pickup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func MilerGetMyBookings(c *fiber.Ctx) error {
|
func MilerGetMyBookings(c *fiber.Ctx) error {
|
||||||
milerUserID := c.Locals("userid").(int)
|
milerUserID := c.Locals("userid").(int)
|
||||||
status := c.Query("status")
|
status := c.Query("status")
|
||||||
@@ -222,19 +238,86 @@ func MilerGetMyBookings(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var bookings []models.PickupBooking
|
var bookings []models.PickupBooking
|
||||||
if err := query.Preload("Parcels").Preload("ServiceOptions").Order("createdat DESC").Find(&bookings).Error; err != nil {
|
if err := query.Preload("Parcels").Preload("ServiceOptions").Preload("Payments").
|
||||||
|
Order("createdat DESC").Find(&bookings).Error; err != nil {
|
||||||
return utils.Internal(c, "failed to fetch bookings")
|
return utils.Internal(c, "failed to fetch bookings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Batch the per-booking lookups so the route stays a handful of queries
|
||||||
|
// regardless of how many stops a rider is carrying.
|
||||||
|
bookingIDs := make([]int, 0, len(bookings))
|
||||||
|
consignmentIDs := make([]int, 0, len(bookings))
|
||||||
|
for _, b := range bookings {
|
||||||
|
bookingIDs = append(bookingIDs, b.Bookingid)
|
||||||
|
if b.Consignmentid != nil {
|
||||||
|
consignmentIDs = append(consignmentIDs, *b.Consignmentid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// step / road-optimized sequence lives on the active assignment row, written
|
||||||
|
// by the express route optimizer. Step 0 = not sequenced (single-stop or
|
||||||
|
// optimizer down), never a position — passed through verbatim.
|
||||||
|
seqByBooking := map[int]models.BookingAssignment{}
|
||||||
|
if len(bookingIDs) > 0 {
|
||||||
|
var assignments []models.BookingAssignment
|
||||||
|
db.DB.Where("mileruserid = ? AND bookingid IN ? AND assignmentstatus IN ?",
|
||||||
|
milerUserID, bookingIDs,
|
||||||
|
[]string{constants.AssignmentAssigned, constants.AssignmentAccepted}).
|
||||||
|
Order("assignedat DESC").Find(&assignments)
|
||||||
|
for _, a := range assignments {
|
||||||
|
if _, seen := seqByBooking[a.Bookingid]; !seen {
|
||||||
|
seqByBooking[a.Bookingid] = a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// COD becomes authoritative on the consignment once picked up; before that it
|
||||||
|
// is whatever cash the booking's payment row says is due.
|
||||||
|
codByConsignment := map[int]models.Consignment{}
|
||||||
|
if len(consignmentIDs) > 0 {
|
||||||
|
var cons []models.Consignment
|
||||||
|
db.DB.Where("consignmentid IN ?", consignmentIDs).Find(&cons)
|
||||||
|
for _, cn := range cons {
|
||||||
|
codByConsignment[cn.Consignmentid] = cn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
response := make([]fiber.Map, 0, len(bookings))
|
response := make([]fiber.Map, 0, len(bookings))
|
||||||
for _, b := range bookings {
|
for _, b := range bookings {
|
||||||
var customer models.AppCustomer
|
var customer models.AppCustomer
|
||||||
db.DB.Where("appcustomerid = ?", b.Appcustomerid).First(&customer)
|
db.DB.Where("appcustomerid = ?", b.Appcustomerid).First(&customer)
|
||||||
|
|
||||||
response = append(response, fiber.Map{
|
// Cash-to-collect: prefer the consignment's COD once it exists, otherwise
|
||||||
|
// fall back to a pending Cash payment on the booking. Prepaid/UPI stays 0.
|
||||||
|
codAmount := 0.0
|
||||||
|
paymentMode := ""
|
||||||
|
if b.Consignmentid != nil {
|
||||||
|
if cn, ok := codByConsignment[*b.Consignmentid]; ok {
|
||||||
|
paymentMode = cn.Paymentmode
|
||||||
|
codAmount = cn.Codamount - cn.Codcollected
|
||||||
|
if codAmount < 0 {
|
||||||
|
codAmount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if codAmount == 0 {
|
||||||
|
for _, p := range b.Payments {
|
||||||
|
if p.Paymentmode == constants.PaymentModeCash && p.Paymentstatus == constants.PaymentStatusPending {
|
||||||
|
codAmount = p.Amount
|
||||||
|
paymentMode = p.Paymentmode
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if paymentMode == "" {
|
||||||
|
paymentMode = p.Paymentmode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
row := fiber.Map{
|
||||||
"bookingid": b.Bookingid,
|
"bookingid": b.Bookingid,
|
||||||
"bookingreference": b.Bookingno,
|
"bookingreference": b.Bookingno,
|
||||||
"status": b.Status,
|
"status": b.Status,
|
||||||
|
"stoptype": milerStopType(b.Status),
|
||||||
"pickupaddress": b.Pickupaddress,
|
"pickupaddress": b.Pickupaddress,
|
||||||
"pickuplatitude": b.Pickuplatitude,
|
"pickuplatitude": b.Pickuplatitude,
|
||||||
"pickuplongitude": b.Pickuplongitude,
|
"pickuplongitude": b.Pickuplongitude,
|
||||||
@@ -245,10 +328,40 @@ func MilerGetMyBookings(c *fiber.Ctx) error {
|
|||||||
"customerphone": customer.Phone,
|
"customerphone": customer.Phone,
|
||||||
"parcels": b.Parcels,
|
"parcels": b.Parcels,
|
||||||
"serviceoptions": b.ServiceOptions,
|
"serviceoptions": b.ServiceOptions,
|
||||||
|
"codamount": codAmount,
|
||||||
|
"paymentmode": paymentMode,
|
||||||
"createdat": b.Createdat,
|
"createdat": b.Createdat,
|
||||||
})
|
// Route sequencing — 0/empty when the stop was never sequenced.
|
||||||
|
"step": 0,
|
||||||
|
"cumulativekms": 0.0,
|
||||||
|
"etaminutes": 0,
|
||||||
|
"cumulativeeta": 0,
|
||||||
|
}
|
||||||
|
if a, ok := seqByBooking[b.Bookingid]; ok {
|
||||||
|
row["step"] = a.Step
|
||||||
|
row["cumulativekms"] = a.Cumulativekms
|
||||||
|
row["etaminutes"] = a.Etaminutes
|
||||||
|
row["cumulativeeta"] = a.Cumulativeeta
|
||||||
|
}
|
||||||
|
response = append(response, row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sequenced stops ascend by step; unsequenced (step 0) fall to the end while
|
||||||
|
// keeping the newest-first order the app already relied on.
|
||||||
|
sort.SliceStable(response, func(i, j int) bool {
|
||||||
|
si, sj := response[i]["step"].(int), response[j]["step"].(int)
|
||||||
|
switch {
|
||||||
|
case si == 0 && sj == 0:
|
||||||
|
return false // preserve existing createdat DESC order
|
||||||
|
case si == 0:
|
||||||
|
return false
|
||||||
|
case sj == 0:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return si < sj
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return utils.List(c, response, int64(len(response)))
|
return utils.List(c, response, int64(len(response)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,6 +622,71 @@ func MilerSkipDelivery(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MilerSkipPickup is the pre-pickup counterpart of MilerSkipDelivery: a rider
|
||||||
|
// defers a booking they were assigned but have not yet picked up (kitchen not
|
||||||
|
// ready, gate shut, arrived too early) without giving it up. Unlike
|
||||||
|
// MilerCancelAssignment this does NOT release the booking for reassignment —
|
||||||
|
// it stays assigned to the same rider so it can be resumed, which is the "route
|
||||||
|
// back" the app had no endpoint for: the delivery-side skip requires a
|
||||||
|
// consignment that a not-yet-picked-up booking does not have.
|
||||||
|
//
|
||||||
|
// The status is left untouched (still pre-pickup and actionable); the attempt
|
||||||
|
// is appended to the booking notes for ops audit, since a pre-pickup booking
|
||||||
|
// has no consignment-history table to write to yet.
|
||||||
|
func MilerSkipPickup(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"`
|
||||||
|
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 booking models.PickupBooking
|
||||||
|
if err := db.DB.Where("bookingid = ? AND assignedmileruserid = ?", bookingID, milerUserID).
|
||||||
|
First(&booking).Error; err != nil {
|
||||||
|
return utils.NotFound(c, "assigned booking not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once the parcel is in the network this is the wrong endpoint — the rider
|
||||||
|
// must use the consignment delivery-skip instead.
|
||||||
|
if booking.Status == constants.BookingPickedUp || booking.Status == constants.BookingConvertedConsignment {
|
||||||
|
return utils.BadRequest(c, "booking already picked up — use the delivery skip on its consignment")
|
||||||
|
}
|
||||||
|
|
||||||
|
note := fmt.Sprintf("[%s] pickup skipped by miler %d at (%.5f, %.5f): %s",
|
||||||
|
time.Now().Format("2006-01-02 15:04"), milerUserID, req.Lat, req.Lon, req.Reason)
|
||||||
|
if booking.Notes != "" {
|
||||||
|
booking.Notes = booking.Notes + "\n" + note
|
||||||
|
} else {
|
||||||
|
booking.Notes = note
|
||||||
|
}
|
||||||
|
booking.Updatedat = time.Now()
|
||||||
|
if err := db.DB.Model(&booking).
|
||||||
|
Updates(map[string]interface{}{"notes": booking.Notes, "updatedat": booking.Updatedat}).Error; err != nil {
|
||||||
|
return utils.Internal(c, "failed to record skipped pickup")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Booking stays assigned and pre-pickup — the app keeps it in the list and
|
||||||
|
// the rider resumes the normal pickup flow when able.
|
||||||
|
return utils.OK(c, fiber.Map{
|
||||||
|
"bookingid": booking.Bookingid,
|
||||||
|
"status": booking.Status,
|
||||||
|
"stoptype": milerStopType(booking.Status),
|
||||||
|
"resumable": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------
|
// --------------------
|
||||||
// EARNINGS
|
// EARNINGS
|
||||||
// --------------------
|
// --------------------
|
||||||
@@ -556,6 +734,18 @@ func MilerGetEarnings(c *fiber.Ctx) error {
|
|||||||
Select("COUNT(*) as completed_stops, COALESCE(SUM(riderkms),0) as total_kms, COALESCE(SUM(ridercharges),0) as total_earnings, COALESCE(SUM(bonuspoints),0) as total_bonus").
|
Select("COUNT(*) as completed_stops, COALESCE(SUM(riderkms),0) as total_kms, COALESCE(SUM(ridercharges),0) as total_earnings, COALESCE(SUM(bonuspoints),0) as total_bonus").
|
||||||
Scan(&summary)
|
Scan(&summary)
|
||||||
|
|
||||||
|
// Cancelled/rejected assignments carry no completedat, so they are counted
|
||||||
|
// over the same window by assignedat. total_stops lets the app show a real
|
||||||
|
// success rate (completed / total) instead of inferring it from completions
|
||||||
|
// alone.
|
||||||
|
var cancelledStops int64
|
||||||
|
db.DB.Model(&models.BookingAssignment{}).
|
||||||
|
Where("mileruserid = ? AND assignmentstatus IN ? AND assignedat >= ? AND assignedat < ?",
|
||||||
|
milerUserID,
|
||||||
|
[]string{constants.AssignmentCancelled, constants.AssignmentRejected},
|
||||||
|
start, end).
|
||||||
|
Count(&cancelledStops)
|
||||||
|
|
||||||
var breakdown []models.BookingAssignment
|
var breakdown []models.BookingAssignment
|
||||||
db.DB.Where("mileruserid = ? AND assignmentstatus = ? AND completedat >= ? AND completedat < ?",
|
db.DB.Where("mileruserid = ? AND assignmentstatus = ? AND completedat >= ? AND completedat < ?",
|
||||||
milerUserID, constants.AssignmentCompleted, start, end).
|
milerUserID, constants.AssignmentCompleted, start, end).
|
||||||
@@ -575,6 +765,8 @@ func MilerGetEarnings(c *fiber.Ctx) error {
|
|||||||
data := fiber.Map{
|
data := fiber.Map{
|
||||||
"period": period,
|
"period": period,
|
||||||
"completed_stops": summary.CompletedStops,
|
"completed_stops": summary.CompletedStops,
|
||||||
|
"cancelled_stops": cancelledStops,
|
||||||
|
"total_stops": summary.CompletedStops + cancelledStops,
|
||||||
"total_kms": summary.TotalKms,
|
"total_kms": summary.TotalKms,
|
||||||
"total_earnings": summary.TotalEarnings,
|
"total_earnings": summary.TotalEarnings,
|
||||||
"total_bonus": summary.TotalBonus,
|
"total_bonus": summary.TotalBonus,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"doormile/config"
|
"doormile/config"
|
||||||
@@ -206,11 +207,16 @@ func UpdateMilerProfile(c *fiber.Ctx) error {
|
|||||||
return utils.NotFound(c, "miler profile not found")
|
return utils.NotFound(c, "miler profile not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Email and Address are pointers so an omitted field is left untouched rather
|
||||||
|
// than blanked. Email lives on AppUser (the login identity), Address on the
|
||||||
|
// profile — the edit screen collects both, so both are persisted here.
|
||||||
type ProfileUpdate struct {
|
type ProfileUpdate struct {
|
||||||
Displayname string `json:"displayname"`
|
Displayname string `json:"displayname"`
|
||||||
Profilephotourl string `json:"profilephotourl"`
|
Profilephotourl string `json:"profilephotourl"`
|
||||||
Defaultvehicletype string `json:"defaultvehicletype"`
|
Defaultvehicletype string `json:"defaultvehicletype"`
|
||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
|
Email *string `json:"email"`
|
||||||
|
Address *string `json:"address"`
|
||||||
}
|
}
|
||||||
|
|
||||||
req := new(ProfileUpdate)
|
req := new(ProfileUpdate)
|
||||||
@@ -224,10 +230,31 @@ func UpdateMilerProfile(c *fiber.Ctx) error {
|
|||||||
if req.Phone != "" {
|
if req.Phone != "" {
|
||||||
profile.Phone = req.Phone
|
profile.Phone = req.Phone
|
||||||
}
|
}
|
||||||
|
if req.Address != nil {
|
||||||
|
profile.Address = *req.Address
|
||||||
|
}
|
||||||
profile.Profilephotourl = req.Profilephotourl
|
profile.Profilephotourl = req.Profilephotourl
|
||||||
profile.Defaultvehicletype = req.Defaultvehicletype
|
profile.Defaultvehicletype = req.Defaultvehicletype
|
||||||
profile.Updatedat = time.Now()
|
profile.Updatedat = time.Now()
|
||||||
|
|
||||||
|
// Email is unique on appusers, so a collision must fail cleanly rather than
|
||||||
|
// 500. Only touch it when a non-empty value that actually changed is sent.
|
||||||
|
if req.Email != nil && *req.Email != "" {
|
||||||
|
var user models.AppUser
|
||||||
|
if err := db.DB.First(&user, milerUserID).Error; err == nil && !strings.EqualFold(user.Email, *req.Email) {
|
||||||
|
var clash int64
|
||||||
|
db.DB.Model(&models.AppUser{}).
|
||||||
|
Where("email = ? AND userid <> ?", *req.Email, milerUserID).Count(&clash)
|
||||||
|
if clash > 0 {
|
||||||
|
return utils.Conflict(c, "that email is already in use")
|
||||||
|
}
|
||||||
|
if err := db.DB.Model(&models.AppUser{}).
|
||||||
|
Where("userid = ?", milerUserID).Update("email", *req.Email).Error; err != nil {
|
||||||
|
return utils.Internal(c, "failed to update email")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := db.DB.Save(&profile).Error; err != nil {
|
if err := db.DB.Save(&profile).Error; err != nil {
|
||||||
return utils.Internal(c, "failed to update profile")
|
return utils.Internal(c, "failed to update profile")
|
||||||
}
|
}
|
||||||
@@ -434,11 +461,15 @@ func RejectMilerAssignment(c *fiber.Ctx) error {
|
|||||||
return utils.BadRequest(c, "invalid assignment ID")
|
return utils.BadRequest(c, "invalid assignment ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The reason may arrive in the JSON body or as a ?reason= query string — the
|
||||||
|
// deployed contract and the app disagreed on which, so accept both and prefer
|
||||||
|
// whichever is non-empty. An absent/invalid body is not an error here.
|
||||||
var req struct {
|
var req struct {
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
}
|
}
|
||||||
if err := c.BodyParser(&req); err != nil {
|
_ = c.BodyParser(&req)
|
||||||
return utils.BadRequest(c, "invalid request body")
|
if req.Reason == "" {
|
||||||
|
req.Reason = c.Query("reason")
|
||||||
}
|
}
|
||||||
if req.Reason == "" {
|
if req.Reason == "" {
|
||||||
req.Reason = "Rejected by rider"
|
req.Reason = "Rejected by rider"
|
||||||
|
|||||||
@@ -99,6 +99,11 @@ type MilerProfile struct {
|
|||||||
// cutover. Nil for riders created natively in Doormile.
|
// cutover. Nil for riders created natively in Doormile.
|
||||||
Legacyuserid *int `json:"legacyuserid,omitempty" gorm:"column:legacyuserid;index"`
|
Legacyuserid *int `json:"legacyuserid,omitempty" gorm:"column:legacyuserid;index"`
|
||||||
Defaultvehicletype string `json:"defaultvehicletype" gorm:"column:defaultvehicletype"`
|
Defaultvehicletype string `json:"defaultvehicletype" gorm:"column:defaultvehicletype"`
|
||||||
|
// Address is the rider's home/base address, editable from the app's profile
|
||||||
|
// screen. App-only until now, so it had nowhere durable to land. Nullable/
|
||||||
|
// additive. (Email is not duplicated here — it lives on AppUser.Email, the
|
||||||
|
// login identity, and the profile update path writes it there.)
|
||||||
|
Address string `json:"address" gorm:"column:address"`
|
||||||
Currentlatitude float64 `json:"currentlatitude" gorm:"column:currentlatitude"`
|
Currentlatitude float64 `json:"currentlatitude" gorm:"column:currentlatitude"`
|
||||||
Currentlongitude float64 `json:"currentlongitude" gorm:"column:currentlongitude"`
|
Currentlongitude float64 `json:"currentlongitude" gorm:"column:currentlongitude"`
|
||||||
Currentpincode string `json:"currentpincode" gorm:"column:currentpincode"`
|
Currentpincode string `json:"currentpincode" gorm:"column:currentpincode"`
|
||||||
|
|||||||
@@ -144,6 +144,9 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
|||||||
milerAuth.Post("/bookings/:bookingid/pickup-complete", controllers.BookingPickupComplete)
|
milerAuth.Post("/bookings/:bookingid/pickup-complete", controllers.BookingPickupComplete)
|
||||||
milerAuth.Post("/bookings/:bookingid/vehicle-required", controllers.BookingVehicleRequiredEscalate)
|
milerAuth.Post("/bookings/:bookingid/vehicle-required", controllers.BookingVehicleRequiredEscalate)
|
||||||
milerAuth.Post("/bookings/:bookingid/cancel", controllers.MilerCancelAssignment)
|
milerAuth.Post("/bookings/:bookingid/cancel", controllers.MilerCancelAssignment)
|
||||||
|
// Pre-pickup skip: defer a not-yet-picked-up booking without releasing it,
|
||||||
|
// so the rider can resume it (the consignment skip needs a consignment).
|
||||||
|
milerAuth.Post("/bookings/:bookingid/skip", controllers.MilerSkipPickup)
|
||||||
|
|
||||||
// Redis periodic telemetry and status logs
|
// Redis periodic telemetry and status logs
|
||||||
milerAuth.Post("/logs", controllers.CreateMilerPeriodicLog)
|
milerAuth.Post("/logs", controllers.CreateMilerPeriodicLog)
|
||||||
|
|||||||
Reference in New Issue
Block a user