diff options
author | 2025-04-28 08:32:07 +0530 | |
---|---|---|
committer | 2025-04-28 08:32:07 +0530 | |
commit | d2c03d9417fb289d455f80f4c6facd7274c31d3e (patch) | |
tree | 3de135eb932ff20aa50abfb39d5a8abba4758d65 /backend/internal/api/handlers | |
parent | 538d933baef56d7ee76f78617b553d63713efa24 (diff) | |
download | finance-d2c03d9417fb289d455f80f4c6facd7274c31d3e.tar.gz finance-d2c03d9417fb289d455f80f4c6facd7274c31d3e.tar.bz2 finance-d2c03d9417fb289d455f80f4c6facd7274c31d3e.zip |
Diffstat (limited to 'backend/internal/api/handlers')
-rw-r--r-- | backend/internal/api/handlers/loan_handler.go | 312 |
1 files changed, 312 insertions, 0 deletions
diff --git a/backend/internal/api/handlers/loan_handler.go b/backend/internal/api/handlers/loan_handler.go index 3edb559..a80c4b7 100644 --- a/backend/internal/api/handlers/loan_handler.go +++ b/backend/internal/api/handlers/loan_handler.go @@ -10,6 +10,7 @@ import ( "finance/backend/internal/models" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) // LoanHandler handles loan-related operations @@ -237,3 +238,314 @@ func (h *LoanHandler) DeleteLoan(c *gin.Context) { 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 +} |