aboutsummaryrefslogtreecommitdiffstats
path: root/backend/internal/api/handlers/loan_handler.go
diff options
context:
space:
mode:
authorLibravatarLibravatar Biswa Kalyan Bhuyan <biswa@surgot.in> 2025-04-28 08:32:07 +0530
committerLibravatarLibravatar Biswa Kalyan Bhuyan <biswa@surgot.in> 2025-04-28 08:32:07 +0530
commitd2c03d9417fb289d455f80f4c6facd7274c31d3e (patch)
tree3de135eb932ff20aa50abfb39d5a8abba4758d65 /backend/internal/api/handlers/loan_handler.go
parent538d933baef56d7ee76f78617b553d63713efa24 (diff)
downloadfinance-d2c03d9417fb289d455f80f4c6facd7274c31d3e.tar.gz
finance-d2c03d9417fb289d455f80f4c6facd7274c31d3e.tar.bz2
finance-d2c03d9417fb289d455f80f4c6facd7274c31d3e.zip
finance/backend: feat: Refined APIs for Loans and Goals based on frontend needsHEADmaster
Diffstat (limited to 'backend/internal/api/handlers/loan_handler.go')
-rw-r--r--backend/internal/api/handlers/loan_handler.go312
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
+}