aboutsummaryrefslogtreecommitdiffstats
path: root/backend/internal/api
diff options
context:
space:
mode:
Diffstat (limited to 'backend/internal/api')
-rw-r--r--backend/internal/api/handlers/goal_handler.go128
-rw-r--r--backend/internal/api/handlers/loan_handler.go312
-rw-r--r--backend/internal/api/v1/goals/goals.go26
-rw-r--r--backend/internal/api/v1/loans/loans.go25
4 files changed, 489 insertions, 2 deletions
diff --git a/backend/internal/api/handlers/goal_handler.go b/backend/internal/api/handlers/goal_handler.go
index 09bea74..efbe7f9 100644
--- a/backend/internal/api/handlers/goal_handler.go
+++ b/backend/internal/api/handlers/goal_handler.go
@@ -6,6 +6,7 @@ import (
"strconv"
"time"
+ "finance/backend/internal/core"
"finance/backend/internal/database"
"finance/backend/internal/models"
@@ -35,13 +36,21 @@ type UpdateGoalProgressInput struct {
CurrentAmount int64 `json:"currentAmount" binding:"required"`
}
+// LinkTransactionInput defines the structure for linking a transaction to a goal
+type LinkTransactionInput struct {
+ TransactionID uint `json:"transactionId" binding:"required"`
+}
+
// GoalHandler handles all goal-related operations in the API
type GoalHandler struct {
+ goalService *core.GoalService
}
// NewGoalHandler creates and returns a new GoalHandler instance
func NewGoalHandler() *GoalHandler {
- return &GoalHandler{}
+ return &GoalHandler{
+ goalService: core.NewGoalService(),
+ }
}
// GetGoals retrieves all goals for the authenticated user
@@ -270,3 +279,120 @@ func (h *GoalHandler) DeleteGoal(c *gin.Context) {
log.Printf("Goal ID %d deleted successfully for user %d", goalID, userID)
c.JSON(http.StatusOK, gin.H{"message": "Goal deleted successfully"})
}
+
+// GetGoalProgressDetails retrieves a goal with detailed progress information
+func (h *GoalHandler) GetGoalProgressDetails(c *gin.Context) {
+ userID := c.MustGet("userID").(uint)
+ goalID, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ log.Printf("Error parsing goal ID: %v", err)
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"})
+ return
+ }
+
+ progress, err := h.goalService.GetGoalProgress(userID, uint(goalID))
+ if err != nil {
+ log.Printf("Error fetching goal progress for ID %d, user %d: %v", goalID, userID, err)
+ c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"})
+ return
+ }
+
+ c.JSON(http.StatusOK, progress)
+}
+
+// GetAllGoalsProgressDetails retrieves all goals with enhanced progress details
+func (h *GoalHandler) GetAllGoalsProgressDetails(c *gin.Context) {
+ userID := c.MustGet("userID").(uint)
+
+ // Filter by status if provided
+ status := c.Query("status")
+
+ progress, err := h.goalService.GetAllGoalsProgress(userID, status)
+ if err != nil {
+ log.Printf("Error fetching all goals progress for user %d: %v", userID, err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get goal progress"})
+ return
+ }
+
+ c.JSON(http.StatusOK, progress)
+}
+
+// LinkTransactionToGoal links a transaction to a specific goal
+func (h *GoalHandler) LinkTransactionToGoal(c *gin.Context) {
+ userID := c.MustGet("userID").(uint)
+ goalID, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ log.Printf("Error parsing goal ID: %v", err)
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"})
+ return
+ }
+
+ var input LinkTransactionInput
+ if err := c.ShouldBindJSON(&input); err != nil {
+ log.Printf("Error binding JSON for linking transaction: %v", err)
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ // Verify the goal belongs to the user
+ var goal models.Goal
+ if err := database.DB.Where("id = ? AND user_id = ?", goalID, userID).First(&goal).Error; err != nil {
+ log.Printf("Error fetching goal ID %d for user %d: %v", goalID, userID, err)
+ c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"})
+ return
+ }
+
+ // Verify the transaction belongs to the user
+ var transaction models.Transaction
+ if err := database.DB.Where("id = ? AND user_id = ?", input.TransactionID, userID).First(&transaction).Error; err != nil {
+ log.Printf("Error fetching transaction ID %d for user %d: %v", input.TransactionID, userID, err)
+ c.JSON(http.StatusNotFound, gin.H{"error": "Transaction not found"})
+ return
+ }
+
+ // Link the transaction to the goal
+ if err := h.goalService.LinkTransactionToGoal(input.TransactionID, uint(goalID)); err != nil {
+ log.Printf("Error linking transaction %d to goal %d: %v", input.TransactionID, goalID, err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to link transaction to goal"})
+ return
+ }
+
+ log.Printf("Transaction ID %d successfully linked to goal ID %d for user %d", input.TransactionID, goalID, userID)
+ c.JSON(http.StatusOK, gin.H{"message": "Transaction linked to goal successfully"})
+}
+
+// RecalculateGoalProgress recalculates a goal's progress based on linked transactions
+func (h *GoalHandler) RecalculateGoalProgress(c *gin.Context) {
+ userID := c.MustGet("userID").(uint)
+ goalID, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ log.Printf("Error parsing goal ID: %v", err)
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"})
+ return
+ }
+
+ // Verify the goal belongs to the user
+ var goal models.Goal
+ if err := database.DB.Where("id = ? AND user_id = ?", goalID, userID).First(&goal).Error; err != nil {
+ log.Printf("Error fetching goal ID %d for user %d: %v", goalID, userID, err)
+ c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"})
+ return
+ }
+
+ // Recalculate the goal progress
+ if err := h.goalService.UpdateGoalFromTransactions(uint(goalID)); err != nil {
+ log.Printf("Error recalculating goal progress for ID %d: %v", goalID, err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to recalculate goal progress"})
+ return
+ }
+
+ // Fetch the updated goal to return in response
+ if err := database.DB.First(&goal, goalID).Error; err != nil {
+ log.Printf("Error fetching updated goal after recalculation: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch updated goal"})
+ return
+ }
+
+ log.Printf("Goal ID %d progress recalculated successfully for user %d", goalID, userID)
+ c.JSON(http.StatusOK, goal)
+}
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
+}
diff --git a/backend/internal/api/v1/goals/goals.go b/backend/internal/api/v1/goals/goals.go
index 1d2cd6f..4f99468 100644
--- a/backend/internal/api/v1/goals/goals.go
+++ b/backend/internal/api/v1/goals/goals.go
@@ -1,7 +1,7 @@
package goals
import (
- "finance/backend/handlers"
+ "finance/backend/internal/api/handlers"
"github.com/gin-gonic/gin"
)
@@ -41,3 +41,27 @@ func UpdateGoalProgress() gin.HandlerFunc {
handler := handlers.NewGoalHandler()
return handler.UpdateGoalProgress
}
+
+// GetGoalProgressDetails returns goal with enhanced progress tracking details
+func GetGoalProgressDetails() gin.HandlerFunc {
+ handler := handlers.NewGoalHandler()
+ return handler.GetGoalProgressDetails
+}
+
+// GetAllGoalsProgressDetails returns all goals with enhanced progress tracking details
+func GetAllGoalsProgressDetails() gin.HandlerFunc {
+ handler := handlers.NewGoalHandler()
+ return handler.GetAllGoalsProgressDetails
+}
+
+// LinkTransactionToGoal links a transaction to a goal for progress tracking
+func LinkTransactionToGoal() gin.HandlerFunc {
+ handler := handlers.NewGoalHandler()
+ return handler.LinkTransactionToGoal
+}
+
+// RecalculateGoalProgress recalculates the progress of a goal based on its transactions
+func RecalculateGoalProgress() gin.HandlerFunc {
+ handler := handlers.NewGoalHandler()
+ return handler.RecalculateGoalProgress
+}
diff --git a/backend/internal/api/v1/loans/loans.go b/backend/internal/api/v1/loans/loans.go
index 1366b3b..49ab320 100644
--- a/backend/internal/api/v1/loans/loans.go
+++ b/backend/internal/api/v1/loans/loans.go
@@ -5,6 +5,7 @@ import (
"strconv"
"time"
+ "finance/backend/internal/api/handlers"
"finance/backend/internal/database"
"finance/backend/internal/models"
@@ -246,3 +247,27 @@ func DeleteLoan() gin.HandlerFunc {
c.JSON(http.StatusOK, gin.H{"message": "Loan deleted successfully"})
}
}
+
+// GetLoanPayments returns all payments for a specific loan
+func GetLoanPayments() gin.HandlerFunc {
+ handler := handlers.NewLoanHandler()
+ return handler.GetLoanPayments
+}
+
+// CreateLoanPayment creates a new payment for a loan
+func CreateLoanPayment() gin.HandlerFunc {
+ handler := handlers.NewLoanHandler()
+ return handler.CreateLoanPayment
+}
+
+// DeleteLoanPayment deletes a loan payment
+func DeleteLoanPayment() gin.HandlerFunc {
+ handler := handlers.NewLoanHandler()
+ return handler.DeleteLoanPayment
+}
+
+// GetLoanPaymentSchedule generates an estimated payment schedule for a loan
+func GetLoanPaymentSchedule() gin.HandlerFunc {
+ handler := handlers.NewLoanHandler()
+ return handler.GetLoanPaymentSchedule
+}