Skip to content

Commit 54b76b4

Browse files
authored
feat: API v4 (#936)
This adds API v4 with the following changes: * the budget ID is removed from the transaction resource * removes calculated fields from the GET /accounts endpoint and adds the /accounts/{id}/recent-envelopes and /accounts/computed endpoints
1 parent 7f34d4d commit 54b76b4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+23830
-873
lines changed

api/docs.go

Lines changed: 5073 additions & 335 deletions
Large diffs are not rendered by default.

api/swagger.json

Lines changed: 5073 additions & 335 deletions
Large diffs are not rendered by default.

api/swagger.yaml

Lines changed: 3530 additions & 203 deletions
Large diffs are not rendered by default.

pkg/controllers/v4/account.go

Lines changed: 486 additions & 0 deletions
Large diffs are not rendered by default.

pkg/controllers/v4/account_test.go

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package v4
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/envelope-zero/backend/v4/pkg/httperrors"
8+
"github.com/envelope-zero/backend/v4/pkg/httputil"
9+
"github.com/envelope-zero/backend/v4/pkg/models"
10+
"github.com/gin-gonic/gin"
11+
"github.com/google/uuid"
12+
"github.com/shopspring/decimal"
13+
)
14+
15+
type AccountEditable struct {
16+
Name string `json:"name" example:"Cash" default:""` // Name of the account
17+
Note string `json:"note" example:"Money in my wallet" default:""` // A longer description for the account
18+
BudgetID uuid.UUID `json:"budgetId" example:"550dc009-cea6-4c12-b2a5-03446eb7b7cf"` // ID of the budget this account belongs to
19+
OnBudget bool `json:"onBudget" example:"true" default:"false"` // Does the account factor into the available budget? Always false when external: true
20+
External bool `json:"external" example:"false" default:"false"` // Does the account belong to the budget owner or not?
21+
InitialBalance decimal.Decimal `json:"initialBalance" example:"173.12" default:"0" minimum:"0.00000001" maximum:"999999999999.99999999" multipleOf:"0.00000001"` // Balance of the account before any transactions were recorded
22+
InitialBalanceDate *time.Time `json:"initialBalanceDate" example:"2017-05-12T00:00:00Z"` // Date of the initial balance
23+
Archived bool `json:"archived" example:"true" default:"false"` // Is the account archived?
24+
ImportHash string `json:"importHash" example:"867e3a26dc0baf73f4bff506f31a97f6c32088917e9e5cf1a5ed6f3f84a6fa70" default:""` // The SHA256 hash of a unique combination of values to use in duplicate detection for imports
25+
}
26+
27+
// model returns the database resource for the editable fields
28+
func (editable AccountEditable) model() models.Account {
29+
return models.Account{
30+
Name: editable.Name,
31+
Note: editable.Note,
32+
BudgetID: editable.BudgetID,
33+
OnBudget: editable.OnBudget,
34+
External: editable.External,
35+
InitialBalance: editable.InitialBalance,
36+
InitialBalanceDate: editable.InitialBalanceDate,
37+
Archived: editable.Archived,
38+
ImportHash: editable.ImportHash,
39+
}
40+
}
41+
42+
type AccountLinks struct {
43+
Self string `json:"self" example:"https://example.com/api/v4/accounts/af892e10-7e0a-4fb8-b1bc-4b6d88401ed2"` // The account itself
44+
RecentEnvelopes string `json:"recentEnvelopes" example:"https://example.com/api/v4/accounts/af892e10-7e0a-4fb8-b1bc-4b6d88401ed2/recent-envelopes"` // Envelopes in recent transactions where this account was the target
45+
ComputedData string `json:"computedData" example:"https://example.com/api/v4/accounts/computed"` // Computed data endpoint for accounts
46+
Transactions string `json:"transactions" example:"https://example.com/api/v4/transactions?account=af892e10-7e0a-4fb8-b1bc-4b6d88401ed2"` // Transactions referencing the account
47+
}
48+
49+
// Account is the API v4 representation of an Account in EZ.
50+
type Account struct {
51+
models.DefaultModel
52+
AccountEditable
53+
Links AccountLinks `json:"links"`
54+
}
55+
56+
func newAccount(c *gin.Context, model models.Account) Account {
57+
url := c.GetString(string(models.DBContextURL))
58+
59+
return Account{
60+
DefaultModel: model.DefaultModel,
61+
AccountEditable: AccountEditable{
62+
Name: model.Name,
63+
Note: model.Note,
64+
BudgetID: model.BudgetID,
65+
OnBudget: model.OnBudget,
66+
External: model.External,
67+
InitialBalance: model.InitialBalance,
68+
InitialBalanceDate: model.InitialBalanceDate,
69+
Archived: model.Archived,
70+
ImportHash: model.ImportHash,
71+
},
72+
Links: AccountLinks{
73+
Self: fmt.Sprintf("%s/v4/accounts/%s", url, model.ID),
74+
RecentEnvelopes: fmt.Sprintf("%s/v4/accounts/%s/recent-envelopes", url, model.ID),
75+
ComputedData: fmt.Sprintf("%s/v4/accounts/computed", url),
76+
Transactions: fmt.Sprintf("%s/v4/transactions?account=%s", url, model.ID),
77+
},
78+
}
79+
}
80+
81+
type AccountListResponse struct {
82+
Data []Account `json:"data"` // List of accounts
83+
Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred
84+
Pagination *Pagination `json:"pagination"` // Pagination information
85+
}
86+
87+
type AccountCreateResponse struct {
88+
Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred
89+
Data []AccountResponse `json:"data"` // List of created Accounts
90+
}
91+
92+
func (a *AccountCreateResponse) appendError(err httperrors.Error, status int) int {
93+
s := err.Error()
94+
a.Data = append(a.Data, AccountResponse{Error: &s})
95+
96+
// The final status code is the highest HTTP status code number
97+
if err.Status > status {
98+
status = err.Status
99+
}
100+
101+
return status
102+
}
103+
104+
type AccountResponse struct {
105+
Data *Account `json:"data"` // Data for the account
106+
Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred for this transaction
107+
}
108+
109+
type AccountQueryFilter struct {
110+
Name string `form:"name" filterField:"false"` // Fuzzy filter for the account name
111+
Note string `form:"note" filterField:"false"` // Fuzzy filter for the note
112+
BudgetID string `form:"budget"` // By budget ID
113+
OnBudget bool `form:"onBudget"` // Is the account on-budget?
114+
External bool `form:"external"` // Is the account external?
115+
Archived bool `form:"archived"` // Is the account archived?
116+
Search string `form:"search" filterField:"false"` // By string in name or note
117+
Offset uint `form:"offset" filterField:"false"` // The offset of the first Account returned. Defaults to 0.
118+
Limit int `form:"limit" filterField:"false"` // Maximum number of Accounts to return. Defaults to 50.
119+
}
120+
121+
func (f AccountQueryFilter) model() (models.Account, httperrors.Error) {
122+
budgetID, err := httputil.UUIDFromString(f.BudgetID)
123+
if !err.Nil() {
124+
return models.Account{}, err
125+
}
126+
127+
return models.Account{
128+
BudgetID: budgetID,
129+
OnBudget: f.OnBudget,
130+
External: f.External,
131+
Archived: f.Archived,
132+
}, httperrors.Error{}
133+
}
134+
135+
type RecentEnvelopesResponse struct {
136+
Data []RecentEnvelope `json:"data"` // Data for the account
137+
Error *string `json:"error" example:"the specified resource ID is not a valid UUID"` // The error, if any occurred for this transaction
138+
}
139+
140+
type RecentEnvelope struct {
141+
Name string `json:"name"`
142+
ID *uuid.UUID `json:"id"`
143+
}
144+
145+
type AccountComputedRequest struct {
146+
Time time.Time `form:"time"` // The time for which the computation is requested
147+
IDs []string `form:"ids"` // A list of UUIDs for the accounts
148+
}
149+
150+
type AccountComputedData struct {
151+
ID uuid.UUID `json:"id" example:"95018a69-758b-46c6-8bab-db70d9614f9d"` // ID of the account
152+
Balance decimal.Decimal `json:"balance" example:"2735.17"` // Balance of the account, including all transactions referencing it
153+
ReconciledBalance decimal.Decimal `json:"reconciledBalance" example:"2539.57"` // Balance of the account, including all reconciled transactions referencing it
154+
}
155+
156+
type AccountComputedDataResponse struct {
157+
Data []AccountComputedData `json:"data"`
158+
Error *string `json:"error"`
159+
}

0 commit comments

Comments
 (0)