Skip to content

Commit c5541d7

Browse files
authored
feat!: transactions now under API root and with links in data (#144)
1 parent 1b042f7 commit c5541d7

File tree

10 files changed

+405
-523
lines changed

10 files changed

+405
-523
lines changed

internal/controllers/account.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func GetAccounts(c *gin.Context) {
111111

112112
models.DB.Find(&accounts)
113113

114-
// When there are no accounts, we want an empty list, not null
114+
// When there are no resources, we want an empty list, not null
115115
// Therefore, we use make to create a slice with zero elements
116116
// which will be marshalled to an empty JSON array
117117
accountObjects := make([]Account, 0)

internal/controllers/budget.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ func RegisterBudgetRoutes(r *gin.RouterGroup) {
5656
}
5757

5858
RegisterCategoryRoutes(r.Group("/:budgetId/categories"))
59-
RegisterTransactionRoutes(r.Group("/:budgetId/transactions"))
6059
}
6160

6261
// @Summary Allowed HTTP verbs
@@ -313,7 +312,7 @@ func getBudgetLinks(c *gin.Context, id uint64) BudgetLinks {
313312
Self: url,
314313
Accounts: httputil.RequestPathV1(c) + fmt.Sprintf("/accounts?budget=%d", id),
315314
Categories: url + "/categories",
316-
Transactions: url + "/transactions",
315+
Transactions: httputil.RequestPathV1(c) + fmt.Sprintf("/transactions?budget=%d", id),
317316
Month: url + "/YYYY-MM",
318317
}
319318
}

internal/controllers/main_list_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var getOverviewTests = []struct {
1313
expected string
1414
}{
1515
{"/", `{ "links": { "v1": "http:///v1", "version": "http:///version", "docs": "http:///docs/index.html" }}`},
16-
{"/v1", `{ "links": { "budgets": "http:///v1/budgets", "accounts": "http:///v1/accounts" }}`},
16+
{"/v1", `{ "links": { "budgets": "http:///v1/budgets", "accounts": "http:///v1/accounts", "transactions": "http:///v1/transactions" }}`},
1717
{"/version", `{"data": { "version": "0.0.0" }}`},
1818
}
1919

internal/controllers/main_options_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ var optionsHeaderTests = []struct {
2525
{"/v1/budgets/1/categories/1/envelopes/1", "GET, PATCH, DELETE"},
2626
{"/v1/budgets/1/categories/1/envelopes/1/allocations", "GET, POST"},
2727
{"/v1/budgets/1/categories/1/envelopes/1/allocations/1", "GET, PATCH, DELETE"},
28-
{"/v1/budgets/1/transactions", "GET, POST"},
29-
{"/v1/budgets/1/transactions/1", "GET, PATCH, DELETE"},
28+
{"/v1/transactions", "GET, POST"},
29+
{"/v1/transactions/1", "GET, PATCH, DELETE"},
3030
}
3131

