Skip to content

Commit 40f250e

Browse files
authored
fix: allow transfers to not have an envelope (#258)
1 parent 62b04fc commit 40f250e

File tree

7 files changed

+128
-22
lines changed

7 files changed

+128
-22
lines changed

pkg/controllers/budget_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ func (suite *TestSuiteEnv) TestBudgetMonth() {
188188
BudgetID: budget.Data.ID,
189189
SourceAccountID: account.Data.ID,
190190
DestinationAccountID: externalAccount.Data.ID,
191-
EnvelopeID: envelope.Data.ID,
191+
EnvelopeID: &envelope.Data.ID,
192192
Reconciled: true,
193193
})
194194

@@ -199,7 +199,7 @@ func (suite *TestSuiteEnv) TestBudgetMonth() {
199199
BudgetID: budget.Data.ID,
200200
SourceAccountID: account.Data.ID,
201201
DestinationAccountID: externalAccount.Data.ID,
202-
EnvelopeID: envelope.Data.ID,
202+
EnvelopeID: &envelope.Data.ID,
203203
Reconciled: true,
204204
})
205205

@@ -210,7 +210,7 @@ func (suite *TestSuiteEnv) TestBudgetMonth() {
210210
BudgetID: budget.Data.ID,
211211
SourceAccountID: account.Data.ID,
212212
DestinationAccountID: externalAccount.Data.ID,
213-
EnvelopeID: envelope.Data.ID,
213+
EnvelopeID: &envelope.Data.ID,
214214
Reconciled: true,
215215
})
216216

pkg/controllers/envelope_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ func (suite *TestSuiteEnv) TestEnvelopeMonth() {
154154
BudgetID: budget.Data.ID,
155155
SourceAccountID: account.Data.ID,
156156
DestinationAccountID: externalAccount.Data.ID,
157-
EnvelopeID: envelope.Data.ID,
157+
EnvelopeID: &envelope.Data.ID,
158158
Reconciled: true,
159159
})
160160

@@ -165,7 +165,7 @@ func (suite *TestSuiteEnv) TestEnvelopeMonth() {
165165
BudgetID: budget.Data.ID,
166166
SourceAccountID: account.Data.ID,
167167
DestinationAccountID: externalAccount.Data.ID,
168-
EnvelopeID: envelope.Data.ID,
168+
EnvelopeID: &envelope.Data.ID,
169169
Reconciled: true,
170170
})
171171

@@ -176,7 +176,7 @@ func (suite *TestSuiteEnv) TestEnvelopeMonth() {
176176
BudgetID: budget.Data.ID,
177177
SourceAccountID: account.Data.ID,
178178
DestinationAccountID: externalAccount.Data.ID,
179-
EnvelopeID: envelope.Data.ID,
179+
EnvelopeID: &envelope.Data.ID,
180180
Reconciled: true,
181181
})
182182

pkg/controllers/transaction.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,21 +92,31 @@ func CreateTransaction(c *gin.Context) {
9292
}
9393

9494
// Check the source account
95-
_, err = getAccountResource(c, transaction.SourceAccountID)
95+
sourceAccount, err := getAccountResource(c, transaction.SourceAccountID)
9696
if err != nil {
9797
return
9898
}
9999

100100
// Check the destination account
101-
_, err = getAccountResource(c, transaction.DestinationAccountID)
101+
destinationAccount, err := getAccountResource(c, transaction.DestinationAccountID)
102102
if err != nil {
103103
return
104104
}
105105

