1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
|
package handlers
import (
"log"
"net/http"
"strconv"
"time"
"finance/backend/internal/core"
"finance/backend/internal/database"
"finance/backend/internal/models"
"github.com/gin-gonic/gin"
)
// CreateGoalInput defines the structure for creating a new financial goal
type CreateGoalInput struct {
Name string `json:"name" binding:"required"`
TargetAmount int64 `json:"targetAmount" binding:"required"`
CurrentAmount int64 `json:"currentAmount"`
TargetDate string `json:"targetDate"` // YYYY-MM-DD format
Status string `json:"status"`
}
// UpdateGoalInput defines the structure for updating an existing financial goal
type UpdateGoalInput struct {
Name string `json:"name"`
TargetAmount int64 `json:"targetAmount"`
CurrentAmount int64 `json:"currentAmount"`
TargetDate string `json:"targetDate"` // YYYY-MM-DD format
Status string `json:"status"`
}
// UpdateGoalProgressInput defines the structure for updating just the progress of a goal
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{
goalService: core.NewGoalService(),
}
}
// GetGoals retrieves all goals for the authenticated user
// Optionally filtered by status if provided as a query parameter
func (h *GoalHandler) GetGoals(c *gin.Context) {
userID := c.MustGet("userID").(uint)
var goals []models.Goal
query := database.DB.Where("user_id = ?", userID)
// Filter by status if provided
status := c.Query("status")
if status != "" {
query = query.Where("status = ?", status)
}
if err := query.Find(&goals).Error; err != nil {
log.Printf("Error fetching goals for user %d: %v", userID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get goals"})
return
}
c.JSON(http.StatusOK, goals)
}
// GetGoal retrieves a specific goal by ID for the authenticated user
func (h *GoalHandler) GetGoal(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 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
}
c.JSON(http.StatusOK, goal)
}
// CreateGoal creates a new financial goal for the authenticated user
func (h *GoalHandler) CreateGoal(c *gin.Context) {
userID := c.MustGet("userID").(uint)
var input CreateGoalInput
if err := c.ShouldBindJSON(&input); err != nil {
log.Printf("Error binding JSON for goal creation: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Set default status if not provided
status := "Active"
if input.Status != "" {
status = input.Status
}
// Parse target date if provided
var targetDate time.Time
if input.TargetDate != "" {
parsedDate, err := time.Parse("2006-01-02", input.TargetDate)
if err != nil {
log.Printf("Error parsing target date for goal creation: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid date format for targetDate. Use YYYY-MM-DD"})
return
}
targetDate = parsedDate
}
goal := models.Goal{
UserID: userID,
Name: input.Name,
TargetAmount: input.TargetAmount,
CurrentAmount: input.CurrentAmount,
Status: status,
}
// Only set target date if it was provided
if !targetDate.IsZero() {
goal.TargetDate = targetDate
}
if err := database.DB.Create(&goal).Error; err != nil {
log.Printf("Error creating goal for user %d: %v", userID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create goal"})
return
}
log.Printf("Goal created successfully for user %d: %s (ID: %d)", userID, goal.Name, goal.ID)
c.JSON(http.StatusCreated, goal)
}
// UpdateGoal updates an existing goal for the authenticated user
func (h *GoalHandler) UpdateGoal(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 for update: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"})
return
}
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 in update: %v", goalID, userID, err)
c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"})
return
}
var input UpdateGoalInput
if err := c.ShouldBindJSON(&input); err != nil {
log.Printf("Error binding JSON for goal update: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Update fields that were provided
if input.Name != "" {
goal.Name = input.Name
}
if input.TargetAmount != 0 {
goal.TargetAmount = input.TargetAmount
}
if input.CurrentAmount != 0 {
goal.CurrentAmount = input.CurrentAmount
}
if input.TargetDate != "" {
parsedDate, err := time.Parse("2006-01-02", input.TargetDate)
if err != nil {
log.Printf("Error parsing target date for goal update: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid date format for targetDate. Use YYYY-MM-DD"})
return
}
goal.TargetDate = parsedDate
}
if input.Status != "" {
goal.Status = input.Status
}
// Check if goal has been achieved
if goal.CurrentAmount >= goal.TargetAmount {
goal.Status = "Achieved"
log.Printf("Goal ID %d for user %d automatically marked as Achieved", goalID, userID)
}
if err := database.DB.Save(&goal).Error; err != nil {
log.Printf("Error saving updated goal ID %d for user %d: %v", goalID, userID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update goal"})
return
}
log.Printf("Goal ID %d updated successfully for user %d", goalID, userID)
c.JSON(http.StatusOK, goal)
}
// UpdateGoalProgress updates just the progress (current amount) of a goal
func (h *GoalHandler) UpdateGoalProgress(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 for progress update: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"})
return
}
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 in progress update: %v", goalID, userID, err)
c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"})
return
}
var input UpdateGoalProgressInput
if err := c.ShouldBindJSON(&input); err != nil {
log.Printf("Error binding JSON for goal progress update: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
goal.CurrentAmount = input.CurrentAmount
// Check if goal has been achieved
if goal.CurrentAmount >= goal.TargetAmount {
goal.Status = "Achieved"
log.Printf("Goal ID %d for user %d automatically marked as Achieved during progress update", goalID, userID)
}
if err := database.DB.Save(&goal).Error; err != nil {
log.Printf("Error saving progress update for goal ID %d for user %d: %v", goalID, userID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update goal progress"})
return
}
log.Printf("Goal ID %d progress updated successfully for user %d: %d/%d", goalID, userID, goal.CurrentAmount, goal.TargetAmount)
c.JSON(http.StatusOK, goal)
}
// DeleteGoal deletes a goal belonging to the authenticated user
func (h *GoalHandler) DeleteGoal(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 for deletion: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"})
return
}
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 for deletion: %v", goalID, userID, err)
c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"})
return
}
if err := database.DB.Delete(&goal).Error; err != nil {
log.Printf("Error deleting goal ID %d for user %d: %v", goalID, userID, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete goal"})
return
}
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)
}
|