From 588f27b91710916d1875ee899c3978d23ac00c23 Mon Sep 17 00:00:00 2001 From: Suriya Date: Mon, 22 Jun 2026 17:38:05 +0530 Subject: [PATCH] feat: Add rider substitution system --- controllers/substitutionController.go | 235 ++++++++++++++++++++++++++ create_table.py | 43 +++++ db/connect.go | 4 + migrate_db.go | 26 +++ models/rider_substitutions.go | 18 ++ routes/routes.go | 8 + 6 files changed, 334 insertions(+) create mode 100644 controllers/substitutionController.go create mode 100644 create_table.py create mode 100644 migrate_db.go create mode 100644 models/rider_substitutions.go diff --git a/controllers/substitutionController.go b/controllers/substitutionController.go new file mode 100644 index 0000000..62c204a --- /dev/null +++ b/controllers/substitutionController.go @@ -0,0 +1,235 @@ +package controllers + +import ( + "nearle/db" + "nearle/models" + "net/http" + "strconv" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm/clause" +) + +func GetSubstitutions(c *fiber.Ctx) error { + tenantID, _ := strconv.Atoi(c.Query("tenant_id")) + if tenantID == 0 { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "code": http.StatusBadRequest, + "message": "Missing tenant_id", + "status": false, + }) + } + + date := c.Query("date") + fromDate := c.Query("from_date") + toDate := c.Query("to_date") + status := c.Query("status") + + query := db.DB.Model(&models.RiderSubstitution{}).Where("tenant_id = ?", tenantID) + + if date != "" { + query = query.Where("sub_date = ?", date) + } + if fromDate != "" && toDate != "" { + query = query.Where("sub_date >= ? AND sub_date <= ?", fromDate, toDate) + } + if status != "" { + query = query.Where("status = ?", status) + } + + var results []models.RiderSubstitution + if err := query.Find(&results).Error; err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "code": http.StatusInternalServerError, + "message": err.Error(), + "status": false, + }) + } + + return c.JSON(fiber.Map{ + "code": http.StatusOK, + "count": len(results), + "details": results, + "status": true, + }) +} + +func GetSubstitutionByID(c *fiber.Ctx) error { + id := c.Params("id") + tenantID, _ := strconv.Atoi(c.Query("tenant_id")) + + var result models.RiderSubstitution + if err := db.DB.Where("id = ? AND tenant_id = ?", id, tenantID).First(&result).Error; err != nil { + return c.Status(http.StatusNotFound).JSON(fiber.Map{ + "code": http.StatusNotFound, + "message": "Record not found", + "status": false, + }) + } + + return c.JSON(fiber.Map{ + "code": http.StatusOK, + "details": result, + "status": true, + }) +} + +type CreateSubstitutionsRequest struct { + TenantID int `json:"tenant_id"` + Substitutions []models.RiderSubstitution `json:"substitutions"` +} + +func CreateSubstitutions(c *fiber.Ctx) error { + var req CreateSubstitutionsRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "code": http.StatusBadRequest, + "message": err.Error(), + "status": false, + }) + } + + for i := range req.Substitutions { + req.Substitutions[i].TenantID = req.TenantID + if req.Substitutions[i].Status == "" { + req.Substitutions[i].Status = "scheduled" + } + } + + if len(req.Substitutions) == 0 { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "code": http.StatusBadRequest, + "message": "No substitutions provided", + "status": false, + }) + } + + // Upsert to handle unique constraint on (tenant_id, sub_date, absent_rider_id) + if err := db.DB.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "tenant_id"}, {Name: "sub_date"}, {Name: "absent_rider_id"}}, + UpdateAll: true, + }).Create(&req.Substitutions).Error; err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "code": http.StatusInternalServerError, + "message": err.Error(), + "status": false, + }) + } + + return c.JSON(fiber.Map{ + "code": http.StatusOK, + "registered": len(req.Substitutions), + "details": req.Substitutions, + "status": true, + }) +} + +func UpdateSubstitution(c *fiber.Ctx) error { + id := c.Params("id") + var updateData map[string]interface{} + + if err := c.BodyParser(&updateData); err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "code": http.StatusBadRequest, + "message": err.Error(), + "status": false, + }) + } + + tenantIDStr := updateData["tenant_id"] + if tenantIDStr == nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "code": http.StatusBadRequest, + "message": "Missing tenant_id in body", + "status": false, + }) + } + + var tenantID int + switch v := tenantIDStr.(type) { + case float64: + tenantID = int(v) + case string: + tenantID, _ = strconv.Atoi(v) + } + + if err := db.DB.Model(&models.RiderSubstitution{}).Where("id = ? AND tenant_id = ?", id, tenantID).Updates(updateData).Error; err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "code": http.StatusInternalServerError, + "message": err.Error(), + "status": false, + }) + } + + return c.JSON(fiber.Map{ + "code": http.StatusOK, + "message": "Updated successfully", + "status": true, + }) +} + +func UpdateSubstitutionStatus(c *fiber.Ctx) error { + id := c.Params("id") + var req struct { + TenantID int `json:"tenant_id"` + Status string `json:"status"` + } + + if err := c.BodyParser(&req); err != nil { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "code": http.StatusBadRequest, + "message": err.Error(), + "status": false, + }) + } + + if req.TenantID == 0 || req.Status == "" { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "code": http.StatusBadRequest, + "message": "Missing tenant_id or status", + "status": false, + }) + } + + if err := db.DB.Model(&models.RiderSubstitution{}).Where("id = ? AND tenant_id = ?", id, req.TenantID).Update("status", req.Status).Error; err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "code": http.StatusInternalServerError, + "message": err.Error(), + "status": false, + }) + } + + return c.JSON(fiber.Map{ + "code": http.StatusOK, + "message": "Status updated successfully", + "status": true, + }) +} + +func DeleteSubstitution(c *fiber.Ctx) error { + id := c.Params("id") + tenantID, _ := strconv.Atoi(c.Query("tenant_id")) + + if tenantID == 0 { + return c.Status(http.StatusBadRequest).JSON(fiber.Map{ + "code": http.StatusBadRequest, + "message": "Missing tenant_id", + "status": false, + }) + } + + // Soft delete by updating status to 'cancelled' + if err := db.DB.Model(&models.RiderSubstitution{}).Where("id = ? AND tenant_id = ?", id, tenantID).Update("status", "cancelled").Error; err != nil { + return c.Status(http.StatusInternalServerError).JSON(fiber.Map{ + "code": http.StatusInternalServerError, + "message": err.Error(), + "status": false, + }) + } + + return c.JSON(fiber.Map{ + "code": http.StatusOK, + "message": "Deleted successfully", + "status": true, + }) +} diff --git a/create_table.py b/create_table.py new file mode 100644 index 0000000..9dbdc0b --- /dev/null +++ b/create_table.py @@ -0,0 +1,43 @@ +import psycopg2 + +def run(): + try: + conn = psycopg2.connect( + host="66.116.207.225", + port="6432", + database="nearledb", + user="admin", + password="Package@123#" + ) + cursor = conn.cursor() + + sql = """ + CREATE TABLE IF NOT EXISTS rider_substitutions ( + id SERIAL PRIMARY KEY, + tenant_id INT NOT NULL, + sub_date DATE NOT NULL, + absent_rider_id INT NOT NULL, + absent_rider_name VARCHAR(100) NOT NULL, + sub_rider_id INT NOT NULL, + sub_rider_name VARCHAR(100) NOT NULL, + reason VARCHAR(255) NULL, + status VARCHAR(50) NOT NULL DEFAULT 'scheduled', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(tenant_id, sub_date, absent_rider_id) + ); + """ + cursor.execute(sql) + conn.commit() + print("Table 'rider_substitutions' successfully created in remote database!") + + except Exception as e: + print(f"Error: {e}") + finally: + if 'cursor' in locals(): + cursor.close() + if 'conn' in locals(): + conn.close() + +if __name__ == "__main__": + run() diff --git a/db/connect.go b/db/connect.go index 2101a4e..f90a871 100644 --- a/db/connect.go +++ b/db/connect.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "nearle/models" "nearle/utils" "os" @@ -88,6 +89,9 @@ func setupDB(database *gorm.DB) { return } + // Auto-migrate the rider_substitutions table + database.AutoMigrate(&models.RiderSubstitution{}) + // 🔥 K8s SAFE CONFIG sqlDB.SetMaxOpenConns(30) // total open connections sqlDB.SetMaxIdleConns(5) // idle connections diff --git a/migrate_db.go b/migrate_db.go new file mode 100644 index 0000000..df9cdf8 --- /dev/null +++ b/migrate_db.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "log" + "nearle/db" + "nearle/models" + + "github.com/joho/godotenv" +) + +func main() { + if err := godotenv.Load(); err != nil { + log.Println("No .env file found, relying on environment variables") + } + + db.Connect() + + fmt.Println("Migrating RiderSubstitution table...") + err := db.DB.AutoMigrate(&models.RiderSubstitution{}) + if err != nil { + log.Fatalf("Failed to auto migrate: %v", err) + } + + fmt.Println("Successfully created rider_substitutions table in the database!") +} diff --git a/models/rider_substitutions.go b/models/rider_substitutions.go new file mode 100644 index 0000000..3c54082 --- /dev/null +++ b/models/rider_substitutions.go @@ -0,0 +1,18 @@ +package models + +import "time" + +// RiderSubstitution represents the rider_substitutions table +type RiderSubstitution struct { + ID int `json:"id" gorm:"primaryKey;autoIncrement"` + TenantID int `json:"tenant_id" gorm:"not null;uniqueIndex:uq_date_absent"` + SubDate string `json:"sub_date" gorm:"type:date;not null;uniqueIndex:uq_date_absent;index:idx_date;index:idx_tenant_date"` + AbsentRiderID int `json:"absent_rider_id" gorm:"not null;uniqueIndex:uq_date_absent"` + AbsentRiderName string `json:"absent_rider_name" gorm:"size:100;not null"` + SubRiderID int `json:"sub_rider_id" gorm:"not null"` + SubRiderName string `json:"sub_rider_name" gorm:"size:100;not null"` + Reason string `json:"reason" gorm:"size:255"` + Status string `json:"status" gorm:"type:enum('scheduled','active','completed','cancelled');default:'scheduled';not null;index:idx_status"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` +} diff --git a/routes/routes.go b/routes/routes.go index 1022070..7c575a5 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -163,6 +163,14 @@ func LiveSetup(app *fiber.App) { partner.Get("/getriderlogs", controllers.GetRiderLogsv1) partner.Delete("/deleteriderlog", controllers.DeleteRiderLogs) + substitutions := live.Group("/v1/substitutions") + substitutions.Get("/", controllers.GetSubstitutions) + substitutions.Get("/:id", controllers.GetSubstitutionByID) + substitutions.Post("/", controllers.CreateSubstitutions) + substitutions.Put("/:id", controllers.UpdateSubstitution) + substitutions.Patch("/:id/status", controllers.UpdateSubstitutionStatus) + substitutions.Delete("/:id", controllers.DeleteSubstitution) + invoice := live.Group("/v1/invoice") invoice.Get("/getseqno", controllers.InvoiceSeqno) invoice.Get("/getinvoiceorders", controllers.GetInvoiceOrders)