Skip to content

Commit d3fd74e

Browse files
authored
feat!: allocations now use RFC3339 timestamps instead of a Month and a Year (u)int (#274)
BREAKING CHANGE: With this, allocations use RFC3339 timestamps like all other resources in the Envelope Zero backend already do.
1 parent 6e9f109 commit d3fd74e

File tree

11 files changed

+76
-105
lines changed

11 files changed

+76
-105
lines changed

api/docs.go

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1901,18 +1901,13 @@ const docTemplate = `{
19011901
"$ref": "#/definitions/controllers.AllocationLinks"
19021902
},
19031903
"month": {
1904-
"type": "integer",
1905-
"maximum": 12,
1906-
"minimum": 1,
1907-
"example": 6
1904+
"description": "Only year and month of this timestamp are used, everything else is ignored. This will always be set to 00:00 UTC on the first of the specified month",
1905+
"type": "string",
1906+
"example": "2021-12-01T00:00:00.000000Z"
19081907
},
19091908
"updatedAt": {
19101909
"type": "string",
19111910
"example": "2022-04-17T20:14:01.048145Z"
1912-
},
1913-
"year": {
1914-
"type": "integer",
1915-
"example": 2022
19161911
}
19171912
}
19181913
},
@@ -2342,14 +2337,9 @@ const docTemplate = `{
23422337
"example": "a0909e84-e8f9-4cb6-82a5-025dff105ff2"
23432338
},
23442339
"month": {
2345-
"type": "integer",
2346-
"maximum": 12,
2347-
"minimum": 1,
2348-
"example": 6
2349-
},
2350-
"year": {
2351-
"type": "integer",
2352-
"example": 2022
2340+
"description": "Only year and month of this timestamp are used, everything else is ignored. This will always be set to 00:00 UTC on the first of the specified month",
2341+
"type": "string",
2342+
"example": "2021-12-01T00:00:00.000000Z"
23532343
}
23542344
}
23552345
},

api/swagger.json

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1889,18 +1889,13 @@
18891889
"$ref": "#/definitions/controllers.AllocationLinks"
18901890
},
18911891
"month": {
1892-
"type": "integer",
1893-
"maximum": 12,
1894-
"minimum": 1,
1895-
"example": 6
1892+
"description": "Only year and month of this timestamp are used, everything else is ignored. This will always be set to 00:00 UTC on the first of the specified month",
1893+
"type": "string",
1894+
"example": "2021-12-01T00:00:00.000000Z"
18961895
},
18971896
"updatedAt": {
18981897
"type": "string",
18991898
"example": "2022-04-17T20:14:01.048145Z"
1900-
},
1901-
"year": {
1902-
"type": "integer",
1903-
"example": 2022
19041899
}
19051900
}
19061901
},
@@ -2330,14 +2325,9 @@
23302325
"example": "a0909e84-e8f9-4cb6-82a5-025dff105ff2"
23312326
},
23322327
"month": {
2333-
"type": "integer",
2334-
"maximum": 12,
2335-
"minimum": 1,
2336-
"example": 6
2337-
},
2338-
"year": {
2339-
"type": "integer",
2340-
"example": 2022
2328+
"description": "Only year and month of this timestamp are used, everything else is ignored. This will always be set to 00:00 UTC on the first of the specified month",
2329+
"type": "string",
2330+
"example": "2021-12-01T00:00:00.000000Z"
23412331
}
23422332
}
23432333
},

api/swagger.yaml

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,14 @@ definitions:
8686
links:
8787
$ref: '#/definitions/controllers.AllocationLinks'
8888
month:
89-
example: 6
90-
maximum: 12
91-
minimum: 1
92-
type: integer
89+
description: Only year and month of this timestamp are used, everything else
90+
is ignored. This will always be set to 00:00 UTC on the first of the specified
91+
month
92+
example: "2021-12-01T00:00:00.000000Z"
93+
type: string
9394
updatedAt:
9495
example: "2022-04-17T20:14:01.048145Z"
9596
type: string
96-
year:
97-
example: 2022
98-
type: integer
9997
type: object
10098
controllers.AllocationLinks:
10199
properties:
@@ -401,13 +399,11 @@ definitions:
401399
example: a0909e84-e8f9-4cb6-82a5-025dff105ff2
402400
type: string
403401
month:
404-
example: 6
405-
maximum: 12
406-
minimum: 1
407-
type: integer
408-
year:
409-
example: 2022
410-
type: integer
402+
description: Only year and month of this timestamp are used, everything else
403+
is ignored. This will always be set to 00:00 UTC on the first of the specified
404+
month
405+
example: "2021-12-01T00:00:00.000000Z"
406+
type: string
411407
type: object
412408
models.BudgetCreate:
413409
properties:

