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" }