106-
// Check the envelope
107-
_, err = getEnvelopeResource(c, transaction.EnvelopeID)
108-
if err != nil {
106+
// Check if the transaction is a transfer. If yes, the envelope can be empty.
107+
//
108+
// Check that the Envelope ID is set for incoming and outgoing transactions
109+
if sourceAccount.External || destinationAccount.External && transaction.EnvelopeID == nil {
110+
httputil.NewError(c, http.StatusBadRequest, errors.New("For incoming and outgoing transactions, an envelope is required"))
109111
return
112+
113+
// Check the envelope ID only if it is set. (This will always evaluate to true for incoming and outgoing transactions,
114+
// but for transfers, can evaluate to false
115+
} else if transaction.EnvelopeID != nil {
116+
_, err = getEnvelopeResource(c, *transaction.EnvelopeID)
117+
if err != nil {
118+
return
119+
}
110120
}
111121

112122
if !decimal.Decimal.IsPositive(transaction.Amount) {

pkg/controllers/transaction_test.go

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ func createTestTransaction(t *testing.T, c models.TransactionCreate) controllers
2525
c.DestinationAccountID = createTestAccount(t, models.AccountCreate{Name: "Destination Account"}).Data.ID
2626
}
2727

28-
if c.EnvelopeID == uuid.Nil {
29-
c.EnvelopeID = createTestEnvelope(t, models.EnvelopeCreate{Name: "Transaction Test Envelope"}).Data.ID
28+
if c.EnvelopeID == &uuid.Nil {
29+
*c.EnvelopeID = createTestEnvelope(t, models.EnvelopeCreate{Name: "Transaction Test Envelope"}).Data.ID
3030
}
3131

3232
r := test.Request(t, http.MethodPost, "http://example.com/v1/transactions", c)
@@ -104,7 +104,7 @@ func (suite *TestSuiteEnv) TestCreateTransactionMissingReference() {
104104
TransactionCreate: models.TransactionCreate{
105105
SourceAccountID: account.Data.ID,
106106
DestinationAccountID: account.Data.ID,
107-
EnvelopeID: envelope.Data.ID,
107+
EnvelopeID: &envelope.Data.ID,
108108
},
109109
})
110110
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &r)
@@ -124,7 +124,7 @@ func (suite *TestSuiteEnv) TestCreateTransactionMissingReference() {
124124
TransactionCreate: models.TransactionCreate{
125125
BudgetID: budget.Data.ID,
126126
DestinationAccountID: account.Data.ID,
127-
EnvelopeID: envelope.Data.ID,
127+
EnvelopeID: &envelope.Data.ID,
128128
},
129129
})
130130
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &r)
@@ -134,7 +134,7 @@ func (suite *TestSuiteEnv) TestCreateTransactionMissingReference() {
134134
TransactionCreate: models.TransactionCreate{
135135
BudgetID: budget.Data.ID,
136136
SourceAccountID: account.Data.ID,
137-
EnvelopeID: envelope.Data.ID,
137+
EnvelopeID: &envelope.Data.ID,
138138
},
139139
})
140140
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &r)
@@ -160,7 +160,7 @@ func (suite *TestSuiteEnv) TestCreateNegativeAmountTransaction() {
160160
BudgetID: budget.Data.ID,
161161
SourceAccountID: account.Data.ID,
162162
DestinationAccountID: account.Data.ID,
163-
EnvelopeID: envelope.Data.ID,
163+
EnvelopeID: &envelope.Data.ID,
164164
Amount: decimal.NewFromFloat(-17.12),
165165
Note: "Negative amounts are not allowed, this must fail",
166166
})
@@ -173,6 +173,48 @@ func (suite *TestSuiteEnv) TestCreateNonExistingBudgetTransaction() {
173173
test.AssertHTTPStatus(suite.T(), http.StatusNotFound, &recorder)
174174
}
175175