main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ func main() {
5252
log.Fatal().Msg(err.Error())
5353
}
5454

55+
// Drop unused constraint in https://github.com/envelope-zero/backend/pull/274
56+
// Can be removed after the 1.0.0 release (we will require everyone to upgrade to 1.0.0 and then to further releases).
57+
err = database.DB.Migrator().DropConstraint(&models.Allocation{}, "month_valid")
58+
if err != nil {
59+
log.Debug().Err(err).Msg("Could not drop month_valid constraint on allocations")
60+
}
61+
5562
// Migrate all models so that the schema is correct
5663
err = database.DB.AutoMigrate(models.Budget{}, models.Account{}, models.Category{}, models.Envelope{}, models.Transaction{}, models.Allocation{})
5764
if err != nil {

pkg/controllers/allocation.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"net/http"
7+
"time"
78

89
"github.com/google/uuid"
910

@@ -96,6 +97,9 @@ func CreateAllocation(c *gin.Context) {
9697
return
9798
}
9899

100+
// Ignore every field that is not Year or Month
101+
allocation.Month = time.Date(allocation.Month.Year(), allocation.Month.Month(), 1, 0, 0, 0, 0, time.UTC)
102+
99103
_, err = getEnvelopeResource(c, allocation.EnvelopeID)
100104
if err != nil {
101105
return
@@ -197,6 +201,9 @@ func UpdateAllocation(c *gin.Context) {
197201
return
198202
}
199203

204+
// Ignore every field that is not Year or Month
205+
allocation.Month = time.Date(allocation.Month.Year(), allocation.Month.Month(), 1, 0, 0, 0, 0, time.UTC)
206+
200207
err = database.DB.Model(&allocation).Select("", updateFields...).Updates(data).Error
201208
if err != nil {
202209
httputil.ErrorHandler(c, err)

pkg/controllers/allocation_test.go

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,14 @@ func (suite *TestSuiteEnv) TestOptionsAllocation() {
3636
recorder = test.Request(suite.T(), http.MethodOptions, "http://example.com/v1/allocations/NotParseableAsUUID", "")
3737
assert.Equal(suite.T(), http.StatusBadRequest, recorder.Code, "Request ID %s", recorder.Header().Get("x-request-id"))
3838

39-
path = createTestAllocation(suite.T(), models.AllocationCreate{Month: 2}).Data.Links.Self
39+
path = createTestAllocation(suite.T(), models.AllocationCreate{Month: time.Date(2022, 2, 1, 0, 0, 0, 0, time.UTC)}).Data.Links.Self
4040
recorder = test.Request(suite.T(), http.MethodOptions, path, "")
4141
assert.Equal(suite.T(), http.StatusNoContent, recorder.Code, "Request ID %s", recorder.Header().Get("x-request-id"))
4242
}
4343

4444
func (suite *TestSuiteEnv) TestGetAllocations() {
4545
_ = createTestAllocation(suite.T(), models.AllocationCreate{
46-
Month: 1,
47-
Year: 2022,
46+
Month: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC),
4847
Amount: decimal.NewFromFloat(20.99),
4948
})
5049

@@ -55,9 +54,7 @@ func (suite *TestSuiteEnv) TestGetAllocations() {
5554

5655
test.AssertHTTPStatus(suite.T(), http.StatusOK, &recorder)
5756
assert.Len(suite.T(), response.Data, 1)
58-
59-
assert.Equal(suite.T(), uint8(1), response.Data[0].Month)
60-
assert.Equal(suite.T(), uint(2022), response.Data[0].Year)
57+
assert.Equal(suite.T(), time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC), response.Data[0].Month)
6158

6259
if !decimal.NewFromFloat(20.99).Equal(response.Data[0].Amount) {
6360
assert.Fail(suite.T(), "Allocation amount does not equal 20.99", response.Data[0].Amount)
@@ -107,8 +104,7 @@ func (suite *TestSuiteEnv) TestAllocationInvalidIDs() {
107104

108105
func (suite *TestSuiteEnv) TestCreateAllocation() {
109106
a := createTestAllocation(suite.T(), models.AllocationCreate{
110-
Month: 10,
111-
Year: 2022,
107+
Month: time.Date(2022, 10, 1, 0, 0, 0, 0, time.UTC),
112108
Amount: decimal.NewFromFloat(15.42),
113109
})
114110

@@ -133,97 +129,88 @@ func (suite *TestSuiteEnv) TestCreateAllocationNonExistingEnvelope() {
133129
}
134130

135131
func (suite *TestSuiteEnv) TestCreateDuplicateAllocation() {
136-
allocation := createTestAllocation(suite.T(), models.AllocationCreate{Year: 2022, Month: 2})
132+
allocation := createTestAllocation(suite.T(), models.AllocationCreate{Month: time.Date(2022, 2, 1, 0, 0, 0, 0, time.UTC)})
137133
recorder := test.Request(suite.T(), http.MethodPost, "http://example.com/v1/allocations", models.AllocationCreate{
138134
EnvelopeID: allocation.Data.EnvelopeID,
139135
Month: allocation.Data.Month,
140-
Year: allocation.Data.Year,
141136
})
142137

143138
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &recorder)
144139
}
145140

146-
func (suite *TestSuiteEnv) TestCreateAllocationNoMonth() {
147-
envelope := createTestEnvelope(suite.T(), models.EnvelopeCreate{})
148-
recorder := test.Request(suite.T(), http.MethodPost, "http://example.com/v1/allocations", models.AllocationCreate{EnvelopeID: envelope.Data.ID, Year: 2022})
149-
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &recorder)
150-
}
151-
152141
func (suite *TestSuiteEnv) TestCreateAllocationNoBody() {
153142
recorder := test.Request(suite.T(), http.MethodPost, "http://example.com/v1/allocations", "")
154143
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &recorder)
155144
}
156145

157146
func (suite *TestSuiteEnv) TestGetAllocation() {
158147
a := createTestAllocation(suite.T(), models.AllocationCreate{
159-
Year: 2022,
160-
Month: 8,
148+
Month: time.Date(2022, 8, 1, 0, 0, 0, 0, time.UTC),
161149
})
162150

163151
r := test.Request(suite.T(), http.MethodGet, a.Data.Links.Self, "")
164152
assert.Equal(suite.T(), http.StatusOK, r.Code)
165153
}
166154

167155
func (suite *TestSuiteEnv) TestUpdateAllocation() {
168-
a := createTestAllocation(suite.T(), models.AllocationCreate{Year: 2100, Month: 6})
156+
a := createTestAllocation(suite.T(), models.AllocationCreate{Month: time.Date(2100, 6, 1, 0, 0, 0, 0, time.UTC)})
169157

170158
r := test.Request(suite.T(), http.MethodPatch, a.Data.Links.Self, map[string]any{
171-
"year": 2022,
159+
"month": time.Date(2022, 6, 1, 0, 0, 0, 0, time.UTC),
172160
})
173161
test.AssertHTTPStatus(suite.T(), http.StatusOK, &r)
174162

175163
var updatedAllocation controllers.AllocationResponse
176164
test.DecodeResponse(suite.T(), &r, &updatedAllocation)
177165

178-
assert.Equal(suite.T(), uint(2022), updatedAllocation.Data.Year)
166+
assert.Equal(suite.T(), 2022, updatedAllocation.Data.Month.Year())
179167
}
180168

181169
func (suite *TestSuiteEnv) TestUpdateAllocationZeroValues() {
182-
a := createTestAllocation(suite.T(), models.AllocationCreate{Year: 2100, Month: 6})
170+
a := createTestAllocation(suite.T(), models.AllocationCreate{Month: time.Date(2100, 8, 1, 0, 0, 0, 0, time.UTC)})
183171

184172
r := test.Request(suite.T(), http.MethodPatch, a.Data.Links.Self, map[string]any{
185-
"year": 0,
186-
"month": 8,
173+
"month": time.Date(0, 8, 1, 0, 0, 0, 0, time.UTC),
187174
})
188175
test.AssertHTTPStatus(suite.T(), http.StatusOK, &r)
189176

190177
var updatedAllocation controllers.AllocationResponse
191178
test.DecodeResponse(suite.T(), &r, &updatedAllocation)
192179

193-
assert.Equal(suite.T(), uint(0), updatedAllocation.Data.Year, "Year is not updated correctly")
180+
assert.Equal(suite.T(), 0, updatedAllocation.Data.Month.Year(), "Year is not updated correctly")
194181
}
195182

196183
func (suite *TestSuiteEnv) TestUpdateAllocationBrokenJSON() {
197-
a := createTestAllocation(suite.T(), models.AllocationCreate{Year: 2100, Month: 6})
184+
a := createTestAllocation(suite.T(), models.AllocationCreate{Month: time.Date(2054, 5, 1, 0, 0, 0, 0, time.UTC)})
198185

199186
r := test.Request(suite.T(), http.MethodPatch, a.Data.Links.Self, `{ "name": 2" }`)
200187
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &r)
201188
}
202189

203190
func (suite *TestSuiteEnv) TestUpdateAllocationInvalidType() {
204-
a := createTestAllocation(suite.T(), models.AllocationCreate{Year: 2100, Month: 6})
191+
a := createTestAllocation(suite.T(), models.AllocationCreate{Month: time.Date(2062, 3, 1, 0, 0, 0, 0, time.UTC)})
205192

206193
r := test.Request(suite.T(), http.MethodPatch, a.Data.Links.Self, map[string]any{
207-
"year": "A long time ago in a galaxy far, far away",
194+
"month": "A long time ago in a galaxy far, far away",
208195
})
209196
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &r)
210197
}
211198

212-
func (suite *TestSuiteEnv) TestUpdateAllocationInvalidCategoryID() {
213-
a := createTestAllocation(suite.T(), models.AllocationCreate{Year: 2100, Month: 6})
199+
func (suite *TestSuiteEnv) TestUpdateAllocationInvalidEnvelopeID() {
200+
a := createTestAllocation(suite.T(), models.AllocationCreate{Month: time.Date(2099, 11, 1, 0, 0, 0, 0, time.UTC)})
214201

215-
// Sets the EnvelopeID to uuid.Nil
216-
r := test.Request(suite.T(), http.MethodPatch, a.Data.Links.Self, models.AllocationCreate{Month: 6, Year: 2100})
202+
// Sets the EnvelopeID to uuid.Nil by not specifying it
203+
r := test.Request(suite.T(), http.MethodPatch, a.Data.Links.Self, models.AllocationCreate{Month: time.Date(2099, 11, 1, 0, 0, 0, 0, time.UTC)})
217204
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &r)
218205
}
219206

220207
func (suite *TestSuiteEnv) TestUpdateNonExistingAllocation() {
221-
recorder := test.Request(suite.T(), http.MethodPatch, "http://example.com/v1/allocations/df684988-31df-444c-8aaa-b53195d55d6e", models.AllocationCreate{Month: 2})
208+
recorder := test.Request(suite.T(), http.MethodPatch, "http://example.com/v1/allocations/df684988-31df-444c-8aaa-b53195d55d6e", models.AllocationCreate{Month: time.Date(2142, 3, 1, 0, 0, 0, 0, time.UTC)})
222209
test.AssertHTTPStatus(suite.T(), http.StatusNotFound, &recorder)
223210
}
224211

225212
func (suite *TestSuiteEnv) TestDeleteAllocation() {
226-
a := createTestAllocation(suite.T(), models.AllocationCreate{Year: 2033, Month: 11})
213+
a := createTestAllocation(suite.T(), models.AllocationCreate{Month: time.Date(2058, 7, 1, 0, 0, 0, 0, time.UTC)})
227214
r := test.Request(suite.T(), http.MethodDelete, a.Data.Links.Self, "")
228215

229216
test.AssertHTTPStatus(suite.T(), http.StatusNoContent, &r)
@@ -235,9 +222,9 @@ func (suite *TestSuiteEnv) TestDeleteNonExistingAllocation() {
235222
}
236223

237224
func (suite *TestSuiteEnv) TestDeleteAllocationWithBody() {
238-
a := createTestAllocation(suite.T(), models.AllocationCreate{Year: 2070, Month: 12})
225+
a := createTestAllocation(suite.T(), models.AllocationCreate{Month: time.Date(2067, 3, 1, 0, 0, 0, 0, time.UTC)})
239226

240-
r := test.Request(suite.T(), http.MethodDelete, a.Data.Links.Self, models.AllocationCreate{Year: 2011, Month: 3})
227+
r := test.Request(suite.T(), http.MethodDelete, a.Data.Links.Self, models.AllocationCreate{Month: time.Date(2067, 3, 1, 0, 0, 0, 0, time.UTC)})
241228
test.AssertHTTPStatus(suite.T(), http.StatusNoContent, &r)
242229
}
243230

pkg/controllers/budget_test.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -176,22 +176,19 @@ func (suite *TestSuiteEnv) TestBudgetMonth() {
176176

177177
_ = createTestAllocation(suite.T(), models.AllocationCreate{
178178
EnvelopeID: envelope.Data.ID,
179-
Month: 1,
180-
Year: 2022,
179+
Month: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC),
181180
Amount: decimal.NewFromFloat(20.99),
182181
})
183182

184183
_ = createTestAllocation(suite.T(), models.AllocationCreate{
185184
EnvelopeID: envelope.Data.ID,
186-
Month: 2,
187-
Year: 2022,
185+
Month: time.Date(2022, 2, 1, 0, 0, 0, 0, time.UTC),
188186
Amount: decimal.NewFromFloat(47.12),
189187
})
190188

191189
_ = createTestAllocation(suite.T(), models.AllocationCreate{
192190
EnvelopeID: envelope.Data.ID,
193-
Month: 3,
194-
Year: 2022,
191+
Month: time.Date(2022, 3, 1, 0, 0, 0, 0, time.UTC),
195192
Amount: decimal.NewFromFloat(31.17),
196193
})
197194

pkg/controllers/envelope_test.go

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -141,22 +141,19 @@ func (suite *TestSuiteEnv) TestEnvelopeMonth() {
141141

142142
_ = createTestAllocation(suite.T(), models.AllocationCreate{
143143
EnvelopeID: envelope.Data.ID,
144-
Month: 1,
145-
Year: 2022,
144+
Month: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC),
146145
Amount: decimal.NewFromFloat(20.99),
147146
})
148147

149148
_ = createTestAllocation(suite.T(), models.AllocationCreate{
150149
EnvelopeID: envelope.Data.ID,
151-
Month: 2,
152-
Year: 2022,
150+
Month: time.Date(2022, 2, 1, 0, 0, 0, 0, time.UTC),
153151
Amount: decimal.NewFromFloat(47.12),
154152
})
155153

156154
_ = createTestAllocation(suite.T(), models.AllocationCreate{
157155
EnvelopeID: envelope.Data.ID,
158-
Month: 3,
159-
Year: 2022,
156+
Month: time.Date(2022, 3, 1, 0, 0, 0, 0, time.UTC),
160157
Amount: decimal.NewFromFloat(31.17),
161158
})
162159

@@ -235,11 +232,11 @@ func (suite *TestSuiteEnv) TestEnvelopeMonth() {
235232
test.AssertHTTPStatus(suite.T(), http.StatusOK, &r)
236233

237234
test.DecodeResponse(suite.T(), &r, &envelopeMonth)
238-
assert.Equal(suite.T(), envelopeMonth.Data.Name, tt.envelopeMonth.Name)
239-
assert.Equal(suite.T(), envelopeMonth.Data.Month, tt.envelopeMonth.Month)
240-
assert.True(suite.T(), envelopeMonth.Data.Spent.Equal(tt.envelopeMonth.Spent), "Monthly spent calculation for %v is wrong: should be %v, but is %v: %#v", envelopeMonth.Data.Month, tt.envelopeMonth.Spent, envelopeMonth.Data.Spent, envelopeMonth.Data)
241-
assert.True(suite.T(), envelopeMonth.Data.Balance.Equal(tt.envelopeMonth.Balance), "Monthly balance calculation for %v is wrong: should be %v, but is %v: %#v", envelopeMonth.Data.Month, tt.envelopeMonth.Balance, envelopeMonth.Data.Balance, envelopeMonth.Data)
242-
assert.True(suite.T(), envelopeMonth.Data.Allocation.Equal(tt.envelopeMonth.Allocation), "Monthly allocation fetch for %v is wrong: should be %v, but is %v: %#v", envelopeMonth.Data.Month, tt.envelopeMonth.Allocation, envelopeMonth.Data.Allocation, envelopeMonth.Data)
235+
assert.Equal(suite.T(), tt.envelopeMonth.Name, envelopeMonth.Data.Name)
236+
assert.Equal(suite.T(), time.Date(tt.envelopeMonth.Month.Year(), tt.envelopeMonth.Month.Month(), 1, 0, 0, 0, 0, time.UTC), envelopeMonth.Data.Month)
237+
assert.True(suite.T(), envelopeMonth.Data.Spent.Equal(tt.envelopeMonth.Spent), "Monthly spent calculation for %v is wrong: should be %v, but is %v: %#v", envelopeMonth.Data.Month.Month(), tt.envelopeMonth.Spent, envelopeMonth.Data.Spent, envelopeMonth.Data)
238+
assert.True(suite.T(), envelopeMonth.Data.Balance.Equal(tt.envelopeMonth.Balance), "Monthly balance calculation for %v is wrong: should be %v, but is %v: %#v", envelopeMonth.Data.Month.Month(), tt.envelopeMonth.Balance, envelopeMonth.Data.Balance, envelopeMonth.Data)
239+
assert.True(suite.T(), envelopeMonth.Data.Allocation.Equal(tt.envelopeMonth.Allocation), "Monthly allocation fetch for %v is wrong: should be %v, but is %v: %#v", envelopeMonth.Data.Month.Month(), tt.envelopeMonth.Allocation, envelopeMonth.Data.Allocation, envelopeMonth.Data)
243240
}
244241
}
245242

0 commit comments

Comments
 (0)