@@ -3,13 +3,25 @@ package controllers
33import (
44 "fmt"
55 "net/http"
6- "strconv"
76
87 "github.com/envelope-zero/backend/internal/httputil"
98 "github.com/envelope-zero/backend/internal/models"
109 "github.com/gin-gonic/gin"
1110)
1211
12+ type AccountListResponse struct {
13+ Data []models.Account `json:"data"`
14+ }
15+
16+ type AccountResponse struct {
17+ Data models.Account `json:"data"`
18+ Links AccountLinks `json:"links"`
19+ }
20+
21+ type AccountLinks struct {
22+ Transactions string `json:"transactions" example:"https://example.com/api/v1/budgets/3/accounts/17/transactions"`
23+ }
24+
1325// RegisterAccountRoutes registers the routes for accounts with
1426// the RouterGroup that is passed.
1527func RegisterAccountRoutes (r * gin.RouterGroup ) {
@@ -39,8 +51,10 @@ func RegisterAccountRoutes(r *gin.RouterGroup) {
3951// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs
4052// @Tags Accounts
4153// @Success 204
42- // @Param budgetId path uint64 true "ID of the budget"
43- // @Param accountId path uint64 true "ID of the account"
54+ // @Failure 400 {object} httputil.HTTPError
55+ // @Failure 404
56+ // @Param budgetId path uint64 true "ID of the budget"
57+ // @Param accountId path uint64 true "ID of the account"
4458// @Router /v1/budgets/{budgetId}/accounts/{accountId}/transactions [options]
4559func OptionsAccountTransactions (c * gin.Context ) {
4660 httputil .OptionsGet (c )
@@ -50,6 +64,8 @@ func OptionsAccountTransactions(c *gin.Context) {
5064// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs
5165// @Tags Accounts
5266// @Success 204
67+ // @Failure 400 {object} httputil.HTTPError
68+ // @Failure 404
5369// @Param budgetId path uint64 true "ID of the budget"
5470// @Router /v1/budgets/{budgetId}/accounts [options]
5571func OptionsAccountList (c * gin.Context ) {
@@ -60,118 +76,211 @@ func OptionsAccountList(c *gin.Context) {
6076// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs
6177// @Tags Accounts
6278// @Success 204
63- // @Param budgetId path uint64 true "ID of the budget"
64- // @Param accountId path uint64 true "ID of the account"
79+ // @Failure 400 {object} httputil.HTTPError
80+ // @Failure 404
81+ // @Param budgetId path uint64 true "ID of the budget"
82+ // @Param accountId path uint64 true "ID of the account"
6583// @Router /v1/budgets/{budgetId}/accounts/{accountId} [options]
6684func OptionsAccountDetail (c * gin.Context ) {
6785 httputil .OptionsGetPatchDelete (c )
6886}
6987
70- // GetAccountTransactions returns all transactions for the account.
88+ // @Summary List all transactions for an account
89+ // @Description Returns a list of all transactions for the account requested
90+ // @Tags Accounts
91+ // @Produce json
92+ // @Success 200 {object} TransactionListResponse
93+ // @Failure 400 {object} httputil.HTTPError
94+ // @Failure 404
95+ // @Failure 500 {object} httputil.HTTPError
96+ // @Param budgetId path uint64 true "ID of the budget"
97+ // @Param accountId path uint64 true "ID of the account"
98+ // @Router /v1/budgets/{budgetId}/accounts/{accountId}/transactions [get]
7199func GetAccountTransactions (c * gin.Context ) {
72- var account models.Account
73- err := models .DB .First (& account , c .Param ("accountId" )).Error
100+ account , err := getAccountResource (c )
74101 if err != nil {
75- httputil .FetchErrorHandler (c , err )
76102 return
77103 }
78104
79- c .JSON (http .StatusOK , gin.H {"data" : account .Transactions ()})
105+ c .JSON (http .StatusOK , TransactionListResponse {
106+ Data : account .Transactions (),
107+ })
80108}
81109
82- // CreateAccount creates a new account.
110+ // @Summary Create account
111+ // @Description Create a new account for a specific budget
112+ // @Tags Accounts
113+ // @Produce json
114+ // @Success 201 {object} AccountResponse
115+ // @Failure 400 {object} httputil.HTTPError
116+ // @Failure 404
117+ // @Failure 500 {object} httputil.HTTPError
118+ // @Param budgetId path uint64 true "ID of the budget"
119+ // @Param account body models.AccountCreate true "Account"
120+ // @Router /v1/budgets/{budgetId}/accounts [post]
83121func CreateAccount (c * gin.Context ) {
84122 var data models.Account
85123
86- if status , err := bindData (c , & data ); err != nil {
87- c .JSON (status , gin.H {"error" : err .Error ()})
124+ if status , err := httputil .BindData (c , & data ); err != nil {
125+ httputil .NewError (c , status , err )
126+ return
127+ }
128+
129+ budget , err := getBudgetResource (c )
130+ if err != nil {
88131 return
89132 }
90133
91- data .BudgetID , _ = strconv . ParseUint ( c . Param ( "budgetId" ), 10 , 0 )
134+ data .BudgetID = budget . ID
92135 models .DB .Create (& data )
93136
94- c .JSON (http .StatusCreated , gin. H { "data" : data })
137+ c .JSON (http .StatusCreated , AccountResponse { Data : data })
95138}
96139
97- // GetAccounts retrieves all accounts.
140+ // @Summary List accounts
141+ // @Description Returns a list of all accounts for the budget
142+ // @Tags Accounts
143+ // @Produce json
144+ // @Success 200 {object} AccountListResponse
145+ // @Failure 400 {object} httputil.HTTPError
146+ // @Failure 404
147+ // @Failure 500 {object} httputil.HTTPError
148+ // @Param budgetId path uint64 true "ID of the budget"
149+ // @Router /v1/budgets/{budgetId}/accounts [get]
98150func GetAccounts (c * gin.Context ) {
99151 var accounts []models.Account
100152
101153 // Check if the budget exists at all
102- budget , err := getBudget (c )
154+ budget , err := getBudgetResource (c )
103155 if err != nil {
104156 return
105157 }
106158
107159 models .DB .Where (& models.Account {
108- BudgetID : budget .ID ,
160+ AccountCreate : models.AccountCreate {
161+ BudgetID : budget .ID ,
162+ },
109163 }).Find (& accounts )
110164
111165 for i , account := range accounts {
112- response , err := account .WithCalculations ()
113- if err != nil {
114- httputil .FetchErrorHandler (c , fmt .Errorf ("could not get values for account %v: %v" , account .Name , err ))
115- return
116- }
117-
118- accounts [i ] = * response
166+ accounts [i ] = account .WithCalculations ()
119167 }
120168
121- c .JSON (http .StatusOK , gin. H { "data" : accounts })
169+ c .JSON (http .StatusOK , AccountListResponse { Data : accounts })
122170}
123171
124- // GetAccount retrieves an account by its ID.
172+ // @Summary Get account
173+ // @Description Returns a specific account
174+ // @Tags Accounts
175+ // @Produce json
176+ // @Success 200 {object} AccountResponse
177+ // @Failure 400 {object} httputil.HTTPError
178+ // @Failure 404
179+ // @Failure 500 {object} httputil.HTTPError
180+ // @Param budgetId path uint64 true "ID of the budget"
181+ // @Param accountId path uint64 true "ID of the account"
182+ // @Router /v1/budgets/{budgetId}/accounts/{accountId} [get]
125183func GetAccount (c * gin.Context ) {
126- account , err := getAccount (c )
127- if err != nil {
128- return
129- }
130-
131- apiResponse , err := account .WithCalculations ()
184+ _ , err := getAccountResource (c )
132185 if err != nil {
133- httputil .FetchErrorHandler (c , fmt .Errorf ("could not get values for account %v: %v" , account .Name , err ))
134186 return
135187 }
136188
137- c .JSON (http .StatusOK , gin.H {
138- "data" : apiResponse ,
139- "links" : map [string ]string {
140- "transactions" : requestURL (c ) + "/transactions" ,
141- },
142- })
189+ c .JSON (http .StatusOK , newAccountResponse (c ))
143190}
144191
145- // UpdateAccount updates an account, selected by the ID parameter.
192+ // @Summary Update account
193+ // @Description Updates an account. Only values to be updated need to be specified.
194+ // @Tags Accounts
195+ // @Produce json
196+ // @Success 200 {object} AccountResponse
197+ // @Failure 400 {object} httputil.HTTPError
198+ // @Failure 404
199+ // @Failure 500 {object} httputil.HTTPError
200+ // @Param budgetId path uint64 true "ID of the budget"
201+ // @Param accountId path uint64 true "ID of the account"
202+ // @Param account body models.AccountCreate true "Account"
203+ // @Router /v1/budgets/{budgetId}/accounts/{accountId} [patch]
146204func UpdateAccount (c * gin.Context ) {
147- var account models.Account
148-
149- err := models .DB .First (& account , c .Param ("accountId" )).Error
205+ account , err := getAccountResource (c )
150206 if err != nil {
151- httputil .FetchErrorHandler (c , err )
152207 return
153208 }
154209
155210 var data models.Account
156- if status , err := bindData (c , & data ); err != nil {
157- c . JSON ( status , gin. H { "error" : err . Error ()} )
211+ if status , err := httputil . BindData (c , & data ); err != nil {
212+ httputil . NewError ( c , status , err )
158213 return
159214 }
160215
161216 models .DB .Model (& account ).Updates (data )
162- c .JSON (http .StatusOK , gin. H { "data" : account } )
217+ c .JSON (http .StatusOK , newAccountResponse ( c ) )
163218}
164219
165- // DeleteAccount removes a account, identified by its ID.
220+ // @Summary Delete account
221+ // @Description Deletes the specified account.
222+ // @Tags Accounts
223+ // @Produce json
224+ // @Success 204
225+ // @Failure 400 {object} httputil.HTTPError
226+ // @Failure 404
227+ // @Failure 500 {object} httputil.HTTPError
228+ // @Param budgetId path uint64 true "ID of the budget"
229+ // @Param accountId path uint64 true "ID of the account"
230+ // @Router /v1/budgets/{budgetId}/accounts/{accountId} [delete]
166231func DeleteAccount (c * gin.Context ) {
167- var account models.Account
168- err := models .DB .First (& account , c .Param ("accountId" )).Error
232+ account , err := getAccountResource (c )
169233 if err != nil {
170- httputil .FetchErrorHandler (c , err )
171234 return
172235 }
173236
174237 models .DB .Delete (& account )
175238
176239 c .JSON (http .StatusNoContent , gin.H {})
177240}
241+
242+ // getAccountResource verifies that the request URI is valid for the account and returns it.
243+ func getAccountResource (c * gin.Context ) (models.Account , error ) {
244+ var account models.Account
245+
246+ budget , err := getBudgetResource (c )
247+ if err != nil {
248+ return models.Account {}, err
249+ }
250+
251+ accountID , err := httputil .ParseID (c , "accountId" )
252+ if err != nil {
253+ return models.Account {}, err
254+ }
255+
256+ err = models .DB .First (& account , & models.Account {
257+ AccountCreate : models.AccountCreate {
258+ BudgetID : budget .ID ,
259+ },
260+ Model : models.Model {
261+ ID : accountID ,
262+ },
263+ }).Error
264+ if err != nil {
265+ httputil .FetchErrorHandler (c , err )
266+ return models.Account {}, err
267+ }
268+
269+ return account , nil
270+ }
271+
272+ // newAccountResponse creates a response object for an account.
273+ func newAccountResponse (c * gin.Context ) AccountResponse {
274+ // When this function is called, all parent resources have already been validated
275+ budget , _ := getBudgetResource (c )
276+ account , _ := getAccountResource (c )
277+
278+ url := httputil .RequestPathV1 (c ) + fmt .Sprintf ("/budgets/%d/accounts/%d" , budget .ID , account .ID )
279+
280+ return AccountResponse {
281+ Data : account .WithCalculations (),
282+ Links : AccountLinks {
283+ Transactions : url + "/transactions" ,
284+ },
285+ }
286+ }
0 commit comments