fix: console tenant scoping, miler identity spoofing, delivery proof, timezone
Security - Express console had no tenant scoping at all: LoginAdmin hardcoded tenantid 0 into every JWT and none of the 85 admin handlers filtered by tenant, so any client given a console login would read every other client's bookings, customers, pricing and reports. Adds DoormileAuth.Tenantid (nil = Doormile staff, unrestricted; set = client, scoped), emits it in the token, and scopes reads, guards writes and pins tenantid on create. - Miler telemetry (/miler/logs, /miler/status, /miler/consignments/logs) took userid from the request body, letting any authenticated rider write another rider's status and GPS trail — data the dispatch layer reasons over. Identity now comes from the token. - POST /miler/reset-pin was unauthenticated and overwrote a PIN given only a phone number, so reset-pin + verify-pin took over any rider account. Now requires admin/manager/executive auth. Correctness - Date ranges compared the container's UTC clock against timestamps the DB writes as IST wall-clock (DSN sets TimeZone=Asia/Kolkata), so "today so far" ended 5h30m in the past and silently dropped everything created after noon IST from every report. Sets TZ in the image and adds utils.DBNow/DBToday, which stay correct regardless of container timezone. - CreateMiler never set Configid, so console-created riders got the column default of 1 while LoginMiler looks up configid 1001 — every such rider was unable to log in, reported as "no miler account found". - Delivery wrote no consignment history row, so a tracking timeline never showed the parcel arriving. Features - Delivery OTP is now real (crypto/rand, issued to the receiver, verified and cleared on delivery) but opt-in per client via Tenant.Requiredeliveryotp, defaulting off — friction worth it for a courier parcel, not a food order. - Express bookings accept pickuplocationid, so the console can name a client site (a DailyGrubs kitchen) instead of retyping its address; validated against the tenant and carried through to the consignment. - TenantLocation.Locationname, miler tenantid/hubid, Nagercoil (629) opened. - PUT /miler/availability accepts both "status" and "availabilitystatus", and /miler/location no longer drops speed/heading — both were contract mismatches against the doc the Flutter dev was given. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
10
Dockerfile
10
Dockerfile
@@ -9,8 +9,14 @@ RUN CGO_ENABLED=0 GOOS=linux go build -o server .
|
|||||||
# Second Stage: Run the compiled binary inside alpine
|
# Second Stage: Run the compiled binary inside alpine
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
|
||||||
# Fix: Alpine needs ca-certificates to verify SSL certificates
|
# Fix: Alpine needs ca-certificates to verify SSL certificates.
|
||||||
RUN apk add --no-cache ca-certificates
|
# tzdata + TZ: the database connection sets TimeZone=Asia/Kolkata, so
|
||||||
|
# CURRENT_TIMESTAMP defaults write IST wall-clock into the timestamp columns.
|
||||||
|
# With the container defaulting to UTC, every time.Now() the app wrote was 5h30m
|
||||||
|
# behind those defaults, and "today so far" date ranges ended 5h30m in the past —
|
||||||
|
# which silently dropped anything created after noon IST from every report.
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
ENV TZ=Asia/Kolkata
|
||||||
|
|
||||||
# Copy from first stage
|
# Copy from first stage
|
||||||
COPY --from=0 /app/server /app/server
|
COPY --from=0 /app/server /app/server
|
||||||
|
|||||||
@@ -21,8 +21,72 @@ import (
|
|||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// consoleTenantID returns the tenant a console login is restricted to, or 0
|
||||||
|
// for Doormile's own staff, who are unrestricted. Client logins carry their
|
||||||
|
// tenant in the JWT (see LoginAdmin); Doormile staff have DoormileAuth.Tenantid
|
||||||
|
// nil and so authenticate with 0.
|
||||||
|
func consoleTenantID(c *fiber.Ctx) int {
|
||||||
|
tenantID, ok := c.Locals("tenantid").(int)
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return tenantID
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDoormileConsoleStaff reports whether the caller sees every tenant's data.
|
||||||
|
func isDoormileConsoleStaff(c *fiber.Ctx) bool {
|
||||||
|
return consoleTenantID(c) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// scopeToOwnTenant restricts a query on a tenant-owned table to the requesting
|
||||||
|
// console user's own tenant. Doormile staff are unrestricted. This is the
|
||||||
|
// admin-console counterpart of scopeBookingsToOwnTenant in hubController.go —
|
||||||
|
// without it, any client given an express-console login reads every other
|
||||||
|
// client's data.
|
||||||
|
//
|
||||||
|
// The column name is taken as a parameter because the tenant key is not always
|
||||||
|
// literally "tenantid": on the tenants table itself it is the primary key.
|
||||||
|
func scopeToOwnTenant(c *fiber.Ctx, query *gorm.DB, column string) *gorm.DB {
|
||||||
|
tenantID := consoleTenantID(c)
|
||||||
|
if tenantID == 0 {
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
return query.Where(column+" = ?", tenantID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// canAccessTenant reports whether the caller may act on the given tenant.
|
||||||
|
// Used where the tenant is addressed by a path parameter or request body rather
|
||||||
|
// than filtered in a query — scoping a WHERE clause does nothing when the
|
||||||
|
// caller names the tenant directly.
|
||||||
|
func canAccessTenant(c *fiber.Ctx, tenantID int) bool {
|
||||||
|
own := consoleTenantID(c)
|
||||||
|
return own == 0 || own == tenantID
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertBookingAccess checks that the caller may act on a booking addressed by
|
||||||
|
// id. Returns nil for Doormile staff. Mutating handlers take the booking id
|
||||||
|
// straight from the path, so a scoped SELECT elsewhere in the handler does not
|
||||||
|
// protect them — this has to run before the write.
|
||||||
|
func assertBookingAccess(c *fiber.Ctx, bookingID int) error {
|
||||||
|
own := consoleTenantID(c)
|
||||||
|
if own == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var booking models.PickupBooking
|
||||||
|
if err := db.DB.Select("bookingid", "tenantid").First(&booking, bookingID).Error; err != nil {
|
||||||
|
return utils.NotFound(c, "booking not found")
|
||||||
|
}
|
||||||
|
// A booking with no tenant predates tenant attribution and can't be proven
|
||||||
|
// to belong to this client, so it stays invisible to them.
|
||||||
|
if booking.Tenantid == nil || *booking.Tenantid != own {
|
||||||
|
return utils.NotFound(c, "booking not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to generate tripsheet number
|
// Helper to generate tripsheet number
|
||||||
func generateTripsheetNo() string {
|
func generateTripsheetNo() string {
|
||||||
b := make([]byte, 4)
|
b := make([]byte, 4)
|
||||||
@@ -68,8 +132,18 @@ func LoginAdmin(cfg *config.Config) fiber.Handler {
|
|||||||
userName = appUser.Authname
|
userName = appUser.Authname
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A client's console login carries their tenant so handlers can scope to
|
||||||
|
// it; Doormile's own staff have Tenantid nil and keep tenantID 0, which
|
||||||
|
// scopeToOwnTenant reads as "unrestricted". Emitting 0 unconditionally
|
||||||
|
// (as this did) is what left every console login able to read every
|
||||||
|
// tenant's data.
|
||||||
|
tenantID := 0
|
||||||
|
if auth.Tenantid != nil {
|
||||||
|
tenantID = *auth.Tenantid
|
||||||
|
}
|
||||||
|
|
||||||
// Important: use appUser.Userid instead of auth.ID to ensure consistent IDs across the system
|
// Important: use appUser.Userid instead of auth.ID to ensure consistent IDs across the system
|
||||||
token, err := utils.GenerateToken(int(appUser.Userid), auth.Email, roleId, 0, 1, cfg.JWTSecret)
|
token, err := utils.GenerateToken(int(appUser.Userid), auth.Email, roleId, tenantID, 1, cfg.JWTSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return utils.Internal(c, "failed to generate token")
|
return utils.Internal(c, "failed to generate token")
|
||||||
}
|
}
|
||||||
@@ -78,10 +152,11 @@ func LoginAdmin(cfg *config.Config) fiber.Handler {
|
|||||||
"success": true,
|
"success": true,
|
||||||
"token": token,
|
"token": token,
|
||||||
"user": fiber.Map{
|
"user": fiber.Map{
|
||||||
"id": appUser.Userid,
|
"id": appUser.Userid,
|
||||||
"name": userName,
|
"name": userName,
|
||||||
"email": auth.Email,
|
"email": auth.Email,
|
||||||
"role": auth.Role,
|
"role": auth.Role,
|
||||||
|
"tenantid": auth.Tenantid,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -95,12 +170,19 @@ func GetAdminDashboard(c *fiber.Ctx) error {
|
|||||||
var totalConsignments int64
|
var totalConsignments int64
|
||||||
var openExceptions int64
|
var openExceptions int64
|
||||||
|
|
||||||
db.DB.Model(&models.Tenant{}).Count(&totalTenants)
|
scopeToOwnTenant(c, db.DB.Model(&models.Tenant{}), "tenantid").Count(&totalTenants)
|
||||||
db.DB.Model(&models.AppCustomer{}).Count(&totalCustomers)
|
scopeToOwnTenant(c, db.DB.Model(&models.PickupBooking{}), "tenantid").Count(&totalBookings)
|
||||||
db.DB.Model(&models.AppUser{}).Where("roleid = 5").Count(&totalMilers)
|
scopeToOwnTenant(c, db.DB.Model(&models.Consignment{}), "tenantid").Count(&totalConsignments)
|
||||||
db.DB.Model(&models.PickupBooking{}).Count(&totalBookings)
|
|
||||||
db.DB.Model(&models.Consignment{}).Count(&totalConsignments)
|
// Customers, milers and exceptions have no tenant column, so there is no
|
||||||
db.DB.Model(&models.ConsignmentException{}).Where("status = ?", "Open").Count(&openExceptions)
|
// way to attribute them to one client here. Rather than show a client
|
||||||
|
// Doormile-wide totals, these are reported as zero for client logins; the
|
||||||
|
// per-client versions need a join through bookings and are not built yet.
|
||||||
|
if isDoormileConsoleStaff(c) {
|
||||||
|
db.DB.Model(&models.AppCustomer{}).Count(&totalCustomers)
|
||||||
|
db.DB.Model(&models.AppUser{}).Where("roleid = 5").Count(&totalMilers)
|
||||||
|
db.DB.Model(&models.ConsignmentException{}).Where("status = ?", "Open").Count(&openExceptions)
|
||||||
|
}
|
||||||
|
|
||||||
return utils.OK(c, fiber.Map{
|
return utils.OK(c, fiber.Map{
|
||||||
"tenants": totalTenants,
|
"tenants": totalTenants,
|
||||||
@@ -131,20 +213,26 @@ func GetAdminReports(c *fiber.Ctx) error {
|
|||||||
tenantID := c.Query("tenantid")
|
tenantID := c.Query("tenantid")
|
||||||
hubID := c.Query("hubid")
|
hubID := c.Query("hubid")
|
||||||
|
|
||||||
|
// ownTenant is 0 for Doormile staff (whole-network view) and the client's
|
||||||
|
// tenant for a client login, which every figure below is restricted to.
|
||||||
|
ownTenant := consoleTenantID(c)
|
||||||
|
|
||||||
var totalBookings int64
|
var totalBookings int64
|
||||||
db.DB.Model(&models.PickupBooking{}).Where("createdat BETWEEN ? AND ?", from, to).Count(&totalBookings)
|
scopeToOwnTenant(c, db.DB.Model(&models.PickupBooking{}), "tenantid").
|
||||||
|
Where("createdat BETWEEN ? AND ?", from, to).Count(&totalBookings)
|
||||||
|
|
||||||
var delivered int64
|
var delivered int64
|
||||||
db.DB.Model(&models.PickupBooking{}).
|
scopeToOwnTenant(c, db.DB.Model(&models.PickupBooking{}), "tenantid").
|
||||||
Where("status = ? AND updatedat BETWEEN ? AND ?", constants.BookingConvertedConsignment, from, to).
|
Where("status = ? AND updatedat BETWEEN ? AND ?", constants.BookingConvertedConsignment, from, to).
|
||||||
Count(&delivered)
|
Count(&delivered)
|
||||||
|
|
||||||
var cancelled int64
|
var cancelled int64
|
||||||
db.DB.Model(&models.PickupBooking{}).
|
scopeToOwnTenant(c, db.DB.Model(&models.PickupBooking{}), "tenantid").
|
||||||
Where("status = ? AND updatedat BETWEEN ? AND ?", constants.BookingCancelled, from, to).
|
Where("status = ? AND updatedat BETWEEN ? AND ?", constants.BookingCancelled, from, to).
|
||||||
Count(&cancelled)
|
Count(&cancelled)
|
||||||
|
|
||||||
consignmentQuery := db.DB.Model(&models.Consignment{}).Where("createdat BETWEEN ? AND ?", from, to)
|
consignmentQuery := scopeToOwnTenant(c, db.DB.Model(&models.Consignment{}), "tenantid").
|
||||||
|
Where("createdat BETWEEN ? AND ?", from, to)
|
||||||
if tenantID != "" {
|
if tenantID != "" {
|
||||||
consignmentQuery = consignmentQuery.Where("tenantid = ?", tenantID)
|
consignmentQuery = consignmentQuery.Where("tenantid = ?", tenantID)
|
||||||
}
|
}
|
||||||
@@ -154,15 +242,26 @@ func GetAdminReports(c *fiber.Ctx) error {
|
|||||||
var totalConsignments int64
|
var totalConsignments int64
|
||||||
consignmentQuery.Count(&totalConsignments)
|
consignmentQuery.Count(&totalConsignments)
|
||||||
|
|
||||||
|
// Payments and exceptions carry no tenant column, so they're restricted
|
||||||
|
// through the bookings/consignments they belong to. COD in particular is a
|
||||||
|
// figure a client genuinely needs, so it's joined rather than suppressed.
|
||||||
var codCollected float64
|
var codCollected float64
|
||||||
db.DB.Model(&models.BookingPayment{}).
|
codQuery := db.DB.Model(&models.BookingPayment{}).
|
||||||
Where("paymentstatus = ? AND createdat BETWEEN ? AND ?", constants.PaymentStatusPaid, from, to).
|
Where("paymentstatus = ? AND createdat BETWEEN ? AND ?", constants.PaymentStatusPaid, from, to)
|
||||||
Select("COALESCE(SUM(amount), 0)").Scan(&codCollected)
|
if ownTenant != 0 {
|
||||||
|
codQuery = codQuery.Where("bookingid IN (?)",
|
||||||
|
db.DB.Model(&models.PickupBooking{}).Select("bookingid").Where("tenantid = ?", ownTenant))
|
||||||
|
}
|
||||||
|
codQuery.Select("COALESCE(SUM(amount), 0)").Scan(&codCollected)
|
||||||
|
|
||||||
var openExceptions int64
|
var openExceptions int64
|
||||||
db.DB.Model(&models.ConsignmentException{}).
|
excQuery := db.DB.Model(&models.ConsignmentException{}).
|
||||||
Where("status != ? AND createdat BETWEEN ? AND ?", constants.ExceptionClosed, from, to).
|
Where("status != ? AND createdat BETWEEN ? AND ?", constants.ExceptionClosed, from, to)
|
||||||
Count(&openExceptions)
|
if ownTenant != 0 {
|
||||||
|
excQuery = excQuery.Where("consignmentid IN (?)",
|
||||||
|
db.DB.Model(&models.Consignment{}).Select("consignmentid").Where("tenantid = ?", ownTenant))
|
||||||
|
}
|
||||||
|
excQuery.Count(&openExceptions)
|
||||||
|
|
||||||
completionRate := 0.0
|
completionRate := 0.0
|
||||||
if totalBookings > 0 {
|
if totalBookings > 0 {
|
||||||
@@ -176,14 +275,23 @@ func GetAdminReports(c *fiber.Ctx) error {
|
|||||||
Delivered int64 `gorm:"column:delivered"`
|
Delivered int64 `gorm:"column:delivered"`
|
||||||
}
|
}
|
||||||
var hubRows []hubRow
|
var hubRows []hubRow
|
||||||
db.DB.Raw(`
|
// The tenant filter belongs in the JOIN condition, not a WHERE — in a WHERE
|
||||||
|
// it would drop hubs with no matching parcels instead of showing them as
|
||||||
|
// zero.
|
||||||
|
hubSQL := `
|
||||||
SELECT h.hubid AS hubid, h.hubname AS hubname, COUNT(c.consignmentid) AS delivered
|
SELECT h.hubid AS hubid, h.hubname AS hubname, COUNT(c.consignmentid) AS delivered
|
||||||
FROM hubs h
|
FROM hubs h
|
||||||
LEFT JOIN consignments c ON c.currenthubid = h.hubid AND c.status = ? AND c.updatedat BETWEEN ? AND ?
|
LEFT JOIN consignments c ON c.currenthubid = h.hubid AND c.status = ? AND c.updatedat BETWEEN ? AND ?`
|
||||||
|
hubArgs := []interface{}{constants.ConsignmentDelivered, from, to}
|
||||||
|
if ownTenant != 0 {
|
||||||
|
hubSQL += ` AND c.tenantid = ?`
|
||||||
|
hubArgs = append(hubArgs, ownTenant)
|
||||||
|
}
|
||||||
|
hubSQL += `
|
||||||
WHERE h.deletedat IS NULL
|
WHERE h.deletedat IS NULL
|
||||||
GROUP BY h.hubid, h.hubname
|
GROUP BY h.hubid, h.hubname
|
||||||
ORDER BY delivered DESC
|
ORDER BY delivered DESC`
|
||||||
`, constants.ConsignmentDelivered, from, to).Scan(&hubRows)
|
db.DB.Raw(hubSQL, hubArgs...).Scan(&hubRows)
|
||||||
|
|
||||||
byHub := make([]fiber.Map, 0, len(hubRows))
|
byHub := make([]fiber.Map, 0, len(hubRows))
|
||||||
for _, r := range hubRows {
|
for _, r := range hubRows {
|
||||||
@@ -197,13 +305,19 @@ func GetAdminReports(c *fiber.Ctx) error {
|
|||||||
Bookings int64 `gorm:"column:bookings"`
|
Bookings int64 `gorm:"column:bookings"`
|
||||||
}
|
}
|
||||||
var tenantRows []tenantRow
|
var tenantRows []tenantRow
|
||||||
db.DB.Raw(`
|
tenantSQL := `
|
||||||
SELECT t.tenantid AS tenantid, t.tenantname AS tenantname, COUNT(c.consignmentid) AS bookings
|
SELECT t.tenantid AS tenantid, t.tenantname AS tenantname, COUNT(c.consignmentid) AS bookings
|
||||||
FROM tenants t
|
FROM tenants t
|
||||||
LEFT JOIN consignments c ON c.tenantid = t.tenantid AND c.createdat BETWEEN ? AND ?
|
LEFT JOIN consignments c ON c.tenantid = t.tenantid AND c.createdat BETWEEN ? AND ?`
|
||||||
|
tenantArgs := []interface{}{from, to}
|
||||||
|
if ownTenant != 0 {
|
||||||
|
tenantSQL += ` WHERE t.tenantid = ?`
|
||||||
|
tenantArgs = append(tenantArgs, ownTenant)
|
||||||
|
}
|
||||||
|
tenantSQL += `
|
||||||
GROUP BY t.tenantid, t.tenantname
|
GROUP BY t.tenantid, t.tenantname
|
||||||
ORDER BY bookings DESC
|
ORDER BY bookings DESC`
|
||||||
`, from, to).Scan(&tenantRows)
|
db.DB.Raw(tenantSQL, tenantArgs...).Scan(&tenantRows)
|
||||||
|
|
||||||
byTenant := make([]fiber.Map, 0, len(tenantRows))
|
byTenant := make([]fiber.Map, 0, len(tenantRows))
|
||||||
for _, r := range tenantRows {
|
for _, r := range tenantRows {
|
||||||
@@ -219,6 +333,9 @@ func GetAdminReports(c *fiber.Ctx) error {
|
|||||||
TotalKms float64 `gorm:"column:total_kms"`
|
TotalKms float64 `gorm:"column:total_kms"`
|
||||||
TotalEarnings float64 `gorm:"column:total_earnings"`
|
TotalEarnings float64 `gorm:"column:total_earnings"`
|
||||||
}
|
}
|
||||||
|
// Rider earnings, kms and completed stops are Doormile's own workforce data
|
||||||
|
// — a client has no business reading them, and there's no per-client view
|
||||||
|
// of a rider who works across tenants. Clients get an empty list.
|
||||||
var riderRows []riderRow
|
var riderRows []riderRow
|
||||||
riderQuery := `
|
riderQuery := `
|
||||||
SELECT mp.userid AS userid, mp.displayname AS displayname,
|
SELECT mp.userid AS userid, mp.displayname AS displayname,
|
||||||
@@ -235,7 +352,9 @@ func GetAdminReports(c *fiber.Ctx) error {
|
|||||||
args = append(args, hubID)
|
args = append(args, hubID)
|
||||||
}
|
}
|
||||||
riderQuery += " GROUP BY mp.userid, mp.displayname ORDER BY completed_stops DESC LIMIT 50"
|
riderQuery += " GROUP BY mp.userid, mp.displayname ORDER BY completed_stops DESC LIMIT 50"
|
||||||
db.DB.Raw(riderQuery, args...).Scan(&riderRows)
|
if isDoormileConsoleStaff(c) {
|
||||||
|
db.DB.Raw(riderQuery, args...).Scan(&riderRows)
|
||||||
|
}
|
||||||
|
|
||||||
byRider := make([]fiber.Map, 0, len(riderRows))
|
byRider := make([]fiber.Map, 0, len(riderRows))
|
||||||
for _, r := range riderRows {
|
for _, r := range riderRows {
|
||||||
@@ -271,13 +390,16 @@ func GetAppUsers(c *fiber.Ctx) error {
|
|||||||
page := utils.ParsePage(c)
|
page := utils.ParsePage(c)
|
||||||
|
|
||||||
var total int64
|
var total int64
|
||||||
if err := db.DB.Model(&models.AppUser{}).Where("roleid != ?", 5).Count(&total).Error; err != nil {
|
if err := scopeToOwnTenant(c, db.DB.Model(&models.AppUser{}), "tenantid").
|
||||||
|
Where("roleid != ?", 5).Count(&total).Error; err != nil {
|
||||||
return utils.Internal(c, "failed to count users")
|
return utils.Internal(c, "failed to count users")
|
||||||
}
|
}
|
||||||
|
|
||||||
var users []models.AppUser
|
var users []models.AppUser
|
||||||
// Exclude Milers (Roleid = 5) from the CRM user list
|
// Exclude Milers (Roleid = 5) from the CRM user list. Scoped as well, so a
|
||||||
if err := page.Apply(db.DB.Where("roleid != ?", 5)).Find(&users).Error; err != nil {
|
// client sees only their own people, not Doormile's staff directory.
|
||||||
|
if err := page.Apply(scopeToOwnTenant(c, db.DB, "tenantid").Where("roleid != ?", 5)).
|
||||||
|
Find(&users).Error; err != nil {
|
||||||
return utils.Internal(c, "failed to fetch users")
|
return utils.Internal(c, "failed to fetch users")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +542,9 @@ func DeleteAppUser(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func GetTenants(c *fiber.Ctx) error {
|
func GetTenants(c *fiber.Ctx) error {
|
||||||
var tenants []models.Tenant
|
var tenants []models.Tenant
|
||||||
if err := db.DB.Find(&tenants).Error; err != nil {
|
// A client login sees only its own tenant row; the tenant key here is the
|
||||||
|
// primary key, not a "tenantid" foreign column.
|
||||||
|
if err := scopeToOwnTenant(c, db.DB, "tenantid").Find(&tenants).Error; err != nil {
|
||||||
return utils.Internal(c, "failed to fetch tenants")
|
return utils.Internal(c, "failed to fetch tenants")
|
||||||
}
|
}
|
||||||
return utils.List(c, tenants, int64(len(tenants)))
|
return utils.List(c, tenants, int64(len(tenants)))
|
||||||
@@ -441,6 +565,9 @@ func CreateTenant(c *fiber.Ctx) error {
|
|||||||
if tenant.Status == "" {
|
if tenant.Status == "" {
|
||||||
tenant.Status = "Active"
|
tenant.Status = "Active"
|
||||||
}
|
}
|
||||||
|
if req.Requiredeliveryotp != nil {
|
||||||
|
tenant.Requiredeliveryotp = *req.Requiredeliveryotp
|
||||||
|
}
|
||||||
|
|
||||||
if err := db.DB.Create(&tenant).Error; err != nil {
|
if err := db.DB.Create(&tenant).Error; err != nil {
|
||||||
return utils.Internal(c, "failed to create tenant")
|
return utils.Internal(c, "failed to create tenant")
|
||||||
@@ -451,7 +578,7 @@ func CreateTenant(c *fiber.Ctx) error {
|
|||||||
func GetTenantDetails(c *fiber.Ctx) error {
|
func GetTenantDetails(c *fiber.Ctx) error {
|
||||||
id, _ := strconv.Atoi(c.Params("id"))
|
id, _ := strconv.Atoi(c.Params("id"))
|
||||||
var tenant models.Tenant
|
var tenant models.Tenant
|
||||||
if err := db.DB.First(&tenant, id).Error; err != nil {
|
if err := scopeToOwnTenant(c, db.DB, "tenantid").First(&tenant, id).Error; err != nil {
|
||||||
return utils.NotFound(c, "tenant not found")
|
return utils.NotFound(c, "tenant not found")
|
||||||
}
|
}
|
||||||
return utils.OK(c, tenant)
|
return utils.OK(c, tenant)
|
||||||
@@ -459,6 +586,9 @@ func GetTenantDetails(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func UpdateTenant(c *fiber.Ctx) error {
|
func UpdateTenant(c *fiber.Ctx) error {
|
||||||
id, _ := strconv.Atoi(c.Params("id"))
|
id, _ := strconv.Atoi(c.Params("id"))
|
||||||
|
if !canAccessTenant(c, id) {
|
||||||
|
return utils.Forbidden(c, "not permitted for this tenant")
|
||||||
|
}
|
||||||
var tenant models.Tenant
|
var tenant models.Tenant
|
||||||
if err := db.DB.First(&tenant, id).Error; err != nil {
|
if err := db.DB.First(&tenant, id).Error; err != nil {
|
||||||
return utils.NotFound(c, "tenant not found")
|
return utils.NotFound(c, "tenant not found")
|
||||||
@@ -481,6 +611,9 @@ func UpdateTenant(c *fiber.Ctx) error {
|
|||||||
if req.Status != "" {
|
if req.Status != "" {
|
||||||
tenant.Status = req.Status
|
tenant.Status = req.Status
|
||||||
}
|
}
|
||||||
|
if req.Requiredeliveryotp != nil {
|
||||||
|
tenant.Requiredeliveryotp = *req.Requiredeliveryotp
|
||||||
|
}
|
||||||
tenant.Updatedat = time.Now()
|
tenant.Updatedat = time.Now()
|
||||||
|
|
||||||
if err := db.DB.Save(&tenant).Error; err != nil {
|
if err := db.DB.Save(&tenant).Error; err != nil {
|
||||||
@@ -491,6 +624,11 @@ func UpdateTenant(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func DeleteTenant(c *fiber.Ctx) error {
|
func DeleteTenant(c *fiber.Ctx) error {
|
||||||
id, _ := strconv.Atoi(c.Params("id"))
|
id, _ := strconv.Atoi(c.Params("id"))
|
||||||
|
// Deleting your own tenant is not a client operation either — this is
|
||||||
|
// Doormile-staff only.
|
||||||
|
if !isDoormileConsoleStaff(c) {
|
||||||
|
return utils.Forbidden(c, "not permitted for this tenant")
|
||||||
|
}
|
||||||
var tenant models.Tenant
|
var tenant models.Tenant
|
||||||
if err := db.DB.First(&tenant, id).Error; err != nil {
|
if err := db.DB.First(&tenant, id).Error; err != nil {
|
||||||
return utils.NotFound(c, "tenant not found")
|
return utils.NotFound(c, "tenant not found")
|
||||||
@@ -504,6 +642,9 @@ func DeleteTenant(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func GetTenantLocations(c *fiber.Ctx) error {
|
func GetTenantLocations(c *fiber.Ctx) error {
|
||||||
tenantID, _ := strconv.Atoi(c.Params("id"))
|
tenantID, _ := strconv.Atoi(c.Params("id"))
|
||||||
|
if !canAccessTenant(c, tenantID) {
|
||||||
|
return utils.Forbidden(c, "not permitted for this tenant")
|
||||||
|
}
|
||||||
var locations []models.TenantLocation
|
var locations []models.TenantLocation
|
||||||
if err := db.DB.Where("tenantid = ?", tenantID).Find(&locations).Error; err != nil {
|
if err := db.DB.Where("tenantid = ?", tenantID).Find(&locations).Error; err != nil {
|
||||||
return utils.Internal(c, "failed to fetch locations")
|
return utils.Internal(c, "failed to fetch locations")
|
||||||
@@ -513,21 +654,25 @@ func GetTenantLocations(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func CreateTenantLocation(c *fiber.Ctx) error {
|
func CreateTenantLocation(c *fiber.Ctx) error {
|
||||||
tenantID, _ := strconv.Atoi(c.Params("id"))
|
tenantID, _ := strconv.Atoi(c.Params("id"))
|
||||||
|
if !canAccessTenant(c, tenantID) {
|
||||||
|
return utils.Forbidden(c, "not permitted for this tenant")
|
||||||
|
}
|
||||||
req := new(dto.TenantLocationCreateRequest)
|
req := new(dto.TenantLocationCreateRequest)
|
||||||
if err := c.BodyParser(req); err != nil {
|
if err := c.BodyParser(req); err != nil {
|
||||||
return utils.BadRequest(c, "invalid request body")
|
return utils.BadRequest(c, "invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
location := models.TenantLocation{
|
location := models.TenantLocation{
|
||||||
Tenantid: tenantID,
|
Tenantid: tenantID,
|
||||||
Address: req.Address,
|
Locationname: req.Locationname,
|
||||||
City: req.City,
|
Address: req.Address,
|
||||||
State: req.State,
|
City: req.City,
|
||||||
Pincode: req.Pincode,
|
State: req.State,
|
||||||
Latitude: req.Latitude,
|
Pincode: req.Pincode,
|
||||||
Longitude: req.Longitude,
|
Latitude: req.Latitude,
|
||||||
Isprimary: req.Isprimary,
|
Longitude: req.Longitude,
|
||||||
Status: req.Status,
|
Isprimary: req.Isprimary,
|
||||||
|
Status: req.Status,
|
||||||
}
|
}
|
||||||
if location.Status == "" {
|
if location.Status == "" {
|
||||||
location.Status = "Active"
|
location.Status = "Active"
|
||||||
@@ -549,12 +694,20 @@ func UpdateTenantLocation(c *fiber.Ctx) error {
|
|||||||
if err := db.DB.First(&location, id).Error; err != nil {
|
if err := db.DB.First(&location, id).Error; err != nil {
|
||||||
return utils.NotFound(c, "tenant location not found")
|
return utils.NotFound(c, "tenant location not found")
|
||||||
}
|
}
|
||||||
|
// The location is addressed by its own id, so the tenant guard has to run
|
||||||
|
// against the row we loaded rather than a path parameter.
|
||||||
|
if !canAccessTenant(c, location.Tenantid) {
|
||||||
|
return utils.Forbidden(c, "not permitted for this tenant")
|
||||||
|
}
|
||||||
|
|
||||||
req := new(dto.TenantLocationCreateRequest)
|
req := new(dto.TenantLocationCreateRequest)
|
||||||
if err := c.BodyParser(req); err != nil {
|
if err := c.BodyParser(req); err != nil {
|
||||||
return utils.BadRequest(c, "invalid request body")
|
return utils.BadRequest(c, "invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Locationname != "" {
|
||||||
|
location.Locationname = req.Locationname
|
||||||
|
}
|
||||||
if req.Address != "" {
|
if req.Address != "" {
|
||||||
location.Address = req.Address
|
location.Address = req.Address
|
||||||
}
|
}
|
||||||
@@ -1177,6 +1330,21 @@ func CreateMiler(c *fiber.Ctx) error {
|
|||||||
appLocID = 1
|
appLocID = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A client login may only create riders under its own tenant.
|
||||||
|
tenantID := req.Tenantid
|
||||||
|
if own := consoleTenantID(c); own != 0 {
|
||||||
|
tenantID = own
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configid must match what LoginMiler looks up by — it queries
|
||||||
|
// "contactno = ? AND configid = ?" defaulting to 1001. Left unset, AppUser's
|
||||||
|
// column default of 1 applies and the rider can never log in, which is what
|
||||||
|
// happened to every miler created through this endpoint until now.
|
||||||
|
configID := req.Configid
|
||||||
|
if configID == 0 {
|
||||||
|
configID = 1001
|
||||||
|
}
|
||||||
|
|
||||||
user := models.AppUser{
|
user := models.AppUser{
|
||||||
Authname: req.Authname,
|
Authname: req.Authname,
|
||||||
Email: req.Email,
|
Email: req.Email,
|
||||||
@@ -1185,6 +1353,9 @@ func CreateMiler(c *fiber.Ctx) error {
|
|||||||
Roleid: 5, // Miler
|
Roleid: 5, // Miler
|
||||||
Status: "Active",
|
Status: "Active",
|
||||||
Applocationid: appLocID,
|
Applocationid: appLocID,
|
||||||
|
Tenantid: tenantID,
|
||||||
|
Hubid: req.Hubid,
|
||||||
|
Configid: configID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Create(&user).Error; err != nil {
|
if err := tx.Create(&user).Error; err != nil {
|
||||||
@@ -1200,6 +1371,7 @@ func CreateMiler(c *fiber.Ctx) error {
|
|||||||
Availabilitystatus: constants.MilerOffline,
|
Availabilitystatus: constants.MilerOffline,
|
||||||
Rating: 5.00,
|
Rating: 5.00,
|
||||||
Applocationid: appLocID,
|
Applocationid: appLocID,
|
||||||
|
Hubid: req.Hubid,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Create(&profile).Error; err != nil {
|
if err := tx.Create(&profile).Error; err != nil {
|
||||||
@@ -1360,6 +1532,9 @@ func GetAdminBookings(c *fiber.Ctx) error {
|
|||||||
if tenantID := c.Query("tenantid"); tenantID != "" {
|
if tenantID := c.Query("tenantid"); tenantID != "" {
|
||||||
query = query.Where("tenantid = ?", tenantID)
|
query = query.Where("tenantid = ?", tenantID)
|
||||||
}
|
}
|
||||||
|
// Applied after the caller's own ?tenantid= filter so a client login can
|
||||||
|
// narrow within their tenant but never widen past it.
|
||||||
|
query = scopeToOwnTenant(c, query, "tenantid")
|
||||||
|
|
||||||
var total int64
|
var total int64
|
||||||
if err := query.Count(&total).Error; err != nil {
|
if err := query.Count(&total).Error; err != nil {
|
||||||
@@ -1388,10 +1563,15 @@ func GetAdminBookings(c *fiber.Ctx) error {
|
|||||||
// (one booking) and AdminBulkCreateBookings (many) — was previously a type
|
// (one booking) and AdminBulkCreateBookings (many) — was previously a type
|
||||||
// local to CreateExpressBooking, promoted to package level so both can use it.
|
// local to CreateExpressBooking, promoted to package level so both can use it.
|
||||||
type AdminBookingRequest struct {
|
type AdminBookingRequest struct {
|
||||||
Tenantid int `json:"tenantid"`
|
Tenantid int `json:"tenantid"`
|
||||||
Appcustomerid int `json:"appcustomerid"`
|
Appcustomerid int `json:"appcustomerid"`
|
||||||
CustomerPhone string `json:"customer_phone"`
|
CustomerPhone string `json:"customer_phone"`
|
||||||
CustomerName string `json:"customer_name"`
|
CustomerName string `json:"customer_name"`
|
||||||
|
// Pickuplocationid names the client site the parcel is collected from — a
|
||||||
|
// DailyGrubs kitchen, for instance. Optional, but supplying it lets the
|
||||||
|
// address/pincode/coordinates be filled from the stored location instead of
|
||||||
|
// retyped, and is the only thing that makes per-site reporting possible.
|
||||||
|
Pickuplocationid *int `json:"pickuplocationid"`
|
||||||
Pickupaddress string `json:"pickupaddress"`
|
Pickupaddress string `json:"pickupaddress"`
|
||||||
Pickuppincode string `json:"pickuppincode"`
|
Pickuppincode string `json:"pickuppincode"`
|
||||||
Pickuplatitude float64 `json:"pickuplatitude"`
|
Pickuplatitude float64 `json:"pickuplatitude"`
|
||||||
@@ -1426,9 +1606,6 @@ func (e *expressBookingValidationError) Error() string { return e.msg }
|
|||||||
// *fiber.Ctx — the original function never touched c after BodyParser, so
|
// *fiber.Ctx — the original function never touched c after BodyParser, so
|
||||||
// both callers can use this identically.
|
// both callers can use this identically.
|
||||||
func createExpressBooking(req AdminBookingRequest) (*models.PickupBooking, error) {
|
func createExpressBooking(req AdminBookingRequest) (*models.PickupBooking, error) {
|
||||||
if req.Pickupaddress == "" || req.Pickuppincode == "" {
|
|
||||||
return nil, &expressBookingValidationError{"pickup address and pincode are required"}
|
|
||||||
}
|
|
||||||
if len(req.Parcels) == 0 {
|
if len(req.Parcels) == 0 {
|
||||||
return nil, &expressBookingValidationError{"at least one parcel is required"}
|
return nil, &expressBookingValidationError{"at least one parcel is required"}
|
||||||
}
|
}
|
||||||
@@ -1440,6 +1617,35 @@ func createExpressBooking(req AdminBookingRequest) (*models.PickupBooking, error
|
|||||||
return nil, &expressBookingValidationError{"tenantid does not match a known tenant"}
|
return nil, &expressBookingValidationError{"tenantid does not match a known tenant"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A named pickup location fills in whatever the caller left blank, so the
|
||||||
|
// console can send a kitchen id instead of restating its address every time.
|
||||||
|
// It must belong to the booking's tenant — otherwise one client could book
|
||||||
|
// against another client's site.
|
||||||
|
if req.Pickuplocationid != nil {
|
||||||
|
var loc models.TenantLocation
|
||||||
|
if err := db.DB.Where("tenantlocationid = ?", *req.Pickuplocationid).First(&loc).Error; err != nil {
|
||||||
|
return nil, &expressBookingValidationError{"pickuplocationid does not match a known location"}
|
||||||
|
}
|
||||||
|
if loc.Tenantid != req.Tenantid {
|
||||||
|
return nil, &expressBookingValidationError{"pickuplocationid does not belong to this tenant"}
|
||||||
|
}
|
||||||
|
if req.Pickupaddress == "" {
|
||||||
|
req.Pickupaddress = loc.Address
|
||||||
|
}
|
||||||
|
if req.Pickuppincode == "" {
|
||||||
|
req.Pickuppincode = loc.Pincode
|
||||||
|
}
|
||||||
|
if req.Pickuplatitude == 0 && req.Pickuplongitude == 0 {
|
||||||
|
req.Pickuplatitude, req.Pickuplongitude = loc.Latitude, loc.Longitude
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checked after the location fill-in, so a caller supplying only a kitchen
|
||||||
|
// id is not rejected for an address it never needed to send.
|
||||||
|
if req.Pickupaddress == "" || req.Pickuppincode == "" {
|
||||||
|
return nil, &expressBookingValidationError{"pickup address and pincode are required"}
|
||||||
|
}
|
||||||
|
|
||||||
tx := db.DB.Begin()
|
tx := db.DB.Begin()
|
||||||
|
|
||||||
customerID := req.Appcustomerid
|
customerID := req.Appcustomerid
|
||||||
@@ -1472,6 +1678,7 @@ func createExpressBooking(req AdminBookingRequest) (*models.PickupBooking, error
|
|||||||
Bookingno: generateBookingNo(),
|
Bookingno: generateBookingNo(),
|
||||||
Tenantid: &tenantID,
|
Tenantid: &tenantID,
|
||||||
Appcustomerid: customerID,
|
Appcustomerid: customerID,
|
||||||
|
Pickuplocationid: req.Pickuplocationid,
|
||||||
Pickupaddress: req.Pickupaddress,
|
Pickupaddress: req.Pickupaddress,
|
||||||
Pickuppincode: req.Pickuppincode,
|
Pickuppincode: req.Pickuppincode,
|
||||||
Pickuplatitude: req.Pickuplatitude,
|
Pickuplatitude: req.Pickuplatitude,
|
||||||
@@ -1641,6 +1848,13 @@ func CreateExpressBooking(c *fiber.Ctx) error {
|
|||||||
return utils.BadRequest(c, "invalid request body")
|
return utils.BadRequest(c, "invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A client login may only book under its own tenant. Left unchecked, the
|
||||||
|
// tenantid is caller-supplied, so a client could attribute bookings — and
|
||||||
|
// their cost — to another client.
|
||||||
|
if own := consoleTenantID(c); own != 0 {
|
||||||
|
req.Tenantid = own
|
||||||
|
}
|
||||||
|
|
||||||
booking, err := createExpressBooking(*req)
|
booking, err := createExpressBooking(*req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*expressBookingValidationError); ok {
|
if _, ok := err.(*expressBookingValidationError); ok {
|
||||||
@@ -1679,7 +1893,14 @@ func AdminBulkCreateBookings(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
results := make([]result, 0, len(req.Bookings))
|
results := make([]result, 0, len(req.Bookings))
|
||||||
|
|
||||||
|
ownTenant := consoleTenantID(c)
|
||||||
|
|
||||||
for i, item := range req.Bookings {
|
for i, item := range req.Bookings {
|
||||||
|
// Same tenant pin as the single-booking path — a bulk import must not
|
||||||
|
// be a way around it.
|
||||||
|
if ownTenant != 0 {
|
||||||
|
item.Tenantid = ownTenant
|
||||||
|
}
|
||||||
booking, err := createExpressBooking(item)
|
booking, err := createExpressBooking(item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
results = append(results, result{Index: i, Success: false, Error: err.Error()})
|
results = append(results, result{Index: i, Success: false, Error: err.Error()})
|
||||||
@@ -1694,7 +1915,8 @@ func AdminBulkCreateBookings(c *fiber.Ctx) error {
|
|||||||
func GetAdminBookingDetails(c *fiber.Ctx) error {
|
func GetAdminBookingDetails(c *fiber.Ctx) error {
|
||||||
id, _ := strconv.Atoi(c.Params("id"))
|
id, _ := strconv.Atoi(c.Params("id"))
|
||||||
var booking models.PickupBooking
|
var booking models.PickupBooking
|
||||||
if err := db.DB.Preload("Parcels").Preload("ServiceOptions").Preload("Payments").First(&booking, id).Error; err != nil {
|
q := scopeToOwnTenant(c, db.DB.Preload("Parcels").Preload("ServiceOptions").Preload("Payments"), "tenantid")
|
||||||
|
if err := q.First(&booking, id).Error; err != nil {
|
||||||
return utils.NotFound(c, "booking not found")
|
return utils.NotFound(c, "booking not found")
|
||||||
}
|
}
|
||||||
return utils.OK(c, booking)
|
return utils.OK(c, booking)
|
||||||
@@ -1702,6 +1924,9 @@ func GetAdminBookingDetails(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func AdminAssignMiler(c *fiber.Ctx) error {
|
func AdminAssignMiler(c *fiber.Ctx) error {
|
||||||
id, _ := strconv.Atoi(c.Params("id"))
|
id, _ := strconv.Atoi(c.Params("id"))
|
||||||
|
if err := assertBookingAccess(c, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
type MilerAssign struct {
|
type MilerAssign struct {
|
||||||
Mileruserid int `json:"mileruserid"`
|
Mileruserid int `json:"mileruserid"`
|
||||||
@@ -1723,6 +1948,9 @@ func AdminAssignMiler(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func AdminAssignVehicle(c *fiber.Ctx) error {
|
func AdminAssignVehicle(c *fiber.Ctx) error {
|
||||||
id, _ := strconv.Atoi(c.Params("id"))
|
id, _ := strconv.Atoi(c.Params("id"))
|
||||||
|
if err := assertBookingAccess(c, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
type VehicleAssign struct {
|
type VehicleAssign struct {
|
||||||
Vehicleid int `json:"vehicleid"`
|
Vehicleid int `json:"vehicleid"`
|
||||||
@@ -1749,6 +1977,9 @@ func AdminAssignVehicle(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func AdminUpdateBookingStatus(c *fiber.Ctx) error {
|
func AdminUpdateBookingStatus(c *fiber.Ctx) error {
|
||||||
id, _ := strconv.Atoi(c.Params("id"))
|
id, _ := strconv.Atoi(c.Params("id"))
|
||||||
|
if err := assertBookingAccess(c, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
type StatusUpdate struct {
|
type StatusUpdate struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
@@ -1783,6 +2014,9 @@ func AdminCancelBooking(c *fiber.Ctx) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return utils.BadRequest(c, "invalid booking ID")
|
return utils.BadRequest(c, "invalid booking ID")
|
||||||
}
|
}
|
||||||
|
if err := assertBookingAccess(c, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var booking models.PickupBooking
|
var booking models.PickupBooking
|
||||||
if err := db.DB.First(&booking, id).Error; err != nil {
|
if err := db.DB.First(&booking, id).Error; err != nil {
|
||||||
@@ -1850,6 +2084,8 @@ func AdminBulkCancelBookings(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
results := make([]result, 0, len(req.Bookingids))
|
results := make([]result, 0, len(req.Bookingids))
|
||||||
|
|
||||||
|
ownTenant := consoleTenantID(c)
|
||||||
|
|
||||||
for _, id := range req.Bookingids {
|
for _, id := range req.Bookingids {
|
||||||
var booking models.PickupBooking
|
var booking models.PickupBooking
|
||||||
if err := db.DB.First(&booking, id).Error; err != nil {
|
if err := db.DB.First(&booking, id).Error; err != nil {
|
||||||
@@ -1857,6 +2093,13 @@ func AdminBulkCancelBookings(c *fiber.Ctx) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reported as not-found rather than forbidden, so a client can't probe
|
||||||
|
// which booking ids belong to other tenants.
|
||||||
|
if ownTenant != 0 && (booking.Tenantid == nil || *booking.Tenantid != ownTenant) {
|
||||||
|
results = append(results, result{Bookingid: id, Success: false, Error: "booking not found"})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if booking.Status == constants.BookingConvertedConsignment || booking.Status == constants.BookingCancelled {
|
if booking.Status == constants.BookingConvertedConsignment || booking.Status == constants.BookingCancelled {
|
||||||
results = append(results, result{Bookingid: id, Success: false, Error: "cannot cancel a delivered or already cancelled booking"})
|
results = append(results, result{Bookingid: id, Success: false, Error: "cannot cancel a delivered or already cancelled booking"})
|
||||||
continue
|
continue
|
||||||
@@ -1905,12 +2148,13 @@ func GetAdminConsignments(c *fiber.Ctx) error {
|
|||||||
page := utils.ParsePage(c)
|
page := utils.ParsePage(c)
|
||||||
|
|
||||||
var total int64
|
var total int64
|
||||||
if err := db.DB.Model(&models.Consignment{}).Count(&total).Error; err != nil {
|
if err := scopeToOwnTenant(c, db.DB.Model(&models.Consignment{}), "tenantid").
|
||||||
|
Count(&total).Error; err != nil {
|
||||||
return utils.Internal(c, "failed to count consignments")
|
return utils.Internal(c, "failed to count consignments")
|
||||||
}
|
}
|
||||||
|
|
||||||
var list []models.Consignment
|
var list []models.Consignment
|
||||||
if err := page.Apply(db.DB).Find(&list).Error; err != nil {
|
if err := page.Apply(scopeToOwnTenant(c, db.DB, "tenantid")).Find(&list).Error; err != nil {
|
||||||
return utils.Internal(c, "failed to fetch consignments")
|
return utils.Internal(c, "failed to fetch consignments")
|
||||||
}
|
}
|
||||||
return utils.Paginated(c, list, total, page)
|
return utils.Paginated(c, list, total, page)
|
||||||
@@ -1919,7 +2163,7 @@ func GetAdminConsignments(c *fiber.Ctx) error {
|
|||||||
func GetAdminConsignmentDetails(c *fiber.Ctx) error {
|
func GetAdminConsignmentDetails(c *fiber.Ctx) error {
|
||||||
id, _ := strconv.Atoi(c.Params("id"))
|
id, _ := strconv.Atoi(c.Params("id"))
|
||||||
var csg models.Consignment
|
var csg models.Consignment
|
||||||
if err := db.DB.First(&csg, id).Error; err != nil {
|
if err := scopeToOwnTenant(c, db.DB, "tenantid").First(&csg, id).Error; err != nil {
|
||||||
return utils.NotFound(c, "consignment not found")
|
return utils.NotFound(c, "consignment not found")
|
||||||
}
|
}
|
||||||
return utils.OK(c, csg)
|
return utils.OK(c, csg)
|
||||||
@@ -1928,7 +2172,8 @@ func GetAdminConsignmentDetails(c *fiber.Ctx) error {
|
|||||||
func GetAdminConsignmentTracking(c *fiber.Ctx) error {
|
func GetAdminConsignmentTracking(c *fiber.Ctx) error {
|
||||||
trackingNo := c.Params("trackingno")
|
trackingNo := c.Params("trackingno")
|
||||||
var consignment models.Consignment
|
var consignment models.Consignment
|
||||||
if err := db.DB.Where("trackingno = ?", trackingNo).First(&consignment).Error; err != nil {
|
if err := scopeToOwnTenant(c, db.DB, "tenantid").
|
||||||
|
Where("trackingno = ?", trackingNo).First(&consignment).Error; err != nil {
|
||||||
return utils.NotFound(c, "consignment not found")
|
return utils.NotFound(c, "consignment not found")
|
||||||
}
|
}
|
||||||
var history []models.ConsignmentHistory
|
var history []models.ConsignmentHistory
|
||||||
@@ -2218,7 +2463,10 @@ func ArriveTripsheet(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func GetPricing(c *fiber.Ctx) error {
|
func GetPricing(c *fiber.Ctx) error {
|
||||||
var pricing []models.Pricing
|
var pricing []models.Pricing
|
||||||
if err := db.DB.Where("deletedat IS NULL").Find(&pricing).Error; err != nil {
|
// Rates are commercially sensitive: one client must never see what another
|
||||||
|
// is charged.
|
||||||
|
if err := scopeToOwnTenant(c, db.DB, "tenantid").
|
||||||
|
Where("deletedat IS NULL").Find(&pricing).Error; err != nil {
|
||||||
return utils.Internal(c, "failed to fetch pricing schedules")
|
return utils.Internal(c, "failed to fetch pricing schedules")
|
||||||
}
|
}
|
||||||
return utils.List(c, pricing, int64(len(pricing)))
|
return utils.List(c, pricing, int64(len(pricing)))
|
||||||
|
|||||||
@@ -96,9 +96,11 @@ func humanizeRelativeTime(t time.Time) string {
|
|||||||
|
|
||||||
// todayMidnight returns the start of the current day in server local time,
|
// todayMidnight returns the start of the current day in server local time,
|
||||||
// used to scope "today" counters on the hub dashboard.
|
// used to scope "today" counters on the hub dashboard.
|
||||||
|
// todayMidnight is the start of the current day as the database records it —
|
||||||
|
// see utils.DBNow. Using the container's own clock here dropped every row
|
||||||
|
// created after noon IST out of "today so far".
|
||||||
func todayMidnight() time.Time {
|
func todayMidnight() time.Time {
|
||||||
now := time.Now()
|
return utils.DBToday()
|
||||||
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseHubDateRange parses optional from/to (YYYY-MM-DD) query params shared
|
// parseHubDateRange parses optional from/to (YYYY-MM-DD) query params shared
|
||||||
@@ -110,17 +112,20 @@ func parseHubDateRange(c *fiber.Ctx) (time.Time, time.Time, error) {
|
|||||||
toStr := c.Query("to")
|
toStr := c.Query("to")
|
||||||
|
|
||||||
if fromStr == "" && toStr == "" {
|
if fromStr == "" && toStr == "" {
|
||||||
return todayMidnight(), time.Now(), nil
|
return todayMidnight(), utils.DBNow(), nil
|
||||||
}
|
}
|
||||||
if fromStr == "" || toStr == "" {
|
if fromStr == "" || toStr == "" {
|
||||||
return time.Time{}, time.Time{}, fmt.Errorf("both from and to query params are required (YYYY-MM-DD)")
|
return time.Time{}, time.Time{}, fmt.Errorf("both from and to query params are required (YYYY-MM-DD)")
|
||||||
}
|
}
|
||||||
|
|
||||||
from, err := time.ParseInLocation("2006-01-02", fromStr, time.Local)
|
// Parsed as UTC, not time.Local: stored timestamps are bare wall-clock
|
||||||
|
// digits, so the bounds must be too — otherwise the window silently shifts
|
||||||
|
// with whatever timezone the container happens to run in.
|
||||||
|
from, err := time.ParseInLocation("2006-01-02", fromStr, time.UTC)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, time.Time{}, fmt.Errorf("invalid from date, expected YYYY-MM-DD")
|
return time.Time{}, time.Time{}, fmt.Errorf("invalid from date, expected YYYY-MM-DD")
|
||||||
}
|
}
|
||||||
toDate, err := time.ParseInLocation("2006-01-02", toStr, time.Local)
|
toDate, err := time.ParseInLocation("2006-01-02", toStr, time.UTC)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, time.Time{}, fmt.Errorf("invalid to date, expected YYYY-MM-DD")
|
return time.Time{}, time.Time{}, fmt.Errorf("invalid to date, expected YYYY-MM-DD")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -274,9 +274,6 @@ func MilerDeliverConsignment(c *fiber.Ctx) error {
|
|||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
return utils.BadRequest(c, "invalid request body")
|
return utils.BadRequest(c, "invalid request body")
|
||||||
}
|
}
|
||||||
if req.Otp == "" {
|
|
||||||
return utils.BadRequest(c, "otp is required")
|
|
||||||
}
|
|
||||||
if req.Deliveredtoname == "" {
|
if req.Deliveredtoname == "" {
|
||||||
return utils.BadRequest(c, "deliveredtoname is required")
|
return utils.BadRequest(c, "deliveredtoname is required")
|
||||||
}
|
}
|
||||||
@@ -296,17 +293,28 @@ func MilerDeliverConsignment(c *fiber.Ctx) error {
|
|||||||
return utils.BadRequest(c, "consignment is not out for delivery")
|
return utils.BadRequest(c, "consignment is not out for delivery")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An OTP is only present when the client asked for one (Tenant.Requiredeliveryotp),
|
||||||
|
// so an empty one means this delivery was never meant to need a code — that
|
||||||
|
// covers food clients like DailyGrubs as well as parcels already in the
|
||||||
|
// network from before OTPs existed, which would otherwise be unclosable.
|
||||||
|
if consignment.Deliveryotp != "" {
|
||||||
|
if req.Otp == "" {
|
||||||
|
return utils.BadRequest(c, "otp is required for this delivery")
|
||||||
|
}
|
||||||
|
if req.Otp != consignment.Deliveryotp {
|
||||||
|
return utils.BadRequest(c, "incorrect delivery OTP")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tx := db.DB.Begin()
|
tx := db.DB.Begin()
|
||||||
|
|
||||||
// OTP generation/storage is Phase 2 — no delivery_otp field exists yet on
|
|
||||||
// consignments, so acceptance of a non-empty OTP is treated as verified.
|
|
||||||
proof := models.DeliveryProof{
|
proof := models.DeliveryProof{
|
||||||
Consignmentid: consignment.Consignmentid,
|
Consignmentid: consignment.Consignmentid,
|
||||||
Deliveredat: time.Now(),
|
Deliveredat: time.Now(),
|
||||||
Deliveredtoname: req.Deliveredtoname,
|
Deliveredtoname: req.Deliveredtoname,
|
||||||
Receiversignatureurl: req.Receiversignatureurl,
|
Receiversignatureurl: req.Receiversignatureurl,
|
||||||
Photourl: req.Photourl,
|
Photourl: req.Photourl,
|
||||||
Otpverified: true,
|
Otpverified: consignment.Deliveryotp != "",
|
||||||
Geolatitude: req.Lat,
|
Geolatitude: req.Lat,
|
||||||
Geolongitude: req.Lon,
|
Geolongitude: req.Lon,
|
||||||
Createdby: milerUserID,
|
Createdby: milerUserID,
|
||||||
@@ -317,12 +325,30 @@ func MilerDeliverConsignment(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
consignment.Status = constants.ConsignmentDelivered
|
consignment.Status = constants.ConsignmentDelivered
|
||||||
|
// Cleared once redeemed so the same code can't close out a second attempt.
|
||||||
|
consignment.Deliveryotp = ""
|
||||||
consignment.Updatedat = time.Now()
|
consignment.Updatedat = time.Now()
|
||||||
if err := tx.Save(&consignment).Error; err != nil {
|
if err := tx.Save(&consignment).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return utils.Internal(c, "failed to mark consignment delivered")
|
return utils.Internal(c, "failed to mark consignment delivered")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every other state change on a consignment writes a history row; delivery
|
||||||
|
// did not, so a customer following the tracking timeline never saw the
|
||||||
|
// parcel arrive — it just stopped at Out_for_Delivery.
|
||||||
|
deliveredEvent := models.ConsignmentHistory{
|
||||||
|
Consignmentid: consignment.Consignmentid,
|
||||||
|
Hubid: consignment.Currenthubid,
|
||||||
|
Userid: &milerUserID,
|
||||||
|
Eventstatus: constants.ConsignmentDelivered,
|
||||||
|
Remarks: fmt.Sprintf("Delivered to %s at (%.5f, %.5f)",
|
||||||
|
req.Deliveredtoname, req.Lat, req.Lon),
|
||||||
|
}
|
||||||
|
if err := tx.Create(&deliveredEvent).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return utils.Internal(c, "failed to record delivery history")
|
||||||
|
}
|
||||||
|
|
||||||
if err := tx.Model(&models.BookingAssignment{}).
|
if err := tx.Model(&models.BookingAssignment{}).
|
||||||
Where("bookingid = ? AND mileruserid = ?", booking.Bookingid, milerUserID).
|
Where("bookingid = ? AND mileruserid = ?", booking.Bookingid, milerUserID).
|
||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
|
|||||||
@@ -293,7 +293,8 @@ func UpdateMilerAvailability(c *fiber.Ctx) error {
|
|||||||
return utils.BadRequest(c, "invalid request body")
|
return utils.BadRequest(c, "invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Status == "" {
|
status := req.ResolvedStatus()
|
||||||
|
if status == "" {
|
||||||
return utils.BadRequest(c, "status is required")
|
return utils.BadRequest(c, "status is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +303,7 @@ func UpdateMilerAvailability(c *fiber.Ctx) error {
|
|||||||
return utils.NotFound(c, "miler profile not found")
|
return utils.NotFound(c, "miler profile not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
profile.Availabilitystatus = req.Status
|
profile.Availabilitystatus = status
|
||||||
profile.Updatedat = time.Now()
|
profile.Updatedat = time.Now()
|
||||||
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 availability")
|
return utils.Internal(c, "failed to update availability")
|
||||||
@@ -796,9 +797,13 @@ func BookingPickupComplete(c *fiber.Ctx) error {
|
|||||||
consignmentTenantID = *booking.Tenantid
|
consignmentTenantID = *booking.Tenantid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Carried over so the consignment stays traceable to the client site it was
|
||||||
|
// collected from — for a food client that's the kitchen, and "how many
|
||||||
|
// parcels went out of which kitchen" is unanswerable without it.
|
||||||
consignment := models.Consignment{
|
consignment := models.Consignment{
|
||||||
Trackingno: trackingNo,
|
Trackingno: trackingNo,
|
||||||
Tenantid: consignmentTenantID,
|
Tenantid: consignmentTenantID,
|
||||||
|
Pickuplocationid: booking.Pickuplocationid,
|
||||||
Pickuplatitude: booking.Pickuplatitude,
|
Pickuplatitude: booking.Pickuplatitude,
|
||||||
Pickuplongitude: booking.Pickuplongitude,
|
Pickuplongitude: booking.Pickuplongitude,
|
||||||
Deliverylatitude: booking.Deliverylatitude,
|
Deliverylatitude: booking.Deliverylatitude,
|
||||||
@@ -829,6 +834,17 @@ func BookingPickupComplete(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A hyperlocal parcel goes straight out for delivery, so its receiver OTP has
|
||||||
|
// to exist before this transaction commits. Anything routed via a hub gets
|
||||||
|
// its OTP when it actually leaves for the final mile instead. Only issued
|
||||||
|
// for clients that ask for it — see Tenant.Requiredeliveryotp.
|
||||||
|
if consignmentStatus == constants.ConsignmentOutForDelivery {
|
||||||
|
var tenant models.Tenant
|
||||||
|
if tx.Where("tenantid = ?", consignmentTenantID).First(&tenant).Error == nil && tenant.Requiredeliveryotp {
|
||||||
|
consignment.Deliveryotp = utils.GenerateNumericOTP(6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := tx.Create(&consignment).Error; err != nil {
|
if err := tx.Create(&consignment).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return utils.Internal(c, "failed to convert booking to consignment")
|
return utils.Internal(c, "failed to convert booking to consignment")
|
||||||
@@ -865,15 +881,18 @@ func BookingPickupComplete(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
var customer models.AppCustomer
|
var customer models.AppCustomer
|
||||||
if err := db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer).Error; err == nil && customer.Devicetoken != "" {
|
if err := db.DB.Where("appcustomerid = ?", booking.Appcustomerid).First(&customer).Error; err == nil && customer.Devicetoken != "" {
|
||||||
if notifyErr := notify.SendToDevice(
|
body := fmt.Sprintf("Parcel picked up — Tracking No: %s", trackingNo)
|
||||||
customer.Devicetoken,
|
payload := map[string]string{
|
||||||
"Parcel Picked Up",
|
"booking_id": strconv.Itoa(bookingID),
|
||||||
fmt.Sprintf("Parcel picked up — Tracking No: %s", trackingNo),
|
"tracking_no": trackingNo,
|
||||||
map[string]string{
|
}
|
||||||
"booking_id": strconv.Itoa(bookingID),
|
// The OTP goes to the receiver and only the receiver — the rider has to
|
||||||
"tracking_no": trackingNo,
|
// be told it at the door, which is what makes it proof of handover.
|
||||||
},
|
if consignment.Deliveryotp != "" {
|
||||||
); notifyErr != nil {
|
body = fmt.Sprintf("%s. Share OTP %s with the rider on delivery.", body, consignment.Deliveryotp)
|
||||||
|
payload["delivery_otp"] = consignment.Deliveryotp
|
||||||
|
}
|
||||||
|
if notifyErr := notify.SendToDevice(customer.Devicetoken, "Parcel Picked Up", body, payload); notifyErr != nil {
|
||||||
utils.Warn("FCM: failed to notify customer on pickup", "booking_id", bookingID, "error", notifyErr)
|
utils.Warn("FCM: failed to notify customer on pickup", "booking_id", bookingID, "error", notifyErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -919,6 +938,11 @@ func CreateMilerPeriodicLog(c *fiber.Ctx) error {
|
|||||||
return utils.BadRequest(c, "invalid request body")
|
return utils.BadRequest(c, "invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The rider's identity comes from their token, never the body. Trusting a
|
||||||
|
// client-supplied userid let any authenticated miler write another miler's
|
||||||
|
// GPS trail, which feeds the location data dispatch reasons over.
|
||||||
|
log.UserID = c.Locals("userid").(int)
|
||||||
|
|
||||||
t, err := time.Parse("2006-01-02 15:04:05", log.LogDate)
|
t, err := time.Parse("2006-01-02 15:04:05", log.LogDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return utils.BadRequest(c, "invalid logdate format — expected YYYY-MM-DD HH:MM:SS")
|
return utils.BadRequest(c, "invalid logdate format — expected YYYY-MM-DD HH:MM:SS")
|
||||||
@@ -987,8 +1011,12 @@ func CreateMilerStatus(c *fiber.Ctx) error {
|
|||||||
return utils.BadRequest(c, "invalid request body")
|
return utils.BadRequest(c, "invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.UserID == 0 || status.Status == "" {
|
// Identity from the token, not the body — otherwise one rider can set
|
||||||
return utils.BadRequest(c, "userid and status are required")
|
// another rider's live status.
|
||||||
|
status.UserID = c.Locals("userid").(int)
|
||||||
|
|
||||||
|
if status.Status == "" {
|
||||||
|
return utils.BadRequest(c, "status is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
key := fmt.Sprintf("miler_status:%d", status.UserID)
|
key := fmt.Sprintf("miler_status:%d", status.UserID)
|
||||||
@@ -1088,10 +1116,16 @@ func PublishConsignmentLogs(c *fiber.Ctx) error {
|
|||||||
return utils.Internal(c, "cache service unavailable")
|
return utils.Internal(c, "cache service unavailable")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
milerUserID := c.Locals("userid").(int)
|
||||||
|
|
||||||
pipe := db.Rdb.TxPipeline()
|
pipe := db.Rdb.TxPipeline()
|
||||||
tx := db.DB.Begin()
|
tx := db.DB.Begin()
|
||||||
|
|
||||||
for _, item := range input {
|
for _, item := range input {
|
||||||
|
// Same rule as the other telemetry writers: the token owns the identity,
|
||||||
|
// so a batch can't be attributed to some other rider.
|
||||||
|
item.UserID = milerUserID
|
||||||
|
|
||||||
logTime, err := time.Parse("2006-01-02 15:04:05", item.LogDate)
|
logTime, err := time.Parse("2006-01-02 15:04:05", item.LogDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logTime = time.Now()
|
logTime = time.Now()
|
||||||
|
|||||||
42
dto/admin.go
42
dto/admin.go
@@ -7,17 +7,23 @@ type TenantCreateRequest struct {
|
|||||||
Primaryemail string `json:"primaryemail"`
|
Primaryemail string `json:"primaryemail"`
|
||||||
Primarycontact string `json:"primarycontact"`
|
Primarycontact string `json:"primarycontact"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
// Requiredeliveryotp is a pointer so an update that omits it leaves the
|
||||||
|
// existing setting alone rather than silently switching OTPs off.
|
||||||
|
Requiredeliveryotp *bool `json:"requiredeliveryotp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TenantLocationCreateRequest struct {
|
type TenantLocationCreateRequest struct {
|
||||||
Address string `json:"address"`
|
// Locationname is the client's own label for the site — "DailyGrubs
|
||||||
City string `json:"city"`
|
// Peelamedu Kitchen" — since an address alone doesn't identify a branch.
|
||||||
State string `json:"state"`
|
Locationname string `json:"locationname"`
|
||||||
Pincode string `json:"pincode"`
|
Address string `json:"address"`
|
||||||
Latitude float64 `json:"latitude"`
|
City string `json:"city"`
|
||||||
Longitude float64 `json:"longitude"`
|
State string `json:"state"`
|
||||||
Isprimary bool `json:"isprimary"`
|
Pincode string `json:"pincode"`
|
||||||
Status string `json:"status"`
|
Latitude float64 `json:"latitude"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
Isprimary bool `json:"isprimary"`
|
||||||
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TenantCustomerCreateRequest struct {
|
type TenantCustomerCreateRequest struct {
|
||||||
@@ -57,13 +63,23 @@ type VehicleCreateRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MilerCreateRequest struct {
|
type MilerCreateRequest struct {
|
||||||
Authname string `json:"authname"`
|
Authname string `json:"authname"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Contactno string `json:"contactno"`
|
Contactno string `json:"contactno"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Displayname string `json:"displayname"`
|
Displayname string `json:"displayname"`
|
||||||
|
// Tenantid attaches a rider to the client they deliver for — riders migrated
|
||||||
|
// from jupiter belong to a specific client (DailyGrubs, Bawa Medicals)
|
||||||
|
// rather than to Doormile's general pool. Zero leaves them unattached.
|
||||||
|
Tenantid int `json:"tenantid"`
|
||||||
Defaultvehicletype string `json:"defaultvehicletype"`
|
Defaultvehicletype string `json:"defaultvehicletype"`
|
||||||
Applocationid int `json:"applocationid"`
|
Applocationid int `json:"applocationid"`
|
||||||
|
// Configid partitions logins; defaults to 1001, which is what the miler app
|
||||||
|
// authenticates against. Only set this if you know why you're changing it.
|
||||||
|
Configid int `json:"configid"`
|
||||||
|
// Hubid is optional: a rider with no hub is still assignable from the
|
||||||
|
// express console, but is invisible to the hub console's miler list.
|
||||||
|
Hubid *int `json:"hubid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PricingCreateRequest struct {
|
type PricingCreateRequest struct {
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ type ParcelRequest struct {
|
|||||||
Itemcategory string `json:"itemcategory"`
|
Itemcategory string `json:"itemcategory"`
|
||||||
Itemdescription string `json:"itemdescription"`
|
Itemdescription string `json:"itemdescription"`
|
||||||
Declaredvalue float64 `json:"declaredvalue"`
|
Declaredvalue float64 `json:"declaredvalue"`
|
||||||
Weight float64 `json:"weight"` // optional — miler weighs at pickup
|
Weight float64 `json:"weight"` // optional — miler weighs at pickup
|
||||||
Length float64 `json:"length"` // optional
|
Length float64 `json:"length"` // optional
|
||||||
Width float64 `json:"width"` // optional
|
Width float64 `json:"width"` // optional
|
||||||
Height float64 `json:"height"` // optional
|
Height float64 `json:"height"` // optional
|
||||||
Isfragile bool `json:"isfragile"`
|
Isfragile bool `json:"isfragile"`
|
||||||
Needsinsurance bool `json:"needsinsurance"`
|
Needsinsurance bool `json:"needsinsurance"`
|
||||||
Requireslargevehicle bool `json:"requireslargevehicle"`
|
Requireslargevehicle bool `json:"requireslargevehicle"`
|
||||||
@@ -31,8 +31,8 @@ type ParcelRequest struct {
|
|||||||
|
|
||||||
type PickupBookingRequest struct {
|
type PickupBookingRequest struct {
|
||||||
Pickuplocationid *int `json:"pickuplocationid"`
|
Pickuplocationid *int `json:"pickuplocationid"`
|
||||||
Pickupaddress string `json:"pickupaddress"` // required
|
Pickupaddress string `json:"pickupaddress"` // required
|
||||||
Pickuppincode string `json:"pickuppincode"` // required
|
Pickuppincode string `json:"pickuppincode"` // required
|
||||||
Pickuplatitude float64 `json:"pickuplatitude"`
|
Pickuplatitude float64 `json:"pickuplatitude"`
|
||||||
Pickuplongitude float64 `json:"pickuplongitude"`
|
Pickuplongitude float64 `json:"pickuplongitude"`
|
||||||
Deliveryaddress string `json:"deliveryaddress"` // optional — can be filled later
|
Deliveryaddress string `json:"deliveryaddress"` // optional — can be filled later
|
||||||
@@ -55,18 +55,36 @@ type MilerLocationUpdateRequest struct {
|
|||||||
Latitude float64 `json:"latitude"`
|
Latitude float64 `json:"latitude"`
|
||||||
Longitude float64 `json:"longitude"`
|
Longitude float64 `json:"longitude"`
|
||||||
Pincode string `json:"pincode"`
|
Pincode string `json:"pincode"`
|
||||||
|
// Speed and Heading are sent by the rider app and were previously dropped on
|
||||||
|
// the floor, since unknown JSON fields parse silently. Accepted here so the
|
||||||
|
// values at least reach the periodic-log telemetry rather than vanishing.
|
||||||
|
Speed float64 `json:"speed"`
|
||||||
|
Heading float64 `json:"heading"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MilerAvailabilityRequest struct {
|
type MilerAvailabilityRequest struct {
|
||||||
Status string `json:"status"` // Offline, Available, Break, etc.
|
Status string `json:"status"` // Offline, Available, Break, etc.
|
||||||
|
// Availabilitystatus is the field name the published Miler App API Contract
|
||||||
|
// v1.0 told the Flutter dev to send, while the code only ever read "status"
|
||||||
|
// — so the documented request 400s. Both are accepted rather than picking a
|
||||||
|
// winner, because either side may already be built against either name.
|
||||||
|
Availabilitystatus string `json:"availabilitystatus"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolvedStatus returns whichever of the two accepted field names was sent.
|
||||||
|
func (r MilerAvailabilityRequest) ResolvedStatus() string {
|
||||||
|
if r.Status != "" {
|
||||||
|
return r.Status
|
||||||
|
}
|
||||||
|
return r.Availabilitystatus
|
||||||
}
|
}
|
||||||
|
|
||||||
type PricingQuoteRequest struct {
|
type PricingQuoteRequest struct {
|
||||||
Pickuppincode string `json:"pickuppincode"`
|
Pickuppincode string `json:"pickuppincode"`
|
||||||
Deliverypincode string `json:"deliverypincode"`
|
Deliverypincode string `json:"deliverypincode"`
|
||||||
Pickuplatitude float64 `json:"pickuplatitude"`
|
Pickuplatitude float64 `json:"pickuplatitude"`
|
||||||
Pickuplongitude float64 `json:"pickuplongitude"`
|
Pickuplongitude float64 `json:"pickuplongitude"`
|
||||||
Deliverylatitude float64 `json:"deliverylatitude"`
|
Deliverylatitude float64 `json:"deliverylatitude"`
|
||||||
Deliverylongitude float64 `json:"deliverylongitude"`
|
Deliverylongitude float64 `json:"deliverylongitude"`
|
||||||
Parcels []ParcelRequest `json:"parcels"`
|
Parcels []ParcelRequest `json:"parcels"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ var operatingCityPrefixes = map[string]string{
|
|||||||
"600": "Chennai",
|
"600": "Chennai",
|
||||||
"560": "Bengaluru",
|
"560": "Bengaluru",
|
||||||
"500": "Hyderabad",
|
"500": "Hyderabad",
|
||||||
|
"629": "Nagercoil",
|
||||||
}
|
}
|
||||||
|
|
||||||
// CityGateMiddleware rejects bookings from pincodes outside Doormile's operating cities.
|
// CityGateMiddleware rejects bookings from pincodes outside Doormile's operating cities.
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ type Consignment struct {
|
|||||||
Chargeableweight float64 `json:"chargeableweight" gorm:"column:chargeableweight;not null"`
|
Chargeableweight float64 `json:"chargeableweight" gorm:"column:chargeableweight;not null"`
|
||||||
Codamount float64 `json:"codamount" gorm:"column:codamount;default:0.00"`
|
Codamount float64 `json:"codamount" gorm:"column:codamount;default:0.00"`
|
||||||
Codcollected float64 `json:"codcollected" gorm:"column:codcollected;default:0.00"`
|
Codcollected float64 `json:"codcollected" gorm:"column:codcollected;default:0.00"`
|
||||||
Paymentmode string `json:"paymentmode" gorm:"column:paymentmode"` // Prepaid, COD, To_Pay
|
Paymentmode string `json:"paymentmode" gorm:"column:paymentmode"` // Prepaid, COD, To_Pay
|
||||||
Billingstatus string `json:"billingstatus" gorm:"column:billingstatus;default:Unbilled"` // Unbilled, Billed, Paid, Settled
|
Billingstatus string `json:"billingstatus" gorm:"column:billingstatus;default:Unbilled"` // Unbilled, Billed, Paid, Settled
|
||||||
Status string `json:"status" gorm:"column:status;default:Created"` // Created, Inwarded_at_Hub, Tripsheet_Loaded, In_Transit, Out_for_Delivery, Delivered, RTO_Initiated, Returned_to_Sender, Missing, Damaged
|
Status string `json:"status" gorm:"column:status;default:Created"` // Created, Inwarded_at_Hub, Tripsheet_Loaded, In_Transit, Out_for_Delivery, Delivered, RTO_Initiated, Returned_to_Sender, Missing, Damaged
|
||||||
Attemptcount int `json:"attemptcount" gorm:"column:attemptcount;default:0"`
|
Attemptcount int `json:"attemptcount" gorm:"column:attemptcount;default:0"`
|
||||||
Estimateddeliveryat *time.Time `json:"estimateddeliveryat" gorm:"column:estimateddeliveryat"`
|
Estimateddeliveryat *time.Time `json:"estimateddeliveryat" gorm:"column:estimateddeliveryat"`
|
||||||
Sladueat *time.Time `json:"sladueat" gorm:"column:sladueat"`
|
Sladueat *time.Time `json:"sladueat" gorm:"column:sladueat"`
|
||||||
@@ -69,11 +69,16 @@ type Consignment struct {
|
|||||||
Parentconsignmentid *int `json:"parentconsignmentid" gorm:"column:parentconsignmentid"`
|
Parentconsignmentid *int `json:"parentconsignmentid" gorm:"column:parentconsignmentid"`
|
||||||
Condition string `json:"condition" gorm:"column:condition;size:50"` // recorded at hub inbound scan: Good, Damaged, etc.
|
Condition string `json:"condition" gorm:"column:condition;size:50"` // recorded at hub inbound scan: Good, Damaged, etc.
|
||||||
Shelf string `json:"shelf" gorm:"column:shelf;size:50"` // hub storage location assigned at inbound scan
|
Shelf string `json:"shelf" gorm:"column:shelf;size:50"` // hub storage location assigned at inbound scan
|
||||||
Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"`
|
// Deliveryotp is issued when the consignment goes out for delivery and is
|
||||||
Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"`
|
// given to the receiver, not the rider — it is the only proof the parcel
|
||||||
Createdby int `json:"createdby" gorm:"column:createdby"`
|
// reached the right person. Never serialised outward: returning it in an API
|
||||||
Updatedby int `json:"updatedby" gorm:"column:updatedby"`
|
// response would hand the rider the code they are supposed to be told.
|
||||||
Deletedat *time.Time `json:"deletedat,omitempty" gorm:"column:deletedat"`
|
Deliveryotp string `json:"-" gorm:"column:deliveryotp;size:6"`
|
||||||
|
Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"`
|
||||||
|
Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"`
|
||||||
|
Createdby int `json:"createdby" gorm:"column:createdby"`
|
||||||
|
Updatedby int `json:"updatedby" gorm:"column:updatedby"`
|
||||||
|
Deletedat *time.Time `json:"deletedat,omitempty" gorm:"column:deletedat"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Consignment) TableName() string {
|
func (Consignment) TableName() string {
|
||||||
@@ -121,12 +126,12 @@ func (ConsignmentException) TableName() string {
|
|||||||
// at that hub. One conversation per (hubid, mileruserid) pair.
|
// at that hub. One conversation per (hubid, mileruserid) pair.
|
||||||
type HubConversation struct {
|
type HubConversation struct {
|
||||||
Hubconversationid int `json:"hubconversationid" gorm:"primaryKey;column:hubconversationid"`
|
Hubconversationid int `json:"hubconversationid" gorm:"primaryKey;column:hubconversationid"`
|
||||||
Hubid int `json:"hubid" gorm:"column:hubid;index;not null"`
|
Hubid int `json:"hubid" gorm:"column:hubid;index;not null"`
|
||||||
Mileruserid *int `json:"mileruserid" gorm:"column:mileruserid;index"`
|
Mileruserid *int `json:"mileruserid" gorm:"column:mileruserid;index"`
|
||||||
Participantname string `json:"participantname" gorm:"column:participantname;not null"`
|
Participantname string `json:"participantname" gorm:"column:participantname;not null"`
|
||||||
Participantrole string `json:"participantrole" gorm:"column:participantrole"`
|
Participantrole string `json:"participantrole" gorm:"column:participantrole"`
|
||||||
Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"`
|
Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"`
|
||||||
Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"`
|
Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (HubConversation) TableName() string {
|
func (HubConversation) TableName() string {
|
||||||
@@ -137,13 +142,13 @@ func (HubConversation) TableName() string {
|
|||||||
// requesting hub staff) or "them" (the other party), matching the hub
|
// requesting hub staff) or "them" (the other party), matching the hub
|
||||||
// console frontend's bubble-side convention.
|
// console frontend's bubble-side convention.
|
||||||
type HubMessage struct {
|
type HubMessage struct {
|
||||||
Hubmessageid int `json:"hubmessageid" gorm:"primaryKey;column:hubmessageid"`
|
Hubmessageid int `json:"hubmessageid" gorm:"primaryKey;column:hubmessageid"`
|
||||||
Hubconversationid int `json:"hubconversationid" gorm:"column:hubconversationid;index;not null"`
|
Hubconversationid int `json:"hubconversationid" gorm:"column:hubconversationid;index;not null"`
|
||||||
Sender string `json:"sender" gorm:"column:sender;not null"` // me, them
|
Sender string `json:"sender" gorm:"column:sender;not null"` // me, them
|
||||||
Senderstaffid *int `json:"senderstaffid" gorm:"column:senderstaffid"`
|
Senderstaffid *int `json:"senderstaffid" gorm:"column:senderstaffid"`
|
||||||
Messagetext string `json:"messagetext" gorm:"column:messagetext;not null"`
|
Messagetext string `json:"messagetext" gorm:"column:messagetext;not null"`
|
||||||
Isread bool `json:"isread" gorm:"column:isread;default:false"`
|
Isread bool `json:"isread" gorm:"column:isread;default:false"`
|
||||||
Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"`
|
Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (HubMessage) TableName() string {
|
func (HubMessage) TableName() string {
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ type DoormileClient struct {
|
|||||||
Phone string `gorm:"uniqueIndex;size:20;not null" json:"phone"`
|
Phone string `gorm:"uniqueIndex;size:20;not null" json:"phone"`
|
||||||
|
|
||||||
// Location
|
// Location
|
||||||
Address string `gorm:"type:text" json:"address"`
|
Address string `gorm:"type:text" json:"address"`
|
||||||
City string `gorm:"size:100" json:"city"`
|
City string `gorm:"size:100" json:"city"`
|
||||||
State string `gorm:"size:100" json:"state"`
|
State string `gorm:"size:100" json:"state"`
|
||||||
Neighbourhood string `gorm:"size:100" json:"neighbourhood"`
|
Neighbourhood string `gorm:"size:100" json:"neighbourhood"`
|
||||||
Pincode string `gorm:"size:20" json:"pincode"`
|
Pincode string `gorm:"size:20" json:"pincode"`
|
||||||
|
|
||||||
// GPS survey data
|
// GPS survey data
|
||||||
SurveyLat float64 `gorm:"column:surveylat" json:"survey_lat"`
|
SurveyLat float64 `gorm:"column:surveylat" json:"survey_lat"`
|
||||||
@@ -41,10 +41,10 @@ type DoormileClient struct {
|
|||||||
|
|
||||||
// Full-consent-only fields (zeroed for basicOnly)
|
// Full-consent-only fields (zeroed for basicOnly)
|
||||||
ParcelVolume float64 `json:"parcel_volume"`
|
ParcelVolume float64 `json:"parcel_volume"`
|
||||||
ActiveContracts int `json:"active_contracts"`
|
ActiveContracts int `json:"active_contracts"`
|
||||||
LogisticsProvider string `gorm:"size:100" json:"logistics_provider"`
|
LogisticsProvider string `gorm:"size:100" json:"logistics_provider"`
|
||||||
ProviderEfficiency string `gorm:"size:100" json:"provider_efficiency"`
|
ProviderEfficiency string `gorm:"size:100" json:"provider_efficiency"`
|
||||||
Notes string `gorm:"type:text" json:"notes"`
|
Notes string `gorm:"type:text" json:"notes"`
|
||||||
|
|
||||||
// Consent & registration tracking
|
// Consent & registration tracking
|
||||||
DataConsent string `gorm:"size:20;default:'full'" json:"data_consent"`
|
DataConsent string `gorm:"size:20;default:'full'" json:"data_consent"`
|
||||||
@@ -63,8 +63,13 @@ type DoormileAuth struct {
|
|||||||
Email string `gorm:"uniqueIndex;size:255;not null" json:"email"`
|
Email string `gorm:"uniqueIndex;size:255;not null" json:"email"`
|
||||||
PasswordHash string `gorm:"not null" json:"-"`
|
PasswordHash string `gorm:"not null" json:"-"`
|
||||||
Role string `gorm:"default:'user'" json:"role"`
|
Role string `gorm:"default:'user'" json:"role"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
// Tenantid scopes an express-console login to one client, using the same
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
// convention as HubStaffAccount.Tenantid: null = Doormile's own staff, who
|
||||||
|
// see every tenant's data; set = a client's own login, restricted to their
|
||||||
|
// tenant. Without this every console login sees all tenants.
|
||||||
|
Tenantid *int `gorm:"column:tenantid;index" json:"tenantid"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (DoormileAuth) TableName() string {
|
func (DoormileAuth) TableName() string {
|
||||||
|
|||||||
@@ -4,13 +4,19 @@ import "time"
|
|||||||
|
|
||||||
// Tenant represents the pre-existing 'tenants' table
|
// Tenant represents the pre-existing 'tenants' table
|
||||||
type Tenant struct {
|
type Tenant struct {
|
||||||
Tenantid int `json:"tenantid" gorm:"primaryKey;column:tenantid"`
|
Tenantid int `json:"tenantid" gorm:"primaryKey;column:tenantid"`
|
||||||
Tenantname string `json:"tenantname" gorm:"column:tenantname"`
|
Tenantname string `json:"tenantname" gorm:"column:tenantname"`
|
||||||
Primaryemail string `json:"primaryemail" gorm:"column:primaryemail"`
|
Primaryemail string `json:"primaryemail" gorm:"column:primaryemail"`
|
||||||
Primarycontact string `json:"primarycontact" gorm:"column:primarycontact"`
|
Primarycontact string `json:"primarycontact" gorm:"column:primarycontact"`
|
||||||
Status string `json:"status" gorm:"column:status;default:Active"`
|
Status string `json:"status" gorm:"column:status;default:Active"`
|
||||||
Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"`
|
// Requiredeliveryotp decides whether the receiver must read a code back to
|
||||||
Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"`
|
// the rider. Worth the friction for a courier handing over a valuable
|
||||||
|
// parcel; not for a food order, where it just slows every drop down.
|
||||||
|
// Defaults off: turning it on platform-wide would block deliveries for
|
||||||
|
// clients whose customer app has no way to show the code yet.
|
||||||
|
Requiredeliveryotp bool `json:"requiredeliveryotp" gorm:"column:requiredeliveryotp;default:false"`
|
||||||
|
Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"`
|
||||||
|
Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Tenant) TableName() string {
|
func (Tenant) TableName() string {
|
||||||
@@ -19,14 +25,14 @@ func (Tenant) TableName() string {
|
|||||||
|
|
||||||
// Customer represents the pre-existing 'customers' table
|
// Customer represents the pre-existing 'customers' table
|
||||||
type Customer struct {
|
type Customer struct {
|
||||||
Customerid int `json:"customerid" gorm:"primaryKey;column:customerid"`
|
Customerid int `json:"customerid" gorm:"primaryKey;column:customerid"`
|
||||||
Firstname string `json:"firstname" gorm:"column:firstname"`
|
Firstname string `json:"firstname" gorm:"column:firstname"`
|
||||||
Lastname string `json:"lastname" gorm:"column:lastname"`
|
Lastname string `json:"lastname" gorm:"column:lastname"`
|
||||||
Contactno string `json:"contactno" gorm:"column:contactno"`
|
Contactno string `json:"contactno" gorm:"column:contactno"`
|
||||||
Email string `json:"email" gorm:"column:email"`
|
Email string `json:"email" gorm:"column:email"`
|
||||||
Status int `json:"status" gorm:"column:status"`
|
Status int `json:"status" gorm:"column:status"`
|
||||||
Createdat time.Time `json:"createdat" gorm:"column:createdat"`
|
Createdat time.Time `json:"createdat" gorm:"column:createdat"`
|
||||||
Updatedat time.Time `json:"updatedat" gorm:"column:updatedat"`
|
Updatedat time.Time `json:"updatedat" gorm:"column:updatedat"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Customer) TableName() string {
|
func (Customer) TableName() string {
|
||||||
@@ -35,15 +41,15 @@ func (Customer) TableName() string {
|
|||||||
|
|
||||||
// CustomerLocation represents the pre-existing 'customerlocations' table
|
// CustomerLocation represents the pre-existing 'customerlocations' table
|
||||||
type CustomerLocation struct {
|
type CustomerLocation struct {
|
||||||
Locationid int `json:"locationid" gorm:"primaryKey;column:locationid"`
|
Locationid int `json:"locationid" gorm:"primaryKey;column:locationid"`
|
||||||
Customerid int `json:"customerid" gorm:"column:customerid"`
|
Customerid int `json:"customerid" gorm:"column:customerid"`
|
||||||
Address string `json:"address" gorm:"column:address"`
|
Address string `json:"address" gorm:"column:address"`
|
||||||
City string `json:"city" gorm:"column:city"`
|
City string `json:"city" gorm:"column:city"`
|
||||||
State string `json:"state" gorm:"column:state"`
|
State string `json:"state" gorm:"column:state"`
|
||||||
Postcode string `json:"postcode" gorm:"column:postcode"`
|
Postcode string `json:"postcode" gorm:"column:postcode"`
|
||||||
Latitude string `json:"latitude" gorm:"column:latitude"`
|
Latitude string `json:"latitude" gorm:"column:latitude"`
|
||||||
Longitude string `json:"longitude" gorm:"column:longitude"`
|
Longitude string `json:"longitude" gorm:"column:longitude"`
|
||||||
Status int `json:"status" gorm:"column:status"`
|
Status int `json:"status" gorm:"column:status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (CustomerLocation) TableName() string {
|
func (CustomerLocation) TableName() string {
|
||||||
|
|||||||
@@ -84,26 +84,26 @@ func (AppUser) TableName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MilerProfile struct {
|
type MilerProfile struct {
|
||||||
Milerprofileid int `json:"milerprofileid" gorm:"primaryKey;column:milerprofileid"`
|
Milerprofileid int `json:"milerprofileid" gorm:"primaryKey;column:milerprofileid"`
|
||||||
Userid int `json:"userid" gorm:"column:userid;unique;not null"`
|
Userid int `json:"userid" gorm:"column:userid;unique;not null"`
|
||||||
Applocationid int `json:"applocationid" gorm:"column:applocationid;default:1"`
|
Applocationid int `json:"applocationid" gorm:"column:applocationid;default:1"`
|
||||||
Displayname string `json:"displayname" gorm:"column:displayname;not null"`
|
Displayname string `json:"displayname" gorm:"column:displayname;not null"`
|
||||||
Phone string `json:"phone" gorm:"column:phone;not null"`
|
Phone string `json:"phone" gorm:"column:phone;not null"`
|
||||||
Profilephotourl string `json:"profilephotourl" gorm:"column:profilephotourl"`
|
Profilephotourl string `json:"profilephotourl" gorm:"column:profilephotourl"`
|
||||||
Vehicleid *int `json:"vehicleid" gorm:"column:vehicleid"`
|
Vehicleid *int `json:"vehicleid" gorm:"column:vehicleid"`
|
||||||
Hubid *int `json:"hubid" gorm:"column:hubid"`
|
Hubid *int `json:"hubid" gorm:"column:hubid"`
|
||||||
Defaultvehicletype string `json:"defaultvehicletype" gorm:"column:defaultvehicletype"`
|
Defaultvehicletype string `json:"defaultvehicletype" gorm:"column:defaultvehicletype"`
|
||||||
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"`
|
||||||
Availabilitystatus string `json:"availabilitystatus" gorm:"column:availabilitystatus;default:Offline"` // Offline, Available, Assigned, On_Pickup, At_Customer, Picked_Up, On_Delivery, Break, Blocked
|
Availabilitystatus string `json:"availabilitystatus" gorm:"column:availabilitystatus;default:Offline"` // Offline, Available, Assigned, On_Pickup, At_Customer, Picked_Up, On_Delivery, Break, Blocked
|
||||||
Rating float64 `json:"rating" gorm:"column:rating;default:5.00"`
|
Rating float64 `json:"rating" gorm:"column:rating;default:5.00"`
|
||||||
Totalcompletedpickups int `json:"totalcompletedpickups" gorm:"column:totalcompletedpickups;default:0"`
|
Totalcompletedpickups int `json:"totalcompletedpickups" gorm:"column:totalcompletedpickups;default:0"`
|
||||||
Totalcancelledpickups int `json:"totalcancelledpickups" gorm:"column:totalcancelledpickups;default:0"`
|
Totalcancelledpickups int `json:"totalcancelledpickups" gorm:"column:totalcancelledpickups;default:0"`
|
||||||
Devicetoken string `json:"device_token,omitempty" gorm:"column:device_token"`
|
Devicetoken string `json:"device_token,omitempty" gorm:"column:device_token"`
|
||||||
Lastlocationupdatedat *time.Time `json:"lastlocationupdatedat" gorm:"column:lastlocationupdatedat"`
|
Lastlocationupdatedat *time.Time `json:"lastlocationupdatedat" gorm:"column:lastlocationupdatedat"`
|
||||||
Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"`
|
Createdat time.Time `json:"createdat" gorm:"column:createdat;default:CURRENT_TIMESTAMP"`
|
||||||
Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"`
|
Updatedat time.Time `json:"updatedat" gorm:"column:updatedat;default:CURRENT_TIMESTAMP"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (MilerProfile) TableName() string {
|
func (MilerProfile) TableName() string {
|
||||||
@@ -158,6 +158,7 @@ func (AppCustomerLocation) TableName() string {
|
|||||||
type TenantLocation struct {
|
type TenantLocation struct {
|
||||||
Tenantlocationid int `json:"tenantlocationid" gorm:"primaryKey;column:tenantlocationid"`
|
Tenantlocationid int `json:"tenantlocationid" gorm:"primaryKey;column:tenantlocationid"`
|
||||||
Tenantid int `json:"tenantid" gorm:"column:tenantid;not null"`
|
Tenantid int `json:"tenantid" gorm:"column:tenantid;not null"`
|
||||||
|
Locationname string `json:"locationname" gorm:"column:locationname"`
|
||||||
Address string `json:"address" gorm:"column:address;not null"`
|
Address string `json:"address" gorm:"column:address;not null"`
|
||||||
City string `json:"city" gorm:"column:city;not null"`
|
City string `json:"city" gorm:"column:city;not null"`
|
||||||
State string `json:"state" gorm:"column:state;not null"`
|
State string `json:"state" gorm:"column:state;not null"`
|
||||||
|
|||||||
@@ -113,7 +113,15 @@ func RegisterRoutes(app *fiber.App, cfg *config.Config) {
|
|||||||
miler := api.Group("/miler")
|
miler := api.Group("/miler")
|
||||||
miler.Post("/login", authThrottle, controllers.LoginMiler(cfg))
|
miler.Post("/login", authThrottle, controllers.LoginMiler(cfg))
|
||||||
miler.Post("/verify-pin", authThrottle, controllers.VerifyMilerPin(cfg))
|
miler.Post("/verify-pin", authThrottle, controllers.VerifyMilerPin(cfg))
|
||||||
miler.Post("/reset-pin", authThrottle, controllers.ResetMilerPin)
|
// PIN reset is console-operated, NOT self-service: ResetMilerPin overwrites
|
||||||
|
// the PIN given only a phone number, and phone numbers are the miler login
|
||||||
|
// identifier rather than a secret. Left unauthenticated, two calls
|
||||||
|
// (reset-pin then verify-pin) take over any miler account. Ops resets a
|
||||||
|
// rider's PIN on request instead, so this carries admin/manager/executive
|
||||||
|
// auth even though it sits under the /miler prefix.
|
||||||
|
miler.Post("/reset-pin", authThrottle,
|
||||||
|
middlewares.AuthMiddleware(cfg), middlewares.RoleCheckMiddleware(1, 3, 4),
|
||||||
|
controllers.ResetMilerPin)
|
||||||
|
|
||||||
// Authenticated Miler App routes
|
// Authenticated Miler App routes
|
||||||
milerAuth := miler.Use(middlewares.AuthMiddleware(cfg), middlewares.RoleCheckMiddleware(5))
|
milerAuth := miler.Use(middlewares.AuthMiddleware(cfg), middlewares.RoleCheckMiddleware(5))
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"errors"
|
"errors"
|
||||||
|
"math/big"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
@@ -13,6 +15,55 @@ func HashPassword(password string) (string, error) {
|
|||||||
return string(bytes), err
|
return string(bytes), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dbLocation is the timezone the database records wall-clock timestamps in: the
|
||||||
|
// connection DSN sets TimeZone=Asia/Kolkata, so CURRENT_TIMESTAMP column
|
||||||
|
// defaults write IST wall-clock into timestamp-without-timezone columns.
|
||||||
|
var dbLocation = func() *time.Location {
|
||||||
|
if loc, err := time.LoadLocation("Asia/Kolkata"); err == nil {
|
||||||
|
return loc
|
||||||
|
}
|
||||||
|
// A container without tzdata can't load the database; IST observes no DST,
|
||||||
|
// so a fixed +05:30 offset is exact rather than an approximation.
|
||||||
|
return time.FixedZone("IST", 5*3600+30*60)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// DBNow returns the current moment expressed as the wall-clock the database
|
||||||
|
// stores, tagged UTC so the driver sends exactly those digits. Use it for any
|
||||||
|
// comparison against a stored timestamp: comparing the container's UTC clock
|
||||||
|
// against IST-stamped rows is what made date-range reports undercount.
|
||||||
|
//
|
||||||
|
// Deliberately independent of the container's own TZ, so it stays correct
|
||||||
|
// whether or not TZ=Asia/Kolkata is set.
|
||||||
|
func DBNow() time.Time {
|
||||||
|
n := time.Now().In(dbLocation)
|
||||||
|
return time.Date(n.Year(), n.Month(), n.Day(), n.Hour(), n.Minute(), n.Second(), n.Nanosecond(), time.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBToday returns midnight at the start of the current database-local day.
|
||||||
|
func DBToday() time.Time {
|
||||||
|
n := DBNow()
|
||||||
|
return time.Date(n.Year(), n.Month(), n.Day(), 0, 0, 0, 0, time.UTC)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateNumericOTP returns a random n-digit code, leading zeros preserved.
|
||||||
|
// crypto/rand rather than math/rand: this is the only thing standing between a
|
||||||
|
// parcel and someone claiming it was delivered, so a predictable sequence would
|
||||||
|
// defeat the point.
|
||||||
|
func GenerateNumericOTP(n int) string {
|
||||||
|
const digits = "0123456789"
|
||||||
|
out := make([]byte, n)
|
||||||
|
for i := range out {
|
||||||
|
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(digits))))
|
||||||
|
if err != nil {
|
||||||
|
// A failing system RNG must not silently downgrade to a guessable
|
||||||
|
// code; the caller treats an empty OTP as "not issued".
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
out[i] = digits[idx.Int64()]
|
||||||
|
}
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
func CheckPasswordHash(password, hash string) bool {
|
func CheckPasswordHash(password, hash string) bool {
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
return err == nil
|
return err == nil
|
||||||
|
|||||||
Reference in New Issue
Block a user