Skip to content

Commit 1dc9f5f

Browse files
authored
feat(importer/ynab-import): automatically use existing accounts with matching names (#685)
1 parent bc7a11b commit 1dc9f5f

File tree

3 files changed

+110
-9
lines changed

3 files changed

+110
-9
lines changed

pkg/controllers/import.go

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,8 @@ func (co Controller) ImportYnabImportPreview(c *gin.Context) {
143143
return
144144
}
145145

146-
if transactions, ok = duplicateTransactions(co, transactions); !ok {
147-
return
148-
}
146+
transactions = duplicateTransactions(co, transactions)
147+
transactions = findAccounts(co, transactions, account.BudgetID)
149148

150149
c.JSON(http.StatusOK, ImportPreviewList{Data: transactions})
151150
}
@@ -243,7 +242,7 @@ func getUploadedFile(c *gin.Context, suffix string) (multipart.File, bool) {
243242
// duplicateTransactions finds duplicate transactions by their import hash. For all input resources,
244243
// existing resources with the same import hash are searched. If any exist, their IDs are set in the
245244
// DuplicateTransactionIDs field.
246-
func duplicateTransactions(co Controller, transactions []importer.TransactionPreview) ([]importer.TransactionPreview, bool) {
245+
func duplicateTransactions(co Controller, transactions []importer.TransactionPreview) []importer.TransactionPreview {
247246
for k, transaction := range transactions {
248247
var duplicates []models.Transaction
249248
co.DB.Find(&duplicates, models.Transaction{
@@ -263,5 +262,51 @@ func duplicateTransactions(co Controller, transactions []importer.TransactionPre
263262
transactions[k] = transaction
264263
}
265264

266-
return transactions, true
265+
return transactions
266+
}
267+
268+
// findAccounts sets the source or destination account ID for a TransactionPreview resource
269+
// if there is exactly one account with a matching name.
270+
func findAccounts(co Controller, transactions []importer.TransactionPreview, budgetID uuid.UUID) []importer.TransactionPreview {
271+
for k, transaction := range transactions {
272+
// Find the right account name
273+
name := transaction.DestinationAccountName
274+
if transaction.SourceAccountName != "" {
275+
name = transaction.SourceAccountName
276+
}
277+
278+
var accounts []models.Account
279+
co.DB.Where(models.Account{
280+
AccountCreate: models.AccountCreate{
281+
Name: name,
282+
BudgetID: budgetID,
283+
Hidden: false,
284+
},
285+
},
286+
// Explicitly specfiy search fields since we use a zero value for Hidden
287+
"Name", "BudgetID", "Hidden").Find(&accounts)
288+
289+
// We cannot determine correctly which account should be used if there are
290+
// multiple accounts, therefore we skip
291+
//
292+
// We also continue if no accounts are found
293+
if len(accounts) != 1 {
294+
continue
295+
}
296+
297+
// Set source or destination, depending on which one we checked for
298+
if accounts[0].ID != uuid.Nil {
299+
if transaction.SourceAccountName != "" {
300+
transaction.Transaction.SourceAccountID = accounts[0].ID
301+
transaction.SourceAccountName = ""
302+
} else {
303+
transaction.Transaction.DestinationAccountID = accounts[0].ID
304+
transaction.DestinationAccountName = ""
305+
}
306+
}
307+
308+
transactions[k] = transaction
309+
}
310+
311+
return transactions
267312
}

pkg/controllers/import_test.go

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,25 +155,25 @@ func (suite *TestSuiteStandard) TestYnabImportPreviewDuplicateDetection() {
155155
account := suite.createTestAccount(models.AccountCreate{})
156156

157157
// Get the import hash of the first transaction and create one with the same import hash
158-
preview := parseComdirectTestCSV(suite, account.Data.ID)
158+
preview := parseCSV(suite, account.Data.ID, "comdirect-ynap.csv")
159159

160160
transaction := suite.createTestTransaction(models.TransactionCreate{
161161
SourceAccountID: account.Data.ID,
162162
ImportHash: preview.Data[0].Transaction.ImportHash,
163163
Amount: decimal.NewFromFloat(1.13),
164164
})
165165

166-
preview = parseComdirectTestCSV(suite, account.Data.ID)
166+
preview = parseCSV(suite, account.Data.ID, "comdirect-ynap.csv")
167167

168168
suite.Assert().Len(preview.Data[0].DuplicateTransactionIDs, 1, "Duplicate transaction IDs field does not have the correct number of IDs")
169169
suite.Assert().Equal(transaction.Data.ID, preview.Data[0].DuplicateTransactionIDs[0], "Duplicate transaction ID is not ID of the transaction that is duplicated")
170170
}
171171

172-
func parseComdirectTestCSV(suite *TestSuiteStandard, accountID uuid.UUID) controllers.ImportPreviewList {
172+
func parseCSV(suite *TestSuiteStandard, accountID uuid.UUID, file string) controllers.ImportPreviewList {
173173
path := fmt.Sprintf("ynab-import-preview?accountId=%s", accountID.String())
174174

175175
// Parse the test CSV
176-
body, headers := suite.loadTestFile("importer/ynab-import/comdirect-ynap.csv")
176+
body, headers := suite.loadTestFile(fmt.Sprintf("importer/ynab-import/%s", file))
177177
recorder := test.Request(suite.controller, suite.T(), http.MethodPost, fmt.Sprintf("http://example.com/v1/import/%s", path), body, headers)
178178
suite.Assert().Equal(http.StatusOK, recorder.Code, "Request ID %s, response %s", recorder.Header().Get("x-request-id"), recorder.Body.String())
179179

@@ -183,3 +183,55 @@ func parseComdirectTestCSV(suite *TestSuiteStandard, accountID uuid.UUID) contro
183183

184184
return response
185185
}
186+
187+
func (suite *TestSuiteStandard) TestYnabImportFindAccounts() {
188+
// Create a budget and two existing accounts to use
189+
budget := suite.createTestBudget(models.BudgetCreate{})
190+
edeka := suite.createTestAccount(models.AccountCreate{BudgetID: budget.Data.ID, Name: "Edeka", External: true})
191+
192+
// Create an archived account named "Edeka" to ensure it is not found. If it were found, the tests for the non-archived
193+
// account being found would fail since we do not use an account if we find more than one with the same name
194+
_ = suite.createTestAccount(models.AccountCreate{BudgetID: budget.Data.ID, Name: "Edeka", Hidden: true})
195+
196+
// Create an account named "Edeka" in another budget to ensure it is not found. If it were found, the tests for the non-archived
197+
// Edeka account being found would fail since we do not use an account if we find more than one with the same name
198+
_ = suite.createTestAccount(models.AccountCreate{Name: "Edeka"})
199+
200+
// Account we import to
201+
internalAccount := suite.createTestAccount(models.AccountCreate{BudgetID: budget.Data.ID, Name: "Envelope Zero Account"})
202+
203+
tests := []struct {
204+
name string // Name of the test
205+
sourceAccountIDs []uuid.UUID // The IDs of the source accounts
206+
sourceAccountNames []string // The sourceAccountName attribute after the find has been performed
207+
destinationAccountIDs []uuid.UUID // The IDs of the destination accounts
208+
destinationAccountNames []string // The destinationAccountName attribute after the find has been performed
209+
preTest func() // Function to execute before running tests
210+
}{
211+
{"No matching (Some Company) & 1 Matching (Edeka) accounts", []uuid.UUID{internalAccount.Data.ID, internalAccount.Data.ID, uuid.Nil}, []string{"", "", "Some Company"}, []uuid.UUID{edeka.Data.ID, uuid.Nil, internalAccount.Data.ID}, []string{"", "Deutsche Bahn", ""}, func() {}},
212+
{"Two matching non-archived accounts", []uuid.UUID{internalAccount.Data.ID, internalAccount.Data.ID, uuid.Nil}, []string{"", "", "Some Company"}, []uuid.UUID{uuid.Nil, uuid.Nil, internalAccount.Data.ID}, []string{"Edeka", "Deutsche Bahn", ""}, func() {
213+
_ = suite.createTestAccount(models.AccountCreate{BudgetID: budget.Data.ID, Name: "Edeka"})
214+
}},
215+
}
216+
217+
for _, tt := range tests {
218+
suite.T().Run(tt.name, func(t *testing.T) {
219+
tt.preTest()
220+
preview := parseCSV(suite, internalAccount.Data.ID, "account-find-test.csv")
221+
222+
for i, transaction := range preview.Data {
223+
line := i + 1
224+
assert.Equal(t, tt.sourceAccountNames[i], transaction.SourceAccountName, "sourceAccountName does not match in line %d", line)
225+
assert.Equal(t, tt.destinationAccountNames[i], transaction.DestinationAccountName, "destinationAccountName does not match in line %d", line)
226+
227+
if tt.sourceAccountIDs[i] != uuid.Nil {
228+
assert.Equal(t, tt.sourceAccountIDs[i], transaction.Transaction.SourceAccountID, "sourceAccountID does not match in line %d", line)
229+
}
230+
231+
if tt.destinationAccountIDs[i] != uuid.Nil {
232+
assert.Equal(t, tt.destinationAccountIDs[i], transaction.Transaction.DestinationAccountID, "destinationAccountID does not match in line %d", line)
233+
}
234+
}
235+
})
236+
}
237+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Date,Payee,Memo,Outflow,Inflow
2+
04/01/2019,Edeka,Test,59.97,
3+
04/01/2019,Deutsche Bahn,Train ticket,80.00,
4+
04/25/2019,Some Company,Salary April 2019,,2350

0 commit comments

Comments
 (0)