176+
func (suite *TestSuiteEnv) TestCreateNoEnvelopeTransactionTransfer() {
177+
c := models.TransactionCreate{
178+
BudgetID: createTestBudget(suite.T(), models.BudgetCreate{Name: "Testing budget for transfer"}).Data.ID,
179+
SourceAccountID: createTestAccount(suite.T(), models.AccountCreate{Name: "Internal Source Account", External: false}).Data.ID,
180+
DestinationAccountID: createTestAccount(suite.T(), models.AccountCreate{Name: "Internal destination account", External: false}).Data.ID,
181+
Amount: decimal.NewFromFloat(500),
182+
}
183+
184+
recorder := test.Request(suite.T(), http.MethodPost, "http://example.com/v1/transactions", c)
185+
test.AssertHTTPStatus(suite.T(), http.StatusCreated, &recorder)
186+
}
187+
188+
func (suite *TestSuiteEnv) TestCreateNoEnvelopeTransactionOutgoing() {
189+
c := models.TransactionCreate{
190+
BudgetID: createTestBudget(suite.T(), models.BudgetCreate{Name: "Testing budget for transfer"}).Data.ID,
191+
SourceAccountID: createTestAccount(suite.T(), models.AccountCreate{Name: "Internal Source Account", External: false}).Data.ID,
192+
DestinationAccountID: createTestAccount(suite.T(), models.AccountCreate{Name: "External destination account", External: true}).Data.ID,
193+
Amount: decimal.NewFromFloat(350),
194+
}
195+
196+
recorder := test.Request(suite.T(), http.MethodPost, "http://example.com/v1/transactions", c)
197+
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &recorder)
198+
199+
err := test.DecodeError(suite.T(), recorder.Body.Bytes())
200+
assert.Equal(suite.T(), "For incoming and outgoing transactions, an envelope is required", err)
201+
}
202+
203+
func (suite *TestSuiteEnv) TestCreateNonExistingEnvelopeTransactionTransfer() {
204+
id := uuid.New()
205+
206+
c := models.TransactionCreate{
207+
BudgetID: createTestBudget(suite.T(), models.BudgetCreate{Name: "Testing budget for transfer"}).Data.ID,
208+
SourceAccountID: createTestAccount(suite.T(), models.AccountCreate{Name: "Internal Source Account", External: false}).Data.ID,
209+
DestinationAccountID: createTestAccount(suite.T(), models.AccountCreate{Name: "External destination account", External: true}).Data.ID,
210+
Amount: decimal.NewFromFloat(350),
211+
EnvelopeID: &id,
212+
}
213+
214+
recorder := test.Request(suite.T(), http.MethodPost, "http://example.com/v1/transactions", c)
215+
test.AssertHTTPStatus(suite.T(), http.StatusNotFound, &recorder)
216+
}
217+
176218
func (suite *TestSuiteEnv) TestCreateTransactionNoBody() {
177219
recorder := test.Request(suite.T(), http.MethodPost, "http://example.com/v1/transactions", "")
178220
test.AssertHTTPStatus(suite.T(), http.StatusBadRequest, &recorder)

pkg/models/account_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func (suite *TestSuiteEnv) TestAccountCalculations() {
6060
incomingTransaction := models.Transaction{
6161
TransactionCreate: models.TransactionCreate{
6262
BudgetID: budget.ID,
63-
EnvelopeID: envelope.ID,
63+
EnvelopeID: &envelope.ID,
6464
SourceAccountID: externalAccount.ID,
6565
DestinationAccountID: account.ID,
6666
Reconciled: true,
@@ -75,7 +75,7 @@ func (suite *TestSuiteEnv) TestAccountCalculations() {
7575
outgoingTransaction := models.Transaction{
7676
TransactionCreate: models.TransactionCreate{
7777
BudgetID: budget.ID,
78-
EnvelopeID: envelope.ID,
78+
EnvelopeID: &envelope.ID,
7979
SourceAccountID: account.ID,
8080
DestinationAccountID: externalAccount.ID,
8181
Amount: decimal.NewFromFloat(17.45),

pkg/models/envelope_test.go

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func (suite *TestSuiteEnv) TestEnvelopeMonthSum() {
6464
transaction := &models.Transaction{
6565
TransactionCreate: models.TransactionCreate{
6666
BudgetID: budget.ID,
67-
EnvelopeID: envelope.ID,
67+
EnvelopeID: &envelope.ID,
6868
Amount: spent,
6969
SourceAccountID: internalAccount.ID,
7070
DestinationAccountID: externalAccount.ID,
@@ -79,7 +79,7 @@ func (suite *TestSuiteEnv) TestEnvelopeMonthSum() {
7979
transactionIn := &models.Transaction{
8080
TransactionCreate: models.TransactionCreate{
8181
BudgetID: budget.ID,
82-
EnvelopeID: envelope.ID,
82+
EnvelopeID: &envelope.ID,
8383
Amount: spent.Neg(),
8484
SourceAccountID: externalAccount.ID,
8585
DestinationAccountID: internalAccount.ID,
@@ -105,3 +105,57 @@ func (suite *TestSuiteEnv) TestEnvelopeMonthSum() {
105105
envelopeMonth = envelope.Month(time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC))
106106
assert.True(suite.T(), envelopeMonth.Spent.Equal(decimal.NewFromFloat(0)), "Month calculation for 2022-01 is wrong: should be %v, but is %v", decimal.NewFromFloat(0), envelopeMonth.Spent)
107107
}
108+
109+
func (suite *TestSuiteEnv) TestCreateTransactionNoEnvelope() {
110+
budget := models.Budget{}
111+
err := database.DB.Save(&budget).Error
112+
if err != nil {
113+
suite.Assert().Fail("Resource could not be saved", err)
114+
}
115+
116+
internalAccount := &models.Account{
117+
AccountCreate: models.AccountCreate{
118+
Name: "Internal Source Account",
119+
BudgetID: budget.ID,
120+
},
121+
}
122+
err = database.DB.Create(internalAccount).Error
123+
if err != nil {
124+
suite.Assert().Fail("Resource could not be saved", err)
125+
}
126+
127+
externalAccount := &models.Account{
128+
AccountCreate: models.AccountCreate{
129+
Name: "External Destination Account",
130+
BudgetID: budget.ID,
131+
External: true,
132+
},
133+
}
134+
err = database.DB.Create(&externalAccount).Error
135+
if err != nil {
136+
suite.Assert().Fail("Resource could not be saved", err)
137+
}
138+
139+
category := models.Category{
140+
CategoryCreate: models.CategoryCreate{
141+
BudgetID: budget.ID,
142+
},
143+
}
144+
err = database.DB.Save(&category).Error
145+
if err != nil {
146+
suite.Assert().Fail("Resource could not be saved", err)
147+
}
148+
149+
transaction := &models.Transaction{
150+
TransactionCreate: models.TransactionCreate{
151+
BudgetID: budget.ID,
152+
Amount: decimal.NewFromFloat(17.32),
153+
SourceAccountID: internalAccount.ID,
154+
DestinationAccountID: externalAccount.ID,
155+
Date: time.Date(2022, 1, 15, 0, 0, 0, 0, time.UTC),
156+
},
157+
}
158+
err = database.DB.Create(&transaction).Error
159+
160+
assert.Nil(suite.T(), err, "Transactions must be able to be created without an envelope (to enable internal transfers without an Envelope)")
161+
}

pkg/models/transaction.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type TransactionCreate struct {
2525
BudgetID uuid.UUID `json:"budgetId" example:"55eecbd8-7c46-4b06-ada9-f287802fb05e"`
2626
SourceAccountID uuid.UUID `json:"sourceAccountId" gorm:"check:source_destination_different,source_account_id != destination_account_id" example:"fd81dc45-a3a2-468e-a6fa-b2618f30aa45"`
2727
DestinationAccountID uuid.UUID `json:"destinationAccountId" example:"8e16b456-a719-48ce-9fec-e115cfa7cbcc"`
28-
EnvelopeID uuid.UUID `json:"envelopeId" example:"2649c965-7999-4873-ae16-89d5d5fa972e"`
28+
EnvelopeID *uuid.UUID `json:"envelopeId" example:"2649c965-7999-4873-ae16-89d5d5fa972e"`
2929
Reconciled bool `json:"reconciled" example:"true" default:"false"`
3030
}
3131

0 commit comments

Comments
 (0)