3232
func TestOptionsHeader(t *testing.T) {

internal/controllers/routing.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ func Router() (*gin.Engine, error) {
127127

128128
RegisterBudgetRoutes(v1.Group("/budgets"))
129129
RegisterAccountRoutes(v1.Group("/accounts"))
130+
RegisterTransactionRoutes(v1.Group("/transactions"))
130131

131132
return r, nil
132133
}
@@ -201,8 +202,9 @@ type V1Response struct {
201202
}
202203

203204
type V1Links struct {
204-
Budgets string `json:"budgets" example:"https://example.com/api/v1/budgets"`
205-
Accounts string `json:"accounts" example:"https://example.com/api/v1/accounts"`
205+
Budgets string `json:"budgets" example:"https://example.com/api/v1/budgets"`
206+
Accounts string `json:"accounts" example:"https://example.com/api/v1/accounts"`
207+
Transactions string `json:"transactions" example:"https://example.com/api/v1/transactions"`
206208
}
207209

208210
// @Sumary v1 API
@@ -213,8 +215,9 @@ type V1Links struct {
213215
func GetV1(c *gin.Context) {
214216
c.JSON(http.StatusOK, V1Response{
215217
Links: V1Links{
216-
Budgets: httputil.RequestPathV1(c) + "/budgets",
217-
Accounts: httputil.RequestPathV1(c) + "/accounts",
218+
Budgets: httputil.RequestPathV1(c) + "/budgets",
219+
Accounts: httputil.RequestPathV1(c) + "/accounts",
220+
Transactions: httputil.RequestPathV1(c) + "/transactions",
218221
},
219222
})
220223
}

internal/controllers/transaction.go

Lines changed: 87 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package controllers
22

33
import (
44
"errors"
5+
"fmt"
56
"net/http"
67

78
"github.com/envelope-zero/backend/internal/httputil"
@@ -11,11 +12,20 @@ import (
1112
)
1213

1314
type TransactionListResponse struct {
14-
Data []models.Transaction `json:"data"`
15+
Data []Transaction `json:"data"`
1516
}
1617

1718
type TransactionResponse struct {
18-
Data models.Transaction `json:"data"`
19+
Data Transaction `json:"data"`
20+
}
21+
22+
type Transaction struct {
23+
models.Transaction
24+
Links TransactionLinks `json:"links"`
25+
}
26+
27+
type TransactionLinks struct {
28+
Self string `json:"self" example:"https://example.com/api/v1/transactions/1741"`
1929
}
2030

2131
// RegisterTransactionRoutes registers the routes for transactions with
@@ -41,8 +51,7 @@ func RegisterTransactionRoutes(r *gin.RouterGroup) {
4151
// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs
4252
// @Tags Transactions
4353
// @Success 204
44-
// @Param budgetId path uint64 true "ID of the budget"
45-
// @Router /v1/budgets/{budgetId}/transactions [options]
54+
// @Router /v1/transactions [options]
4655
func OptionsTransactionList(c *gin.Context) {
4756
httputil.OptionsGetPost(c)
4857
}
@@ -51,45 +60,44 @@ func OptionsTransactionList(c *gin.Context) {
5160
// @Description Returns an empty response with the HTTP Header "allow" set to the allowed HTTP verbs
5261
// @Tags Transactions
5362
// @Success 204
54-
// @Param budgetId path uint64 true "ID of the budget"
5563
// @Param transactionId path uint64 true "ID of the transaction"
56-
// @Router /v1/budgets/{budgetId}/transactions/{transactionId} [options]
64+
// @Router /v1/transactions/{transactionId} [options]
5765
func OptionsTransactionDetail(c *gin.Context) {
5866
httputil.OptionsGetPatchDelete(c)
5967
}
6068

6169
// @Summary Create transaction
62-
// @Description Create a new transaction for the specified budget
70+
// @Description Create a new transaction
6371
// @Tags Transactions
6472
// @Produce json
6573
// @Success 201 {object} TransactionResponse
6674
// @Failure 400 {object} httputil.HTTPError
6775
// @Failure 404
6876
// @Failure 500 {object} httputil.HTTPError
69-
// @Param budgetId path uint64 true "ID of the budget"
7077
// @Param transaction body models.TransactionCreate true "Transaction"
71-
// @Router /v1/budgets/{budgetId}/transactions [post]
78+
// @Router /v1/transactions [post]
7279
func CreateTransaction(c *gin.Context) {
73-
var data models.Transaction
80+
var transaction models.Transaction
7481

75-
err := httputil.BindData(c, &data)
76-
if err != nil {
82+
if err := httputil.BindData(c, &transaction); err != nil {
7783
return
7884
}
7985

80-
// Convert and validate data
81-
data.BudgetID, err = httputil.ParseID(c, "budgetId")
86+
// Check if the budget that the transaction shoud belong to exists
87+
_, err := getBudgetResource(c, transaction.BudgetID)
8288
if err != nil {
8389
return
8490
}
8591

86-
if !decimal.Decimal.IsPositive(data.Amount) {
92+
if !decimal.Decimal.IsPositive(transaction.Amount) {
8793
httputil.NewError(c, http.StatusBadRequest, errors.New("The transaction amount must be positive"))
8894
return
8995
}
9096

91-
models.DB.Create(&data)
92-
c.JSON(http.StatusCreated, TransactionResponse{Data: data})
97+
models.DB.Create(&transaction)
98+
99+
transactionObject, _ := getTransactionObject(c, transaction.ID)
100+
c.JSON(http.StatusCreated, TransactionResponse{Data: transactionObject})
93101
}
94102

95103
// @Summary Get all transactions
@@ -99,27 +107,23 @@ func CreateTransaction(c *gin.Context) {
99107
// @Success 200 {object} TransactionListResponse
100108
// @Failure 400 {object} httputil.HTTPError
101109
// @Failure 404
102-
// @Failure 500 {object} httputil.HTTPError
103-
// @Param budgetId path uint64 true "ID of the budget"
104-
// @Router /v1/budgets/{budgetId}/transactions [get]
110+
// @Failure 500 {object} httputil.HTTPError
111+
// @Router /v1/transactions [get]
105112
func GetTransactions(c *gin.Context) {
106113
var transactions []models.Transaction
107114

108-
budgetID, _ := httputil.ParseID(c, "budgetId")
115+
models.DB.Find(&transactions)
109116

110-
// Check if the budget exists at all
111-
budget, err := getBudgetResource(c, budgetID)
112-
if err != nil {
113-
return
117+
// When there are no resources, we want an empty list, not null
118+
// Therefore, we use make to create a slice with zero elements
119+
// which will be marshalled to an empty JSON array
120+
transactionObjects := make([]Transaction, 0)
121+
for _, transaction := range transactions {
122+
o, _ := getTransactionObject(c, transaction.ID)
123+
transactionObjects = append(transactionObjects, o)
114124
}
115125

116-
models.DB.Where(&models.Category{
117-
CategoryCreate: models.CategoryCreate{
118-
BudgetID: budget.ID,
119-
},
120-
}).Find(&transactions)
121-
122-
c.JSON(http.StatusOK, TransactionListResponse{Data: transactions})
126+
c.JSON(http.StatusOK, TransactionListResponse{Data: transactionObjects})
123127
}
124128

125129
// @Summary Get transaction
@@ -130,16 +134,20 @@ func GetTransactions(c *gin.Context) {
130134
// @Failure 400 {object} httputil.HTTPError
131135
// @Failure 404
132136
// @Failure 500 {object} httputil.HTTPError
133-
// @Param budgetId path uint64 true "ID of the budget"
134137
// @Param transactionId path uint64 true "ID of the transaction"
135-
// @Router /v1/budgets/{budgetId}/transactions/{transactionId} [get]
138+
// @Router /v1/transactions/{transactionId} [get]
136139
func GetTransaction(c *gin.Context) {
137-
t, err := getTransactionResource(c)
140+
id, err := httputil.ParseID(c, "transactionId")
141+
if err != nil {
142+
return
143+
}
144+
145+
transactionObject, err := getTransactionObject(c, id)
138146
if err != nil {
139147
return
140148
}
141149

142-
c.JSON(http.StatusOK, TransactionResponse{Data: t})
150+
c.JSON(http.StatusOK, TransactionResponse{Data: transactionObject})
143151
}
144152

145153
// @Summary Update a transaction
@@ -151,12 +159,16 @@ func GetTransaction(c *gin.Context) {
151159
// @Failure 400 {object} httputil.HTTPError
152160
// @Failure 404
153161
// @Failure 500 {object} httputil.HTTPError
154-
// @Param budgetId path uint64 true "ID of the budget"
155162
// @Param transactionId path uint64 true "ID of the transaction"
156163
// @Param transaction body models.TransactionCreate true "Transaction"
157-
// @Router /v1/budgets/{budgetId}/transactions/{transactionId} [patch]
164+
// @Router /v1/transactions/{transactionId} [patch]
158165
func UpdateTransaction(c *gin.Context) {
159-
transaction, err := getTransactionResource(c)
166+
id, err := httputil.ParseID(c, "transactionId")
167+
if err != nil {
168+
return
169+
}
170+
171+
transaction, err := getTransactionResource(c, id)
160172
if err != nil {
161173
return
162174
}
@@ -178,7 +190,8 @@ func UpdateTransaction(c *gin.Context) {
178190
}
179191

180192
models.DB.Model(&transaction).Updates(data)
181-
c.JSON(http.StatusOK, TransactionResponse{Data: transaction})
193+
transactionObject, _ := getTransactionObject(c, id)
194+
c.JSON(http.StatusOK, TransactionResponse{Data: transactionObject})
182195
}
183196

184197
// @Summary Delete a transaction
@@ -188,11 +201,15 @@ func UpdateTransaction(c *gin.Context) {
188201
// @Failure 400 {object} httputil.HTTPError
189202
// @Failure 404
190203
// @Failure 500 {object} httputil.HTTPError
191-
// @Param budgetId path uint64 true "ID of the budget"
192204
// @Param transactionId path uint64 true "ID of the transaction"
193-
// @Router /v1/budgets/{budgetId}/transactions/{transactionId} [delete]
205+
// @Router /v1/transactions/{transactionId} [delete]
194206
func DeleteTransaction(c *gin.Context) {
195-
transaction, err := getTransactionResource(c)
207+
id, err := httputil.ParseID(c, "transactionId")
208+
if err != nil {
209+
return
210+
}
211+
212+
transaction, err := getTransactionResource(c, id)
196213
if err != nil {
197214
return
198215
}
@@ -203,27 +220,12 @@ func DeleteTransaction(c *gin.Context) {
203220
}
204221

205222
// getTransactionResource verifies that the request URI is valid for the transaction and returns it.
206-
func getTransactionResource(c *gin.Context) (models.Transaction, error) {
223+
func getTransactionResource(c *gin.Context, id uint64) (models.Transaction, error) {
207224
var transaction models.Transaction
208225

209-
budgetID, _ := httputil.ParseID(c, "budgetId")
210-
211-
budget, err := getBudgetResource(c, budgetID)
212-
if err != nil {
213-
return models.Transaction{}, err
214-
}
215-
216-
accountID, err := httputil.ParseID(c, "transactionId")
217-
if err != nil {
218-
return models.Transaction{}, err
219-
}
220-
221-
err = models.DB.First(&transaction, &models.Transaction{
222-
TransactionCreate: models.TransactionCreate{
223-
BudgetID: budget.ID,
224-
},
226+
err := models.DB.First(&transaction, &models.Transaction{
225227
Model: models.Model{
226-
ID: accountID,
228+
ID: id,
227229
},
228230
}).Error
229231
if err != nil {
@@ -233,3 +235,27 @@ func getTransactionResource(c *gin.Context) (models.Transaction, error) {
233235

234236
return transaction, nil
235237
}
238+
239+
func getTransactionObject(c *gin.Context, id uint64) (Transaction, error) {
240+
resource, err := getTransactionResource(c, id)
241+
if err != nil {
242+
return Transaction{}, err
243+
}
244+
245+
return Transaction{
246+
resource,
247+
getTransactionLinks(c, id),
248+
}, nil
249+
}
250+
251+
// getTransactionLinks returns a TransactionLinks struct.
252+
//
253+
// This function is only needed for getTransactionObject as we cannot create an instance of Transaction
254+
// with mixed named and unnamed parameters.
255+
func getTransactionLinks(c *gin.Context, id uint64) TransactionLinks {
256+
url := httputil.RequestPathV1(c) + fmt.Sprintf("/transactions/%d", id)
257+
258+
return TransactionLinks{
259+
Self: url,
260+
}
261+
}

0 commit comments

Comments
 (0)