Skip to content

Commit 9ecd380

Browse files
authored
fix: importing of overspend handling now accounts for YNAB 4 oddities (#446)
In YNAB 4, when the overspendHandling is set to "Confined", it affects all months until it is explicitly set back to "AffectsBuffer". EZ on the other hand uses AFFECT_AVAILABLE as default (as does YNAB 4 with "AffectsBuffer") but only changes to AFFECT_ENVELOPE (= "Confined" on YNAB 4) when explicitly configured for that month. This commit introduces parsing logic that accounts for this difference in behaviour when importing.
1 parent 0710575 commit 9ecd380

File tree

3 files changed

+876
-650
lines changed

3 files changed

+876
-650
lines changed

docs/import.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ The following is **not yet supported** in Envelope Zero and will therefore be ig
1212

1313
- Recurring transactions. They will be implemented with [milestone #5](https://github.com/envelope-zero/backend/milestone/5) and import supported with https://github.com/envelope-zero/backend/issues/379.
1414
- Payee rename rules are not yet supported in Envelope Zero (https://github.com/envelope-zero/backend/issues/373)
15-
- Different handling of overspend. In Envelope Zero, overspend is always carried over to the next months budget for the category (https://github.com/envelope-zero/backend/issues/327)
1615

1716
The following **work differently** on Envelope Zero:
1817

1918
- Date formatting. While YNAB 4 does date formatting per Budget, in Envelope Zero, the formatting is decided by the browser (https://github.com/envelope-zero/frontend/issues/145) or by the users configuration (https://github.com/envelope-zero/backend/issues/33)
2019
- Transactions always need to have a source and destination. Transactions that do not have a Payee set in YNAB 4 will be imported with the opposing account as „YNAB 4 Import - No Payee“. If an account or Payee named „YNAB 4 Import - No Payee“ already exists in your budget, it will be used for those transactions.
2120
- Transactions can not have an amount of 0 - if no money was moved, no transaction is needed. Any transaction with an amount of 0 will be ignored during the import.
21+
- In YNAB 4, setting the overspend handling for an Envelope to “Subtract it from next month's category balance” affects all future months for that envelope until you set it back to ”Subtract it from next month's ’Available to Budget‘”. In Envelope Zero, setting overspend handling to `AFFECT_ENVELOPE` (where it is subtracted from the balance of the envelope in the next month) only affects the month it is set on. All months where no overspend handling is configured default to subtract it from the “Available to Budget” sum of the month after. If you set overspend handling to “Subtract it from next month's category balance” for a category in YNAB 4 and never changed it back on any of the following months, during the import, EZ will keep this behaviour for all months until the one you're executing the import in. For following months, you will have to set it manually.
2222

2323
### How to import
2424

pkg/importer/parser/ynab4/parse.go

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88
"regexp"
9+
"sort"
910
"strings"
1011
"time"
1112

@@ -67,6 +68,9 @@ func Parse(f io.Reader) (types.ParsedResources, error) {
6768
return types.ParsedResources{}, fmt.Errorf("error parsing budget allocations: %w", err)
6869
}
6970

71+
// Translate YNAB overspend handling behaviour to EZ overspending handling behaviour
72+
fixOverspendHandling(&resources)
73+
7074
return resources, nil
7175
}
7276

@@ -369,18 +373,15 @@ func parseMonthlyBudgets(resources *types.ParsedResources, monthlyBudgets []Mont
369373
continue
370374
}
371375

372-
// There's two modes in YNAB4: Confined, which equals AFFECT_ENVELOPE in EZ
373-
// and "AffectsBuffer", which equals AFFECT_AVAILABLE in EZ.
374-
// Since AFFECT_AVAILABLE is the default, we can skip everything that does not
375-
// lead to AFFECT_ENVELOPE.
376-
if subCategoryBudget.OverspendingHandling != "Confined" {
377-
continue
376+
var mode models.OverspendMode = "AFFECT_AVAILABLE"
377+
if subCategoryBudget.OverspendingHandling == "Confined" {
378+
mode = "AFFECT_ENVELOPE"
378379
}
379380

380381
resources.MonthConfigs = append(resources.MonthConfigs, types.MonthConfig{
381382
Model: models.MonthConfig{
382383
MonthConfigCreate: models.MonthConfigCreate{
383-
OverspendMode: "AFFECT_ENVELOPE",
384+
OverspendMode: mode,
384385
},
385386
Month: month,
386387
},
@@ -393,3 +394,92 @@ func parseMonthlyBudgets(resources *types.ParsedResources, monthlyBudgets []Mont
393394

394395
return nil
395396
}
397+
398+
// fixOverspendHandling translates the overspend handling behaviour of YNAB 4 into
399+
// the overspend handling of EZ. In YNAB 4, when the overspendHandling is set to "Confined",
400+
// it affects all months until it is explicitly set back to "AffectsBuffer".
401+
//
402+
// EZ on the other hand uses AFFECT_AVAILABLE as default (as does YNAB 4 with "AffectsBuffer")
403+
// but only changes to AFFECT_ENVELOPE (= "Confined" on YNAB 4) when explicitly configured for
404+
// that month.
405+
func fixOverspendHandling(resources *types.ParsedResources) {
406+
// sorter is a map of category names to a map of envelope names to the month configs
407+
sorter := make(map[string]map[string][]types.MonthConfig, 0)
408+
409+
// Sort by envelope
410+
for _, monthConfig := range resources.MonthConfigs {
411+
_, ok := sorter[monthConfig.Category]
412+
if !ok {
413+
sorter[monthConfig.Category] = make(map[string][]types.MonthConfig, 0)
414+
}
415+
416+
_, ok = sorter[monthConfig.Category][monthConfig.Envelope]
417+
if !ok {
418+
sorter[monthConfig.Category][monthConfig.Envelope] = make([]types.MonthConfig, 0)
419+
}
420+
421+
sorter[monthConfig.Category][monthConfig.Envelope] = append(sorter[monthConfig.Category][monthConfig.Envelope], monthConfig)
422+
}
423+
424+
// New slice for final MonthConfigs
425+
var monthConfigs []types.MonthConfig
426+
427+
// Fix handling for all envelopes
428+
for _, category := range sorter {
429+
for _, envelope := range category {
430+
// Sort by time so that earlier months are first
431+
sort.Slice(envelope, func(i, j int) bool {
432+
return envelope[i].Model.Month.Before(envelope[j].Model.Month)
433+
})
434+
435+
for i, mConfig := range envelope {
436+
// If we are switching back to "Available for budget", we don't need to do anything
437+
if mConfig.Model.OverspendMode == "AFFECT_AVAILABLE" || mConfig.Model.OverspendMode == "" {
438+
continue
439+
}
440+
441+
monthConfigs = append(monthConfigs, mConfig)
442+
443+
// Start with the next month since we already appended the current one
444+
checkMonth := mConfig.Model.Month.AddDate(0, 1, 0)
445+
446+
// If this is the last month, we set all months including the one of today to "AFFECT_ENVELOPE"
447+
// to preserve the YNAB 4 behaviour up to the switch to EZ
448+
if i+1 == len(envelope) {
449+
for ok := true; ok; ok = !checkMonth.After(time.Now()) {
450+
monthConfigs = append(monthConfigs, types.MonthConfig{
451+
Model: models.MonthConfig{
452+
Month: checkMonth,
453+
},
454+
Category: mConfig.Category,
455+
Envelope: mConfig.Envelope,
456+
})
457+
458+
checkMonth = checkMonth.AddDate(0, 1, 0)
459+
}
460+
461+
continue
462+
}
463+
464+
// Set all months up to the next one with a configuration to "AFFECT_ENVELOPE"
465+
for ok := !checkMonth.Equal(envelope[i+1].Model.Month); ok; ok = !checkMonth.Equal(envelope[i+1].Model.Month) {
466+
monthConfigs = append(monthConfigs, types.MonthConfig{
467+
Model: models.MonthConfig{
468+
Month: checkMonth,
469+
MonthConfigCreate: models.MonthConfigCreate{
470+
OverspendMode: "AFFECT_ENVELOPE",
471+
},
472+
},
473+
Category: mConfig.Category,
474+
Envelope: mConfig.Envelope,
475+
})
476+
477+
checkMonth = checkMonth.AddDate(0, 1, 0)
478+
}
479+
}
480+
}
481+
}
482+
483+
// Overwrite the original MonthConfigs with the fixed ones
484+
resources.MonthConfigs = monthConfigs
485+
}

0 commit comments

Comments
 (0)