package handlers import ( "log" "net/http" "strconv" "time" "finance/backend/internal/database" "finance/backend/internal/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // LoanHandler handles loan-related operations type LoanHandler struct { } // NewLoanHandler creates and returns a new LoanHandler instance func NewLoanHandler() *LoanHandler { return &LoanHandler{} } // GetLoans retrieves all loans for the authenticated user func (h *LoanHandler) GetLoans(c *gin.Context) { userID := c.MustGet("userID").(uint) var loans []models.Loan if err := database.DB.Where("user_id = ?", userID).Find(&loans).Error; err != nil { log.Printf("Error fetching loans for user %d: %v", userID, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get loans"}) return } c.JSON(http.StatusOK, loans) } // GetLoanByID retrieves a specific loan by ID for the authenticated user func (h *LoanHandler) GetLoanByID(c *gin.Context) { userID := c.MustGet("userID").(uint) loanID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid loan ID"}) return } var loan models.Loan if err := database.DB.Where("id = ? AND user_id = ?", loanID, userID).First(&loan).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Loan not found"}) return } c.JSON(http.StatusOK, loan) } // CreateLoan creates a new loan for the authenticated user func (h *LoanHandler) CreateLoan(c *gin.Context) { userID := c.MustGet("userID").(uint) // Define input structure type CreateLoanInput struct { Name string `json:"name" binding:"required"` OriginalAmount int64 `json:"originalAmount" binding:"required"` CurrentBalance int64 `json:"currentBalance" binding:"required"` InterestRate float64 `json:"interestRate"` StartDate string `json:"startDate"` EndDate string `json:"endDate"` AccountID *uint `json:"accountId"` } var input CreateLoanInput if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Parse dates if provided var startDate, endDate time.Time var err error if input.StartDate != "" { startDate, err = time.Parse("2006-01-02", input.StartDate) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid start date format. Use YYYY-MM-DD"}) return } } if input.EndDate != "" { endDate, err = time.Parse("2006-01-02", input.EndDate) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid end date format. Use YYYY-MM-DD"}) return } } // Validate account if provided if input.AccountID != nil { var account models.Account if err := database.DB.Where("id = ? AND user_id = ?", *input.AccountID, userID).First(&account).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Associated account not found or doesn't belong to user"}) return } } // Create loan loan := models.Loan{ UserID: userID, AccountID: input.AccountID, Name: input.Name, OriginalAmount: input.OriginalAmount, CurrentBalance: input.CurrentBalance, InterestRate: input.InterestRate, } // Only set dates if they were provided if !startDate.IsZero() { loan.StartDate = startDate } if !endDate.IsZero() { loan.EndDate = endDate } if err := database.DB.Create(&loan).Error; err != nil { log.Printf("Error creating loan for user %d: %v", userID, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create loan"}) return } c.JSON(http.StatusCreated, loan) } // UpdateLoan updates an existing loan for the authenticated user func (h *LoanHandler) UpdateLoan(c *gin.Context) { userID := c.MustGet("userID").(uint) loanID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid loan ID"}) return } // Find loan var loan models.Loan if err := database.DB.Where("id = ? AND user_id = ?", loanID, userID).First(&loan).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Loan not found"}) return } // Define update structure type UpdateLoanInput struct { Name string `json:"name"` OriginalAmount int64 `json:"originalAmount"` CurrentBalance int64 `json:"currentBalance"` InterestRate float64 `json:"interestRate"` StartDate string `json:"startDate"` EndDate string `json:"endDate"` AccountID *uint `json:"accountId"` } var input UpdateLoanInput if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Update fields if provided if input.Name != "" { loan.Name = input.Name } if input.OriginalAmount != 0 { loan.OriginalAmount = input.OriginalAmount } if input.CurrentBalance != 0 { loan.CurrentBalance = input.CurrentBalance } if input.InterestRate != 0 { loan.InterestRate = input.InterestRate } if input.StartDate != "" { startDate, err := time.Parse("2006-01-02", input.StartDate) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid start date format. Use YYYY-MM-DD"}) return } loan.StartDate = startDate } if input.EndDate != "" { endDate, err := time.Parse("2006-01-02", input.EndDate) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid end date format. Use YYYY-MM-DD"}) return } loan.EndDate = endDate } if input.AccountID != nil { // Validate account if provided var account models.Account if err := database.DB.Where("id = ? AND user_id = ?", *input.AccountID, userID).First(&account).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Associated account not found or doesn't belong to user"}) return } loan.AccountID = input.AccountID } // Save changes if err := database.DB.Save(&loan).Error; err != nil { log.Printf("Error updating loan ID %d for user %d: %v", loanID, userID, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update loan"}) return } c.JSON(http.StatusOK, loan) } // DeleteLoan deletes a loan belonging to the authenticated user func (h *LoanHandler) DeleteLoan(c *gin.Context) { userID := c.MustGet("userID").(uint) loanID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid loan ID"}) return } // Check if loan exists and belongs to user var loan models.Loan if err := database.DB.Where("id = ? AND user_id = ?", loanID, userID).First(&loan).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Loan not found"}) return } // Delete loan if err := database.DB.Delete(&loan).Error; err != nil { log.Printf("Error deleting loan ID %d for user %d: %v", loanID, userID, err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete loan"}) return } c.JSON(http.StatusOK, gin.H{"message": "Loan deleted successfully"}) } // CreateLoanPayment creates a new payment for a loan func (h *LoanHandler) CreateLoanPayment(c *gin.Context) { // Get user from context (set by auth middleware) user, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } userObj := user.(models.User) // Get loan ID from URL parameter loanID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid loan ID format"}) return } // Check if the loan exists and belongs to the user var loan models.Loan if err := database.DB.Where("id = ? AND user_id = ?", loanID, userObj.ID).First(&loan).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Loan not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch loan"}) } return } // Define a struct to bind the request JSON var input struct { Amount int64 `json:"amount" binding:"required"` PaymentDate string `json:"paymentDate" binding:"required"` Principal int64 `json:"principal"` Interest int64 `json:"interest"` TransactionID *uint `json:"transactionId"` Notes string `json:"notes"` } // Bind JSON to struct if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Parse date paymentDate, err := time.Parse("2006-01-02", input.PaymentDate) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payment date format"}) return } // Create payment object payment := models.LoanPayment{ UserID: userObj.ID, LoanID: uint(loanID), Amount: input.Amount, PaymentDate: paymentDate, Principal: input.Principal, Interest: input.Interest, TransactionID: input.TransactionID, Notes: input.Notes, } // Save to database in a transaction tx := database.DB.Begin() if err := tx.Create(&payment).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create payment"}) return } // Update loan balance loan.CurrentBalance -= input.Principal if err := tx.Save(&loan).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update loan balance"}) return } tx.Commit() c.JSON(http.StatusCreated, gin.H{"payment": payment, "updatedLoanBalance": loan.CurrentBalance}) } // GetLoanPayments returns all payments for a specific loan func (h *LoanHandler) GetLoanPayments(c *gin.Context) { // Get user from context (set by auth middleware) user, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } userObj := user.(models.User) // Get loan ID from URL parameter loanID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid loan ID format"}) return } // Check if the loan exists and belongs to the user var loan models.Loan if err := database.DB.Where("id = ? AND user_id = ?", loanID, userObj.ID).First(&loan).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Loan not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch loan"}) } return } // Fetch all payments for the loan var payments []models.LoanPayment if err := database.DB.Where("loan_id = ?", loanID).Order("payment_date DESC").Find(&payments).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch loan payments"}) return } c.JSON(http.StatusOK, gin.H{"payments": payments}) } // DeleteLoanPayment deletes a payment for a loan func (h *LoanHandler) DeleteLoanPayment(c *gin.Context) { // Get user from context (set by auth middleware) user, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } userObj := user.(models.User) // Get payment ID from URL parameter paymentID, err := strconv.ParseUint(c.Param("paymentId"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payment ID format"}) return } // Check if the payment exists and belongs to the user var payment models.LoanPayment if err := database.DB.Where("id = ? AND user_id = ?", paymentID, userObj.ID).First(&payment).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Payment not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch payment"}) } return } // Get the loan to update its balance var loan models.Loan if err := database.DB.First(&loan, payment.LoanID).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch related loan"}) return } // Update loan and delete payment in a transaction tx := database.DB.Begin() // Reverse the principal payment (add it back to the loan balance) loan.CurrentBalance += payment.Principal if err := tx.Save(&loan).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update loan balance"}) return } // Delete the payment if err := tx.Delete(&payment).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete payment"}) return } tx.Commit() c.JSON(http.StatusOK, gin.H{"message": "Payment deleted successfully", "updatedLoanBalance": loan.CurrentBalance}) } // GetLoanPaymentSchedule generates an estimated payment schedule for a loan func (h *LoanHandler) GetLoanPaymentSchedule(c *gin.Context) { // Get user from context (set by auth middleware) user, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } userObj := user.(models.User) // Get loan ID from URL parameter loanID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid loan ID format"}) return } // Check if the loan exists and belongs to the user var loan models.Loan if err := database.DB.Where("id = ? AND user_id = ?", loanID, userObj.ID).First(&loan).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Loan not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch loan"}) } return } // Parse payment frequency parameter (defaults to monthly) frequency := c.DefaultQuery("frequency", "monthly") if frequency != "monthly" && frequency != "biweekly" && frequency != "weekly" { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid frequency. Allowed values: monthly, biweekly, weekly"}) return } // Calculate remaining months (approximately) now := time.Now() remainingMonths := int(loan.EndDate.Sub(now).Hours() / 24 / 30) if remainingMonths <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Loan end date is in the past"}) return } // Calculate number of payments based on frequency paymentsCount := remainingMonths var intervalDays int if frequency == "weekly" { paymentsCount = remainingMonths * 4 // ~4 weeks per month intervalDays = 7 } else if frequency == "biweekly" { paymentsCount = remainingMonths * 2 // ~2 bi-weeks per month intervalDays = 14 } else { // Monthly intervalDays = 30 } // Simple amortization calculation rate := loan.InterestRate / 12 / 100 // Monthly interest rate if frequency == "weekly" { rate = loan.InterestRate / 52 / 100 } else if frequency == "biweekly" { rate = loan.InterestRate / 26 / 100 } // For zero interest loans var paymentAmount int64 if loan.InterestRate <= 0 { paymentAmount = loan.CurrentBalance / int64(paymentsCount) } else { // Formula: PMT = P * (r * (1+r)^n) / ((1+r)^n - 1) // Where PMT = payment, P = principal, r = rate per period, n = number of periods // Simplified calculation (not exact but good approximation) totalWithInterest := float64(loan.CurrentBalance) * (1 + float64(paymentsCount)*rate) paymentAmount = int64(totalWithInterest) / int64(paymentsCount) } // Generate payment schedule var schedule []map[string]interface{} balance := loan.CurrentBalance currentDate := now for i := 0; i < paymentsCount && balance > 0; i++ { // Calculate interest for this period interestPayment := int64(float64(balance) * rate) principalPayment := paymentAmount - interestPayment // Adjust last payment if needed if principalPayment > balance { principalPayment = balance paymentAmount = principalPayment + interestPayment } // Update remaining balance balance -= principalPayment // Create payment entry payment := map[string]interface{}{ "paymentNumber": i + 1, "date": currentDate.Format("2006-01-02"), "totalPayment": paymentAmount, "principalPayment": principalPayment, "interestPayment": interestPayment, "remainingBalance": balance, } schedule = append(schedule, payment) // Increment date based on interval currentDate = currentDate.AddDate(0, 0, intervalDays) } c.JSON(http.StatusOK, gin.H{ "loanDetails": loan, "schedule": schedule, "summary": map[string]interface{}{ "totalPayments": len(schedule), "frequency": frequency, "estimatedPayoffDate": schedule[len(schedule)-1]["date"], "totalInterestPaid": sumInterest(schedule), }, }) } // Helper function to sum up total interest in a payment schedule func sumInterest(schedule []map[string]interface{}) int64 { var total int64 for _, payment := range schedule { total += payment["interestPayment"].(int64) } return total }