Initial commit including .env
This commit is contained in:
1948
controllers/adminController.go
Normal file
1948
controllers/adminController.go
Normal file
File diff suppressed because it is too large
Load Diff
76
controllers/bookingCacheController.go
Normal file
76
controllers/bookingCacheController.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"doormile/db"
|
||||
"doormile/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func GetBookingFromCache(c *fiber.Ctx) error {
|
||||
bookingID := c.Params("booking_id")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
data, err := db.Rdb.HGetAll(ctx, fmt.Sprintf("bookings:%s", bookingID)).Result()
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to fetch booking from cache")
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return utils.NotFound(c, "booking not found in cache")
|
||||
}
|
||||
|
||||
return utils.OK(c, data)
|
||||
}
|
||||
|
||||
func GetCustomerBookingsFromCache(c *fiber.Ctx) error {
|
||||
customerID := c.Params("customer_id")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
ids, err := db.Rdb.SMembers(ctx, fmt.Sprintf("bookings:customer:%s", customerID)).Result()
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to fetch customer booking IDs from cache")
|
||||
}
|
||||
|
||||
bookings := make([]map[string]string, 0, len(ids))
|
||||
for _, bid := range ids {
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
bdata, berr := db.Rdb.HGetAll(ctx2, fmt.Sprintf("bookings:%s", bid)).Result()
|
||||
cancel2()
|
||||
if berr == nil && len(bdata) > 0 {
|
||||
bookings = append(bookings, bdata)
|
||||
}
|
||||
}
|
||||
|
||||
return utils.List(c, bookings, int64(len(bookings)))
|
||||
}
|
||||
|
||||
func ListAllBookingsFromCache(c *fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
ids, err := db.Rdb.SMembers(ctx, "bookings:all").Result()
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to fetch booking IDs from cache")
|
||||
}
|
||||
|
||||
bookings := make([]map[string]string, 0, len(ids))
|
||||
for _, bid := range ids {
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
bdata, berr := db.Rdb.HGetAll(ctx2, fmt.Sprintf("bookings:%s", bid)).Result()
|
||||
cancel2()
|
||||
if berr == nil && len(bdata) > 0 {
|
||||
bookings = append(bookings, bdata)
|
||||
}
|
||||
}
|
||||
|
||||
return utils.List(c, bookings, int64(len(bookings)))
|
||||
}
|
||||
386
controllers/clientController.go
Normal file
386
controllers/clientController.go
Normal file
@@ -0,0 +1,386 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"doormile/db"
|
||||
"doormile/dto"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func RegisterClient(c *fiber.Ctx) error {
|
||||
var input dto.CreateClientRequest
|
||||
if err := c.BodyParser(&input); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if input.FirstName == "" || input.Phone == "" {
|
||||
return utils.BadRequest(c, "first_name and phone are required")
|
||||
}
|
||||
|
||||
if input.RegistrationSource == "" {
|
||||
input.RegistrationSource = DetermineSource(string(c.Request().Header.UserAgent()))
|
||||
}
|
||||
if input.DataConsent == "" {
|
||||
input.DataConsent = "full"
|
||||
}
|
||||
if input.Status == "" {
|
||||
input.Status = "newClient"
|
||||
}
|
||||
|
||||
createAuth := input.Email != "" && input.Password != ""
|
||||
|
||||
var hashedPassword string
|
||||
if createAuth {
|
||||
var err error
|
||||
hashedPassword, err = utils.HashPassword(input.Password)
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to process registration")
|
||||
}
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
|
||||
var existingClient models.DoormileClient
|
||||
if tx.Where("phone = ?", input.Phone).First(&existingClient).Error == nil {
|
||||
tx.Rollback()
|
||||
return utils.Conflict(c, "a client with this phone number already exists")
|
||||
}
|
||||
|
||||
if createAuth {
|
||||
var existingAuth models.DoormileAuth
|
||||
if tx.Where("email = ?", input.Email).First(&existingAuth).Error == nil {
|
||||
tx.Rollback()
|
||||
return utils.Conflict(c, "this email address is already registered")
|
||||
}
|
||||
}
|
||||
|
||||
client := models.DoormileClient{
|
||||
FirstName: input.FirstName,
|
||||
LastName: input.LastName,
|
||||
Phone: input.Phone,
|
||||
Address: input.Address,
|
||||
City: input.City,
|
||||
State: input.State,
|
||||
Neighbourhood: input.Neighbourhood,
|
||||
Pincode: input.Pincode,
|
||||
SurveyLat: input.SurveyLat,
|
||||
SurveyLong: input.SurveyLong,
|
||||
SurveyAddress: input.SurveyAddress,
|
||||
SurveyZone: input.SurveyZone,
|
||||
SurveyPincode: input.SurveyPincode,
|
||||
BusinessType: input.BusinessType,
|
||||
Status: input.Status,
|
||||
ShippingFrequency: input.ShippingFrequency,
|
||||
LogisticsSegment: input.LogisticsSegment,
|
||||
TransitFrom: input.TransitFrom,
|
||||
TransitTo: input.TransitTo,
|
||||
DataConsent: input.DataConsent,
|
||||
RegistrationSource: input.RegistrationSource,
|
||||
RegisteredByID: input.RegisteredByID,
|
||||
}
|
||||
|
||||
if input.DataConsent == "full" {
|
||||
client.ParcelVolume = input.ParcelVolume
|
||||
client.ActiveContracts = input.ActiveContracts
|
||||
client.LogisticsProvider = input.LogisticsProvider
|
||||
client.ProviderEfficiency = input.ProviderEfficiency
|
||||
client.Notes = input.Notes
|
||||
} else {
|
||||
client.ParcelVolume = 0
|
||||
client.ActiveContracts = 0
|
||||
client.LogisticsProvider = "Not disclosed"
|
||||
client.ProviderEfficiency = ""
|
||||
client.Notes = ""
|
||||
}
|
||||
|
||||
if err := tx.Create(&client).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to create client")
|
||||
}
|
||||
|
||||
email, role := "", ""
|
||||
if createAuth {
|
||||
auth := models.DoormileAuth{
|
||||
ClientID: &client.ID,
|
||||
Email: input.Email,
|
||||
PasswordHash: hashedPassword,
|
||||
Role: "user",
|
||||
}
|
||||
if err := tx.Create(&auth).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to create auth credentials")
|
||||
}
|
||||
email = auth.Email
|
||||
role = auth.Role
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
return utils.Created(c, buildClientResponse(client, email, role))
|
||||
}
|
||||
|
||||
func GetClients(c *fiber.Ctx) error {
|
||||
var clients []models.DoormileClient
|
||||
if err := db.DB.Find(&clients).Error; err != nil {
|
||||
return utils.Internal(c, "failed to fetch clients")
|
||||
}
|
||||
|
||||
// Bulk load auth records once and map by client_id for O(1) lookup
|
||||
var auths []models.DoormileAuth
|
||||
db.DB.Find(&auths)
|
||||
authByClientID := make(map[uint64]models.DoormileAuth, len(auths))
|
||||
for _, a := range auths {
|
||||
if a.ClientID != nil {
|
||||
authByClientID[*a.ClientID] = a
|
||||
}
|
||||
}
|
||||
|
||||
responses := make([]dto.ClientResponse, 0, len(clients))
|
||||
for _, client := range clients {
|
||||
auth := authByClientID[client.ID]
|
||||
responses = append(responses, buildClientResponse(client, auth.Email, auth.Role))
|
||||
}
|
||||
|
||||
return utils.List(c, responses, int64(len(responses)))
|
||||
}
|
||||
|
||||
func GetClientDetails(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return utils.BadRequest(c, "invalid client ID")
|
||||
}
|
||||
|
||||
var client models.DoormileClient
|
||||
if err := db.DB.First(&client, id).Error; err != nil {
|
||||
return utils.NotFound(c, "client not found")
|
||||
}
|
||||
|
||||
var auth models.DoormileAuth
|
||||
email, role := "", ""
|
||||
if db.DB.Where("client_id = ?", id).First(&auth).Error == nil {
|
||||
email = auth.Email
|
||||
role = auth.Role
|
||||
}
|
||||
|
||||
return utils.OK(c, buildClientResponse(client, email, role))
|
||||
}
|
||||
|
||||
func UpdateClient(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return utils.BadRequest(c, "invalid client ID")
|
||||
}
|
||||
|
||||
var client models.DoormileClient
|
||||
if err := db.DB.First(&client, id).Error; err != nil {
|
||||
return utils.NotFound(c, "client not found")
|
||||
}
|
||||
|
||||
var input dto.CreateClientRequest
|
||||
if err := c.BodyParser(&input); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if input.FirstName != "" {
|
||||
client.FirstName = input.FirstName
|
||||
}
|
||||
if input.LastName != "" {
|
||||
client.LastName = input.LastName
|
||||
}
|
||||
if input.Phone != "" {
|
||||
client.Phone = input.Phone
|
||||
}
|
||||
if input.Address != "" {
|
||||
client.Address = input.Address
|
||||
}
|
||||
if input.City != "" {
|
||||
client.City = input.City
|
||||
}
|
||||
if input.State != "" {
|
||||
client.State = input.State
|
||||
}
|
||||
if input.Neighbourhood != "" {
|
||||
client.Neighbourhood = input.Neighbourhood
|
||||
}
|
||||
if input.Pincode != "" {
|
||||
client.Pincode = input.Pincode
|
||||
}
|
||||
if input.SurveyLat != 0 {
|
||||
client.SurveyLat = input.SurveyLat
|
||||
}
|
||||
if input.SurveyLong != 0 {
|
||||
client.SurveyLong = input.SurveyLong
|
||||
}
|
||||
if input.SurveyAddress != "" {
|
||||
client.SurveyAddress = input.SurveyAddress
|
||||
}
|
||||
if input.SurveyZone != "" {
|
||||
client.SurveyZone = input.SurveyZone
|
||||
}
|
||||
if input.SurveyPincode != "" {
|
||||
client.SurveyPincode = input.SurveyPincode
|
||||
}
|
||||
if input.BusinessType != "" {
|
||||
client.BusinessType = input.BusinessType
|
||||
}
|
||||
if input.Status != "" {
|
||||
client.Status = input.Status
|
||||
}
|
||||
if input.ShippingFrequency != "" {
|
||||
client.ShippingFrequency = input.ShippingFrequency
|
||||
}
|
||||
if input.LogisticsSegment != "" {
|
||||
client.LogisticsSegment = input.LogisticsSegment
|
||||
}
|
||||
if input.TransitFrom != "" {
|
||||
client.TransitFrom = input.TransitFrom
|
||||
}
|
||||
if input.TransitTo != "" {
|
||||
client.TransitTo = input.TransitTo
|
||||
}
|
||||
if input.RegistrationSource != "" {
|
||||
client.RegistrationSource = input.RegistrationSource
|
||||
}
|
||||
if input.RegisteredByID != 0 {
|
||||
client.RegisteredByID = input.RegisteredByID
|
||||
}
|
||||
client.Notes = input.Notes
|
||||
|
||||
if input.DataConsent != "" {
|
||||
client.DataConsent = input.DataConsent
|
||||
}
|
||||
|
||||
if client.DataConsent == "full" {
|
||||
client.ParcelVolume = input.ParcelVolume
|
||||
client.ActiveContracts = input.ActiveContracts
|
||||
client.LogisticsProvider = input.LogisticsProvider
|
||||
client.ProviderEfficiency = input.ProviderEfficiency
|
||||
} else {
|
||||
client.ParcelVolume = 0
|
||||
client.ActiveContracts = 0
|
||||
client.LogisticsProvider = "Not disclosed"
|
||||
client.ProviderEfficiency = ""
|
||||
client.Notes = ""
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
|
||||
if err := tx.Save(&client).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to update client")
|
||||
}
|
||||
|
||||
var auth models.DoormileAuth
|
||||
email, role := "", ""
|
||||
if db.DB.Where("client_id = ?", id).First(&auth).Error == nil {
|
||||
email = auth.Email
|
||||
role = auth.Role
|
||||
}
|
||||
|
||||
authUpdated := false
|
||||
if input.Email != "" && input.Email != auth.Email {
|
||||
auth.Email = input.Email
|
||||
email = input.Email
|
||||
authUpdated = true
|
||||
}
|
||||
if input.Password != "" {
|
||||
hashed, err := utils.HashPassword(input.Password)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to process password update")
|
||||
}
|
||||
auth.PasswordHash = hashed
|
||||
authUpdated = true
|
||||
}
|
||||
if authUpdated && auth.ID != 0 {
|
||||
if err := tx.Save(&auth).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to update credentials")
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
return utils.OK(c, buildClientResponse(client, email, role))
|
||||
}
|
||||
|
||||
func DeleteClient(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return utils.BadRequest(c, "invalid client ID")
|
||||
}
|
||||
|
||||
var client models.DoormileClient
|
||||
if err := db.DB.First(&client, id).Error; err != nil {
|
||||
return utils.NotFound(c, "client not found")
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
|
||||
if err := tx.Where("client_id = ?", id).Delete(&models.DoormileAuth{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to delete client credentials")
|
||||
}
|
||||
|
||||
if err := tx.Delete(&client).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to delete client")
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
return utils.Message(c, "client deleted successfully")
|
||||
}
|
||||
|
||||
func buildClientResponse(client models.DoormileClient, email, role string) dto.ClientResponse {
|
||||
return dto.ClientResponse{
|
||||
ID: client.ID,
|
||||
CreatedAt: client.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||
LastUpdated: client.UpdatedAt.Format("2006-01-02"),
|
||||
FirstName: client.FirstName,
|
||||
LastName: client.LastName,
|
||||
Email: email,
|
||||
Phone: client.Phone,
|
||||
Address: client.Address,
|
||||
City: client.City,
|
||||
State: client.State,
|
||||
Neighbourhood: client.Neighbourhood,
|
||||
Pincode: client.Pincode,
|
||||
SurveyLat: client.SurveyLat,
|
||||
SurveyLong: client.SurveyLong,
|
||||
SurveyAddress: client.SurveyAddress,
|
||||
SurveyZone: client.SurveyZone,
|
||||
SurveyPincode: client.SurveyPincode,
|
||||
BusinessType: client.BusinessType,
|
||||
Status: client.Status,
|
||||
ShippingFrequency: client.ShippingFrequency,
|
||||
LogisticsSegment: client.LogisticsSegment,
|
||||
TransitFrom: client.TransitFrom,
|
||||
TransitTo: client.TransitTo,
|
||||
ParcelVolume: client.ParcelVolume,
|
||||
ActiveContracts: client.ActiveContracts,
|
||||
LogisticsProvider: client.LogisticsProvider,
|
||||
ProviderEfficiency: client.ProviderEfficiency,
|
||||
Notes: client.Notes,
|
||||
DataConsent: client.DataConsent,
|
||||
RegistrationSource: client.RegistrationSource,
|
||||
RegisteredByID: client.RegisteredByID,
|
||||
Role: role,
|
||||
}
|
||||
}
|
||||
|
||||
func DetermineSource(userAgent string) string {
|
||||
ua := strings.ToLower(userAgent)
|
||||
if strings.Contains(ua, "dart") || strings.Contains(ua, "flutter") || strings.Contains(ua, "doormile") {
|
||||
return "mobile"
|
||||
}
|
||||
if strings.Contains(ua, "mozilla") || strings.Contains(ua, "chrome") || strings.Contains(ua, "safari") {
|
||||
return "web"
|
||||
}
|
||||
return "api_tool"
|
||||
}
|
||||
644
controllers/customerController.go
Normal file
644
controllers/customerController.go
Normal file
@@ -0,0 +1,644 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"doormile/config"
|
||||
"doormile/constants"
|
||||
"doormile/db"
|
||||
"doormile/dto"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func generateBookingNo() string {
|
||||
b := make([]byte, 4)
|
||||
rand.Read(b)
|
||||
return fmt.Sprintf("DM-BK-%X-%d", b, time.Now().Unix()%100000)
|
||||
}
|
||||
|
||||
func calculateDistance(lat1, lon1, lat2, lon2 float64) float64 {
|
||||
const R = 6371.0
|
||||
dLat := (lat2 - lat1) * math.Pi / 180.0
|
||||
dLon := (lon2 - lon1) * math.Pi / 180.0
|
||||
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
||||
math.Cos(lat1*math.Pi/180.0)*math.Cos(lat2*math.Pi/180.0)*
|
||||
math.Sin(dLon/2)*math.Sin(dLon/2)
|
||||
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
||||
return R * c
|
||||
}
|
||||
|
||||
func calculateVolumetricWeight(length, width, height float64) float64 {
|
||||
return (length * width * height) / 5000.0
|
||||
}
|
||||
|
||||
func RegisterCustomer(cfg *config.Config) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
req := new(dto.CustomerRegisterRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Phone == "" || req.Firstname == "" || req.Pin == "" {
|
||||
return utils.BadRequest(c, "phone, firstname, and pin are required")
|
||||
}
|
||||
|
||||
pinHash, err := utils.HashPassword(req.Pin)
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to process registration")
|
||||
}
|
||||
|
||||
configID := req.Configid
|
||||
if configID == 0 {
|
||||
configID = 1001
|
||||
}
|
||||
|
||||
var existing models.AppCustomer
|
||||
if err := db.DB.Where("phone = ? AND configid = ?", req.Phone, configID).First(&existing).Error; err == nil {
|
||||
return utils.Conflict(c, "a customer with this phone number already exists")
|
||||
}
|
||||
|
||||
customer := models.AppCustomer{
|
||||
Firstname: req.Firstname,
|
||||
Lastname: req.Lastname,
|
||||
Phone: req.Phone,
|
||||
Email: req.Email,
|
||||
Loginpinhash: pinHash,
|
||||
Status: "Active",
|
||||
Configid: configID,
|
||||
}
|
||||
|
||||
if err := db.DB.Create(&customer).Error; err != nil {
|
||||
return utils.Internal(c, "failed to register customer")
|
||||
}
|
||||
|
||||
token, err := utils.GenerateToken(customer.Appcustomerid, customer.Phone, 9, 0, customer.Configid, cfg.JWTSecret)
|
||||
if err != nil {
|
||||
return utils.Internal(c, "registration successful but failed to generate token")
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
||||
"success": true,
|
||||
"token": token,
|
||||
"user": customer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func LoginCustomer(c *fiber.Ctx) error {
|
||||
req := new(dto.CustomerLoginRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Phone == "" {
|
||||
return utils.BadRequest(c, "phone is required")
|
||||
}
|
||||
|
||||
configID := req.Configid
|
||||
if configID == 0 {
|
||||
configID = 1001
|
||||
}
|
||||
|
||||
var customer models.AppCustomer
|
||||
if err := db.DB.Where("phone = ? AND configid = ?", req.Phone, configID).First(&customer).Error; err != nil {
|
||||
return utils.NotFound(c, "no account found for this phone number")
|
||||
}
|
||||
|
||||
if customer.Status == "Blocked" {
|
||||
return utils.Forbidden(c, "this account has been blocked")
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"message": "PIN verification required",
|
||||
"phone": req.Phone,
|
||||
})
|
||||
}
|
||||
|
||||
func VerifyCustomerPin(cfg *config.Config) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
req := new(dto.CustomerPinVerifyRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Phone == "" || req.Pin == "" {
|
||||
return utils.BadRequest(c, "phone and pin are required")
|
||||
}
|
||||
|
||||
configID := req.Configid
|
||||
if configID == 0 {
|
||||
configID = 1001
|
||||
}
|
||||
|
||||
var customer models.AppCustomer
|
||||
if err := db.DB.Where("phone = ? AND configid = ?", req.Phone, configID).First(&customer).Error; err != nil {
|
||||
return utils.NotFound(c, "customer not found")
|
||||
}
|
||||
|
||||
if !utils.CheckPasswordHash(req.Pin, customer.Loginpinhash) {
|
||||
return utils.Unauthorized(c, "incorrect PIN")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
customer.Lastloginat = &now
|
||||
db.DB.Save(&customer)
|
||||
|
||||
token, err := utils.GenerateToken(customer.Appcustomerid, customer.Phone, 9, 0, customer.Configid, cfg.JWTSecret)
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to generate token")
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"token": token,
|
||||
"user": customer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ResetCustomerPin(c *fiber.Ctx) error {
|
||||
req := new(dto.CustomerResetPinRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Phone == "" || req.NewPin == "" {
|
||||
return utils.BadRequest(c, "phone and new_pin are required")
|
||||
}
|
||||
|
||||
configID := req.Configid
|
||||
if configID == 0 {
|
||||
configID = 1001
|
||||
}
|
||||
|
||||
var customer models.AppCustomer
|
||||
if err := db.DB.Where("phone = ? AND configid = ?", req.Phone, configID).First(&customer).Error; err != nil {
|
||||
return utils.NotFound(c, "customer not found")
|
||||
}
|
||||
|
||||
pinHash, err := utils.HashPassword(req.NewPin)
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to process PIN reset")
|
||||
}
|
||||
|
||||
customer.Loginpinhash = pinHash
|
||||
if err := db.DB.Save(&customer).Error; err != nil {
|
||||
return utils.Internal(c, "failed to reset PIN")
|
||||
}
|
||||
|
||||
return utils.Message(c, "PIN reset successfully")
|
||||
}
|
||||
|
||||
func GetCustomerProfile(c *fiber.Ctx) error {
|
||||
customerID := c.Locals("userid").(int)
|
||||
|
||||
var customer models.AppCustomer
|
||||
if err := db.DB.First(&customer, customerID).Error; err != nil {
|
||||
return utils.NotFound(c, "profile not found")
|
||||
}
|
||||
|
||||
return utils.OK(c, customer)
|
||||
}
|
||||
|
||||
func UpdateCustomerProfile(c *fiber.Ctx) error {
|
||||
customerID := c.Locals("userid").(int)
|
||||
|
||||
var customer models.AppCustomer
|
||||
if err := db.DB.First(&customer, customerID).Error; err != nil {
|
||||
return utils.NotFound(c, "profile not found")
|
||||
}
|
||||
|
||||
type ProfileUpdate struct {
|
||||
Firstname string `json:"firstname"`
|
||||
Lastname string `json:"lastname"`
|
||||
Email string `json:"email"`
|
||||
Defaultlatitude float64 `json:"defaultlatitude"`
|
||||
Defaultlongitude float64 `json:"defaultlongitude"`
|
||||
Defaultpincode string `json:"defaultpincode"`
|
||||
}
|
||||
|
||||
req := new(ProfileUpdate)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Firstname != "" {
|
||||
customer.Firstname = req.Firstname
|
||||
}
|
||||
customer.Lastname = req.Lastname
|
||||
customer.Email = req.Email
|
||||
if req.Defaultlatitude != 0 {
|
||||
customer.Defaultlatitude = req.Defaultlatitude
|
||||
}
|
||||
if req.Defaultlongitude != 0 {
|
||||
customer.Defaultlongitude = req.Defaultlongitude
|
||||
}
|
||||
if req.Defaultpincode != "" {
|
||||
customer.Defaultpincode = req.Defaultpincode
|
||||
}
|
||||
|
||||
if err := db.DB.Save(&customer).Error; err != nil {
|
||||
return utils.Internal(c, "failed to update profile")
|
||||
}
|
||||
|
||||
return utils.OK(c, customer)
|
||||
}
|
||||
|
||||
func GetCustomerLocations(c *fiber.Ctx) error {
|
||||
customerID := c.Locals("userid").(int)
|
||||
|
||||
var locations []models.AppCustomerLocation
|
||||
if err := db.DB.Where("appcustomerid = ? AND status = ?", customerID, "Active").Find(&locations).Error; err != nil {
|
||||
return utils.Internal(c, "failed to fetch locations")
|
||||
}
|
||||
|
||||
return utils.List(c, locations, int64(len(locations)))
|
||||
}
|
||||
|
||||
func CreateCustomerLocation(c *fiber.Ctx) error {
|
||||
customerID := c.Locals("userid").(int)
|
||||
|
||||
var count int64
|
||||
db.DB.Model(&models.AppCustomerLocation{}).Where("appcustomerid = ? AND status = ?", customerID, "Active").Count(&count)
|
||||
if count >= 10 {
|
||||
return utils.BadRequest(c, "maximum of 10 saved locations allowed")
|
||||
}
|
||||
|
||||
req := new(dto.LocationCreateRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Address == "" || req.Pincode == "" || req.Latitude == 0 || req.Longitude == 0 {
|
||||
return utils.BadRequest(c, "address, pincode, latitude, and longitude are required")
|
||||
}
|
||||
|
||||
if req.Isdefault {
|
||||
db.DB.Model(&models.AppCustomerLocation{}).Where("appcustomerid = ?", customerID).Update("isdefault", false)
|
||||
}
|
||||
|
||||
location := models.AppCustomerLocation{
|
||||
Appcustomerid: customerID,
|
||||
Label: req.Label,
|
||||
Receivername: req.Receivername,
|
||||
Receiverphone: req.Receiverphone,
|
||||
Address: req.Address,
|
||||
Landmark: req.Landmark,
|
||||
City: req.City,
|
||||
State: req.State,
|
||||
Pincode: req.Pincode,
|
||||
Latitude: req.Latitude,
|
||||
Longitude: req.Longitude,
|
||||
Isdefault: req.Isdefault,
|
||||
Status: "Active",
|
||||
}
|
||||
|
||||
if err := db.DB.Create(&location).Error; err != nil {
|
||||
return utils.Internal(c, "failed to save location")
|
||||
}
|
||||
|
||||
return utils.Created(c, location)
|
||||
}
|
||||
|
||||
func UpdateCustomerLocation(c *fiber.Ctx) error {
|
||||
customerID := c.Locals("userid").(int)
|
||||
locationID, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid location ID")
|
||||
}
|
||||
|
||||
var location models.AppCustomerLocation
|
||||
if err := db.DB.Where("appcustomerlocationid = ? AND appcustomerid = ?", locationID, customerID).First(&location).Error; err != nil {
|
||||
return utils.NotFound(c, "location not found")
|
||||
}
|
||||
|
||||
req := new(dto.LocationCreateRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Label != "" {
|
||||
location.Label = req.Label
|
||||
}
|
||||
location.Receivername = req.Receivername
|
||||
location.Receiverphone = req.Receiverphone
|
||||
if req.Address != "" {
|
||||
location.Address = req.Address
|
||||
}
|
||||
location.Landmark = req.Landmark
|
||||
location.City = req.City
|
||||
location.State = req.State
|
||||
if req.Pincode != "" {
|
||||
location.Pincode = req.Pincode
|
||||
}
|
||||
if req.Latitude != 0 {
|
||||
location.Latitude = req.Latitude
|
||||
}
|
||||
if req.Longitude != 0 {
|
||||
location.Longitude = req.Longitude
|
||||
}
|
||||
location.Isdefault = req.Isdefault
|
||||
|
||||
if req.Isdefault {
|
||||
db.DB.Model(&models.AppCustomerLocation{}).Where("appcustomerid = ?", customerID).Update("isdefault", false)
|
||||
}
|
||||
|
||||
if err := db.DB.Save(&location).Error; err != nil {
|
||||
return utils.Internal(c, "failed to update location")
|
||||
}
|
||||
|
||||
return utils.OK(c, location)
|
||||
}
|
||||
|
||||
func DeleteCustomerLocation(c *fiber.Ctx) error {
|
||||
customerID := c.Locals("userid").(int)
|
||||
locationID, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid location ID")
|
||||
}
|
||||
|
||||
var location models.AppCustomerLocation
|
||||
if err := db.DB.Where("appcustomerlocationid = ? AND appcustomerid = ?", locationID, customerID).First(&location).Error; err != nil {
|
||||
return utils.NotFound(c, "location not found")
|
||||
}
|
||||
|
||||
location.Status = "InActive"
|
||||
db.DB.Save(&location)
|
||||
|
||||
return utils.Message(c, "location deleted successfully")
|
||||
}
|
||||
|
||||
func CreateCustomerBooking(c *fiber.Ctx) error {
|
||||
customerID := c.Locals("userid").(int)
|
||||
|
||||
req := new(dto.PickupBookingRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Pickupaddress == "" || req.Pickuppincode == "" {
|
||||
return utils.BadRequest(c, "pickup address and pincode are required")
|
||||
}
|
||||
|
||||
if len(req.Parcels) == 0 {
|
||||
return utils.BadRequest(c, "at least one parcel is required")
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
|
||||
booking := models.PickupBooking{
|
||||
Bookingno: generateBookingNo(),
|
||||
Appcustomerid: customerID,
|
||||
Pickuplocationid: req.Pickuplocationid,
|
||||
Pickupaddress: req.Pickupaddress,
|
||||
Pickuppincode: req.Pickuppincode,
|
||||
Pickuplatitude: req.Pickuplatitude,
|
||||
Pickuplongitude: req.Pickuplongitude,
|
||||
Deliveryaddress: req.Deliveryaddress,
|
||||
Deliverypincode: req.Deliverypincode,
|
||||
Deliverylatitude: req.Deliverylatitude,
|
||||
Deliverylongitude: req.Deliverylongitude,
|
||||
Bookingsource: "Customer_App",
|
||||
Status: constants.BookingPendingPickup,
|
||||
Preferredpickupfrom: req.Preferredpickupfrom,
|
||||
Preferredpickupto: req.Preferredpickupto,
|
||||
}
|
||||
|
||||
if err := tx.Create(&booking).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to create booking")
|
||||
}
|
||||
|
||||
var totalWeight float64
|
||||
var totalVolume float64
|
||||
var requiresLargeVehicle bool
|
||||
for _, p := range req.Parcels {
|
||||
volumetric := calculateVolumetricWeight(p.Length, p.Width, p.Height)
|
||||
totalWeight += math.Max(p.Weight, volumetric)
|
||||
totalVolume += p.Length * p.Width * p.Height
|
||||
|
||||
parcel := models.BookingParcel{
|
||||
Bookingid: booking.Bookingid,
|
||||
Itemcategory: p.Itemcategory,
|
||||
Itemdescription: p.Itemdescription,
|
||||
Declaredvalue: p.Declaredvalue,
|
||||
Weight: p.Weight,
|
||||
Length: p.Length,
|
||||
Width: p.Width,
|
||||
Height: p.Height,
|
||||
Isfragile: p.Isfragile,
|
||||
Needsinsurance: p.Needsinsurance,
|
||||
Requireslargevehicle: p.Requireslargevehicle,
|
||||
}
|
||||
|
||||
if p.Requireslargevehicle {
|
||||
requiresLargeVehicle = true
|
||||
}
|
||||
|
||||
if p.Needsinsurance {
|
||||
parcel.Insuranceamount = p.Declaredvalue * 0.01
|
||||
}
|
||||
|
||||
if err := tx.Create(&parcel).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to save parcel details")
|
||||
}
|
||||
}
|
||||
|
||||
var distance float64
|
||||
if booking.Deliverylatitude != 0 && booking.Deliverylongitude != 0 {
|
||||
distance = calculateDistance(booking.Pickuplatitude, booking.Pickuplongitude, booking.Deliverylatitude, booking.Deliverylongitude)
|
||||
}
|
||||
|
||||
var pricing models.Pricing
|
||||
pricingErr := tx.Where("status = ? AND ? BETWEEN effectivefrom AND effectiveto", "Active", time.Now()).Order("priority DESC").First(&pricing).Error
|
||||
|
||||
var estimatedPrice float64
|
||||
var pricingID *int
|
||||
if pricingErr == nil {
|
||||
pricingID = &pricing.Pricingid
|
||||
kmExtra := math.Max(0, distance-pricing.Basedistance)
|
||||
kgExtra := math.Max(0, totalWeight-pricing.Baseweight)
|
||||
estimatedPrice = pricing.Baseprice + (kmExtra * pricing.Priceperkm) + (kgExtra * pricing.Priceperkg) + pricing.Handlingcharges
|
||||
} else {
|
||||
estimatedPrice = 50.0 + (distance * 5.0) + (totalWeight * 10.0)
|
||||
}
|
||||
|
||||
serviceType := req.ServiceOption
|
||||
if serviceType == "" {
|
||||
serviceType = "Normal"
|
||||
}
|
||||
|
||||
if serviceType == "Fast" {
|
||||
estimatedPrice *= 1.25
|
||||
} else if serviceType == "Superfast" {
|
||||
estimatedPrice *= 1.5
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
estDelivery := now.Add(24 * time.Hour)
|
||||
slaDue := now.Add(36 * time.Hour)
|
||||
if serviceType == "Fast" {
|
||||
estDelivery = now.Add(12 * time.Hour)
|
||||
slaDue = now.Add(18 * time.Hour)
|
||||
} else if serviceType == "Superfast" {
|
||||
estDelivery = now.Add(6 * time.Hour)
|
||||
slaDue = now.Add(9 * time.Hour)
|
||||
}
|
||||
|
||||
srvOption := models.BookingServiceOption{
|
||||
Bookingid: booking.Bookingid,
|
||||
Servicetype: serviceType,
|
||||
Estimatedprice: estimatedPrice,
|
||||
Estimateddeliveryat: &estDelivery,
|
||||
Sladueat: &slaDue,
|
||||
Pricingid: pricingID,
|
||||
}
|
||||
|
||||
if err := tx.Create(&srvOption).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to save service option")
|
||||
}
|
||||
|
||||
if requiresLargeVehicle || totalVolume > 0 && totalWeight > 20.0 {
|
||||
reqVeh := models.BookingVehicleRequirement{
|
||||
Bookingid: booking.Bookingid,
|
||||
Requiredvehicletype: "truck",
|
||||
Reason: "Oversized package / heavy weight",
|
||||
Status: "Required",
|
||||
}
|
||||
tx.Create(&reqVeh)
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
if db.Js != nil {
|
||||
payload := map[string]interface{}{
|
||||
"booking_id": booking.Bookingid,
|
||||
"booking_no": booking.Bookingno,
|
||||
"customer_id": booking.Appcustomerid,
|
||||
"pickup_address": booking.Pickupaddress,
|
||||
"pickup_pincode": booking.Pickuppincode,
|
||||
"delivery_address": booking.Deliveryaddress,
|
||||
"delivery_pincode": booking.Deliverypincode,
|
||||
"status": constants.BookingPendingPickup,
|
||||
"created_at": time.Now().UnixMilli(),
|
||||
}
|
||||
if data, err := json.Marshal(payload); err == nil {
|
||||
if _, err := db.Js.Publish("api.v1.bookings.create", data); err != nil {
|
||||
utils.Warn("Failed to publish booking.create to NATS", "booking_id", booking.Bookingid, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.DB.Preload("Parcels").Preload("ServiceOptions").First(&booking, booking.Bookingid)
|
||||
|
||||
return utils.Created(c, booking)
|
||||
}
|
||||
|
||||
func GetCustomerBookings(c *fiber.Ctx) error {
|
||||
customerID := c.Locals("userid").(int)
|
||||
|
||||
var bookings []models.PickupBooking
|
||||
if err := db.DB.Preload("Parcels").Preload("ServiceOptions").Where("appcustomerid = ?", customerID).Order("createdat DESC").Find(&bookings).Error; err != nil {
|
||||
return utils.Internal(c, "failed to fetch bookings")
|
||||
}
|
||||
|
||||
return utils.List(c, bookings, int64(len(bookings)))
|
||||
}
|
||||
|
||||
func GetCustomerBookingDetails(c *fiber.Ctx) error {
|
||||
customerID := c.Locals("userid").(int)
|
||||
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid booking ID")
|
||||
}
|
||||
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.Preload("Parcels").Preload("ServiceOptions").Preload("Payments").Where("bookingid = ? AND appcustomerid = ?", bookingID, customerID).First(&booking).Error; err != nil {
|
||||
return utils.NotFound(c, "booking not found")
|
||||
}
|
||||
|
||||
return utils.OK(c, booking)
|
||||
}
|
||||
|
||||
func CancelCustomerBooking(c *fiber.Ctx) error {
|
||||
customerID := c.Locals("userid").(int)
|
||||
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid booking ID")
|
||||
}
|
||||
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.Where("bookingid = ? AND appcustomerid = ?", bookingID, customerID).First(&booking).Error; err != nil {
|
||||
return utils.NotFound(c, "booking not found")
|
||||
}
|
||||
|
||||
if booking.Status == constants.BookingPickedUp || booking.Status == constants.BookingConvertedConsignment {
|
||||
return utils.BadRequest(c, "booking cannot be cancelled after the package has been picked up")
|
||||
}
|
||||
|
||||
booking.Status = constants.BookingCancelled
|
||||
booking.Updatedat = time.Now()
|
||||
db.DB.Save(&booking)
|
||||
|
||||
if db.Js != nil {
|
||||
payload := map[string]interface{}{
|
||||
"booking_id": booking.Bookingid,
|
||||
"booking_no": booking.Bookingno,
|
||||
"customer_id": booking.Appcustomerid,
|
||||
"status": "Cancelled",
|
||||
"cancelled_at": time.Now().UnixMilli(),
|
||||
}
|
||||
if data, err := json.Marshal(payload); err == nil {
|
||||
if _, err := db.Js.Publish("api.v1.bookings.cancel", data); err != nil {
|
||||
utils.Warn("Failed to publish booking.cancel to NATS", "booking_id", booking.Bookingid, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return utils.OK(c, booking)
|
||||
}
|
||||
|
||||
func GetCustomerBookingQuote(c *fiber.Ctx) error {
|
||||
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid booking ID")
|
||||
}
|
||||
|
||||
var serviceOpt models.BookingServiceOption
|
||||
if err := db.DB.Where("bookingid = ?", bookingID).Order("createdat DESC").First(&serviceOpt).Error; err != nil {
|
||||
return utils.NotFound(c, "price quote not found for this booking")
|
||||
}
|
||||
|
||||
return utils.OK(c, serviceOpt)
|
||||
}
|
||||
|
||||
func TrackConsignment(c *fiber.Ctx) error {
|
||||
trackingNo := c.Params("trackingno")
|
||||
if trackingNo == "" {
|
||||
return utils.BadRequest(c, "tracking number is required")
|
||||
}
|
||||
|
||||
var consignment models.Consignment
|
||||
if err := db.DB.Where("trackingno = ?", trackingNo).First(&consignment).Error; err != nil {
|
||||
return utils.NotFound(c, "no shipment found for this tracking number")
|
||||
}
|
||||
|
||||
var history []models.ConsignmentHistory
|
||||
db.DB.Where("consignmentid = ?", consignment.Consignmentid).Order("createdat DESC").Find(&history)
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"consignment": consignment,
|
||||
"history": history,
|
||||
})
|
||||
}
|
||||
472
controllers/doormilePricingController.go
Normal file
472
controllers/doormilePricingController.go
Normal file
@@ -0,0 +1,472 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"doormile/db"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// Valid enum values
|
||||
var validZones = map[string]bool{
|
||||
"Local": true,
|
||||
"Interstate": true,
|
||||
"OtherState": true,
|
||||
}
|
||||
|
||||
var validCategories = map[string]bool{
|
||||
"General": true,
|
||||
"Documents": true,
|
||||
"Electronics": true,
|
||||
"Clothing": true,
|
||||
"Fragile": true,
|
||||
"Medical": true,
|
||||
"Automotive": true,
|
||||
"Food": true,
|
||||
}
|
||||
|
||||
var validServiceTypes = map[string]bool{
|
||||
"Normal": true,
|
||||
"Express": true,
|
||||
}
|
||||
|
||||
var categoryLabels = map[string]string{
|
||||
"General": "General Goods",
|
||||
"Documents": "Books & Documents",
|
||||
"Electronics": "Electronics & Gadgets",
|
||||
"Clothing": "Clothing & Textiles",
|
||||
"Fragile": "Fragile Items",
|
||||
"Medical": "Medical & Pharma",
|
||||
"Automotive": "Automotive Parts",
|
||||
"Food": "Food & Perishables",
|
||||
}
|
||||
|
||||
// pricingCacheKey returns the Redis key for a zone+servicetype slab.
|
||||
// Only 6 possible keys — 3 zones × 2 service types.
|
||||
func pricingCacheKey(zone, servicetype string) string {
|
||||
return fmt.Sprintf("doormile:pricing:%s:%s", zone, servicetype)
|
||||
}
|
||||
|
||||
// invalidatePricingCache drops the Redis key for a zone+servicetype so the next
|
||||
// CheckPrice request re-warms it from Postgres.
|
||||
func invalidatePricingCache(zone, servicetype string) {
|
||||
if db.Rdb == nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
key := pricingCacheKey(zone, servicetype)
|
||||
if err := db.Rdb.Del(ctx, key).Err(); err != nil {
|
||||
utils.Warn("Failed to invalidate pricing cache", "key", key, "error", err)
|
||||
} else {
|
||||
utils.Info("Pricing cache invalidated", "key", key)
|
||||
}
|
||||
}
|
||||
|
||||
// loadFromPostgres fetches all Active rules for a zone+servicetype and writes
|
||||
// them into Redis. Returns the rules regardless of whether Redis succeeds.
|
||||
func loadFromPostgres(zone, servicetype string) ([]models.DoormilePricing, error) {
|
||||
var rules []models.DoormilePricing
|
||||
err := db.DB.
|
||||
Where("zone = ? AND servicetype = ? AND status = ? AND deletedat IS NULL", zone, servicetype, "Active").
|
||||
Order("category ASC").
|
||||
Find(&rules).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Warm the cache — no TTL because we invalidate explicitly on admin writes
|
||||
if db.Rdb != nil && len(rules) > 0 {
|
||||
data, merr := json.Marshal(rules)
|
||||
if merr == nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
if rerr := db.Rdb.Set(ctx, pricingCacheKey(zone, servicetype), data, 0).Err(); rerr != nil {
|
||||
utils.Warn("Failed to warm pricing cache", "zone", zone, "servicetype", servicetype, "error", rerr)
|
||||
} else {
|
||||
utils.Info("Pricing cache warmed", "zone", zone, "servicetype", servicetype, "count", len(rules))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
// applyFilters takes the full slab for a zone+servicetype and applies the
|
||||
// weight and optional category filters in memory.
|
||||
func applyFilters(rules []models.DoormilePricing, weight float64, category string) []models.DoormilePricing {
|
||||
out := make([]models.DoormilePricing, 0, len(rules))
|
||||
for _, r := range rules {
|
||||
if r.Minweight > weight || r.Maxweight < weight {
|
||||
continue
|
||||
}
|
||||
if category != "" && r.Category != category {
|
||||
continue
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// buildPriceResponse constructs the final JSON response from a filtered rule set.
|
||||
func buildPriceResponse(zone, servicetype string, weight float64, rules []models.DoormilePricing) fiber.Map {
|
||||
if len(rules) == 0 {
|
||||
return fiber.Map{
|
||||
"found": false,
|
||||
"zone": zone,
|
||||
"service_type": servicetype,
|
||||
"weight": weight,
|
||||
"message": "no pricing configured for this combination",
|
||||
"results": []interface{}{},
|
||||
"total": 0,
|
||||
}
|
||||
}
|
||||
|
||||
results := make([]fiber.Map, 0, len(rules))
|
||||
for _, r := range rules {
|
||||
results = append(results, fiber.Map{
|
||||
"category": r.Category,
|
||||
"category_label": categoryLabels[r.Category],
|
||||
"min_price": r.Minprice,
|
||||
"max_price": r.Maxprice,
|
||||
})
|
||||
}
|
||||
|
||||
return fiber.Map{
|
||||
"found": true,
|
||||
"zone": zone,
|
||||
"service_type": servicetype,
|
||||
"weight": weight,
|
||||
"currency": rules[0].Currency,
|
||||
"total": len(results),
|
||||
"results": results,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckPrice is the public endpoint Flutter calls after the customer fills the form.
|
||||
// Reads from Redis first (full zone+servicetype slab), filters by weight in memory.
|
||||
// Falls back to Postgres on cache miss and re-warms Redis before returning.
|
||||
//
|
||||
// POST /api/v1/pricing/check
|
||||
// Body: { zone, service_type, weight, category? }
|
||||
func CheckPrice(c *fiber.Ctx) error {
|
||||
type req struct {
|
||||
Zone string `json:"zone"`
|
||||
ServiceType string `json:"service_type"`
|
||||
Weight float64 `json:"weight"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
body := new(req)
|
||||
if err := c.BodyParser(body); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if !validZones[body.Zone] {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"message": "invalid zone",
|
||||
"valid_zones": []string{"Local", "Interstate", "OtherState"},
|
||||
})
|
||||
}
|
||||
if !validServiceTypes[body.ServiceType] {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"message": "invalid service_type",
|
||||
"valid_service_types": []string{"Normal", "Express"},
|
||||
})
|
||||
}
|
||||
if body.Weight <= 0 {
|
||||
return utils.BadRequest(c, "weight must be greater than 0")
|
||||
}
|
||||
if body.Category != "" && !validCategories[body.Category] {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"success": false,
|
||||
"message": "invalid category",
|
||||
"valid_categories": []string{"General", "Documents", "Electronics", "Clothing", "Fragile", "Medical", "Automotive", "Food"},
|
||||
})
|
||||
}
|
||||
|
||||
var allRules []models.DoormilePricing
|
||||
|
||||
if db.Rdb != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
cached, err := db.Rdb.Get(ctx, pricingCacheKey(body.Zone, body.ServiceType)).Result()
|
||||
cancel()
|
||||
|
||||
if err == nil {
|
||||
if jerr := json.Unmarshal([]byte(cached), &allRules); jerr != nil {
|
||||
utils.Warn("Corrupt pricing cache entry, evicting", "zone", body.Zone, "servicetype", body.ServiceType)
|
||||
invalidatePricingCache(body.Zone, body.ServiceType)
|
||||
allRules = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if allRules == nil {
|
||||
var err error
|
||||
allRules, err = loadFromPostgres(body.Zone, body.ServiceType)
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to fetch pricing")
|
||||
}
|
||||
}
|
||||
|
||||
matched := applyFilters(allRules, body.Weight, body.Category)
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"data": buildPriceResponse(body.Zone, body.ServiceType, body.Weight, matched),
|
||||
})
|
||||
}
|
||||
|
||||
// GetDoormilePricing returns all pricing rules for admin review.
|
||||
// Always reads from Postgres — admins need the authoritative view.
|
||||
// GET /api/v1/admin/doormile-pricing
|
||||
func GetDoormilePricing(c *fiber.Ctx) error {
|
||||
query := db.DB.Where("deletedat IS NULL")
|
||||
|
||||
if z := c.Query("zone"); z != "" {
|
||||
query = query.Where("zone = ?", z)
|
||||
}
|
||||
if cat := c.Query("category"); cat != "" {
|
||||
query = query.Where("category = ?", cat)
|
||||
}
|
||||
if st := c.Query("service_type"); st != "" {
|
||||
query = query.Where("servicetype = ?", st)
|
||||
}
|
||||
|
||||
var rules []models.DoormilePricing
|
||||
if err := query.Order("zone, category, servicetype, min_weight").Find(&rules).Error; err != nil {
|
||||
return utils.Internal(c, "failed to fetch pricing rules")
|
||||
}
|
||||
|
||||
return utils.List(c, rules, int64(len(rules)))
|
||||
}
|
||||
|
||||
// CreateDoormilePricing adds a new pricing band and invalidates the relevant cache.
|
||||
// POST /api/v1/admin/doormile-pricing
|
||||
func CreateDoormilePricing(c *fiber.Ctx) error {
|
||||
type req struct {
|
||||
Zone string `json:"zone"`
|
||||
Category string `json:"category"`
|
||||
ServiceType string `json:"service_type"`
|
||||
MinWeight float64 `json:"min_weight"`
|
||||
MaxWeight float64 `json:"max_weight"`
|
||||
MinPrice float64 `json:"min_price"`
|
||||
MaxPrice float64 `json:"max_price"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
body := new(req)
|
||||
if err := c.BodyParser(body); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if !validZones[body.Zone] || !validCategories[body.Category] || !validServiceTypes[body.ServiceType] {
|
||||
return utils.BadRequest(c, "invalid zone, category, or service_type")
|
||||
}
|
||||
if body.MinWeight < 0 || body.MaxWeight <= body.MinWeight {
|
||||
return utils.BadRequest(c, "max_weight must be greater than min_weight")
|
||||
}
|
||||
if body.MinPrice <= 0 || body.MaxPrice <= body.MinPrice {
|
||||
return utils.BadRequest(c, "max_price must be greater than min_price")
|
||||
}
|
||||
|
||||
currency := body.Currency
|
||||
if currency == "" {
|
||||
currency = "INR"
|
||||
}
|
||||
|
||||
rule := models.DoormilePricing{
|
||||
Zone: body.Zone,
|
||||
Category: body.Category,
|
||||
Servicetype: body.ServiceType,
|
||||
Minweight: body.MinWeight,
|
||||
Maxweight: body.MaxWeight,
|
||||
Minprice: body.MinPrice,
|
||||
Maxprice: body.MaxPrice,
|
||||
Currency: currency,
|
||||
Status: "Active",
|
||||
}
|
||||
|
||||
if err := db.DB.Create(&rule).Error; err != nil {
|
||||
return utils.Internal(c, "failed to create pricing rule")
|
||||
}
|
||||
|
||||
invalidatePricingCache(rule.Zone, rule.Servicetype)
|
||||
|
||||
return utils.Created(c, rule)
|
||||
}
|
||||
|
||||
// UpdateDoormilePricing updates a pricing band and invalidates affected cache keys.
|
||||
// If zone or service_type changes, both the old and new cache keys are invalidated.
|
||||
// PUT /api/v1/admin/doormile-pricing/:id
|
||||
func UpdateDoormilePricing(c *fiber.Ctx) error {
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid pricing rule ID")
|
||||
}
|
||||
|
||||
var rule models.DoormilePricing
|
||||
if err := db.DB.Where("doormile_pricing_id = ? AND deletedat IS NULL", id).First(&rule).Error; err != nil {
|
||||
return utils.NotFound(c, "pricing rule not found")
|
||||
}
|
||||
|
||||
// Capture old keys before any mutation
|
||||
oldZone := rule.Zone
|
||||
oldServicetype := rule.Servicetype
|
||||
|
||||
type req struct {
|
||||
Zone string `json:"zone"`
|
||||
Category string `json:"category"`
|
||||
ServiceType string `json:"service_type"`
|
||||
MinWeight float64 `json:"min_weight"`
|
||||
MaxWeight float64 `json:"max_weight"`
|
||||
MinPrice float64 `json:"min_price"`
|
||||
MaxPrice float64 `json:"max_price"`
|
||||
Currency string `json:"currency"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
body := new(req)
|
||||
if err := c.BodyParser(body); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if body.Zone != "" {
|
||||
if !validZones[body.Zone] {
|
||||
return utils.BadRequest(c, "invalid zone")
|
||||
}
|
||||
rule.Zone = body.Zone
|
||||
}
|
||||
if body.Category != "" {
|
||||
if !validCategories[body.Category] {
|
||||
return utils.BadRequest(c, "invalid category")
|
||||
}
|
||||
rule.Category = body.Category
|
||||
}
|
||||
if body.ServiceType != "" {
|
||||
if !validServiceTypes[body.ServiceType] {
|
||||
return utils.BadRequest(c, "invalid service_type")
|
||||
}
|
||||
rule.Servicetype = body.ServiceType
|
||||
}
|
||||
if body.MinWeight >= 0 {
|
||||
rule.Minweight = body.MinWeight
|
||||
}
|
||||
if body.MaxWeight > 0 {
|
||||
rule.Maxweight = body.MaxWeight
|
||||
}
|
||||
if body.MinPrice > 0 {
|
||||
rule.Minprice = body.MinPrice
|
||||
}
|
||||
if body.MaxPrice > 0 {
|
||||
rule.Maxprice = body.MaxPrice
|
||||
}
|
||||
if body.Currency != "" {
|
||||
rule.Currency = body.Currency
|
||||
}
|
||||
if body.Status != "" {
|
||||
rule.Status = body.Status
|
||||
}
|
||||
rule.Updatedat = time.Now()
|
||||
|
||||
if err := db.DB.Save(&rule).Error; err != nil {
|
||||
return utils.Internal(c, "failed to update pricing rule")
|
||||
}
|
||||
|
||||
invalidatePricingCache(oldZone, oldServicetype)
|
||||
if rule.Zone != oldZone || rule.Servicetype != oldServicetype {
|
||||
invalidatePricingCache(rule.Zone, rule.Servicetype)
|
||||
}
|
||||
|
||||
return utils.OK(c, rule)
|
||||
}
|
||||
|
||||
// DeleteDoormilePricing soft-deletes a pricing band and invalidates its cache key.
|
||||
// DELETE /api/v1/admin/doormile-pricing/:id
|
||||
func DeleteDoormilePricing(c *fiber.Ctx) error {
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid pricing rule ID")
|
||||
}
|
||||
|
||||
var rule models.DoormilePricing
|
||||
if err := db.DB.Where("doormile_pricing_id = ? AND deletedat IS NULL", id).First(&rule).Error; err != nil {
|
||||
return utils.NotFound(c, "pricing rule not found")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
rule.Deletedat = &now
|
||||
db.DB.Save(&rule)
|
||||
|
||||
invalidatePricingCache(rule.Zone, rule.Servicetype)
|
||||
|
||||
return utils.Message(c, "pricing rule deleted successfully")
|
||||
}
|
||||
|
||||
// WarmPricingCache loads every Active pricing rule from Postgres into Redis on
|
||||
// server startup. Call this once from main.go after DB and Redis are ready.
|
||||
// If Redis is unavailable it logs a warning and returns — the lazy-load fallback
|
||||
// in CheckPrice will handle individual misses at request time.
|
||||
func WarmPricingCache() {
|
||||
if db.Rdb == nil {
|
||||
utils.Warn("WarmPricingCache: Redis not available, skipping pre-warm")
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("WarmPricingCache: warming pricing cache from Postgres...")
|
||||
|
||||
// Iterate every zone × servicetype combination
|
||||
zones := []string{"Local", "Interstate", "OtherState"}
|
||||
serviceTypes := []string{"Normal", "Express"}
|
||||
|
||||
warmed := 0
|
||||
for _, zone := range zones {
|
||||
for _, st := range serviceTypes {
|
||||
rules, err := loadFromPostgres(zone, st)
|
||||
if err != nil {
|
||||
utils.Error("WarmPricingCache: failed to load", "zone", zone, "servicetype", st, "error", err)
|
||||
continue
|
||||
}
|
||||
if len(rules) > 0 {
|
||||
warmed++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
utils.Info("WarmPricingCache: done", "slabs_warmed", warmed)
|
||||
}
|
||||
|
||||
// GetPricingMeta returns the valid enum values so Flutter can populate its dropdowns.
|
||||
// GET /api/v1/pricing/meta
|
||||
func GetPricingMeta(c *fiber.Ctx) error {
|
||||
return utils.OK(c, fiber.Map{
|
||||
"zones": []fiber.Map{
|
||||
{"value": "Local", "label": "Local / Same City"},
|
||||
{"value": "Interstate", "label": "Interstate"},
|
||||
{"value": "OtherState", "label": "Other State"},
|
||||
},
|
||||
"categories": []fiber.Map{
|
||||
{"value": "General", "label": "General Goods"},
|
||||
{"value": "Documents", "label": "Books & Documents"},
|
||||
{"value": "Electronics", "label": "Electronics & Gadgets"},
|
||||
{"value": "Clothing", "label": "Clothing & Textiles"},
|
||||
{"value": "Fragile", "label": "Fragile Items"},
|
||||
{"value": "Medical", "label": "Medical & Pharma"},
|
||||
{"value": "Automotive", "label": "Automotive Parts"},
|
||||
{"value": "Food", "label": "Food & Perishables"},
|
||||
},
|
||||
"service_types": []fiber.Map{
|
||||
{"value": "Normal", "label": "Normal"},
|
||||
{"value": "Express", "label": "Express"},
|
||||
},
|
||||
})
|
||||
}
|
||||
225
controllers/enquiryController.go
Normal file
225
controllers/enquiryController.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"doormile/db"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// ─── Competitor Branches ────────────────────────────────────────────────────
|
||||
|
||||
func GetCompetitorBranches(c *fiber.Ctx) error {
|
||||
company := c.Query("company")
|
||||
area := c.Query("area")
|
||||
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.Query("limit", "50"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 50
|
||||
}
|
||||
if limit > 2000 {
|
||||
limit = 2000
|
||||
}
|
||||
offset := (page - 1) * limit
|
||||
|
||||
q := db.DB.Model(&models.CompetitorBranch{})
|
||||
if company != "" {
|
||||
q = q.Where("company ILIKE ?", "%"+company+"%")
|
||||
}
|
||||
if area != "" {
|
||||
q = q.Where("area ILIKE ?", "%"+area+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
q.Count(&total)
|
||||
|
||||
var branches []models.CompetitorBranch
|
||||
if err := q.Order("company, area").Offset(offset).Limit(limit).Find(&branches).Error; err != nil {
|
||||
return utils.Internal(c, "failed to fetch branches")
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"data": branches,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
|
||||
func CreateCompetitorBranch(c *fiber.Ctx) error {
|
||||
var b models.CompetitorBranch
|
||||
if err := c.BodyParser(&b); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
if b.Company == "" {
|
||||
return utils.BadRequest(c, "company is required")
|
||||
}
|
||||
|
||||
userID := uint64(c.Locals("userid").(int))
|
||||
b.CreatedBy = userID
|
||||
b.UpdatedBy = userID
|
||||
b.ID = 0
|
||||
|
||||
if err := db.DB.Create(&b).Error; err != nil {
|
||||
return utils.Internal(c, "failed to create branch")
|
||||
}
|
||||
return utils.Created(c, b)
|
||||
}
|
||||
|
||||
func UpdateCompetitorBranch(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid branch ID")
|
||||
}
|
||||
|
||||
var existing models.CompetitorBranch
|
||||
if err := db.DB.First(&existing, id).Error; err != nil {
|
||||
return utils.NotFound(c, "branch not found")
|
||||
}
|
||||
|
||||
var input models.CompetitorBranch
|
||||
if err := c.BodyParser(&input); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
input.ID = existing.ID
|
||||
input.CreatedAt = existing.CreatedAt
|
||||
input.CreatedBy = existing.CreatedBy
|
||||
|
||||
userID := uint64(c.Locals("userid").(int))
|
||||
input.UpdatedBy = userID
|
||||
|
||||
if input.Company == "" {
|
||||
input.Company = existing.Company
|
||||
}
|
||||
|
||||
if err := db.DB.Save(&input).Error; err != nil {
|
||||
return utils.Internal(c, "failed to update branch")
|
||||
}
|
||||
return utils.OK(c, input)
|
||||
}
|
||||
|
||||
func DeleteCompetitorBranch(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid branch ID")
|
||||
}
|
||||
if err := db.DB.Delete(&models.CompetitorBranch{}, id).Error; err != nil {
|
||||
return utils.Internal(c, "failed to delete branch")
|
||||
}
|
||||
return utils.Message(c, "branch deleted successfully")
|
||||
}
|
||||
|
||||
// ─── Carrier Pricing ────────────────────────────────────────────────────────
|
||||
|
||||
func GetCarrierPricing(c *fiber.Ctx) error {
|
||||
company := c.Query("company")
|
||||
zone := c.Query("zone")
|
||||
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.Query("limit", "100"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 100
|
||||
}
|
||||
if limit > 2000 {
|
||||
limit = 2000
|
||||
}
|
||||
offset := (page - 1) * limit
|
||||
|
||||
q := db.DB.Model(&models.CarrierPricing{})
|
||||
if company != "" {
|
||||
q = q.Where("company ILIKE ?", "%"+company+"%")
|
||||
}
|
||||
if zone != "" {
|
||||
q = q.Where("zone ILIKE ?", "%"+zone+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
q.Count(&total)
|
||||
|
||||
var pricing []models.CarrierPricing
|
||||
if err := q.Order("company, weight_slab").Offset(offset).Limit(limit).Find(&pricing).Error; err != nil {
|
||||
return utils.Internal(c, "failed to fetch carrier pricing")
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"data": pricing,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
|
||||
func CreateCarrierPricing(c *fiber.Ctx) error {
|
||||
var p models.CarrierPricing
|
||||
if err := c.BodyParser(&p); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
if p.Company == "" {
|
||||
return utils.BadRequest(c, "company is required")
|
||||
}
|
||||
|
||||
userID := uint64(c.Locals("userid").(int))
|
||||
p.CreatedBy = userID
|
||||
p.UpdatedBy = userID
|
||||
p.ID = 0
|
||||
|
||||
if err := db.DB.Create(&p).Error; err != nil {
|
||||
return utils.Internal(c, "failed to create carrier pricing")
|
||||
}
|
||||
return utils.Created(c, p)
|
||||
}
|
||||
|
||||
func UpdateCarrierPricing(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid pricing ID")
|
||||
}
|
||||
|
||||
var existing models.CarrierPricing
|
||||
if err := db.DB.First(&existing, id).Error; err != nil {
|
||||
return utils.NotFound(c, "carrier pricing not found")
|
||||
}
|
||||
|
||||
var input models.CarrierPricing
|
||||
if err := c.BodyParser(&input); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
input.ID = existing.ID
|
||||
input.CreatedAt = existing.CreatedAt
|
||||
input.CreatedBy = existing.CreatedBy
|
||||
|
||||
userID := uint64(c.Locals("userid").(int))
|
||||
input.UpdatedBy = userID
|
||||
|
||||
if input.Company == "" {
|
||||
input.Company = existing.Company
|
||||
}
|
||||
|
||||
if err := db.DB.Save(&input).Error; err != nil {
|
||||
return utils.Internal(c, "failed to update carrier pricing")
|
||||
}
|
||||
return utils.OK(c, input)
|
||||
}
|
||||
|
||||
func DeleteCarrierPricing(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid pricing ID")
|
||||
}
|
||||
if err := db.DB.Delete(&models.CarrierPricing{}, id).Error; err != nil {
|
||||
return utils.Internal(c, "failed to delete carrier pricing")
|
||||
}
|
||||
return utils.Message(c, "carrier pricing deleted successfully")
|
||||
}
|
||||
883
controllers/milerController.go
Normal file
883
controllers/milerController.go
Normal file
@@ -0,0 +1,883 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"doormile/config"
|
||||
"doormile/constants"
|
||||
"doormile/db"
|
||||
"doormile/dto"
|
||||
"doormile/models"
|
||||
"doormile/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func generateTrackingNo() string {
|
||||
b := make([]byte, 4)
|
||||
rand.Read(b)
|
||||
return fmt.Sprintf("DM-TRK-%X-%d", b, time.Now().Unix()%100000)
|
||||
}
|
||||
|
||||
func LoginMiler(cfg *config.Config) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
req := new(dto.MilerLoginRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Phone == "" {
|
||||
return utils.BadRequest(c, "phone is required")
|
||||
}
|
||||
|
||||
configID := req.Configid
|
||||
if configID == 0 {
|
||||
configID = 1001
|
||||
}
|
||||
|
||||
var user models.AppUser
|
||||
if err := db.DB.Where("contactno = ? AND configid = ?", req.Phone, configID).First(&user).Error; err != nil {
|
||||
return utils.NotFound(c, "no miler account found for this phone number")
|
||||
}
|
||||
|
||||
if user.Roleid != 5 {
|
||||
return utils.Forbidden(c, "this endpoint is restricted to miler accounts")
|
||||
}
|
||||
|
||||
if user.Status != "Active" {
|
||||
return utils.Forbidden(c, "miler account is not active")
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"message": "PIN verification required",
|
||||
"phone": req.Phone,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func VerifyMilerPin(cfg *config.Config) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
req := new(dto.MilerPinVerifyRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Phone == "" || req.Pin == "" {
|
||||
return utils.BadRequest(c, "phone and pin are required")
|
||||
}
|
||||
|
||||
configID := req.Configid
|
||||
if configID == 0 {
|
||||
configID = 1001
|
||||
}
|
||||
|
||||
var user models.AppUser
|
||||
if err := db.DB.Where("contactno = ? AND configid = ?", req.Phone, configID).First(&user).Error; err != nil {
|
||||
return utils.NotFound(c, "no miler account found for this phone number")
|
||||
}
|
||||
|
||||
if user.Roleid != 5 {
|
||||
return utils.Forbidden(c, "this endpoint is restricted to miler accounts")
|
||||
}
|
||||
|
||||
if user.Status != "Active" {
|
||||
return utils.Forbidden(c, "miler account is not active")
|
||||
}
|
||||
|
||||
if !utils.CheckPasswordHash(req.Pin, user.Password) {
|
||||
return utils.Unauthorized(c, "incorrect PIN")
|
||||
}
|
||||
|
||||
token, err := utils.GenerateToken(user.Userid, user.Email, user.Roleid, user.Tenantid, user.Configid, cfg.JWTSecret)
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to generate token")
|
||||
}
|
||||
|
||||
var profile models.MilerProfile
|
||||
if err := db.DB.Where("userid = ?", user.Userid).First(&profile).Error; err != nil {
|
||||
profile = models.MilerProfile{
|
||||
Userid: user.Userid,
|
||||
Displayname: user.Authname,
|
||||
Phone: user.Contactno,
|
||||
Availabilitystatus: constants.MilerOffline,
|
||||
Rating: 5.0,
|
||||
Applocationid: user.Applocationid,
|
||||
}
|
||||
db.DB.Create(&profile)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"success": true,
|
||||
"token": token,
|
||||
"user": fiber.Map{
|
||||
"userid": user.Userid,
|
||||
"authname": user.Authname,
|
||||
"email": user.Email,
|
||||
"contactno": user.Contactno,
|
||||
"profile": profile,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func GetMilerProfile(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
|
||||
var user models.AppUser
|
||||
if err := db.DB.First(&user, milerUserID).Error; err != nil {
|
||||
return utils.NotFound(c, "user not found")
|
||||
}
|
||||
|
||||
var profile models.MilerProfile
|
||||
if err := db.DB.Where("userid = ?", milerUserID).First(&profile).Error; err != nil {
|
||||
return utils.NotFound(c, "miler profile not found")
|
||||
}
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"userid": user.Userid,
|
||||
"authname": user.Authname,
|
||||
"email": user.Email,
|
||||
"contactno": user.Contactno,
|
||||
"profile": profile,
|
||||
})
|
||||
}
|
||||
|
||||
func UpdateMilerProfile(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
|
||||
var profile models.MilerProfile
|
||||
if err := db.DB.Where("userid = ?", milerUserID).First(&profile).Error; err != nil {
|
||||
return utils.NotFound(c, "miler profile not found")
|
||||
}
|
||||
|
||||
type ProfileUpdate struct {
|
||||
Displayname string `json:"displayname"`
|
||||
Profilephotourl string `json:"profilephotourl"`
|
||||
Defaultvehicletype string `json:"defaultvehicletype"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
req := new(ProfileUpdate)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Displayname != "" {
|
||||
profile.Displayname = req.Displayname
|
||||
}
|
||||
if req.Phone != "" {
|
||||
profile.Phone = req.Phone
|
||||
}
|
||||
profile.Profilephotourl = req.Profilephotourl
|
||||
profile.Defaultvehicletype = req.Defaultvehicletype
|
||||
profile.Updatedat = time.Now()
|
||||
|
||||
if err := db.DB.Save(&profile).Error; err != nil {
|
||||
return utils.Internal(c, "failed to update profile")
|
||||
}
|
||||
|
||||
return utils.OK(c, profile)
|
||||
}
|
||||
|
||||
func UpdateMilerLocation(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
|
||||
req := new(dto.MilerLocationUpdateRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Latitude == 0 || req.Longitude == 0 {
|
||||
return utils.BadRequest(c, "latitude and longitude are required")
|
||||
}
|
||||
|
||||
var profile models.MilerProfile
|
||||
if err := db.DB.Where("userid = ?", milerUserID).First(&profile).Error; err != nil {
|
||||
return utils.NotFound(c, "miler profile not found")
|
||||
}
|
||||
|
||||
profile.Currentlatitude = req.Latitude
|
||||
profile.Currentlongitude = req.Longitude
|
||||
profile.Currentpincode = req.Pincode
|
||||
now := time.Now()
|
||||
profile.Lastlocationupdatedat = &now
|
||||
profile.Updatedat = now
|
||||
|
||||
if err := db.DB.Save(&profile).Error; err != nil {
|
||||
return utils.Internal(c, "failed to update location")
|
||||
}
|
||||
|
||||
if db.Rdb != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
redisKey := fmt.Sprintf("miler:gps:%d", milerUserID)
|
||||
val := fmt.Sprintf("%f,%f", req.Latitude, req.Longitude)
|
||||
db.Rdb.Set(ctx, redisKey, val, 30*time.Minute)
|
||||
|
||||
db.Rdb.GeoAdd(ctx, "milers:locations", &redis.GeoLocation{
|
||||
Name: strconv.Itoa(milerUserID),
|
||||
Latitude: req.Latitude,
|
||||
Longitude: req.Longitude,
|
||||
})
|
||||
}
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"latitude": req.Latitude,
|
||||
"longitude": req.Longitude,
|
||||
"pincode": req.Pincode,
|
||||
})
|
||||
}
|
||||
|
||||
func UpdateMilerAvailability(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
|
||||
req := new(dto.MilerAvailabilityRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Status == "" {
|
||||
return utils.BadRequest(c, "status is required")
|
||||
}
|
||||
|
||||
var profile models.MilerProfile
|
||||
if err := db.DB.Where("userid = ?", milerUserID).First(&profile).Error; err != nil {
|
||||
return utils.NotFound(c, "miler profile not found")
|
||||
}
|
||||
|
||||
profile.Availabilitystatus = req.Status
|
||||
profile.Updatedat = time.Now()
|
||||
if err := db.DB.Save(&profile).Error; err != nil {
|
||||
return utils.Internal(c, "failed to update availability")
|
||||
}
|
||||
|
||||
var user models.AppUser
|
||||
if err := db.DB.First(&user, milerUserID).Error; err == nil {
|
||||
if req.Status == constants.MilerOffline || req.Status == constants.MilerBlocked {
|
||||
user.Onduty = 0
|
||||
} else {
|
||||
user.Onduty = 1
|
||||
}
|
||||
db.DB.Save(&user)
|
||||
}
|
||||
|
||||
return utils.OK(c, profile)
|
||||
}
|
||||
|
||||
func GetMilerAssignments(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
|
||||
var assignments []models.BookingAssignment
|
||||
if err := db.DB.Where("mileruserid = ?", milerUserID).Order("assignedat DESC").Find(&assignments).Error; err != nil {
|
||||
return utils.Internal(c, "failed to fetch assignments")
|
||||
}
|
||||
|
||||
return utils.List(c, assignments, int64(len(assignments)))
|
||||
}
|
||||
|
||||
func GetMilerAssignmentDetails(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
assignmentID, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid assignment ID")
|
||||
}
|
||||
|
||||
var assignment models.BookingAssignment
|
||||
if err := db.DB.Where("bookingassignmentid = ? AND mileruserid = ?", assignmentID, milerUserID).First(&assignment).Error; err != nil {
|
||||
return utils.NotFound(c, "assignment not found")
|
||||
}
|
||||
|
||||
var booking models.PickupBooking
|
||||
db.DB.Preload("Parcels").Preload("ServiceOptions").First(&booking, assignment.Bookingid)
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"assignment": assignment,
|
||||
"booking": booking,
|
||||
})
|
||||
}
|
||||
|
||||
func AcceptMilerAssignment(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
assignmentID, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid assignment ID")
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
|
||||
var assignment models.BookingAssignment
|
||||
if err := tx.Where("bookingassignmentid = ? AND mileruserid = ?", assignmentID, milerUserID).First(&assignment).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.NotFound(c, "assignment not found")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
assignment.Assignmentstatus = constants.AssignmentAccepted
|
||||
assignment.Acceptedat = &now
|
||||
tx.Save(&assignment)
|
||||
|
||||
var booking models.PickupBooking
|
||||
if err := tx.First(&booking, assignment.Bookingid).Error; err == nil {
|
||||
booking.Status = constants.BookingPickupScheduled
|
||||
booking.Assignedmileruserid = &milerUserID
|
||||
booking.Updatedat = now
|
||||
tx.Save(&booking)
|
||||
}
|
||||
|
||||
tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAssigned)
|
||||
|
||||
tx.Commit()
|
||||
return utils.Message(c, "assignment accepted successfully")
|
||||
}
|
||||
|
||||
func RejectMilerAssignment(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
assignmentID, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid assignment ID")
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
|
||||
var assignment models.BookingAssignment
|
||||
if err := tx.Where("bookingassignmentid = ? AND mileruserid = ?", assignmentID, milerUserID).First(&assignment).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.NotFound(c, "assignment not found")
|
||||
}
|
||||
|
||||
assignment.Assignmentstatus = constants.AssignmentRejected
|
||||
assignment.Remarks = c.Query("reason", "Rejected by rider")
|
||||
tx.Save(&assignment)
|
||||
|
||||
var booking models.PickupBooking
|
||||
if err := tx.First(&booking, assignment.Bookingid).Error; err == nil {
|
||||
booking.Status = constants.BookingCreated
|
||||
booking.Assignedmileruserid = nil
|
||||
booking.Updatedat = time.Now()
|
||||
tx.Save(&booking)
|
||||
}
|
||||
|
||||
tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAvailable)
|
||||
|
||||
tx.Commit()
|
||||
return utils.Message(c, "assignment rejected")
|
||||
}
|
||||
|
||||
func BookingReachedCustomer(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid booking ID")
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
|
||||
var booking models.PickupBooking
|
||||
if err := tx.Where("bookingid = ? AND assignedmileruserid = ?", bookingID, milerUserID).First(&booking).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.NotFound(c, "assigned booking not found")
|
||||
}
|
||||
|
||||
tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAtCustomer)
|
||||
|
||||
tx.Commit()
|
||||
return utils.Message(c, "arrival at customer confirmed")
|
||||
}
|
||||
|
||||
func BookingParcelConfirm(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid booking ID")
|
||||
}
|
||||
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.Where("bookingid = ? AND assignedmileruserid = ?", bookingID, milerUserID).First(&booking).Error; err != nil {
|
||||
return utils.NotFound(c, "assigned booking not found")
|
||||
}
|
||||
|
||||
type ParcelUpdate struct {
|
||||
Weight float64 `json:"weight"`
|
||||
Length float64 `json:"length"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
}
|
||||
|
||||
req := new(ParcelUpdate)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
var parcel models.BookingParcel
|
||||
if err := db.DB.Where("bookingid = ?", bookingID).First(&parcel).Error; err != nil {
|
||||
return utils.NotFound(c, "parcel details not found")
|
||||
}
|
||||
|
||||
parcel.Weight = req.Weight
|
||||
parcel.Length = req.Length
|
||||
parcel.Width = req.Width
|
||||
parcel.Height = req.Height
|
||||
parcel.Updatedat = time.Now()
|
||||
db.DB.Save(&parcel)
|
||||
|
||||
return utils.OK(c, parcel)
|
||||
}
|
||||
|
||||
func BookingPaymentCollect(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid booking ID")
|
||||
}
|
||||
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.Where("bookingid = ? AND assignedmileruserid = ?", bookingID, milerUserID).First(&booking).Error; err != nil {
|
||||
return utils.NotFound(c, "assigned booking not found")
|
||||
}
|
||||
|
||||
req := new(dto.PaymentRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if req.Amount <= 0 {
|
||||
return utils.BadRequest(c, "payment amount must be greater than zero")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
payment := models.BookingPayment{
|
||||
Bookingid: bookingID,
|
||||
Amount: req.Amount,
|
||||
Paymentmode: req.Paymentmode,
|
||||
Paymentstatus: constants.PaymentStatusPaid,
|
||||
Collectedbyuserid: &milerUserID,
|
||||
Transactionref: req.Transactionref,
|
||||
Paidat: &now,
|
||||
}
|
||||
|
||||
if err := db.DB.Create(&payment).Error; err != nil {
|
||||
return utils.Internal(c, "failed to record payment")
|
||||
}
|
||||
|
||||
return utils.Created(c, payment)
|
||||
}
|
||||
|
||||
func BookingPickupComplete(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid booking ID")
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
|
||||
var booking models.PickupBooking
|
||||
if err := tx.Where("bookingid = ? AND assignedmileruserid = ?", bookingID, milerUserID).First(&booking).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.NotFound(c, "assigned booking not found")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
booking.Status = constants.BookingPickedUp
|
||||
booking.Updatedat = now
|
||||
tx.Save(&booking)
|
||||
|
||||
var profile models.MilerProfile
|
||||
if err := tx.Where("userid = ?", milerUserID).First(&profile).Error; err == nil {
|
||||
profile.Totalcompletedpickups += 1
|
||||
profile.Availabilitystatus = constants.MilerPickedUp
|
||||
profile.Updatedat = now
|
||||
tx.Save(&profile)
|
||||
}
|
||||
|
||||
var parcel models.BookingParcel
|
||||
tx.Where("bookingid = ?", bookingID).First(&parcel)
|
||||
|
||||
volumetric := calculateVolumetricWeight(parcel.Length, parcel.Width, parcel.Height)
|
||||
chargeable := math.Max(parcel.Weight, volumetric)
|
||||
|
||||
trackingNo := generateTrackingNo()
|
||||
|
||||
var defaultHubID *int
|
||||
var hub models.Hub
|
||||
if tx.First(&hub).Error == nil {
|
||||
defaultHubID = &hub.Hubid
|
||||
}
|
||||
|
||||
consignment := models.Consignment{
|
||||
Trackingno: trackingNo,
|
||||
Tenantid: c.Locals("tenantid").(int),
|
||||
Pickuplatitude: booking.Pickuplatitude,
|
||||
Pickuplongitude: booking.Pickuplongitude,
|
||||
Deliverylatitude: booking.Deliverylatitude,
|
||||
Deliverylongitude: booking.Deliverylongitude,
|
||||
Pickuppincode: booking.Pickuppincode,
|
||||
Deliverypincode: booking.Deliverypincode,
|
||||
Length: parcel.Length,
|
||||
Width: parcel.Width,
|
||||
Height: parcel.Height,
|
||||
Deadweight: parcel.Weight,
|
||||
Volumetricweight: volumetric,
|
||||
Chargeableweight: chargeable,
|
||||
Paymentmode: "Prepaid",
|
||||
Status: constants.ConsignmentInwardedAtHub,
|
||||
Estimateddeliveryat: nil,
|
||||
Createdby: milerUserID,
|
||||
Originhubid: defaultHubID,
|
||||
Currenthubid: defaultHubID,
|
||||
}
|
||||
|
||||
var payment models.BookingPayment
|
||||
if tx.Where("bookingid = ?", bookingID).First(&payment).Error == nil {
|
||||
if payment.Paymentstatus == constants.PaymentStatusPaid {
|
||||
consignment.Codcollected = payment.Amount
|
||||
} else {
|
||||
consignment.Codamount = payment.Amount
|
||||
consignment.Paymentmode = "COD"
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Create(&consignment).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to convert booking to consignment")
|
||||
}
|
||||
|
||||
booking.Consignmentid = &consignment.Consignmentid
|
||||
booking.Status = constants.BookingConvertedConsignment
|
||||
tx.Save(&booking)
|
||||
|
||||
history := models.ConsignmentHistory{
|
||||
Consignmentid: consignment.Consignmentid,
|
||||
Hubid: defaultHubID,
|
||||
Userid: &milerUserID,
|
||||
Eventstatus: constants.ConsignmentInwardedAtHub,
|
||||
Remarks: "Package collected by miler and converted to consignment",
|
||||
}
|
||||
tx.Create(&history)
|
||||
|
||||
tx.Model(&models.MilerProfile{}).Where("userid = ?", milerUserID).Update("availabilitystatus", constants.MilerAvailable)
|
||||
|
||||
tx.Commit()
|
||||
|
||||
return utils.OK(c, fiber.Map{
|
||||
"tracking_no": trackingNo,
|
||||
"consignment_id": consignment.Consignmentid,
|
||||
"booking_no": booking.Bookingno,
|
||||
})
|
||||
}
|
||||
|
||||
func BookingVehicleRequiredEscalate(c *fiber.Ctx) error {
|
||||
milerUserID := c.Locals("userid").(int)
|
||||
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid booking ID")
|
||||
}
|
||||
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.Where("bookingid = ? AND assignedmileruserid = ?", bookingID, milerUserID).First(&booking).Error; err != nil {
|
||||
return utils.NotFound(c, "assigned booking not found")
|
||||
}
|
||||
|
||||
reqVeh := models.BookingVehicleRequirement{
|
||||
Bookingid: bookingID,
|
||||
Requiredvehicletype: c.Query("type", "truck"),
|
||||
Reason: c.Query("reason", "Package is too large for bike rider"),
|
||||
Status: "Required",
|
||||
}
|
||||
|
||||
if err := db.DB.Create(&reqVeh).Error; err != nil {
|
||||
return utils.Internal(c, "failed to register vehicle requirement")
|
||||
}
|
||||
|
||||
return utils.Created(c, reqVeh)
|
||||
}
|
||||
|
||||
func CreateMilerPeriodicLog(c *fiber.Ctx) error {
|
||||
ctx := context.Background()
|
||||
|
||||
var log models.MilerLog
|
||||
if err := c.BodyParser(&log); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
t, err := time.Parse("2006-01-02 15:04:05", log.LogDate)
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid logdate format — expected YYYY-MM-DD HH:MM:SS")
|
||||
}
|
||||
|
||||
timestamp := t.Unix()
|
||||
logKey := fmt.Sprintf("miler_periodic_log:%d:%d", log.UserID, timestamp)
|
||||
data, _ := json.Marshal(log)
|
||||
|
||||
if db.Rdb != nil {
|
||||
if err := db.Rdb.Set(ctx, logKey, data, 0).Err(); err != nil {
|
||||
return utils.Internal(c, "failed to store log")
|
||||
}
|
||||
|
||||
userZsetKey := fmt.Sprintf("miler_periodic_logs:%d", log.UserID)
|
||||
db.Rdb.ZAdd(ctx, userZsetKey, redis.Z{Score: float64(timestamp), Member: logKey})
|
||||
db.Rdb.ZAdd(ctx, "miler_periodic_logs_all", redis.Z{Score: float64(timestamp), Member: logKey})
|
||||
}
|
||||
|
||||
return utils.Message(c, "miler periodic log stored successfully")
|
||||
}
|
||||
|
||||
func GetMilerPeriodicLogs(c *fiber.Ctx) error {
|
||||
ctx := context.Background()
|
||||
|
||||
if db.Rdb == nil {
|
||||
return utils.Internal(c, "cache service unavailable")
|
||||
}
|
||||
|
||||
userID := c.Query("userid")
|
||||
|
||||
var keys []string
|
||||
var err error
|
||||
|
||||
if userID != "" {
|
||||
zsetKey := fmt.Sprintf("miler_periodic_logs:%s", userID)
|
||||
keys, err = db.Rdb.ZRevRange(ctx, zsetKey, 0, 0).Result()
|
||||
} else {
|
||||
keys, err = db.Rdb.ZRevRange(ctx, "miler_periodic_logs_all", 0, 0).Result()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to fetch logs")
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
return utils.List(c, []interface{}{}, 0)
|
||||
}
|
||||
|
||||
val, err := db.Rdb.Get(ctx, keys[0]).Result()
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to retrieve log data")
|
||||
}
|
||||
|
||||
var log map[string]interface{}
|
||||
json.Unmarshal([]byte(val), &log)
|
||||
|
||||
return utils.OK(c, log)
|
||||
}
|
||||
|
||||
func CreateMilerStatus(c *fiber.Ctx) error {
|
||||
ctx := context.Background()
|
||||
|
||||
var status models.MilerStatus
|
||||
if err := c.BodyParser(&status); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if status.UserID == 0 || status.Status == "" {
|
||||
return utils.BadRequest(c, "userid and status are required")
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("miler_status:%d", status.UserID)
|
||||
data, _ := json.Marshal(status)
|
||||
|
||||
if db.Rdb != nil {
|
||||
if err := db.Rdb.Set(ctx, key, data, 0).Err(); err != nil {
|
||||
return utils.Internal(c, "failed to store status")
|
||||
}
|
||||
|
||||
db.Rdb.ZAdd(ctx, "miler_status_all", redis.Z{
|
||||
Score: float64(time.Now().Unix()),
|
||||
Member: key,
|
||||
})
|
||||
}
|
||||
|
||||
return utils.Message(c, "miler status updated successfully")
|
||||
}
|
||||
|
||||
func GetMilerStatus(c *fiber.Ctx) error {
|
||||
ctx := context.Background()
|
||||
|
||||
if db.Rdb == nil {
|
||||
return utils.Internal(c, "cache service unavailable")
|
||||
}
|
||||
|
||||
userIDStr := c.Query("userid")
|
||||
|
||||
if userIDStr != "" {
|
||||
key := fmt.Sprintf("miler_status:%s", userIDStr)
|
||||
|
||||
val, err := db.Rdb.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
return utils.NotFound(c, "status not found for this miler")
|
||||
}
|
||||
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal([]byte(val), &data)
|
||||
|
||||
return utils.OK(c, data)
|
||||
}
|
||||
|
||||
pageStr := c.Query("page")
|
||||
pageSizeStr := c.Query("pagesize")
|
||||
|
||||
page, _ := strconv.Atoi(pageStr)
|
||||
pageSize, _ := strconv.Atoi(pageSizeStr)
|
||||
|
||||
var start, end int64
|
||||
if page > 0 && pageSize > 0 {
|
||||
offset := (page - 1) * pageSize
|
||||
start = int64(offset)
|
||||
end = int64(offset + pageSize - 1)
|
||||
} else {
|
||||
start = 0
|
||||
end = -1
|
||||
}
|
||||
|
||||
keys, err := db.Rdb.ZRevRange(ctx, "miler_status_all", start, end).Result()
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to fetch statuses")
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
return utils.List(c, []interface{}{}, 0)
|
||||
}
|
||||
|
||||
values, err := db.Rdb.MGet(ctx, keys...).Result()
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to retrieve status data")
|
||||
}
|
||||
|
||||
var result []map[string]interface{}
|
||||
for _, val := range values {
|
||||
if val == nil {
|
||||
continue
|
||||
}
|
||||
var item map[string]interface{}
|
||||
json.Unmarshal([]byte(val.(string)), &item)
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
return utils.List(c, result, int64(len(result)))
|
||||
}
|
||||
|
||||
func PublishConsignmentLogs(c *fiber.Ctx) error {
|
||||
var input []models.ConsignmentLog
|
||||
if err := c.BodyParser(&input); err != nil {
|
||||
return utils.BadRequest(c, "invalid request body")
|
||||
}
|
||||
|
||||
if len(input) == 0 {
|
||||
return utils.BadRequest(c, "at least one log entry is required")
|
||||
}
|
||||
|
||||
if db.Rdb == nil {
|
||||
return utils.Internal(c, "cache service unavailable")
|
||||
}
|
||||
|
||||
pipe := db.Rdb.TxPipeline()
|
||||
tx := db.DB.Begin()
|
||||
|
||||
for _, item := range input {
|
||||
logTime, err := time.Parse("2006-01-02 15:04:05", item.LogDate)
|
||||
if err != nil {
|
||||
logTime = time.Now()
|
||||
}
|
||||
|
||||
ts := logTime.Unix()
|
||||
logKey := "Consignmentlogs:" + strconv.Itoa(item.ConsignmentID)
|
||||
userIndexKey := "user:consignmentlogs:" + strconv.Itoa(item.UserID)
|
||||
|
||||
jsonData, _ := json.Marshal(item)
|
||||
|
||||
pipe.RPush(db.Ctx, logKey, jsonData)
|
||||
pipe.ZAdd(db.Ctx, userIndexKey, redis.Z{
|
||||
Score: float64(ts),
|
||||
Member: item.ConsignmentID,
|
||||
})
|
||||
|
||||
history := models.ConsignmentHistory{
|
||||
Consignmentid: item.ConsignmentID,
|
||||
Userid: &item.UserID,
|
||||
Eventstatus: item.Status,
|
||||
Remarks: fmt.Sprintf("GPS Update: Lat %s, Lon %s. Speed %s. Remarks: %s", item.Latitude, item.Longitude, item.Speed, item.Remarks),
|
||||
Createdat: logTime,
|
||||
}
|
||||
if err := tx.Create(&history).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to persist consignment log")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := pipe.Exec(db.Ctx); err != nil {
|
||||
tx.Rollback()
|
||||
return utils.Internal(c, "failed to publish logs to cache")
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
return utils.Message(c, "consignment logs published successfully")
|
||||
}
|
||||
|
||||
func GetConsignmentLogs(c *fiber.Ctx) error {
|
||||
consignmentID, err := strconv.Atoi(c.Params("consignmentid"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid consignment ID")
|
||||
}
|
||||
|
||||
if db.Rdb == nil {
|
||||
return utils.Internal(c, "cache service unavailable")
|
||||
}
|
||||
|
||||
logKey := "Consignmentlogs:" + strconv.Itoa(consignmentID)
|
||||
redisList, err := db.Rdb.LRange(db.Ctx, logKey, 0, -1).Result()
|
||||
|
||||
if err == nil && len(redisList) > 0 {
|
||||
var logs []map[string]interface{}
|
||||
for _, raw := range redisList {
|
||||
var m map[string]interface{}
|
||||
json.Unmarshal([]byte(raw), &m)
|
||||
logs = append(logs, m)
|
||||
}
|
||||
return utils.List(c, logs, int64(len(logs)))
|
||||
}
|
||||
|
||||
var history []models.ConsignmentHistory
|
||||
if err := db.DB.Where("consignmentid = ?", consignmentID).Order("createdat ASC").Find(&history).Error; err != nil {
|
||||
return utils.Internal(c, "failed to fetch consignment logs")
|
||||
}
|
||||
|
||||
return utils.List(c, history, int64(len(history)))
|
||||
}
|
||||
|
||||
func GetUserConsignmentLogs(c *fiber.Ctx) error {
|
||||
userID, err := strconv.Atoi(c.Params("userid"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid user ID")
|
||||
}
|
||||
|
||||
if db.Rdb == nil {
|
||||
return utils.Internal(c, "cache service unavailable")
|
||||
}
|
||||
|
||||
userIndexKey := "user:consignmentlogs:" + strconv.Itoa(userID)
|
||||
members, err := db.Rdb.ZRevRange(db.Ctx, userIndexKey, 0, -1).Result()
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to fetch consignment log index")
|
||||
}
|
||||
|
||||
var logs []map[string]interface{}
|
||||
for _, consignmentIDStr := range members {
|
||||
logKey := "Consignmentlogs:" + consignmentIDStr
|
||||
rawList, err := db.Rdb.LRange(db.Ctx, logKey, -1, -1).Result()
|
||||
if err == nil && len(rawList) > 0 {
|
||||
var m map[string]interface{}
|
||||
json.Unmarshal([]byte(rawList[0]), &m)
|
||||
logs = append(logs, m)
|
||||
}
|
||||
}
|
||||
|
||||
return utils.List(c, logs, int64(len(logs)))
|
||||
}
|
||||
Reference in New Issue
Block a user