8
8
import com .jongsoft .finance .providers .BudgetProvider ;
9
9
import com .jongsoft .finance .providers .ExpenseProvider ;
10
10
import com .jongsoft .finance .providers .TransactionProvider ;
11
+ import com .jongsoft .finance .rest .ApiDefaults ;
11
12
import com .jongsoft .finance .rest .model .BudgetResponse ;
12
13
import com .jongsoft .finance .rest .model .ExpenseResponse ;
13
14
import com .jongsoft .finance .security .CurrentUserProvider ;
14
15
import com .jongsoft .lang .Collections ;
15
16
import io .micronaut .core .annotation .Nullable ;
17
+ import io .micronaut .http .HttpStatus ;
16
18
import io .micronaut .http .annotation .*;
19
+ import io .micronaut .http .hateoas .JsonError ;
17
20
import io .micronaut .security .annotation .Secured ;
18
21
import io .micronaut .security .rules .SecurityRule ;
19
22
import io .swagger .v3 .oas .annotations .Operation ;
20
23
import io .swagger .v3 .oas .annotations .Parameter ;
21
24
import io .swagger .v3 .oas .annotations .enums .ParameterIn ;
25
+ import io .swagger .v3 .oas .annotations .media .Content ;
22
26
import io .swagger .v3 .oas .annotations .media .Schema ;
27
+ import io .swagger .v3 .oas .annotations .responses .ApiResponse ;
23
28
import io .swagger .v3 .oas .annotations .tags .Tag ;
24
29
import jakarta .inject .Inject ;
25
30
import jakarta .validation .Valid ;
26
31
import lombok .RequiredArgsConstructor ;
32
+ import lombok .extern .slf4j .Slf4j ;
27
33
28
34
import java .math .BigDecimal ;
29
35
import java .time .LocalDate ;
30
36
import java .util .List ;
37
+ import java .util .Objects ;
31
38
39
+ @ Slf4j
32
40
@ Tag (name = "Budget" )
33
41
@ Controller ("/api/budgets" )
34
42
@ Secured (SecurityRule .IS_AUTHENTICATED )
35
43
@ RequiredArgsConstructor (onConstructor_ = @ Inject )
36
44
public class BudgetResource {
37
45
38
- private final CurrentUserProvider currentUserProvider ;
39
46
private final BudgetProvider budgetProvider ;
40
47
private final ExpenseProvider expenseProvider ;
41
48
private final FilterFactory filterFactory ;
42
49
50
+ private final CurrentUserProvider currentUserProvider ;
43
51
private final TransactionProvider transactionProvider ;
44
52
45
- @ Get
53
+ @ Get ( "/current" )
46
54
@ Operation (
47
- summary = "First budget start " ,
48
- description = "Computes the date of the start of the first budget registered in FinTrack "
55
+ summary = "Current month " ,
56
+ description = "Get the budget for the current month. "
49
57
)
50
- LocalDate firstBudget () {
51
- return budgetProvider .first ()
52
- .map (Budget ::getStart )
53
- .getOrThrow (() -> StatusException .notFound ("No budget found" ));
58
+ @ ApiDefaults
59
+ BudgetResponse currentMonth () {
60
+ return budgetProvider .lookup (LocalDate .now ().getYear (), LocalDate .now ().getMonthValue ())
61
+ .map (BudgetResponse ::new )
62
+ .getOrThrow (() -> StatusException .notFound ("Budget not found for current month." ));
54
63
}
55
64
56
65
@ Get ("/{year}/{month}" )
57
66
@ Operation (
58
- summary = "Get budget" ,
59
- description = "Lookup the active budget during the provided year and month" ,
60
- parameters = {
61
- @ Parameter (name = "year" , in = ParameterIn .PATH , schema = @ Schema (implementation = Integer .class , description = "The year" )),
62
- @ Parameter (name = "month" , in = ParameterIn .PATH , schema = @ Schema (implementation = Integer .class , description = "The month" ))
63
- }
67
+ summary = "Get any month" ,
68
+ description = "Get the budget for the given year and month combination."
64
69
)
65
- BudgetResponse budget (@ PathVariable int year , @ PathVariable int month ) {
70
+ @ ApiDefaults
71
+ BudgetResponse givenMonth (@ PathVariable int year , @ PathVariable int month ) {
66
72
return budgetProvider .lookup (year , month )
67
73
.map (BudgetResponse ::new )
68
- .getOrThrow (() -> StatusException .notFound ("No budget found" ));
74
+ .getOrThrow (() -> StatusException .notFound ("Budget not found for month. " ));
69
75
}
70
76
71
77
@ Get ("/auto-complete{?token}" )
72
78
@ Operation (
73
79
summary = "Lookup expense" ,
74
- description = "Search in FinTrack for expenses that match the provided token" ,
80
+ description = "Search for expenses that match the provided token" ,
75
81
parameters = @ Parameter (name = "token" , in = ParameterIn .QUERY , schema = @ Schema (implementation = String .class ))
76
82
)
77
83
List <ExpenseResponse > autocomplete (@ Nullable String token ) {
@@ -81,49 +87,89 @@ List<ExpenseResponse> autocomplete(@Nullable String token) {
81
87
.toJava ();
82
88
}
83
89
84
- @ Put
90
+ @ Get
85
91
@ Operation (
86
- summary = "Create budget" ,
87
- description = "Create a new budget in the system with the provided start date "
92
+ summary = "First budget start " ,
93
+ description = "Computes the date of the start of the first budget registered in FinTrack "
88
94
)
89
- BudgetResponse create (@ Valid @ Body BudgetCreateRequest budgetCreateRequest ) {
90
- LocalDate startDate = LocalDate .of (budgetCreateRequest .getYear (), budgetCreateRequest .getMonth (), 1 );
95
+ LocalDate firstBudget () {
96
+ return budgetProvider .first ()
97
+ .map (Budget ::getStart )
98
+ .getOrThrow (() -> StatusException .notFound ("No budget found" ));
99
+ }
91
100
92
- var budget = currentUserProvider .currentUser ()
93
- .createBudget (startDate , budgetCreateRequest .getIncome ());
94
- return new BudgetResponse (budget );
101
+ @ Put
102
+ @ Operation (
103
+ summary = "Create initial budget" ,
104
+ description = "Create a new budget in the system."
105
+ )
106
+ @ ApiResponse (
107
+ responseCode = "400" ,
108
+ content = @ Content (schema = @ Schema (implementation = JsonError .class )),
109
+ description = "There is already an open budget."
110
+ )
111
+ @ Status (HttpStatus .CREATED )
112
+ void create (@ Valid @ Body BudgetCreateRequest createRequest ) {
113
+ var startDate = createRequest .getStart ();
114
+ var existing = budgetProvider .lookup (startDate .getYear (), startDate .getMonthValue ());
115
+ if (existing .isPresent ()) {
116
+ throw StatusException .badRequest ("Cannot start a new budget, there is already a budget open." );
117
+ }
118
+
119
+ currentUserProvider .currentUser ()
120
+ .createBudget (startDate , createRequest .getIncome ());
95
121
}
96
122
97
- @ Post
123
+ @ Patch
98
124
@ Operation (
99
- summary = "Index budget" ,
100
- description = "Indexing a budget will change it expenses and expected income by a percentage "
125
+ summary = "Patch budget. " ,
126
+ description = "Update an existing budget that is not yet closed in the system. "
101
127
)
102
- BudgetResponse index (@ Valid @ Body BudgetCreateRequest budgetUpdateRequest ) {
103
- var startDate = LocalDate . of ( budgetUpdateRequest . getYear (), budgetUpdateRequest . getMonth (), 1 );
128
+ BudgetResponse patchBudget (@ Valid @ Body BudgetCreateRequest patchRequest ) {
129
+ var startDate = patchRequest . getStart ( );
104
130
105
- return budgetProvider .lookup (budgetUpdateRequest .getYear (), budgetUpdateRequest .getMonth ())
106
- .map (budget -> budget .indexBudget (startDate , budgetUpdateRequest .getIncome ()))
131
+ var budget = budgetProvider .lookup (startDate .getYear (), startDate .getMonthValue ())
132
+ .getOrThrow (() -> StatusException .notFound ("No budget is active yet, create a budget first." ));
133
+
134
+ budget .indexBudget (startDate , patchRequest .getIncome ());
135
+ return budgetProvider .lookup (startDate .getYear (), startDate .getMonthValue ())
107
136
.map (BudgetResponse ::new )
108
- .getOrThrow (() -> StatusException .notFound ( "No budget found " ));
137
+ .getOrThrow (() -> StatusException .internalError ( "Could not get budget after updating the period. " ));
109
138
}
110
139
111
- @ Put ("/expenses" )
140
+ @ Patch ("/expenses" )
112
141
@ Operation (
113
- summary = "Create expense " ,
114
- description = "Add a new expense to all existing budgets "
142
+ summary = "Patch Expenses " ,
143
+ description = "Create or update an expense in the currents month budget. "
115
144
)
116
- BudgetResponse createExpense (@ Valid @ Body ExpenseCreateRequest createRequest ) {
117
- var now = LocalDate .now ();
118
-
119
- return budgetProvider .lookup (now .getYear (), now .getMonthValue ())
120
- .map (budget -> {
121
- budget .createExpense (createRequest .getName (), createRequest .getLowerBound (), createRequest .getUpperBound ());
122
- return budgetProvider .lookup (now .getYear (), now .getMonthValue ())
123
- .map (BudgetResponse ::new )
124
- .getOrThrow (() -> StatusException .notFound ("No budget found" ));
125
- })
126
- .getOrThrow (() -> StatusException .notFound ("No budget found" ));
145
+ BudgetResponse patchExpenses (@ Valid @ Body ExpensePatchRequest patchRequest ) {
146
+ var currentDate = LocalDate .now ().withDayOfMonth (1 );
147
+
148
+ var budget = budgetProvider .lookup (currentDate .getYear (), currentDate .getMonthValue ())
149
+ .getOrThrow (() -> StatusException .notFound ("Cannot update expenses, no budget available yet." ));
150
+
151
+ if (patchRequest .expenseId () != null ) {
152
+ log .debug ("Updating expense {} within active budget." , patchRequest .expenseId ());
153
+
154
+ if (budget .getStart ().isBefore (currentDate )) {
155
+ log .info ("Starting new budget period as the current period {} is after the existing start of {}" , currentDate , budget .getStart ());
156
+ budget .indexBudget (currentDate , budget .getExpectedIncome ());
157
+ budget = budgetProvider .lookup (currentDate .getYear (), currentDate .getMonthValue ())
158
+ .getOrThrow (() -> StatusException .internalError ("Updating of budget failed." ));
159
+ }
160
+
161
+ var toUpdate = budget .getExpenses ()
162
+ .first (expense -> Objects .equals (expense .getId (), patchRequest .expenseId ()))
163
+ .getOrThrow (() -> StatusException .badRequest ("Attempted to update a non existing expense." ));
164
+
165
+ toUpdate .updateExpense (patchRequest .amount ());
166
+ } else {
167
+ budget .createExpense (patchRequest .name (), patchRequest .amount () - 0.01 , patchRequest .amount ());
168
+ }
169
+
170
+ return budgetProvider .lookup (currentDate .getYear (), currentDate .getMonthValue ())
171
+ .map (BudgetResponse ::new )
172
+ .getOrThrow (() -> StatusException .internalError ("Error whilst fetching updated budget." ));
127
173
}
128
174
129
175
@ Get ("/expenses/{id}/{year}/{month}" )
0 commit comments