Compare commits

..

739 Commits

Author SHA1 Message Date
45af54d995 Add bass.build
Some checks failed
continuous-integration/drone/push Build is failing
2024-08-21 20:42:14 +00:00
a241298170 Merge pull request 'Use accounts transactions instead of global (Fixes #65)' (#78) from add-new-to-top into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #78
2022-09-10 23:26:13 +02:00
9248c12aab Use accounts transactions instead of global
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-09-10 21:24:02 +00:00
8817e665e9 Merge pull request 'Migrate from gin to echo framework' (#77) from migrate-to-echo into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #77
2022-08-21 22:04:28 +02:00
0b36048f47 use null instead of undefined
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-08-21 20:03:04 +00:00
0c2438373d js lint
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2022-08-21 20:00:13 +00:00
2f636b5f9c Wrap some errors
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2022-08-21 19:56:55 +00:00
4ef0b759ef Fix linter errors
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2022-08-21 19:51:56 +00:00
08fbacc7e6 Set Content-Type in test and handle error earlier
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2022-08-21 19:41:04 +00:00
1c003486ca Fix linter issues
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2022-08-21 19:35:51 +00:00
71be1ac49f Send Content-Type on POST 2022-08-21 19:33:12 +00:00
d258ab63e4 Finish migration 2022-08-21 19:33:12 +00:00
9da4a6f03e Continue migration to echo 2022-08-21 19:33:12 +00:00
1a11555075 Continue migration to echo 2022-08-21 19:33:12 +00:00
b573553424 Start migration to echo 2022-08-21 19:33:12 +00:00
dd0f620b7a Merge pull request 'Add abilty to filter transactions by account, payee and category' (#55) from filtered-transactions into master
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #55
2022-08-21 21:32:49 +02:00
42b732eedc Fix filters 2022-08-21 21:32:49 +02:00
f437491823 Implement filtering on frontend 2022-08-21 21:32:49 +02:00
dc15ae307b Remove old logging 2022-08-21 21:32:49 +02:00
f8a8a6fc0c Add date filtering to backend 2022-08-21 21:32:49 +02:00
a031dd5bb1 Add date filters to UI 2022-08-21 21:32:49 +02:00
d3ca3c87bf Make menu entry generic 2022-08-21 21:32:49 +02:00
77c1a6dd18 Make UI work for problematic and filtered transactions 2022-08-21 21:32:49 +02:00
a7e1826f52 Implement UI 2022-08-21 21:32:49 +02:00
94b5c4bbd3 Improve Backend 2022-08-21 21:32:49 +02:00
fdb64b2000 Add filters to UI 2022-08-21 21:32:49 +02:00
2ca3328f0f Implement backend 2022-08-21 21:32:49 +02:00
bd026732c8 Add 'Contributing' to README
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2022-07-26 00:02:22 +02:00
6032f6f526 „README.md“ ändern
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2022-07-25 23:42:34 +02:00
e1c6bd22d5 Merge pull request 'Improve available balance and show overspent last month' (#54) from available-balance into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #54
2022-04-24 21:23:17 +02:00
91ed57f83d Fix magic numbers for months
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-04-23 20:14:20 +00:00
0817f18e33 Only save available for last month and add overspent
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2022-04-23 20:12:37 +00:00
704a520993 Implement Next and Previous for month datatype 2022-04-23 20:12:20 +00:00
a48509e041 Calculate overspent for last month 2022-04-23 20:12:05 +00:00
b812b19b34 Indentation 2022-04-23 20:11:52 +00:00
0667e5a779 Merge pull request 'Display problematic transactions' (#53) from problematic-transactions into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #53
2022-04-23 13:32:39 +02:00
f47ede2060 Only execute main pipeline for master branch 2022-04-23 13:32:39 +02:00
91ac05b575 ESLINT 3 2022-04-23 13:32:39 +02:00
c30b33a070 ESLINT 2 2022-04-23 13:32:39 +02:00
4fa227c452 ESLINT 2022-04-23 13:32:39 +02:00
cfb1dbe276 Rename menu to problematic transactions 2022-04-23 13:32:39 +02:00
8f99499c2f Hide Index when logged in 2022-04-23 13:32:39 +02:00
d22816dfd1 Implement account editing and autocomplete 2022-04-23 13:32:39 +02:00
75a1839456 Add page for all accounts 2022-04-23 13:32:39 +02:00
9fd70cd773 Optionally add account to grid 2022-04-23 13:32:39 +02:00
3b465f17cf Implement problematic transactions endpoint 2022-04-23 13:32:39 +02:00
f56d095016 Reintroduce alternating row-colors by date 2022-04-23 13:32:39 +02:00
741589dc71 Remove alternating row-colors as they're non-functional 2022-04-23 13:32:39 +02:00
44254ed4d0 Group transactions by date to simplify grouping in UI 2022-04-23 13:32:39 +02:00
777520e9df Simplify query 2022-04-23 13:32:39 +02:00
07c0c56d11 Add budgetID param 2022-04-23 13:32:39 +02:00
43a4647d74 Add query to find problematic transactions 2022-04-23 13:32:39 +02:00
c595f016b8 Merge pull request 'Small layout fixes' (#52) from layout-fixes into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #52
2022-04-19 23:18:34 +02:00
6702a775fb Prevent wrapping in accounts names
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-04-19 21:17:53 +00:00
e4df2f0de5 Improve layout of new transaction on mobile 2022-04-19 21:13:52 +00:00
ff06693f60 Hide menu on navigation on mobile 2022-04-19 21:02:05 +00:00
5c5429ab05 Fix Header overlapping content mobile 2022-04-19 20:59:19 +00:00
65424a2587 Extract MainMenu component 2022-04-19 20:46:58 +00:00
4563fcbbaf Merge pull request 'Also handle transactions without category' (#51) from transactions_without_category into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #51
2022-04-14 22:35:32 +02:00
df7b691bdd Add dummy category for uncategorized transactions
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-04-14 20:33:51 +00:00
f5274f69fc Return all transactions until paging is implemented 2022-04-14 20:33:39 +00:00
c50fc63303 Add on_budget query to transactions_by_month 2022-04-14 20:23:07 +00:00
88e1dbdfde Simplify query by using COALESCE 2022-04-14 20:21:55 +00:00
0e84cdfa5f Merge pull request 'Fix amount for available balance' (#50) from available-balance into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #50
2022-04-08 22:43:21 +02:00
d29405bef3 Go can't add empty dirs
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-04-08 20:41:36 +00:00
46e665efc5 Remove not actually needed deps
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-04-08 20:40:43 +00:00
f3eb19ef91 Add empty dir 2022-04-08 20:40:23 +00:00
2ca05b5d48 Run yarn only once
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-04-08 20:36:14 +00:00
ed2eb53f93 cover also needs frontend
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-04-08 20:28:30 +00:00
8bf4fcfd42 Add generates to yarn
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-04-08 20:27:49 +00:00
38222c0777 lint also needs frontend
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-04-08 20:23:59 +00:00
f43feb66be Define sources for js-mod
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-04-08 20:20:57 +00:00
49be1d5be6 Parallelize deps of backend with frontend
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-04-08 20:19:35 +00:00
8ec69e4b8f build before analyzing
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-04-08 20:16:38 +00:00
6b3026dc8e Improve dev container and taskfile
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-04-08 20:14:40 +00:00
9b5119d3dd Fix eslint errors
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-04-08 19:01:28 +00:00
08f48a63ad Split activity into income and spending
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-04-08 18:56:36 +00:00
c4fc80e47d Add type-information to GetBudgeted
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-04-08 18:28:52 +00:00
23bd12147c Fix redirecting after deleting budget
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-04-08 18:23:39 +00:00
3ca7bfeade Fix number-input not returning numbers 2022-04-08 18:23:29 +00:00
249aa534c8 Update testdata
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-04-08 18:08:35 +00:00
76ee88a1c6 Update testdata
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-07 08:16:45 +00:00
bb664494f6 Use available from API instead of category 2022-04-07 08:06:58 +00:00
4f681d6d89 Update testdata 2022-04-07 07:54:23 +00:00
11df0fbff1 Remove unused cumultative columns 2022-04-07 07:53:48 +00:00
a2280e50ec Fix test without available property 2022-04-07 07:53:25 +00:00
ed321d3895 Fix income category not being correct for newly created budget 2022-04-07 07:52:06 +00:00
8fe91293e1 Add unittests for month
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-06 22:16:50 +00:00
35690681cf Fix calls to isAfter and so on 2022-04-06 21:44:43 +00:00
54e591bc5e Extract Month class 2022-04-06 21:41:55 +00:00
01c407d4f1 Remove income category from result
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-06 20:29:56 +00:00
cef62574bb Add assigned, deassigned and activity to budgeting screen 2022-04-06 20:22:02 +00:00
ae529665a3 Merge pull request 'Remove container_name, stdin and tty from dc' (#49) from microservices-dev into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #49
2022-04-06 22:21:51 +02:00
2eba947d9d Remove defineProps import
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-04-06 20:20:17 +00:00
4d70f50683 Remove tty settings and names 2022-04-06 20:20:17 +00:00
9b67e700be Merge pull request 'Remove unused code' (#48) from cleanup into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #48
2022-04-06 22:18:59 +02:00
cce9f29f17 Rename budgeting to budget 2022-04-06 22:18:59 +02:00
eb2b31e622 Split new and update-transaction into separate methods 2022-04-06 22:18:59 +02:00
967f1c71e5 Remove GET for login route 2022-04-06 22:18:59 +02:00
585951d5e2 Remove dashboard API 2022-04-06 22:18:59 +02:00
e7af03b702 Remove clean-negative 2022-04-06 22:18:59 +02:00
a1ec7e9997 Merge pull request 'Add test for account balances' (#47) from integration-test into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #47
2022-04-06 21:45:28 +02:00
880ed731f8 Add test for account balances
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-04-06 19:43:11 +00:00
a46ddded67 Merge pull request 'Implement integration-test based on YNAB-Export' (#46) from integration-test into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #46
2022-04-06 21:26:46 +02:00
56485b8deb Panic on err when connecting to db
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-04-06 19:10:28 +00:00
f67f6ff0d8 Run coverage as well 2022-04-06 19:09:47 +00:00
150a7d562b Use subtests
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-04-05 20:52:37 +00:00
49d96be1e3 Extract function 2022-04-05 20:45:00 +00:00
8df7968175 Use available balance from test case file
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-04-05 20:41:39 +00:00
bd99f58ab4 Actually use files 2022-04-05 20:33:04 +00:00
9471c5b63b Fix missing image
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-04-05 20:07:20 +00:00
60d6967728 Fetch submodules
Some checks reported errors
continuous-integration/drone/push Build encountered an error
continuous-integration/drone/pr Build encountered an error
2022-04-05 20:06:04 +00:00
1913d9eaf0 Move migration to init
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-05 20:03:18 +00:00
8954cffb7a Move config to global variable
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-05 20:00:25 +00:00
95eb302d26 Build real connection before using txdb
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-05 19:56:35 +00:00
93679c2932 Rename postgres service
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-05 19:52:54 +00:00
cb8ce79107 Run postgres for CI
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-05 19:51:25 +00:00
80c0f0a231 Add submodule for testdata
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-05 19:49:01 +00:00
d73eeb1b40 Call t.Helper 2022-04-05 19:47:04 +00:00
ce466e0031 Styleguide 2022-04-05 19:34:45 +00:00
684efffbdf Fix bugs in calculateBalances and handle not found categories
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-05 19:33:41 +00:00
cf39db52fb Move code to other method 2022-04-05 19:33:12 +00:00
f0993fd9c3 Read tests from file 2022-04-05 19:08:59 +00:00
5f6bea4ee2 Remove unused prop 2022-04-05 19:00:40 +00:00
4332a1537b Remove invalid test 2022-04-05 16:25:34 +00:00
2feefea737 Check available balance 2022-04-05 16:22:20 +00:00
2d9c380cf4 Implement ynab import in test and use pgtx 2022-04-05 16:11:11 +00:00
4a66d9fdfc Add test setup 2022-04-05 15:44:26 +00:00
967ea784dc Merge pull request 'Split dev container into multiple containers' (#45) from microservices-dev into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #45
2022-03-17 21:09:58 +01:00
23b7a4dbdd Remove tmux and dev.sh
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-17 19:54:45 +00:00
4798cb95b0 Split dev docker-container into two containers 2022-03-17 19:41:24 +00:00
8eabf3d1c8 Merge pull request 'Fix input on mobile' (#44) from mobile-input into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #44
2022-03-15 22:12:31 +01:00
f0084b683e Fix ts issues
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-15 21:11:24 +00:00
84c95d0c4e Bring submit button to work
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-03-15 21:05:12 +00:00
6576c994c9 Improve layout 2022-03-15 20:21:34 +00:00
a552983ac4 Merge pull request 'Setup and enable ESLint' (#43) from eslint into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #43
2022-03-15 21:11:01 +01:00
f99efe0e8e Finish Rename
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-03-15 20:09:59 +00:00
e1b6056d77 Fix more imports
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-03-15 20:08:36 +00:00
94329ac331 Fix imports in router
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-03-15 20:05:42 +00:00
bfa67578cf Fix tsc issues
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-03-15 20:04:01 +00:00
eae196aa69 Run eslint in CI 2022-03-15 20:00:12 +00:00
d2dd7d5280 Ignore left-over issues
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-03-15 19:58:16 +00:00
f707197f42 Ignore index.css 2022-03-15 19:58:03 +00:00
66322ebd86 Fix some linter issues 2022-03-15 19:50:49 +00:00
3ea90a5ebe Run eslint with autofix 2022-03-15 12:55:22 +00:00
d717ef1b4d Use spaces 2022-03-15 12:52:23 +00:00
61a534610f Run prettier
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-15 12:50:20 +00:00
d8d713f841 Change prettier configuration 2022-03-15 12:50:20 +00:00
7ac06012ec Initialize eslint 2022-03-15 12:50:20 +00:00
9af412a8bd Merge pull request 'Hide hidden categories from autocomplete results' (#42) from hide-hidden-categories into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #42
2022-03-15 13:50:10 +01:00
3b61028576 Hide hidden categories from searchresults 2022-03-15 13:50:10 +01:00
87b78e7b67 Make search queries case insensitive 2022-03-15 13:50:10 +01:00
6a71d9643c Merge pull request 'Persist last_reconciled instead of using last transactions date' (#41) from reconciled-as-field into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #41
2022-03-14 21:01:08 +01:00
450324d29e Extract AccountWithReconciled
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-14 19:48:45 +00:00
6f4e286b7a Initialize last_reconciled from transactions 2022-03-14 19:46:21 +00:00
324e13f5c5 Show age in days 2022-03-14 19:34:17 +00:00
0201b368d4 Add column and use instead of last transactions date
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-14 19:26:54 +00:00
984a2aa296 Merge pull request 'Show sums for category groups' (#40) from category-group-summary into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #40
2022-03-14 20:15:20 +01:00
d1a0296a58 Add margin before group
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-03-14 19:12:46 +00:00
d9efa9f4a1 Improve layout of groups 2022-03-14 19:10:09 +00:00
b176ce26ba Implement summing per category group
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-05 22:23:41 +00:00
97de326527 Use new UpdateAssignmentWithDifference for ynab-import to prevent errors on duplicates 2022-03-05 22:23:13 +00:00
a62ab543b0 Merge pull request 'Implement basic budgeting input' (#39) from budgeting into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #39
2022-03-05 22:12:14 +01:00
244333adbc Fix null reference in GetIncomeAvailable
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-05 20:48:36 +00:00
67ef4745a0 Handle login redirect 2022-03-05 20:48:27 +00:00
3856e8a8a5 Update assignment on modification
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-04 22:24:50 +00:00
db96fddde2 Implement update Assignment on backend 2022-03-04 22:24:38 +00:00
ecb801b2f2 Move getDate to util file 2022-03-04 21:56:24 +00:00
fd6b77f154 Replace id column with natural PK 2022-03-04 21:56:12 +00:00
442e87234c Show available income in Budgeting header
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-04 21:31:13 +00:00
caf39fe5ea Set Available balance of income category 2022-03-04 21:18:44 +00:00
0dde94f855 Remove logging 2022-03-04 21:15:24 +00:00
34bbdf48b7 Also return income category 2022-03-04 21:13:59 +00:00
1e495a5e47 Remove unneeded loop 2022-03-04 21:13:50 +00:00
fc29b1409d Use in-place additions more 2022-03-04 21:13:38 +00:00
8ecfd6b87a Use in-place calculations 2022-03-04 20:55:38 +00:00
d89e702669 Move sidebar layout to BudgetSidebar 2022-03-04 20:55:14 +00:00
3795150508 Implement numeric operations in-place 2022-03-04 20:54:23 +00:00
67c013799d Calculate available balance locally 2022-03-03 10:44:50 +00:00
0038db90ac Merge pull request 'Fix docker build' (#38) from docker-build into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #38
2022-03-02 23:55:29 +01:00
9759a7cc1e Fix docker build
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-02 22:55:21 +00:00
81c31a3d5f Merge pull request 'Cleanup' (#37) from cleanup into master
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #37
2022-03-02 23:53:15 +01:00
38e21786a7 Fix app bar for small screens
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-03-02 22:52:12 +00:00
971c3d3be5 Improve sidebar and show next to appbar instead of below 2022-03-02 22:52:12 +00:00
946f14c1cc Fix const 2022-03-02 22:51:20 +00:00
9ce0da0182 Move CSS to _index.css 2022-03-02 22:51:20 +00:00
4c93e4635d Extract const for default expiration 2022-03-02 22:51:20 +00:00
f3a50c790b Hide .task, build and dist folders 2022-03-02 22:51:20 +00:00
0c5f68ed80 Hide files generated by sqlc 2022-03-02 22:51:20 +00:00
7fdd8bd935 Split jwt into two files 2022-03-02 22:51:20 +00:00
d4287f8aac Remove example config 2022-03-02 22:51:20 +00:00
6712af10d9 Move production Dockerfile to docker directory 2022-03-02 22:51:20 +00:00
70edb382e1 Move compose files to docker directory 2022-03-02 22:51:20 +00:00
390a042441 Rename development Dockerfile and build script 2022-03-02 22:51:20 +00:00
e8028dae34 Remove woodpecker config 2022-03-02 22:51:20 +00:00
fc249adc9e Remove Earthfile 2022-03-02 22:51:20 +00:00
c186a14644 go mod tidy 2022-03-02 22:51:20 +00:00
8a27303670 Remove earthignore 2022-03-02 22:51:20 +00:00
44e9bb6ec0 Merge pull request 'Implement closing of accounts' (#36) from closed-accounts into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #36
2022-03-02 22:54:02 +01:00
42d431ba8b Fix modal layout for dark theme
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-02 21:52:35 +00:00
3727061065 Prevent closing of accounts with balance 2022-03-02 21:52:16 +00:00
c7a8adb3ab Remove closed account from sidebar
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-02 21:22:15 +00:00
29f534bf10 Hide closed accounts menuentry 2022-03-02 21:22:06 +00:00
15381c84f6 Navigate to budgeting from closed account 2022-03-02 21:21:56 +00:00
a0cabbf4f7 Pass is_open to API from EditAccount 2022-03-02 21:16:06 +00:00
f26ee8f472 Add ability to change is_open from API 2022-03-02 21:15:56 +00:00
4fb3c2a335 Show is_open in EditAccount 2022-03-02 21:14:12 +00:00
8899ff5772 Return and filter by is_open 2022-03-02 21:14:00 +00:00
347a0c9e50 Add column is_open to accounts 2022-03-02 21:13:42 +00:00
66b8e1f69f Fix edit-accounts' font being off 2022-03-02 21:07:53 +00:00
5d1b49c896 Merge pull request 'Implement custom checkbox' (#35) from checkbox into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #35
2022-03-02 21:34:27 +01:00
42dc51fe9a Improve layout of Reconcilation
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-03-02 20:33:01 +00:00
1ca95f8768 Remove old logging calls
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-02 20:06:33 +00:00
a73f7c2934 Bind to value instead of using v-model 2022-03-02 20:06:16 +00:00
489aa88c4b Add custom checkbox component 2022-03-02 20:06:06 +00:00
7b5b16c1b2 Merge pull request 'Design fixes for dark mode' (#34) from design-fixes into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #34
2022-03-02 20:54:26 +01:00
29cfeb6fa6 Use Input component for all inputs except file-input 2022-03-02 20:54:26 +01:00
42baafd273 Invert if 2022-03-02 20:54:26 +01:00
1e79f193be Extract Input component and add some dark color tags 2022-03-02 20:54:26 +01:00
f0ec7fb30d Merge pull request 'Show symbol for accounts that haven't been recently reconciled' (#33) from recently-reconciled into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #33
2022-03-02 20:53:41 +01:00
034f4f2a90 Show * in sidebar for unreconciled accounts
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-02 19:25:07 +00:00
51ece59866 Calculate last reconciled date on backend 2022-03-02 19:24:30 +00:00
bbfda6f402 Improve layout of balances header
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-01 20:59:06 +00:00
2b3afbf448 Merge pull request 'Implement simple dark mode' (#32) from dark-mode into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #32
2022-03-01 21:48:05 +01:00
a53c3d23a4 Implement simple dark mode
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-01 20:47:55 +00:00
c899c21256 Merge pull request 'Fix budget data being one month off' (#31) from hotfix-budgeting-off-by-one into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #31
2022-03-01 21:38:23 +01:00
cccc948048 Fix budget data being one month off
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-01 20:38:12 +00:00
e8a0670a83 Merge pull request 'Improve mobile usability' (#30) from mobile-usability into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #30
2022-03-01 21:33:07 +01:00
9638676b8f Add new transaction button on small screens
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-01 20:32:05 +00:00
08bda8d14f Make buttonText optional 2022-03-01 20:31:22 +00:00
d0ade0f2f1 Remove height from Card 2022-03-01 20:31:13 +00:00
466239ce11 Remove defaults from Button 2022-03-01 20:31:01 +00:00
d52e5c63d4 Hide Input on small screens and match columns 2022-03-01 20:12:27 +00:00
3dcb362372 Prevent wrap in balance-labels 2022-03-01 20:12:11 +00:00
67c9b53e91 Extract date to own row on small screens 2022-03-01 20:11:59 +00:00
7c08ddacb7 Remove widths from TransactionInputRow and use Button 2022-03-01 20:11:41 +00:00
ecbb85aeaa Decrease displayed information 2022-03-01 18:52:04 +00:00
3696bbde43 Check empty secret in other spots
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-03-01 18:37:07 +00:00
09a227d08d Wrap error for more details 2022-03-01 18:34:22 +00:00
dae6185857 Prevent startup on empty secret
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-01 18:33:46 +00:00
6dd8a3791f Use grid instead of table 2022-03-01 08:36:03 +00:00
18149eef8b Hide Leftover on small screens 2022-03-01 08:33:01 +00:00
f0f97a2e77 Merge pull request 'Fix budgeting not being openable' (#29) from hotfix-budget into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #29
2022-03-01 09:11:33 +01:00
71b2f8a9a3 Fix budgeting not being openable
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-28 20:14:27 +00:00
8f666a0f26 Merge pull request 'Improve Transactions handling' (#28) from transactions into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #28
2022-02-28 14:28:38 +01:00
2b9c76960e Run vue-tsc on CI build
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-28 13:27:02 +00:00
7fd057f9f6 Remove vuetify plugin 2022-02-28 13:25:14 +00:00
a9be9367a9 Use transactionid for TransactionRow 2022-02-28 13:23:30 +00:00
bc75757ac7 Complete migration two transactions store 2022-02-28 13:13:21 +00:00
a3e12df2e2 Extract transactions store 2022-02-28 13:09:15 +00:00
024c5e0a1c Refactor transactions store 2022-02-28 13:03:53 +00:00
27372199f7 Improve logger 2022-02-28 09:34:57 +00:00
b3b878854e Extract GetAccount 2022-02-28 09:28:53 +00:00
bbb12a788d Improve layout of Account header 2022-02-28 07:58:51 +00:00
7dfbef60a4 Fix date input getting stuck on invalid input 2022-02-27 22:10:47 +00:00
d8a96535dc Merge pull request 'Fix GroupID not being set for original transaction' (#27) from hotfix-transfers into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #27
2022-02-27 22:53:19 +01:00
ce26e76e8f Fix GroupID not being set for original transaction
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-27 21:44:15 +00:00
6b3ac199fc Merge pull request 'Implement reconciliation' (#26) from reconcilation into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #26
2022-02-27 22:16:01 +01:00
4c6d21c2b4 Improve editing and creating of transactions
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-02-27 21:15:46 +00:00
faef975f1a Pass amount as string and add transaction correctly
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-27 21:06:34 +00:00
bd686e0c00 Remove duplicate order by 2022-02-27 21:06:11 +00:00
4844889e0b Disable nilnil linter for CreateReconcilationTransaction 2022-02-27 20:53:57 +00:00
2775578713 Extract method CreateReconcilationTransaction 2022-02-27 20:50:26 +00:00
27dd6e923c Actually handle passed reconciliation amount 2022-02-27 20:45:18 +00:00
2cf6b815bf Use view everywhere 2022-02-27 20:45:02 +00:00
81aacf339e Extract view for transactions 2022-02-27 20:35:10 +00:00
16bcf516f6 Add ReconcilationTransactionAmount 2022-02-27 20:34:10 +00:00
52503a4c92 Improve frontend 2022-02-27 20:02:57 +00:00
b4321395d9 Implement backend for reconcilation 2022-02-27 20:02:23 +00:00
d7058a49b0 Add SQL Query for update status 2022-02-27 20:01:35 +00:00
eb9fc722aa Show reconcile button and current reconciling-balance 2022-02-27 19:20:13 +00:00
511081298e Merge pull request 'Split account balances in working, cleared and reconciled' (#25) from account-balances into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #25
2022-02-27 19:33:44 +01:00
422a74704b Show all balances in Account view
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-27 18:32:00 +00:00
c4995bcbaf Show cleared balance by default 2022-02-27 15:22:54 +00:00
79fd95e152 Calculate working, cleared and reconciled balance 2022-02-27 15:22:18 +00:00
af8fda178d Merge pull request 'Improve CI image' (#24) from ci-improvements into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #24
2022-02-26 00:01:31 +01:00
71f4d2384b Fetch image only if not in PR
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-25 23:00:15 +00:00
eb52bd5def Fix pull definition
Some checks reported errors
continuous-integration/drone/pr Build encountered an error
continuous-integration/drone/push Build is passing
2022-02-25 22:57:19 +00:00
adcb64f4ad Remove unused method
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-25 22:55:48 +00:00
a73c5ce04b Build images only on master push or tag
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-02-25 22:54:51 +00:00
52bb343402 Reorganize routes and remove legacy ones
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-02-25 22:52:08 +00:00
27ce8d250f Move WORKDIR call
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-02-25 22:44:09 +00:00
408207df92 Fix yarn call not doing anything
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-25 22:39:49 +00:00
98890f10eb Merge pull request 'Implement editing of transactions' (#23) from edit-transaction into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #23
2022-02-25 23:35:10 +01:00
5621d63436 Actually call backend for edit
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-25 22:34:09 +00:00
05099e469f Delete transaction when amount is zero 2022-02-25 22:33:58 +00:00
ae9e9d34c9 Use type instead of isAccount flag
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-25 22:28:22 +00:00
32439e3e87 rename type to model 2022-02-25 22:16:41 +00:00
4ed15b740b Remove unneeded imports and methods 2022-02-25 22:14:56 +00:00
5f161b2163 Rename Transaction to TX to match input row 2022-02-25 22:14:13 +00:00
10ea73663f Extract UpdateTransaction 2022-02-25 22:09:59 +00:00
07804e4241 Update existing transaction if transactionid was passed 2022-02-25 22:05:15 +00:00
2f4f8a7568 Extract method CreateTransferForOtherAccount 2022-02-25 22:02:57 +00:00
13d0194632 Pass date using ISO without time
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-25 21:52:56 +00:00
c864666eb6 Use local date format
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-25 21:50:15 +00:00
464931babe Pass amount as string 2022-02-25 21:46:20 +00:00
480a95e096 Implement custom date input 2022-02-25 21:45:58 +00:00
2d37ec147c Pass only category Id as categories are not to be created on the fly 2022-02-25 21:21:55 +00:00
75b48be20d Also pass Suggestion-Object from EditRow 2022-02-25 21:20:41 +00:00
306edbf817 Update TransactionInputRow to new models 2022-02-25 21:19:34 +00:00
be3829baf8 Replace modelValue by models for id and name 2022-02-25 21:10:21 +00:00
a452482381 Normalize transaction store 2022-02-25 20:47:58 +00:00
0f6990407d Remove widths from rows as header decides 2022-02-25 20:37:49 +00:00
97be5abc8c Add abilty to switch to edit mode 2022-02-25 20:37:31 +00:00
1e80ba6ca8 Also return PayeeID and CategoryID from backend 2022-02-25 20:36:24 +00:00
1331304639 Extract EditAccount Dialog 2022-02-25 20:17:54 +00:00
7435ac3667 Merge pull request 'Implement YNAB Export from UI' (#22) from export-from-ui into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #22
2022-02-25 21:07:40 +01:00
55dffbbe89 Implmeent expand/collapse of category-groups
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-02-25 20:06:26 +00:00
212c81ab81 Implement download from UI 2022-02-25 20:06:10 +00:00
5b5b8215c3 Merge pull request 'Make ynab export equivalent to original export' (#21) from ynab-export-fixes into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #21
2022-02-25 16:35:18 +01:00
79c0fceafe Fix formatting for negative numbers
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-25 15:32:39 +00:00
1ea3590fd6 Add tests with negative numbers 2022-02-25 15:32:32 +00:00
5bb2c9c8b8 Use empty string as fallback for TransferAccount
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-25 15:27:06 +00:00
f9e512c593 Fix floating points < 1 2022-02-25 15:26:51 +00:00
4f72351ee9 Add more unittests for numeric 2022-02-25 15:26:39 +00:00
9ed4df7053 Fix leading space before category separator
All checks were successful
continuous-integration/drone/push Build is passing
2022-02-25 15:04:30 +00:00
78389e4beb Also export transfers 2022-02-25 15:04:09 +00:00
8ac3c22826 Merge pull request 'Implement editing of Accounts' (#20) from account-edit into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #20
2022-02-24 23:15:54 +01:00
ab07d3472d Handle EditAccount in Store
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-24 22:12:35 +00:00
466df523ab Implement EditAccount in Frontend 2022-02-24 22:12:26 +00:00
f51807e459 Implement EditAccount in Backend 2022-02-24 22:12:10 +00:00
03d1d1e520 Use grid instead of width for buttons 2022-02-24 21:38:57 +00:00
d09f5be69b Extract modal component 2022-02-24 21:37:26 +00:00
b5a03b40db Merge pull request 'Implement transfer creation' (#19) from enable-transfers into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #19
2022-02-24 00:13:20 +01:00
966c0ce0eb Redesign Budget Settings and introduce Button component
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-02-23 23:12:39 +00:00
635f4de402 Fix initialize of Budgets after login
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-02-23 23:03:31 +00:00
ddf51b5922 Use store instead of props in BudgetSidebar 2022-02-23 23:03:18 +00:00
bbbeff92e8 Reset amount to positive after saving transfer transaction
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-02-23 22:55:15 +00:00
5ccec61465 Implement transfer creation
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-23 22:53:04 +00:00
696fbee7cc Merge pull request 'Extract package for Numeric datatype and add unittests' (#18) from numeric-package into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #18
2022-02-23 23:13:18 +01:00
7c694fb32c Skip account_test when no db available
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-02-23 22:12:09 +00:00
5f746f5889 Also run tests in CI
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-02-23 22:10:16 +00:00
24e5b18ded Implement linter fixes
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-02-23 22:08:47 +00:00
253ecd1720 Add negative testcases
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-02-23 22:04:19 +00:00
f445f19233 Write all tests equally
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2022-02-23 22:02:29 +00:00
ea6d198bff Add some unit-tests for numeric
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2022-02-23 21:59:14 +00:00
28c20aacd3 Extract package 2022-02-23 21:59:14 +00:00
d89a8f4e2e Switch between payees and accounts depending on prefix 2022-02-23 21:59:14 +00:00
dae9abeeea Merge pull request 'Handle more of categories locally and update Modal' (#17) from categories-improvements into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #17
2022-02-23 22:58:33 +01:00
510c91205d Rewrite categories to be nested below groups
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-23 21:56:20 +00:00
f93888cbbc Move logic for hidden categories to client 2022-02-23 21:56:20 +00:00
674bef667b Hide checkmark icon 2022-02-23 21:56:20 +00:00
fffc91269e Extract NewCategoryWithBalance 2022-02-23 21:56:20 +00:00
c3175b9be6 Update NewBudget to use modal again 2022-02-23 21:56:20 +00:00
1d81aa2fb3 Do not try to set categories, if none are available 2022-02-23 21:56:20 +00:00
e5cf439231 Update BudgetSettings to tailwindcss 2022-02-23 21:56:20 +00:00
cfd2388de0 Disable cache for apk calls and add yarn.lock 2022-02-23 21:56:20 +00:00
0dfa698ada Merge pull request 'Fix inverted condition for Authorization and update expected JSON body' (#16) from minor-fixes into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #16
2022-02-23 22:56:04 +01:00
b4aec52d4f Fix changed json contract
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-23 21:55:29 +00:00
b5e3e7bea0 Fix inverted condition 2022-02-23 21:55:21 +00:00
e98e0d4779 Merge pull request 'Implement Export in YNAB-Format' (#15) from ynab-export into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #15
2022-02-23 22:18:01 +01:00
6686904539 Use zero Numeric for export instead of hardcoding 0,00
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-02-23 21:17:43 +00:00
0478d82c1f Fix order of fields
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-02-23 21:09:33 +00:00
34b6e450de Handle nil in Numeric 2022-02-23 21:09:25 +00:00
bc65249c03 Split transactions and assignments export into two endpoints
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-02-23 20:52:29 +00:00
e0dc7800af Remove limit for GetAllTransactionsForBudget 2022-02-23 20:52:29 +00:00
a7cd3512bb Handle errors of Write and add dots to comments 2022-02-23 20:52:29 +00:00
ece610419f Disable linter lll because no exceptions in comments are possible 2022-02-23 20:52:29 +00:00
27188e2e27 Implement ynab-export 2022-02-23 20:52:29 +00:00
4c7c61e820 Add string method to numeric 2022-02-23 20:52:29 +00:00
2adb70fa01 Fix comment and add csv example 2022-02-23 20:52:29 +00:00
eeb2d425e5 Merge pull request 'Fix registration not displaying' (#14) from registration into master
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #14
2022-02-22 14:44:29 +01:00
484b1062e1 Fix returning number as string for numbers
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-22 13:42:44 +00:00
a4ca21bb37 Add missing spaces
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-21 21:42:16 +00:00
ffabf1bca9 Also fix in register
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-21 21:41:24 +00:00
e9d4ed1b3e Fix router initialization in eventhandler
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
useRouter has to be called in setup or returns undefined otherwise.
See https://github.com/vuejs/vue-router/issues/3379
2022-02-21 21:40:38 +00:00
4085868cd7 Update register to tailwindcss
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
2022-02-21 21:26:12 +00:00
4019656e4d Merge pull request 'Improve handling of transactions' (#12) from transaction-handling into master
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #12
2022-02-21 00:03:55 +01:00
16b7049438 Add Transaction to list after saving
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-20 23:01:25 +00:00
73e3b49b40 Extract component for new Transaction 2022-02-20 23:00:09 +00:00
953d348bed Handle new Payees 2022-02-20 23:00:07 +00:00
a6eb2a2253 Merge pull request 'Add code linting to build' (#13) from linting into master
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #13
2022-02-20 23:53:47 +01:00
578e7d071c Get session secret from env instead of hardcoding
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-20 22:51:54 +00:00
4688d2d94d Remove commented code and TODOs 2022-02-20 22:46:51 +00:00
cfda327a5d Enable linting nfor CI
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
2022-02-20 22:41:56 +00:00
1bd38bb367 Move comment
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-20 22:40:58 +00:00
19d2ddbd65 Minor improvements 2022-02-20 22:37:34 +00:00
3cb39d978a Fix multiple linter errors 2022-02-20 22:33:29 +00:00
e08a21b750 Disable linter varnamelen 2022-02-20 22:33:15 +00:00
6929c940c4 Improve ynab import by extracting methods
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-20 21:51:07 +00:00
2423bdd3ee Improvements
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
2022-02-20 21:33:18 +00:00
22ec0433bf Disable gci 2022-02-20 21:33:05 +00:00
c03d16878a Minimal linting improvements 2022-02-20 21:19:28 +00:00
545f223a97 Comments 2022-02-20 21:06:17 +00:00
260ac2d4ad Run gci 2022-02-20 21:03:55 +00:00
d815e8c3cd Ignore forcetypeassert for token map 2022-02-20 21:00:30 +00:00
91b8cc06b2 imports, comments and formatting 2022-02-20 20:58:48 +00:00
75a6ce1577 Use all gofiles as sources for go build 2022-02-20 20:53:18 +00:00
a0d89ee93a Revert "UNSURE explicitly init NullUUIDs"
This reverts commit 62085cb69439d36db301dc9097ae1f259fdd0d9e.
2022-02-20 20:52:15 +00:00
e39d1dc6e3 Disable exhaustivstruct 2022-02-20 20:52:01 +00:00
1dcd0d2f6d Reformat config.go 2022-02-20 20:51:10 +00:00
62085cb694 UNSURE explicitly init NullUUIDs 2022-02-20 20:46:20 +00:00
ca51ac5e27 Use AbortWithStatusJSON instead of AbortWithError
All checks were successful
continuous-integration/drone/pr Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
continuous-integration/drone/push Build is passing
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-20 20:42:57 +00:00
36b2f12183 Renames 2022-02-20 20:42:27 +00:00
96b514ccf8 Fix capitalization of errors 2022-02-20 20:42:16 +00:00
b52ed21d1d Remove cost and just use defaultCost 2022-02-20 20:42:07 +00:00
787165b7f1 Disable ifshort 2022-02-20 20:41:58 +00:00
8035403416 Use camelCase for json identifiers 2022-02-20 16:51:48 +00:00
1a1971246d Use short syntax for errcheck where possible 2022-02-20 16:51:28 +00:00
9b92e2b551 Run tests in parallel 2022-02-20 16:51:17 +00:00
c5be03ab6b Extract error consts 2022-02-20 16:51:01 +00:00
77afe700ae Fix missing value usage
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-19 22:13:55 +00:00
f0961ccc3c Disable linting for prod build while open errors exist but enable for dev instead
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-19 22:06:11 +00:00
1d2ae0e394 Renames and dots in comments
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
2022-02-19 22:04:51 +00:00
b0175542f1 Return own error 2022-02-19 22:04:35 +00:00
558fddc139 Disable linter testpackage 2022-02-19 22:04:27 +00:00
02ba80a555 Some linting fixes 2022-02-19 21:53:30 +00:00
1a19d3a197 Enable formatting via gofumpt 2022-02-19 21:53:13 +00:00
052a2628ab Disable forbidigo and nlreturn linters 2022-02-19 21:42:19 +00:00
46b9b82f30 Small refactorings 2022-02-19 21:38:57 +00:00
c5a0f49719 Disable wsl linter
Seems to be excessive
2022-02-19 21:38:38 +00:00
649f937254 Remove context from YNABImport struct 2022-02-19 21:28:28 +00:00
0f2501dcbd Rename parameter to other 2022-02-19 21:28:18 +00:00
daadfd45bc Rename http package to server 2022-02-19 21:28:04 +00:00
72b5bdde4f Minor fixes
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
2022-02-15 12:59:53 +00:00
737d5fb101 Fix wrong executable name 2022-02-15 12:59:41 +00:00
bb4548c50d Improve error messages
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
2022-02-15 12:41:12 +00:00
7b20bc9822 Disable deprecated linters 2022-02-15 12:38:30 +00:00
2f45c415e0 Move init of StaticFS and rename some vars 2022-02-15 12:37:23 +00:00
74a53954de Wrap more errors 2022-02-15 12:37:04 +00:00
7a0c4a17a2 Ignore gin.Context for varnamelen 2022-02-15 12:36:33 +00:00
71c54c9373 Enable all linters 2022-02-15 12:36:19 +00:00
584e7ef393 Add explicit CI task to Taskfile 2022-02-15 12:27:15 +00:00
aaf16dbe92 Add deps for dev-docker target 2022-02-15 12:26:55 +00:00
d8e0f5a160 Add explicit CI task to Taskfile 2022-02-15 12:26:48 +00:00
38dfa540b4 Fix issues from golangci 2022-02-15 11:52:06 +00:00
835a15ec08 Ad config for golangci
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
2022-02-15 11:51:38 +00:00
4bbbc0be13 Add vet fmt and linters to build
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
ci/woodpecker/pr/woodpecker Pipeline failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-02-15 09:33:17 +00:00
8f6974e151 Add golang-ci to dev image 2022-02-15 09:30:54 +00:00
368ac7f15d Merge pull request 'Use vue's Composition API in components' (#11) from vue-composition into master
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #11
2022-02-15 10:13:47 +01:00
0d20d9bfb8 Use setTitle everywhere
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
continuous-integration/drone/pr Build is passing
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-15 08:35:29 +00:00
4276c51268 Remove unused interface
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
2022-02-15 08:27:09 +00:00
57930d0e5d Rewrite addCategoriesForMonth with patch call
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
2022-02-15 08:25:41 +00:00
fe018e1953 Reformat 2022-02-15 08:25:30 +00:00
e7a085273b Fix missing computed calls in Account 2022-02-15 08:25:12 +00:00
5bbd096fc8 Convert other components 2022-02-15 08:20:04 +00:00
452d63c329 Define Transaction interface and use number instead of Number 2022-02-15 08:04:42 +00:00
d28c894d21 Convert NewBudget and TransactionRow 2022-02-15 08:04:25 +00:00
1a79177422 Fix missing computed calls in BudgetSidebar
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
2022-02-14 22:49:30 +00:00
0aa877d7d4 Merge pull request 'Use vue's Composition API' (#10) from vue-composition into master
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #10
2022-02-14 23:44:00 +01:00
87a70ee5fa Revert
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
continuous-integration/drone/pr Build is passing
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-14 22:39:14 +00:00
0a030eaee1 Convert other pages to composition API 2022-02-14 22:24:42 +00:00
d11c0036b5 Do not use a store for API 2022-02-14 08:12:41 +00:00
ca93e9cd55 Migrate Account.vue to composition API
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
2022-02-14 08:06:16 +00:00
a061ffd350 Merge pull request 'Implement minor fixes' (#9) from minor-fixes into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
continuous-integration/drone/push Build is passing
Reviewed-on: #9
2022-02-14 08:48:07 +01:00
5633c029ac Update Earthfile and production docker-compose.yml
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-13 13:20:18 +00:00
a97d050ead Specify Map serializer for budgets
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-13 13:16:50 +00:00
958929fd16 Update docker-compose to use new tag 2022-02-13 13:16:50 +00:00
a61d80ee1f Implement SPA handling in Backend 2022-02-13 13:16:50 +00:00
41c5095b8b Merge pull request 'Use woodpecker for CI' (#8) from woodpecker into master
All checks were successful
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #8
2022-02-13 14:16:38 +01:00
c074dfe865 Fix Taskfile
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-12 00:00:33 +00:00
fa8a2854f2 Add node packages to image
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-12 00:00:19 +00:00
15bb73de30 Add secrects
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-11 23:54:26 +00:00
e506510fde Remove user and add go deps from builder image 2022-02-11 23:49:43 +00:00
11ac8758da Disable docker from taskfile
Some checks failed
continuous-integration/drone/pr Build is passing
ci/woodpecker/push/woodpecker Pipeline failed
continuous-integration/drone/push Build is passing
ci/woodpecker/pr/woodpecker Pipeline was successful
2022-02-11 23:43:55 +00:00
3db5e1e72c Reenable docker push
Some checks failed
continuous-integration/drone/push Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
continuous-integration/drone/pr Build is failing
ci/woodpecker/pr/woodpecker Pipeline failed
2022-02-11 23:42:03 +00:00
4e2a783b2e Extract variable 2022-02-11 23:40:46 +00:00
bb83563bc6 Use docker build-target
Some checks failed
continuous-integration/drone/push Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
2022-02-11 23:35:15 +00:00
0a21c59eff Fetch deps before build
Some checks failed
continuous-integration/drone/push Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
2022-02-11 23:33:47 +00:00
3308b58524 Fix sources in Taskfile
Some checks failed
continuous-integration/drone/push Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
2022-02-11 23:30:53 +00:00
941b642f39 Build docker within task
Some checks failed
continuous-integration/drone/push Build is failing
ci/woodpecker/push/woodpecker Pipeline failed
2022-02-11 23:16:57 +00:00
6a77c71df4 Always pull dev image
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
continuous-integration/drone/push Build is failing
2022-02-11 22:59:38 +00:00
bf20914c1c Use default build name
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
continuous-integration/drone/push Build is failing
2022-02-11 22:58:31 +00:00
7874ef69a2 Fix Taskfile
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
continuous-integration/drone/push Build is failing
2022-02-11 22:57:26 +00:00
2e719b590e Add admin page 2022-02-11 22:52:00 +00:00
95d8e4fccc Rewrite event filter
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
continuous-integration/drone/push Build is failing
2022-02-11 22:49:06 +00:00
7cf106eb85 Rename 2022-02-11 22:41:35 +00:00
148fc18cd8 Fix schema 2022-02-11 22:41:07 +00:00
47095ae6ec Add .woodpecker.yaml 2022-02-11 22:39:19 +00:00
200fa1835a Merge pull request 'Replace vuex by pinia' (#7) from pinia into master
Reviewed-on: #7
2022-02-11 23:20:06 +01:00
cd0b370b50 Fix typescript errors 2022-02-11 22:18:00 +00:00
1af69b047d Fix mapping for menu in App.vue 2022-02-11 22:03:37 +00:00
f6cb6d4163 Do not use useStorage for root state 2022-02-11 22:03:26 +00:00
45389e01be Use vueuse useStorage instead of manually using localStorage 2022-02-11 21:38:06 +00:00
5868c3310e Fix Map.values() access and convert to array 2022-02-10 18:07:52 +00:00
1dc818f51f Add pinia-logger 2022-02-10 16:30:53 +00:00
0e342f2bcf Try to move perstisting to stores 2022-02-10 16:30:42 +00:00
21dcd7837b Bugfixes 2022-02-10 16:07:29 +00:00
c693625e34 Remove vuex 2022-02-10 15:49:30 +00:00
8b0e368d58 Fix return type of AuthHeaders 2022-02-10 13:35:51 +00:00
0c094d6f6b And on 2022-02-09 23:16:16 +00:00
9b8ae7a44d And on… 2022-02-09 23:14:48 +00:00
08330ce33c Migrate farther 2022-02-09 23:12:26 +00:00
2d0737a10c First step from vuex to pinia 2022-02-09 23:08:43 +00:00
2137460187 Merge pull request 'Implement first unit-tests' (#6) from unit-tests into master
Reviewed-on: #6
2022-02-09 23:41:08 +01:00
6ab8a96888 Implement first db-test using go-txdb 2022-02-09 22:40:35 +00:00
c3db535e10 go get go-txdb 2022-02-09 22:40:35 +00:00
5d292b2a31 Add first test 2022-02-09 22:40:35 +00:00
e934d407c2 Use simple connection string in config 2022-02-09 22:40:35 +00:00
2c71c521f9 Add bordersr to inputs 2022-02-09 22:40:35 +00:00
3bd5845068 Focus next element on Enter 2022-02-09 22:40:35 +00:00
c3a1564c4e Extract LoadRoutes method 2022-02-09 22:40:35 +00:00
922041856f Extract struct for return value of getTransactionsForAccount 2022-02-09 22:40:35 +00:00
6cafaec422 Inline tv and bv vars 2022-02-09 22:40:35 +00:00
499d78a781 Merge pull request 'design-improvements' (#5) from design-improvements into master
Reviewed-on: #5
2022-02-09 23:40:27 +01:00
227c2a7564 Align amount input right 2022-02-09 22:36:21 +00:00
07cd7a7c2b Add table Header 2022-02-09 22:36:21 +00:00
bd1e1cbfb8 Move Transaction row to own component 2022-02-09 22:36:21 +00:00
09a242f6f6 Merge pull request 'Show transfer account in transaction list' (#4) from show-transfer-account into master
Reviewed-on: #4
2022-02-09 23:35:36 +01:00
4eeecc2bd9 Show transfer account if filled 2022-02-08 21:28:55 +00:00
378d764220 Merge pull request 'Convert frontend to Vue' (#3) from vue into master
Reviewed-on: #3
2022-02-08 22:20:11 +01:00
2559e40ebd Merge branch 'master' into vue 2022-02-08 21:19:47 +00:00
0579ec5106 Save and restore ExpandMenu state 2022-02-08 21:14:44 +00:00
33bdfaddc4 Move budgeting to own route 2022-02-08 21:14:36 +00:00
eb195dfd29 Redirect to current route from site without date 2022-02-08 11:13:55 +00:00
d2b414f328 Always treat Currency.value as number 2022-02-08 10:53:06 +00:00
e873795562 Show sum for all on/off-budget accounts 2022-02-07 22:38:46 +00:00
33c83c0a69 Shrink date input to match column width 2022-02-07 22:19:59 +00:00
2646c5d3b7 Use Currency component for Account header 2022-02-07 22:19:48 +00:00
3ef5836607 Implement simple keyboard selection 2022-02-07 22:19:24 +00:00
b5c657978c Alternate row colors for transactions 2022-02-07 16:39:06 +00:00
0136e3b978 Use currency component for transactions 2022-02-07 16:38:53 +00:00
427e7e5359 Extract value formatting to computed property 2022-02-07 16:38:26 +00:00
139b6ec636 Convert multiple class binding to array syntax 2022-02-07 16:38:08 +00:00
9dad1dabbd Implement new transaction 2022-02-07 16:17:14 +00:00
95fcb9a586 Split Menu between md and smaller devices 2022-02-07 16:07:54 +00:00
3c1d83d8a2 Fix negative values smaller than zero 2022-02-07 16:07:37 +00:00
f09a2a4ca7 Fix multiplication instead of division 2022-02-06 22:21:04 +00:00
487aa89f18 Use Numeric in JSON output 2022-02-06 22:12:48 +00:00
5763409aa8 Split store 2022-02-06 20:15:59 +00:00
5ca09d2825 Fix calling dispatch for mutation 2022-02-06 20:15:29 +00:00
e18e68c964 Comment not implemented routes 2022-02-06 20:15:14 +00:00
065159a817 Cleanup unused backend code and routes 2022-02-06 20:15:00 +00:00
68c2d3ff28 Fix typing issues in Settings 2022-02-05 15:00:26 +00:00
0f2db4985f Remove old plugins 2022-02-05 14:54:14 +00:00
f532ffc0c1 Rename routes.ts 2022-02-05 14:36:20 +00:00
4edd1a8bf1 Cleanup more deps 2022-02-05 14:36:01 +00:00
a7890bce16 Add vue shim 2022-02-05 14:27:20 +00:00
ddaf647d87 Add getters for current budget ID and Name 2022-02-05 14:26:56 +00:00
80b5a7abfe Small layout improvements 2022-02-05 13:48:31 +00:00
2204188600 Actually call API 2022-02-04 21:43:51 +00:00
3d1d1308ac Use JSON params instead of PostForm 2022-02-04 21:43:35 +00:00
d5904a7c4a Implement testing endpoint for new transaction 2022-02-04 21:43:03 +00:00
d9c03e231e Improve error when no auth supplied 2022-02-04 21:40:22 +00:00
dba1e8c276 Change type of modelValue 2022-02-04 21:18:24 +00:00
ebc2286116 Display name if selected 2022-02-04 21:14:02 +00:00
d647650142 Order payees by name 2022-02-04 21:13:40 +00:00
057c576831 Implement autocomplete for categories 2022-02-04 21:13:30 +00:00
056071c6e6 Make autocomplete usable for multiple types 2022-02-04 20:47:50 +00:00
0305aa86c1 Add Card component 2022-02-04 20:44:08 +00:00
d9aed7603e Implement custom autocomplete component 2022-02-04 20:43:57 +00:00
e18ef79839 Implement minimal autocomplete for Payees 2022-02-04 20:24:29 +00:00
b93e44930a Use hamburger menu icon instead of 'Home'-text 2022-02-04 20:01:26 +00:00
c8e9a8b375 Improve login page 2022-02-04 19:59:21 +00:00
1d0fe60ea4 Improve layout 2022-02-04 19:54:32 +00:00
3f7f646120 Extract card and use for budget list 2022-02-04 17:16:03 +00:00
dfa8f369f0 Make it work slightly ;-) 2022-02-04 16:56:17 +00:00
d825379a01 Try to use tailwind 2022-02-04 16:34:23 +00:00
f7dfc7b455 Initialize tailwindcss 2022-02-04 15:53:56 +00:00
2203542d90 Regenerate yarn.lock 2022-02-04 15:53:14 +00:00
72f9a6413f Remove vuetify 2022-02-04 15:51:50 +00:00
bf25922fc4 Add fuzzy module 2022-02-04 15:46:17 +00:00
46d727c650 Try to implement autocomplete for payees 2022-02-01 21:08:37 +00:00
b6628dd8cb Fallback to empty list if budgets or accounts are unset 2022-02-01 20:23:46 +00:00
132ae75755 Add index page 2022-02-01 20:23:32 +00:00
2b885ca189 Implement dummy row for new transaction 2022-02-01 20:23:19 +00:00
37a842b395 Add main.ts 2022-02-01 08:15:19 +00:00
9353d82648 Fix diverse errors 2022-01-31 21:45:19 +00:00
b350fe7d74 Handle some differences between js/ts 2022-01-31 21:37:24 +00:00
7cba471de7 Convert most code to ts 2022-01-31 21:14:13 +00:00
27508af3a7 Convert Register.vue to TS 2022-01-31 20:47:56 +00:00
e0981630ab Enable typescript support 2022-01-31 20:47:45 +00:00
a7178e39c9 Add vue CLI to dev image 2022-01-31 20:40:15 +00:00
29f0c51e35 Extract some types 2022-01-31 20:34:32 +00:00
9c0126b14c Format document 2022-01-31 19:58:06 +00:00
34b9c14419 Redirect to dashboard and remove budget on delete 2022-01-31 16:12:17 +00:00
8e2a62929e Fix ignoring return value 2022-01-31 16:03:56 +00:00
95b1ac5943 Implement Delete Budget 2022-01-31 15:50:13 +00:00
1be3b6930d Install go packages as non-root in Dockerfile 2022-01-31 15:49:49 +00:00
e5fe3d06c4 Remember menu state 2022-01-31 14:43:49 +00:00
136f15badc Move showMenu to store 2022-01-31 14:43:20 +00:00
f091ce8945 Handle LOGIN via action 2022-01-29 23:32:11 +00:00
24370c9d32 Show negative colors in red 2022-01-29 23:29:17 +00:00
e8dbb54086 Merge stores and handle fetching from router 2022-01-29 23:26:57 +00:00
6f7aa28d22 Enable vuex logging 2022-01-29 22:39:13 +00:00
e85cf0ea55 Improve layout of Account view 2022-01-29 22:38:23 +00:00
58683a33fb Improve layout of Account 2022-01-28 10:46:56 +00:00
a45d02f9e0 Redesign app bar 2022-01-28 10:35:29 +00:00
0334b35041 Improve Layout of BudgetSettings 2022-01-28 09:44:30 +00:00
b52c4cb787 Make layout more flexible 2022-01-27 21:41:01 +00:00
0db6d6c67a Organize settings in cards 2022-01-27 21:37:04 +00:00
0b1f673c05 Extract action for API calls 2022-01-26 21:55:36 +00:00
0e438e0244 Implement clear 2022-01-26 21:38:41 +00:00
430c4d52da Formatting & remove redundant headline 2022-01-26 21:33:19 +00:00
d5a414266b Split build into dev and prod 2022-01-26 21:33:05 +00:00
47cbaf9660 Implement YNAB Import in Vue 2022-01-26 21:12:42 +00:00
7776ba90f0 Add interactive build to dev container 2022-01-26 20:47:08 +00:00
80c00171c8 Add dev docker-compose 2022-01-26 20:46:22 +00:00
9cc06bbe93 Add plain run command without docker and rename old run to rundocker 2022-01-26 20:45:46 +00:00
829881bd2a Fix missing exeExt in sources 2022-01-26 20:45:26 +00:00
0ad97ce4ff Remove unused HTML endpoints 2022-01-26 20:22:39 +00:00
1aa6d8d58f Add build script to dev container 2022-01-26 20:22:23 +00:00
db8553995c Add simple data stucture to README 2022-01-25 22:31:47 +00:00
de6054359a Remove dashboard call and fetch Budgets with login 2022-01-25 22:29:35 +00:00
9e3dde8076 Rename module 2022-01-25 20:58:32 +00:00
d6165c6ede Add Settings.vue 2022-01-25 20:56:57 +00:00
404bd6625f Add mutation-types.js 2022-01-25 20:56:43 +00:00
463a52fb19 Merge modules 2022-01-25 20:56:32 +00:00
ffed94f586 Implement dummy Budget-Settings and extract setTitle mutation 2022-01-25 20:40:00 +00:00
74c4c7cb02 Improve login/logout
Extract mutation types to mutation-types.js
2022-01-25 20:30:47 +00:00
6dcf7da861 Implement watch for accounts as well 2022-01-25 20:06:26 +00:00
4f2b3b7b22 Display selected budget and update sidebar on change 2022-01-25 19:25:11 +00:00
3b130b8621 Fix amount display and hide account 2022-01-25 19:20:54 +00:00
a4f94cd9d9 Remove old logging 2022-01-25 19:18:19 +00:00
c2bbaebfd2 Implement API endpoint for transactions 2022-01-25 19:18:08 +00:00
458f4f0e8f Implement account-view 2022-01-25 19:03:58 +00:00
e184ef933f Add container to wrap budget list 2022-01-25 12:54:33 +00:00
a2cc19d310 Use props for budgetid 2022-01-25 12:54:25 +00:00
626e0ada40 Add toggle menu button 2022-01-25 12:54:06 +00:00
339c230756 fetchBudget whenever current budget changes 2022-01-25 08:55:54 +00:00
59ae9f978a Rename Budget to BudgetSidebar 2022-01-25 08:55:39 +00:00
af3252277c Implement currentBudget and move infos to sidebar 2022-01-25 08:37:38 +00:00
33990bdccf Exclude node_modules and vendor in VSCode 2022-01-24 19:24:12 +00:00
5fb7542176 Add new budget dialog 2022-01-24 19:23:49 +00:00
c2bcd815d5 API returns Budget directly 2022-01-24 19:22:53 +00:00
be821bc90a Remove unneeded assignment 2022-01-24 19:22:38 +00:00
746680a4d7 Add empty styles file 2022-01-24 16:00:55 +00:00
fa4c9bcaba Add vuetify and webfontloader plugins 2022-01-24 16:00:39 +00:00
3de0447eba Implement register and login using vuetify 2022-01-24 16:00:01 +00:00
d59b9bdbec Add vuetify 2022-01-24 14:47:47 +00:00
93b1712fb5 Add Dockerfile for dev environment 2022-01-24 14:45:05 +00:00
b2b934cfb3 Add logout action 2022-01-23 22:38:38 +00:00
1d39fc7976 Remove old code for accounts 2022-01-23 22:38:25 +00:00
dd160cab17 Convert logout link to method 2022-01-23 22:38:12 +00:00
0e3ece9830 Add Getter for on/offbudget accounts and remove from backend 2022-01-23 22:33:36 +00:00
27298a9860 Add budget module to store 2022-01-23 22:24:40 +00:00
e26bb257cc Add dummy-logout link 2022-01-23 22:24:28 +00:00
0bd63636bb Implement Budget-List via Getter 2022-01-23 22:24:17 +00:00
6086447126 Implement budget display 2022-01-23 22:24:02 +00:00
aae8bbb44e Return JSON and move /budget/:budgetid to API endpoint 2022-01-23 22:23:30 +00:00
f33f0880c4 Move store to own folder 2022-01-23 21:54:46 +00:00
c069a33890 Remove logging after api request 2022-01-23 21:37:26 +00:00
8b4ebadec2 Only save partial state 2022-01-23 21:36:46 +00:00
2b556222ed Fix display of menu-items by login state 2022-01-23 21:35:43 +00:00
3da2e0f2f8 Remove authentication Cookies from Backend 2022-01-23 21:35:23 +00:00
4f72751ed6 Extract variable 2022-01-23 21:15:33 +00:00
c2326060b3 Rename index.js to routes.js 2022-01-22 16:23:10 +00:00
dad3a149d8 Use router-link to budget page 2022-01-22 16:21:53 +00:00
1bdd66a45a Remove HelloWorld and logging, add Budget page 2022-01-22 16:21:53 +00:00
b0ba63118e Add login page 2022-01-22 16:11:42 +00:00
18d11358b2 Remove logo 2022-01-22 16:11:34 +00:00
4af82805ff Save store to localStorage 2022-01-22 16:10:46 +00:00
15ab8a3dac Use Header instead of Cookie 2022-01-21 15:07:15 +00:00
99cf27b649 Return forbidden status in API 2022-01-21 15:07:01 +00:00
bc78cab715 Add proxy to backend to vite config 2022-01-21 14:20:58 +00:00
ddab5998bc Move dashboard to api 2022-01-21 14:20:45 +00:00
929db00a47 Get Dashboards on mounted
Some checks failed
continuous-integration/drone/push Build is failing
2022-01-19 21:32:02 +00:00
8a6322bc7e Fix binding href 2022-01-19 21:32:02 +00:00
5601c965dc Fix script exporting computed 2022-01-19 21:32:02 +00:00
2fba4381df Allow access from other hosts 2022-01-19 21:32:02 +00:00
bdf942a17b Import esm-bundler 2022-01-19 21:32:02 +00:00
d354a680f9 Set version correctly 2022-01-19 21:32:02 +00:00
6899e4c88e Try to add store 2022-01-19 20:53:19 +00:00
e0f822dbcc Add vuex 2022-01-18 13:30:47 +00:00
d0e52ddfa6 Implement basic routing 2022-01-18 13:29:51 +00:00
2112192670 Add frontend to build script 2022-01-18 13:29:24 +00:00
13063b47ba Merge pull request 'Add row to create new transaction' (#2) from new-transaction-row into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #2
2022-01-18 11:43:20 +01:00
3cda536854 Add vue-router 2022-01-17 22:17:29 +00:00
6d49a549a0 Remove templating completely 2022-01-17 22:17:23 +00:00
aa33c148cb Remove templates and add vue template 2022-01-17 22:05:43 +00:00
663f247080 Import and display Status
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-16 15:43:05 +00:00
e094c2a740 Merge branch 'master' into new-transaction-row
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2022-01-10 11:18:56 +01:00
7fba9e7e4c Fix .drone.yml
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-10 10:18:42 +00:00
cc58c0d012 Add row to create new transaction 2022-01-10 10:15:55 +00:00
e138c264ea Merge pull request 'Import transfers as actual Transfers' (#1) from handle-transfers into master
Reviewed-on: #1
2022-01-10 11:15:05 +01:00
a8edeaafa1 Disable docker build for prs 2022-01-10 10:14:10 +00:00
6fe30231d8 Save all unmatched transfers as regular transactions
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2022-01-10 10:10:56 +00:00
49af9cd2ef Fix logging wrong objects 2022-01-10 10:10:56 +00:00
ac27dc783e Also fetch GroupID and highlight groups in transactions-view 2022-01-10 10:10:56 +00:00
3c17d674f9 Fix migration 2022-01-10 10:10:56 +00:00
92725c0f26 Reword clear actions description
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-10 10:10:47 +00:00
2ec9c923df Implement matching 2022-01-10 10:10:02 +00:00
beff7afcf7 Add group_id 2022-01-10 10:10:02 +00:00
337d588c3c Include all accounts
All checks were successful
continuous-integration/drone/push Build is passing
Before this change, accounts without transactions in the specified timespan had not been included
2022-01-10 10:09:34 +00:00
951e827d20 Add transfer_id 2022-01-09 20:47:43 +00:00
2f3e4bc748 Add transfers to list and skip 2022-01-09 20:27:51 +00:00
d71eb17092 Remove new transaction from transaction edit dialog
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 20:41:18 +00:00
53dd31fa35 Extract util.go 2021-12-28 20:41:06 +00:00
1a4267186a Add ability to edit payees 2021-12-28 20:40:53 +00:00
5018e5b973 Give sensible name to caching method
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 20:16:44 +00:00
ed9e75d57a Enable drone CI
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 20:08:19 +00:00
ed361324dd Fix indentation 2021-12-28 16:14:48 +00:00
6bac09a38e Implement update and delete for transactions 2021-12-27 23:38:30 +00:00
ab43387f06 Use wrapper to prevent password change popups from screwing up the layout 2021-12-27 23:14:50 +00:00
c112d95a41 Add delete button 2021-12-27 23:07:16 +00:00
6fdc0e3b1d Try to fix indentation 2021-12-27 23:07:07 +00:00
f08784ffa7 Fix typo 2021-12-27 23:06:59 +00:00
8188184ac9 Add tag for self-hosted registry 2021-12-27 23:06:38 +00:00
81b3bf334a Add transaction detail view 2021-12-14 15:02:51 +00:00
d0ad0dcb3a Implement categories for new transactions 2021-12-14 14:41:10 +00:00
1ab1fa74e0 Improve new-transaction form by adding account_id and default date 2021-12-14 14:15:08 +00:00
33c54c9f4c Make import available via UI 2021-12-14 14:14:35 +00:00
1ed9344586 Add home button to sidebar 2021-12-14 14:14:12 +00:00
a8bd03a805 Handle circular required keys
Use a dummy-value at first and update it later.
Deferrable doesn't seem to work for NOT NULL - only
for FOREIGN KEYs.
2021-12-14 14:13:23 +00:00
9e01be699a Fix modals not opening 2021-12-14 14:12:01 +00:00
84ddb36d62 Fix reset only undoing one version 2021-12-14 14:11:11 +00:00
8b6a8c3697 Add income_category_id only after categories table exists 2021-12-11 22:08:08 +00:00
208ffce968 Wrap errors 2021-12-11 22:07:53 +00:00
bfba5f4028 Also rebuild on schema change
Files in postgres/schema/ are embedded in an embed.FS
2021-12-11 22:07:34 +00:00
1f2d81f173 Add task build command 2021-12-11 22:07:09 +00:00
c3a93377d9 Fix schema 2021-12-11 21:55:33 +00:00
40a299141d Implement new budget with transaction to be able to satisfy not null columns 2021-12-11 20:19:52 +00:00
935499e3a8 Improve UI
Highlight future transactions
clarify settings are for budget
2021-12-11 20:19:28 +00:00
915964fa4e Add now to funcs available from templates 2021-12-11 20:18:27 +00:00
e9adc763b2 Remove Repository and use Database instead 2021-12-11 20:18:09 +00:00
d5ebf5a5cf Group hidden categories 2021-12-11 15:10:51 +00:00
466775817f Merge added and assigned into moneyUsed 2021-12-11 13:03:26 +00:00
e2413290b4 Extract another method 2021-12-11 13:01:35 +00:00
18cd29cca2 Exctract getDate 2021-12-11 12:52:52 +00:00
caf0126b86 Implement budgeting views by calculating most values locally 2021-12-11 12:47:41 +00:00
6da1b26a2f update go.mod 2021-12-11 12:47:07 +00:00
13993b6b5a Try to calculate balances locally 2021-12-10 18:56:56 +00:00
625e0635fd Fix indentation 2021-12-10 17:09:43 +00:00
1826274ccc Show budget name in sidebar 2021-12-10 16:40:02 +00:00
defbbd1884 Improve documentation for YNAB Import 2021-12-10 16:39:56 +00:00
8116238d48 Add settings page linking to clean and clear 2021-12-10 16:39:44 +00:00
e0eeaadc60 Use same template for account and all-accounts 2021-12-10 16:38:10 +00:00
4cd81592e4 Also watch for templates changes 2021-12-10 16:37:11 +00:00
5d9693838f Complete Taskfile
- Use checksums for go.mod/go.sum
- Disable CGO to be able to use 'FROM SCRATCH' Dockerfile
- Add run command that updates docker-compose
2021-12-10 09:43:59 +00:00
3bec0857d5 Add Taskfile 2021-12-10 09:36:16 +00:00
5e18d51b5d Only show last month's overflow 2021-12-08 15:21:10 +00:00
11179a1593 Also load available last month 2021-12-08 15:15:49 +00:00
7cb7527704 Use date_trunc instead of splitting date into year and month 2021-12-08 15:15:35 +00:00
c3a022b595 Add views with results grouped by month 2021-12-08 14:40:11 +00:00
a0ebdd01aa Implement cleaning to set all historic negative balances to zero 2021-12-07 21:59:06 +00:00
edd1319222 Show available balance including activities of this month 2021-12-07 21:32:36 +00:00
a19d3d6932 Try to enable caching 2021-12-07 21:32:20 +00:00
f4ddf12214 Split displayed accounts by on- or off-budget 2021-12-07 21:20:35 +00:00
04fd687324 Handle null values in numeric 2021-12-07 21:20:35 +00:00
cbda69e827 Handle on_budget in available balance 2021-12-07 21:20:35 +00:00
e3f3dc6748 Add on_budget column to accounts 2021-12-07 21:20:35 +00:00
915379f5cb Make available balance date-dependent 2021-12-07 21:20:35 +00:00
284685fb52 Update Bootstrap 2021-12-07 21:20:35 +00:00
5f4c5d9d51 Display zero values in grey 2021-12-07 20:35:49 +00:00
8c9c78a789 Fix between call being inclusive 2021-12-07 20:28:48 +00:00
64822912d9 Add available balance 2021-12-07 20:22:40 +00:00
1d4bc158a8 Improve handling of context 2021-12-07 19:08:53 +00:00
fbd283cd1c Show date in without time 2021-12-07 15:42:39 +00:00
0ee3f269b5 Split routes into own files 2021-12-07 15:42:29 +00:00
197 changed files with 16882 additions and 13730 deletions

View File

@ -8,3 +8,4 @@ config.example.json
.vscode/ .vscode/
budgeteer budgeteer
budgeteer.exe budgeteer.exe
**/node_modules/

75
.drone.yml Normal file
View File

@ -0,0 +1,75 @@
---
kind: pipeline
type: docker
name: budgeteer
steps:
- name: submodules
image: alpine/git
commands:
- git submodule update --recursive --init
- name: Taskfile.dev PR
image: hub.javil.eu/budgeteer:dev
commands:
- task ci
when:
event:
- pull_request
- name: Taskfile.dev
image: hub.javil.eu/budgeteer:dev
pull: always
commands:
- task ci
when:
branch:
- master
event:
- push
- name: docker
image: plugins/docker
settings:
registry: hub.javil.eu
username:
from_secret: docker_user
password:
from_secret: docker_password
repo: hub.javil.eu/budgeteer
context: build
dockerfile: docker/Dockerfile
tags:
- latest
when:
branch:
- master
event:
- push
- name: docker tag
image: plugins/docker
settings:
registry: hub.javil.eu
username:
from_secret: docker_user
password:
from_secret: docker_password
repo: hub.javil.eu/budgeteer
context: build
dockerfile: docker/Dockerfile
auto_tag: true
when:
event:
- tag
services:
- name: db
image: postgres:alpine
environment:
POSTGRES_USER: budgeteer
POSTGRES_PASSWORD: budgeteer
POSTGRES_DB: budgeteer_test
image_pull_secrets:
- hub.javil.eu

View File

@ -1,10 +0,0 @@
build/
.git/
docker-compose.yml
README.md
Earthfile
config.example.json
.gitignore
.vscode/
budgeteer
budgeteer.exe

6
.gitignore vendored
View File

@ -4,6 +4,7 @@
# Unignore all with extensions # Unignore all with extensions
!*.* !*.*
!Dockerfile
# Unignore all dirs # Unignore all dirs
!*/ !*/
@ -86,3 +87,8 @@ GitHub.sublime-settings
!.vscode/*.code-snippets !.vscode/*.code-snippets
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,sublimetext,go # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,sublimetext,go
node_modules
.DS_Store
dist
dist-ssr
*.local

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "testdata"]
path = testdata
url = https://git.javil.eu/jacob1123/budgeteer-testdata.git

43
.golangci.yml Normal file
View File

@ -0,0 +1,43 @@
linters:
enable-all: true
disable:
- golint
- scopelint
- maligned
- interfacer
- wsl
- forbidigo
- nlreturn
- testpackage
- ifshort
- exhaustivestruct
- gci # not working, shows errors on freshly formatted file
- varnamelen
- lll
linters-settings:
errcheck:
exclude-functions:
- io/ioutil.ReadFile
- io.Copy(*bytes.Buffer)
- (*github.com/gin-gonic/gin.Context).AbortWithError
- (*github.com/gin-gonic/gin.Context).AbortWithError
- io.Copy(os.Stdout)
varnamelen:
ignore-decls:
- c *gin.Context
wrapcheck:
ignoreSigs:
- .JSON(
- .Redirect(
- .String(
- .Errorf(
- errors.New(
- errors.Unwrap(
- .Wrap(
- .Wrapf(
- .WithMessage(
- .WithMessagef(
- .WithStack(
ignorePackageGlobs:
- git.javil.eu/jacob1123/budgeteer/postgres

15
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
"files.exclude": {
"**/node_modules": true,
"**/vendor": true,
"**/*.sql.go": true,
".task/": true,
"build/": true,
"web/dist/": true
},
"gopls": {
"formatting.gofumpt": true,
},
"editor.detectIndentation": false,
"editor.tabSize": 2
}

11
.vscode/tasks.json vendored
View File

@ -4,14 +4,21 @@
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{ {
"label": "earthly +run", "label": "task watch +run",
"type": "shell", "type": "shell",
"command": "earthly +run", "command": "task -w run",
"problemMatcher": [], "problemMatcher": [],
"group": { "group": {
"kind": "build", "kind": "build",
"isDefault": true "isDefault": true
} }
},
{
"label": "earthly +run",
"type": "shell",
"command": "earthly +run",
"problemMatcher": [],
"group": "build"
} }
] ]
} }

View File

@ -1,21 +0,0 @@
FROM golang:1.17
WORKDIR /src
build:
COPY go.mod go.sum .
RUN go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build go build -o build/budgeteer ./cmd/budgeteer
SAVE ARTIFACT build/budgeteer /budgeteer AS LOCAL build/budgeteer
docker:
WORKDIR /app
COPY +build/budgeteer .
ENTRYPOINT ["/app/budgeteer"]
SAVE IMAGE budgeteer:latest
run:
LOCALLY
WITH DOCKER --load=+docker
RUN docker-compose up -d
END

View File

@ -1,3 +1,18 @@
# Budgeteer # Budgeteer
Budgeting Web-Application Budgeting Web-Application written in Go and inspired by [YNAB](https://youneedabudget.com).
## Getting started
The fastest way to get up and running quickly, is using docker-compose. Just download the [docker-compose.yml](https://git.javil.eu/jacob1123/budgeteer/src/branch/master/docker/docker-compose.yml) to some empty directory and run `docker-compose up -d`. This starts budgeteer, a postgres database and an adminer instance. The latter is optional and can be removed from the docker-compose.yml.
## Known issues
Currently the application is usable when importing from YNAB via their CSV export. All balances should match the balances from YNAB. There are even unit-tests that confirm that using my personal budget.
For people wishing to start fresh in Budgeteer, there currently are some blockers though:
- The ability to create new accounts and categories is missing (#59)
## Contributing
If you're willing to help, please check the issues for [help-wanted labels](https://git.javil.eu/jacob1123/budgeteer/issues?labels=4). Just using Budgeteer and reporting any issues is although very helpful.

142
Taskfile.yml Normal file
View File

@ -0,0 +1,142 @@
version: '3'
vars:
IMAGE_NAME: hub.javil.eu/budgeteer
tasks:
default:
desc: Build budgeteer in production mode
deps: [frontend, go-mod, go-sqlc]
cmds:
- task: backend
run:
desc: Start budgeteer
deps: [backend, go-mod, go-sqlc]
cmds:
- ./build/budgeteer{{exeExt}}
dev:
desc: Build budgeteer in dev mode (without frontend)
deps: [go-mod, go-sqlc]
cmds:
- task: backend
ci:
desc: Run CI build
deps: [default, static]
static:
deps: [go-lint, go-vet, go-fmt, js-tsc, js-lint, cover]
backend:
desc: Build budgeteer
sources:
- ./go.mod
- ./go.sum
- ./**/*.go
- ./web/dist/**/*
- ./postgres/schema/*
generates:
- build/budgeteer{{exeExt}}
env:
CGO_ENABLED: '0'
cmds:
- go build -o ./build/budgeteer{{exeExt}} ./cmd/budgeteer
go-vet:
cmds:
- go vet
go-fmt:
cmds:
- go fmt
go-lint:
cmds:
- golangci-lint run
go-sqlc:
desc: sqlc code generation
sources:
- ./sqlc.yaml
- ./postgres/schema/*
- ./postgres/queries/*
generates:
- ./postgres/*.sql.go
cmds:
- sqlc generate
go-mod:
desc: Go modules
sources:
- ./go.mod
- ./go.sum
method: checksum
cmds:
- go mod download
cover:
desc: Run test and analyze coverage
cmds:
- go test ./... -coverprofile=coverage.out -covermode=atomic
- go tool cover -html=coverage.out -o=coverage.html
frontend:
desc: Build vue frontend
deps: [js-build]
sources:
- web/src/**/*
generates:
- web/dist/**/*
frontend-dev:
desc: Run dev-server for frontend
dir: web
cmds:
- yarn run dev
js-build:
dir: web
deps: [js-mod]
cmds:
- yarn build
js-mod:
run: once
sources:
- web/src/package.json
- web/src/yarn.lock
generates:
- web/node_modules/**/*
dir: web
cmds:
- yarn
js-tsc:
dir: web
deps: [js-mod]
cmds:
- yarn run vue-tsc --noEmit
js-lint:
dir: web
deps: [js-mod]
cmds:
- yarn run eslint "./src/**"
dev-docker:
desc: Build budgeeter:dev
sources:
- ./docker/Dockerfile.dev
- ./web/package.json
- ./web/yarn.lock
cmds:
- docker build -t {{.IMAGE_NAME}}:dev . -f docker/Dockerfile.dev
- docker push {{.IMAGE_NAME}}:dev
run-dev:
desc: Run dev environment in docker
deps: [dev-docker]
cmds:
- docker-compose -f docker/docker-compose.dev.yml -p budgeteer up -d

12
bass.build Normal file
View File

@ -0,0 +1,12 @@
(def go
(from (linux/alpine)
($ apk add go)))
(-> ($ go mod download)
(with-image go)
(with-mount *dir*/go.mod ./go.mod)
(with-mount *dir*/go.sum ./go.sum))
(def go-mods
(from go
($ go mod download)))

View File

@ -1,23 +1,30 @@
package bcrypt package bcrypt
import "golang.org/x/crypto/bcrypt" import (
"fmt"
// Verifier verifys passwords using Bcrypt "golang.org/x/crypto/bcrypt"
type Verifier struct { )
cost int
}
// Verify verifys a Password // Verifier verifys passwords using Bcrypt.
func (bv *Verifier) Verify(password string, hashOnDb string) error { type Verifier struct{}
return bcrypt.CompareHashAndPassword([]byte(hashOnDb), []byte(password))
}
// Hash calculates a hash to be stored on the database // Verify verifys a Password.
func (bv *Verifier) Hash(password string) (string, error) { func (bv *Verifier) Verify(password string, hashOnDB string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bv.cost) err := bcrypt.CompareHashAndPassword([]byte(hashOnDB), []byte(password))
if err != nil { if err != nil {
return "", err return fmt.Errorf("verify password: %w", err)
} }
return string(hash[:]), nil return nil
}
// Hash calculates a hash to be stored on the database.
func (bv *Verifier) Hash(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("hash password: %w", err)
}
return string(hash), nil
} }

View File

@ -1,39 +1,46 @@
package main package main
import ( import (
"fmt"
"io/fs"
"log" "log"
"net/http"
"git.javil.eu/jacob1123/budgeteer/bcrypt" "git.javil.eu/jacob1123/budgeteer/bcrypt"
"git.javil.eu/jacob1123/budgeteer/config" "git.javil.eu/jacob1123/budgeteer/config"
"git.javil.eu/jacob1123/budgeteer/http"
"git.javil.eu/jacob1123/budgeteer/jwt" "git.javil.eu/jacob1123/budgeteer/jwt"
"git.javil.eu/jacob1123/budgeteer/postgres" "git.javil.eu/jacob1123/budgeteer/postgres"
"git.javil.eu/jacob1123/budgeteer/server"
"git.javil.eu/jacob1123/budgeteer/web"
) )
func main() { func main() {
cfg, err := config.LoadConfig() cfg, err := config.LoadConfig()
if err != nil { if err != nil {
log.Fatalf("Could not load Config: %v", err) log.Fatalf("load config: %v", err)
} }
bv := &bcrypt.Verifier{} queries, err := postgres.Connect("pgx", cfg.DatabaseConnection)
q, db, err := postgres.Connect(cfg.DatabaseHost, cfg.DatabaseUser, cfg.DatabasePassword, cfg.DatabaseName)
if err != nil { if err != nil {
log.Fatalf("Failed connecting to DB: %v", err) log.Fatalf("connect to database: %v", err)
} }
us, err := postgres.NewRepository(q, db) static, err := fs.Sub(web.Static, "dist")
if err != nil { if err != nil {
log.Fatalf("Failed building Repository: %v", err) panic("open static files")
} }
tv := &jwt.TokenVerifier{} tokenVerifier, err := jwt.NewTokenVerifier(cfg.SessionSecret)
if err != nil {
h := &http.Handler{ panic(fmt.Errorf("create token verifier: %w", err))
Service: us,
TokenVerifier: tv,
CredentialsVerifier: bv,
} }
h.Serve()
handler := &server.Handler{
Service: queries,
TokenVerifier: tokenVerifier,
CredentialsVerifier: &bcrypt.Verifier{},
StaticFS: http.FS(static),
}
handler.Serve()
} }

View File

@ -1,6 +0,0 @@
{
"DatabaseHost": "localhost",
"DatabaseUser": "user",
"DatabasePassword": "thisismypassword",
"DatabaseName": "budgeteer"
}

View File

@ -4,25 +4,17 @@ import (
"os" "os"
) )
// Config contains all needed configurations // Config contains all needed configurations.
type Config struct { type Config struct {
DatabaseUser string DatabaseConnection string
DatabaseHost string SessionSecret string
DatabasePassword string
DatabaseName string
} }
// LoadConfig from path // LoadConfig from path.
func LoadConfig() (*Config, error) { func LoadConfig() (*Config, error) {
configuration := Config{ configuration := Config{
DatabaseUser: os.Getenv("BUDGETEER_DB_USER"), DatabaseConnection: os.Getenv("BUDGETEER_DB"),
DatabaseHost: os.Getenv("BUDGETEER_DB_HOST"), SessionSecret: os.Getenv("BUDGETEER_SESSION_SECRET"),
DatabasePassword: os.Getenv("BUDGETEER_DB_PASS"),
DatabaseName: os.Getenv("BUDGETEER_DB_NAME"),
}
if configuration.DatabaseName == "" {
configuration.DatabaseName = "budgeteer"
} }
return &configuration, nil return &configuration, nil

3
docker/Dockerfile Normal file
View File

@ -0,0 +1,3 @@
FROM scratch
COPY ./budgeteer /app/budgeteer
ENTRYPOINT ["/app/budgeteer"]

15
docker/Dockerfile.dev Normal file
View File

@ -0,0 +1,15 @@
FROM alpine as godeps
RUN apk --no-cache add go
RUN go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
RUN go install github.com/go-task/task/v3/cmd/task@latest
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
FROM alpine
RUN apk --no-cache add go nodejs yarn bash curl git git-perl
ENV PATH="/root/.yarn/bin/:${PATH}"
WORKDIR /src/web
ADD web/package.json web/yarn.lock /src/web/
WORKDIR /src
VOLUME /go
VOLUME /.cache
COPY --from=godeps /root/go/bin/task /root/go/bin/sqlc /root/go/bin/golangci-lint /usr/local/bin/

7
docker/dev.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/sh
tmux new-session -d -s watch 'cd web; yarn dev'
tmux split-window;
tmux send 'task -w run' ENTER;
tmux split-window;
tmux a;

View File

@ -0,0 +1,50 @@
version: '3.7'
services:
backend:
image: hub.javil.eu/budgeteer:dev
command: task -w run
ports:
- 1323:1323
volumes:
- ~/budgeteer:/src
- go-cache:/go
- yarn-cache:/.cache
environment:
BUDGETEER_DB: postgres://budgeteer:budgeteer@db:5432/budgeteer
BUDGETEER_SESSION_SECRET: random string for JWT authorization
depends_on:
- db
frontend:
image: hub.javil.eu/budgeteer:dev
command: task frontend-dev
ports:
- 3000:3000
volumes:
- ~/budgeteer:/src
depends_on:
- backend
db:
image: postgres:14
ports:
- 5432:5432
volumes:
- db:/var/lib/postgresql/data
environment:
POSTGRES_USER: budgeteer
POSTGRES_PASSWORD: budgeteer
POSTGRES_DBE: budgeteer
adminer:
image: adminer
ports:
- 1424:8080
depends_on:
- db
volumes:
db:
go-cache:
yarn-cache:

View File

@ -2,7 +2,7 @@ version: '3.7'
services: services:
app: app:
image: budgeteer:latest image: hub.javil.eu/budgeteer:latest
container_name: budgeteer container_name: budgeteer
ports: ports:
- 1323:1323 - 1323:1323

37
go.mod
View File

@ -4,8 +4,6 @@ go 1.17
require ( require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-gonic/gin v1.7.4
github.com/gofrs/uuid v4.0.0+incompatible
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/jackc/pgx/v4 v4.13.0 github.com/jackc/pgx/v4 v4.13.0
github.com/pressly/goose/v3 v3.3.1 github.com/pressly/goose/v3 v3.3.1
@ -13,28 +11,31 @@ require (
) )
require ( require (
github.com/gin-contrib/sse v0.1.0 // indirect github.com/DATA-DOG/go-txdb v0.1.5
github.com/go-playground/locales v0.13.0 // indirect github.com/labstack/echo/v4 v4.8.0
github.com/go-playground/universal-translator v0.17.0 // indirect )
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/golang/protobuf v1.5.0 // indirect require (
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/labstack/gommon v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
)
require (
github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.10.0 // indirect github.com/jackc/pgconn v1.10.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.1.1 // indirect github.com/jackc/pgproto3/v2 v2.1.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.8.1 // indirect github.com/jackc/pgtype v1.8.1 // direct
github.com/json-iterator/go v1.1.9 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/shopspring/decimal v1.3.1 // indirect github.com/shopspring/decimal v1.3.1 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect golang.org/x/text v0.3.7 // indirect
golang.org/x/text v0.3.6 // indirect
google.golang.org/protobuf v1.26.0 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
) )

68
go.sum
View File

@ -4,6 +4,8 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/ClickHouse/clickhouse-go v1.5.1/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= github.com/ClickHouse/clickhouse-go v1.5.1/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
github.com/DATA-DOG/go-txdb v0.1.5 h1:kKzz+LYk9qw1+fMyo8/9yDQiNXrJ2HbfX/TY61HkkB4=
github.com/DATA-DOG/go-txdb v0.1.5/go.mod h1:DhAhxMXZpUJVGnT+p9IbzJoRKvlArO2pkHjnGX7o0n0=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
@ -54,24 +56,13 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@ -80,13 +71,14 @@ github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@ -94,7 +86,6 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -102,9 +93,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
@ -165,8 +154,6 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@ -175,14 +162,14 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/labstack/echo/v4 v4.8.0 h1:wdc6yKVaHxkNOEdz4cRZs1pQkwSXPiRjq69yWP4QQS8=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/labstack/echo/v4 v4.8.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@ -195,10 +182,13 @@ github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
@ -208,10 +198,6 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
@ -280,11 +266,11 @@ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@ -319,6 +305,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -340,6 +327,7 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -373,17 +361,23 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -403,7 +397,6 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@ -416,11 +409,9 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
@ -428,11 +419,10 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -1,57 +0,0 @@
package http
import (
"net/http"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type AccountsData struct {
AlwaysNeededData
}
func (h *Handler) accounts(c *gin.Context) {
d := AccountsData{
c.MustGet("data").(AlwaysNeededData),
}
c.HTML(http.StatusOK, "accounts.html", d)
}
type AccountData struct {
AlwaysNeededData
Account *postgres.Account
Transactions []postgres.GetTransactionsForAccountRow
}
func (h *Handler) account(c *gin.Context) {
accountID := c.Param("accountid")
accountUUID, err := uuid.Parse(accountID)
if err != nil {
c.Redirect(http.StatusTemporaryRedirect, "/login")
return
}
account, err := h.Service.DB.GetAccount(c.Request.Context(), accountUUID)
if err != nil {
c.AbortWithError(http.StatusNotFound, err)
return
}
transactions, err := h.Service.DB.GetTransactionsForAccount(c.Request.Context(), accountUUID)
if err != nil {
c.AbortWithError(http.StatusNotFound, err)
return
}
d := AccountData{
c.MustGet("data").(AlwaysNeededData),
&account,
transactions,
}
c.HTML(http.StatusOK, "account.html", d)
}

View File

@ -1,31 +0,0 @@
package http
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/pressly/goose/v3"
)
type AdminData struct {
}
func (h *Handler) admin(c *gin.Context) {
d := AdminData{}
c.HTML(http.StatusOK, "admin.html", d)
}
func (h *Handler) clearDatabase(c *gin.Context) {
d := AdminData{}
if err := goose.Down(h.Service.LegacyDB, "schema"); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
}
if err := goose.Up(h.Service.LegacyDB, "schema"); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
}
c.HTML(http.StatusOK, "admin.html", d)
}

View File

@ -1,45 +0,0 @@
package http
import (
"context"
"net/http"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type AlwaysNeededData struct {
Budget postgres.Budget
Accounts []postgres.GetAccountsWithBalanceRow
}
func (h *Handler) getImportantData(c *gin.Context) {
budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
c.Redirect(http.StatusTemporaryRedirect, "/login")
c.Abort()
return
}
budget, err := h.Service.DB.GetBudget(context.Background(), budgetUUID)
if err != nil {
c.AbortWithError(http.StatusNotFound, err)
return
}
accounts, err := h.Service.DB.GetAccountsWithBalance(c.Request.Context(), budgetUUID)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
base := AlwaysNeededData{
Accounts: accounts,
Budget: budget,
}
c.Set("data", base)
c.Next()
}

View File

@ -1,37 +0,0 @@
package http
import (
"context"
"net/http"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type BudgetData struct {
AlwaysNeededData
Transactions []postgres.GetTransactionsForBudgetRow
}
func (h *Handler) budget(c *gin.Context) {
budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
c.Redirect(http.StatusTemporaryRedirect, "/login")
return
}
transactions, err := h.Service.DB.GetTransactionsForBudget(context.Background(), budgetUUID)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
d := BudgetData{
c.MustGet("data").(AlwaysNeededData),
transactions,
}
c.HTML(http.StatusOK, "budget.html", d)
}

View File

@ -1,103 +0,0 @@
package http
import (
"context"
"fmt"
"net/http"
"strconv"
"time"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type BudgetingData struct {
AlwaysNeededData
Categories []postgres.GetCategoriesWithBalanceRow
Date time.Time
Next time.Time
Previous time.Time
}
func (h *Handler) budgeting(c *gin.Context) {
budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
c.Redirect(http.StatusTemporaryRedirect, "/login")
return
}
now := time.Now()
var year, month int
yearString := c.Param("year")
monthString := c.Param("month")
if yearString != "" && monthString != "" {
year, err = strconv.Atoi(yearString)
if err != nil {
c.Redirect(http.StatusTemporaryRedirect, "/budget/"+budgetUUID.String())
return
}
month, err = strconv.Atoi(monthString)
if err != nil {
c.Redirect(http.StatusTemporaryRedirect, "/budget/"+budgetUUID.String())
return
}
} else {
var monthM time.Month
year, monthM, _ = now.Date()
month = int(monthM)
}
firstOfMonth := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, now.Location())
firstOfNextMonth := firstOfMonth.AddDate(0, 1, 0)
firstOfPreviousMonth := firstOfMonth.AddDate(0, -1, 0)
params := postgres.GetCategoriesWithBalanceParams{
BudgetID: budgetUUID,
FromDate: firstOfMonth,
ToDate: firstOfNextMonth,
}
categories, err := h.Service.DB.GetCategoriesWithBalance(context.Background(), params)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
d := BudgetingData{
c.MustGet("data").(AlwaysNeededData),
categories,
firstOfMonth,
firstOfNextMonth,
firstOfPreviousMonth,
}
c.HTML(http.StatusOK, "budgeting.html", d)
}
func (h *Handler) clearBudget(c *gin.Context) {
budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
c.Redirect(http.StatusTemporaryRedirect, "/login")
return
}
rows, err := h.Service.DB.DeleteAllAssignments(c.Request.Context(), budgetUUID)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
fmt.Printf("Deleted %d assignments\n", rows)
rows, err = h.Service.DB.DeleteAllTransactions(c.Request.Context(), budgetUUID)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
fmt.Printf("Deleted %d transactions\n", rows)
}

View File

@ -1,26 +0,0 @@
package http
import (
"net/http"
"git.javil.eu/jacob1123/budgeteer"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/gin-gonic/gin"
)
func (h *Handler) dashboard(c *gin.Context) {
userID := c.MustGet("token").(budgeteer.Token).GetID()
budgets, err := h.Service.BudgetsForUser(userID)
if err != nil {
return
}
d := DashboardData{
Budgets: budgets,
}
c.HTML(http.StatusOK, "dashboard.html", d)
}
type DashboardData struct {
Budgets []postgres.Budget
}

View File

@ -1,203 +0,0 @@
package http
import (
"fmt"
"io/fs"
"net/http"
"time"
"git.javil.eu/jacob1123/budgeteer"
"git.javil.eu/jacob1123/budgeteer/bcrypt"
"git.javil.eu/jacob1123/budgeteer/postgres"
"git.javil.eu/jacob1123/budgeteer/web"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// Handler handles incoming requests
type Handler struct {
Service *postgres.Repository
TokenVerifier budgeteer.TokenVerifier
CredentialsVerifier *bcrypt.Verifier
}
const (
expiration = 72
authCookie = "authentication"
)
// Serve starts the HTTP Server
func (h *Handler) Serve() {
router := gin.Default()
templates, err := NewTemplates(router.FuncMap)
if err != nil {
panic(err)
}
router.HTMLRender = templates
static, err := fs.Sub(web.Static, "static")
if err != nil {
panic("couldn't open static files")
}
router.StaticFS("/static", http.FS(static))
router.GET("/", func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", nil) })
router.GET("/login", h.login)
router.GET("/register", h.register)
withLogin := router.Group("")
withLogin.Use(h.verifyLoginWithRedirect)
withLogin.GET("/dashboard", h.dashboard)
withLogin.GET("/admin", h.admin)
withLogin.GET("/admin/clear-database", h.clearDatabase)
withBudget := router.Group("")
withBudget.Use(h.verifyLoginWithRedirect)
withBudget.Use(h.getImportantData)
withBudget.GET("/budget/:budgetid", h.budgeting)
withBudget.GET("/budget/:budgetid/clear", h.clearBudget)
withBudget.GET("/budget/:budgetid/:year/:month", h.budgeting)
withBudget.GET("/budget/:budgetid/all-accounts", h.budget)
withBudget.GET("/budget/:budgetid/accounts", h.accounts)
withBudget.GET("/budget/:budgetid/account/:accountid", h.account)
api := router.Group("/api/v1")
unauthenticated := api.Group("/user")
unauthenticated.GET("/login", func(c *gin.Context) { c.Redirect(http.StatusPermanentRedirect, "/login") })
unauthenticated.POST("/login", h.loginPost)
unauthenticated.POST("/register", h.registerPost)
authenticated := api.Group("")
authenticated.Use(h.verifyLoginWithRedirect)
user := authenticated.Group("/user")
user.GET("/logout", logout)
budget := authenticated.Group("/budget")
budget.POST("/new", h.newBudget)
transaction := authenticated.Group("/transaction")
transaction.POST("/new", h.newTransaction)
transaction.POST("/import/ynab", h.importYNAB)
router.Run(":1323")
}
func (h *Handler) importYNAB(c *gin.Context) {
budgetID, succ := c.GetPostForm("budget_id")
if !succ {
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("no budget_id specified"))
return
}
budgetUUID, err := uuid.Parse(budgetID)
if !succ {
c.AbortWithError(http.StatusBadRequest, err)
return
}
ynab, err := NewYNABImport(h.Service.DB, budgetUUID)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
transactionsFile, err := c.FormFile("transactions")
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
transactions, err := transactionsFile.Open()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
err = ynab.ImportTransactions(transactions)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
assignmentsFile, err := c.FormFile("assignments")
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
assignments, err := assignmentsFile.Open()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
err = ynab.ImportAssignments(assignments)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
func (h *Handler) newTransaction(c *gin.Context) {
transactionMemo, succ := c.GetPostForm("memo")
if !succ {
c.AbortWithStatus(http.StatusNotAcceptable)
return
}
transactionAccount, succ := c.GetPostForm("account_id")
if !succ {
c.AbortWithStatus(http.StatusNotAcceptable)
return
}
transactionAccountID, err := uuid.Parse(transactionAccount)
if !succ {
c.AbortWithStatus(http.StatusNotAcceptable)
return
}
transactionDate, succ := c.GetPostForm("date")
if !succ {
c.AbortWithStatus(http.StatusNotAcceptable)
return
}
transactionDateValue, err := time.Parse("2006-01-02", transactionDate)
if err != nil {
c.AbortWithStatus(http.StatusNotAcceptable)
return
}
new := postgres.CreateTransactionParams{
Memo: transactionMemo,
Date: transactionDateValue,
Amount: postgres.Numeric{},
AccountID: transactionAccountID,
}
_, err = h.Service.DB.CreateTransaction(c.Request.Context(), new)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
func (h *Handler) newBudget(c *gin.Context) {
budgetName, succ := c.GetPostForm("name")
if !succ {
c.AbortWithStatus(http.StatusNotAcceptable)
return
}
userID := c.MustGet("token").(budgeteer.Token).GetID()
_, err := h.Service.NewBudget(budgetName, userID)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}

View File

@ -1,121 +0,0 @@
package http
import (
"context"
"fmt"
"net/http"
"time"
"git.javil.eu/jacob1123/budgeteer"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/gin-gonic/gin"
)
func (h *Handler) verifyLogin(c *gin.Context) (budgeteer.Token, error) {
tokenString, err := c.Cookie(authCookie)
if err != nil {
return nil, fmt.Errorf("get cookie: %w", err)
}
token, err := h.TokenVerifier.VerifyToken(tokenString)
if err != nil {
c.SetCookie(authCookie, "", -1, "", "", false, false)
return nil, fmt.Errorf("verify token '%s': %w", tokenString, err)
}
return token, nil
}
func (h *Handler) verifyLoginWithRedirect(c *gin.Context) {
token, err := h.verifyLogin(c)
if err != nil {
c.Redirect(http.StatusTemporaryRedirect, "/login")
c.Abort()
return
}
c.Set("token", token)
c.Next()
}
func (h *Handler) login(c *gin.Context) {
if _, err := h.verifyLogin(c); err == nil {
c.Redirect(http.StatusTemporaryRedirect, "/dashboard")
return
}
c.HTML(http.StatusOK, "login.html", nil)
}
func (h *Handler) register(c *gin.Context) {
if _, err := h.verifyLogin(c); err == nil {
c.Redirect(http.StatusTemporaryRedirect, "/dashboard")
return
}
c.HTML(http.StatusOK, "register.html", nil)
}
func logout(c *gin.Context) {
clearLogin(c)
}
func clearLogin(c *gin.Context) {
c.SetCookie(authCookie, "", -1, "", "", false, true)
}
func (h *Handler) loginPost(c *gin.Context) {
username, _ := c.GetPostForm("username")
password, _ := c.GetPostForm("password")
user, err := h.Service.DB.GetUserByUsername(context.Background(), username)
if err != nil {
c.AbortWithError(http.StatusUnauthorized, err)
return
}
if err = h.CredentialsVerifier.Verify(password, user.Password); err != nil {
c.AbortWithError(http.StatusUnauthorized, err)
return
}
t, err := h.TokenVerifier.CreateToken(&user)
if err != nil {
c.AbortWithError(http.StatusUnauthorized, err)
}
_, _ = h.Service.DB.UpdateLastLogin(context.Background(), user.ID)
maxAge := (int)((expiration * time.Hour).Seconds())
c.SetCookie(authCookie, t, maxAge, "", "", false, true)
c.JSON(http.StatusOK, map[string]string{
"token": t,
})
}
func (h *Handler) registerPost(c *gin.Context) {
email, _ := c.GetPostForm("email")
password, _ := c.GetPostForm("password")
name, _ := c.GetPostForm("name")
_, err := h.Service.DB.GetUserByUsername(context.Background(), email)
if err == nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
hash, err := h.CredentialsVerifier.Hash(password)
if err != nil {
c.AbortWithError(http.StatusUnauthorized, err)
return
}
createUser := postgres.CreateUserParams{
Name: name,
Password: hash,
Email: email,
}
_, err = h.Service.DB.CreateUser(context.Background(), createUser)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
}
}

View File

@ -1,45 +0,0 @@
package http
import (
"fmt"
"html/template"
"io/fs"
"git.javil.eu/jacob1123/budgeteer/web"
"github.com/gin-gonic/gin/render"
)
type Templates struct {
templates map[string]*template.Template
}
func NewTemplates(funcMap template.FuncMap) (*Templates, error) {
templates, err := fs.Glob(web.Templates, "*.tpl")
if err != nil {
return nil, err
}
result := &Templates{
templates: make(map[string]*template.Template, 0),
}
pages, err := fs.Glob(web.Templates, "*.html")
for _, page := range pages {
allTemplates := append(templates, page)
tpl, err := template.New(page).Funcs(funcMap).ParseFS(web.Templates, allTemplates...)
fmt.Printf("page: %s, templates: %v\n", page, templates)
if err != nil {
return nil, err
}
result.templates[page] = tpl
}
return result, nil
}
func (tpl *Templates) Instance(name string, obj interface{}) render.Render {
return render.HTML{
Template: tpl.templates[name],
Name: name,
Data: obj,
}
}

View File

@ -1,299 +0,0 @@
package http
import (
"context"
"encoding/csv"
"fmt"
"io"
"strings"
"time"
"unicode/utf8"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/google/uuid"
)
type YNABImport struct {
Context context.Context
accounts []postgres.Account
payees []postgres.Payee
categories []postgres.GetCategoriesRow
categoryGroups []postgres.CategoryGroup
queries *postgres.Queries
budgetID uuid.UUID
}
func NewYNABImport(q *postgres.Queries, budgetID uuid.UUID) (*YNABImport, error) {
accounts, err := q.GetAccounts(context.Background(), budgetID)
if err != nil {
return nil, err
}
payees, err := q.GetPayees(context.Background(), budgetID)
if err != nil {
return nil, err
}
categories, err := q.GetCategories(context.Background(), budgetID)
if err != nil {
return nil, err
}
categoryGroups, err := q.GetCategoryGroups(context.Background(), budgetID)
if err != nil {
return nil, err
}
return &YNABImport{
Context: context.Background(),
accounts: accounts,
payees: payees,
categories: categories,
categoryGroups: categoryGroups,
queries: q,
budgetID: budgetID,
}, nil
}
func (ynab *YNABImport) ImportAssignments(r io.Reader) error {
csv := csv.NewReader(r)
csv.Comma = '\t'
csv.LazyQuotes = true
csvData, err := csv.ReadAll()
if err != nil {
return fmt.Errorf("could not read from tsv: %w", err)
}
count := 0
for _, record := range csvData[1:] {
//"Month" "Category Group/Category" "Category Group" "Category" "Budgeted" "Activity" "Available"
//"Apr 2019" "Income: Next Month" "Income" "Next Month" 0,00€ 0,00€ 0,00€
dateString := record[0]
date, err := time.Parse("Jan 2006", dateString)
if err != nil {
return fmt.Errorf("could not parse date %s: %w", dateString, err)
}
categoryGroup, categoryName := record[2], record[3] //also in 1 joined by :
category, err := ynab.GetCategory(categoryGroup, categoryName)
if err != nil {
return fmt.Errorf("could not get category %s/%s: %w", categoryGroup, categoryName, err)
}
amountString := record[4]
amount, err := GetAmount(amountString, "0,00€")
if err != nil {
return fmt.Errorf("could not parse amount %s: %w", amountString, err)
}
if amount.Int.Int64() == 0 {
continue
}
assignment := postgres.CreateAssignmentParams{
Date: date,
CategoryID: category.UUID,
Amount: amount,
}
_, err = ynab.queries.CreateAssignment(ynab.Context, assignment)
if err != nil {
return fmt.Errorf("could not save assignment %v: %w", assignment, err)
}
count++
}
fmt.Printf("Imported %d assignments\n", count)
return nil
}
func (ynab *YNABImport) ImportTransactions(r io.Reader) error {
csv := csv.NewReader(r)
csv.Comma = '\t'
csv.LazyQuotes = true
csvData, err := csv.ReadAll()
if err != nil {
return fmt.Errorf("could not read from tsv: %w", err)
}
count := 0
for _, record := range csvData[1:] {
accountName := record[0]
account, err := ynab.GetAccount(accountName)
if err != nil {
return fmt.Errorf("could not get account %s: %w", accountName, err)
}
//flag := record[1]
dateString := record[2]
date, err := time.Parse("02.01.2006", dateString)
if err != nil {
return fmt.Errorf("could not parse date %s: %w", dateString, err)
}
payeeName := record[3]
payeeID, err := ynab.GetPayee(payeeName)
if err != nil {
return fmt.Errorf("could not get payee %s: %w", payeeName, err)
}
categoryGroup, categoryName := record[5], record[6] //also in 4 joined by :
category, err := ynab.GetCategory(categoryGroup, categoryName)
if err != nil {
return fmt.Errorf("could not get category %s/%s: %w", categoryGroup, categoryName, err)
}
memo := record[7]
outflow := record[8]
inflow := record[9]
amount, err := GetAmount(inflow, outflow)
if err != nil {
return fmt.Errorf("could not parse amount from (%s/%s): %w", inflow, outflow, err)
}
//cleared := record[10]
transaction := postgres.CreateTransactionParams{
Date: date,
Memo: memo,
AccountID: account.ID,
PayeeID: payeeID,
CategoryID: category,
Amount: amount,
}
_, err = ynab.queries.CreateTransaction(ynab.Context, transaction)
if err != nil {
return fmt.Errorf("could not save transaction %v: %w", transaction, err)
}
count++
}
fmt.Printf("Imported %d transactions\n", count)
return nil
}
func trimLastChar(s string) string {
r, size := utf8.DecodeLastRuneInString(s)
if r == utf8.RuneError && (size == 0 || size == 1) {
size = 0
}
return s[:len(s)-size]
}
func GetAmount(inflow string, outflow string) (postgres.Numeric, error) {
// Remove trailing currency
inflow = strings.Replace(trimLastChar(inflow), ",", ".", 1)
outflow = strings.Replace(trimLastChar(outflow), ",", ".", 1)
num := postgres.Numeric{}
err := num.Set(inflow)
if err != nil {
return num, fmt.Errorf("Could not parse inflow %s: %w", inflow, err)
}
// if inflow is zero, use outflow
if num.Int.Int64() != 0 {
return num, nil
}
err = num.Set("-" + outflow)
if err != nil {
return num, fmt.Errorf("Could not parse outflow %s: %w", inflow, err)
}
return num, nil
}
func (ynab *YNABImport) GetAccount(name string) (*postgres.Account, error) {
for _, acc := range ynab.accounts {
if acc.Name == name {
return &acc, nil
}
}
account, err := ynab.queries.CreateAccount(ynab.Context, postgres.CreateAccountParams{Name: name, BudgetID: ynab.budgetID})
if err != nil {
return nil, err
}
ynab.accounts = append(ynab.accounts, account)
return &account, nil
}
func (ynab *YNABImport) GetPayee(name string) (uuid.NullUUID, error) {
if name == "" {
return uuid.NullUUID{}, nil
}
for _, pay := range ynab.payees {
if pay.Name == name {
return uuid.NullUUID{UUID: pay.ID, Valid: true}, nil
}
}
payee, err := ynab.queries.CreatePayee(ynab.Context, postgres.CreatePayeeParams{Name: name, BudgetID: ynab.budgetID})
if err != nil {
return uuid.NullUUID{}, err
}
ynab.payees = append(ynab.payees, payee)
return uuid.NullUUID{UUID: payee.ID, Valid: true}, nil
}
func (ynab *YNABImport) GetCategory(group string, name string) (uuid.NullUUID, error) {
if group == "" || name == "" {
return uuid.NullUUID{}, nil
}
for _, category := range ynab.categories {
if category.Name == name && category.Group == group {
return uuid.NullUUID{UUID: category.ID, Valid: true}, nil
}
}
for _, categoryGroup := range ynab.categoryGroups {
if categoryGroup.Name == group {
createCategory := postgres.CreateCategoryParams{Name: name, CategoryGroupID: categoryGroup.ID}
category, err := ynab.queries.CreateCategory(ynab.Context, createCategory)
if err != nil {
return uuid.NullUUID{}, err
}
getCategory := postgres.GetCategoriesRow{
ID: category.ID,
CategoryGroupID: category.CategoryGroupID,
Name: category.Name,
Group: categoryGroup.Name,
}
ynab.categories = append(ynab.categories, getCategory)
return uuid.NullUUID{UUID: category.ID, Valid: true}, nil
}
}
categoryGroup, err := ynab.queries.CreateCategoryGroup(ynab.Context, postgres.CreateCategoryGroupParams{Name: group, BudgetID: ynab.budgetID})
if err != nil {
return uuid.NullUUID{}, err
}
ynab.categoryGroups = append(ynab.categoryGroups, categoryGroup)
category, err := ynab.queries.CreateCategory(ynab.Context, postgres.CreateCategoryParams{Name: name, CategoryGroupID: categoryGroup.ID})
if err != nil {
return uuid.NullUUID{}, err
}
getCategory := postgres.GetCategoriesRow{
ID: category.ID,
CategoryGroupID: category.CategoryGroupID,
Name: category.Name,
Group: categoryGroup.Name,
}
ynab.categories = append(ynab.categories, getCategory)
return uuid.NullUUID{UUID: category.ID, Valid: true}, nil
}

View File

@ -1,105 +0,0 @@
package jwt
import (
"fmt"
"time"
"git.javil.eu/jacob1123/budgeteer"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/dgrijalva/jwt-go"
"github.com/google/uuid"
)
// TokenVerifier verifies Tokens
type TokenVerifier struct {
}
// Token contains everything to authenticate a user
type Token struct {
username string
name string
expiry float64
id uuid.UUID
}
const (
expiration = 72
secret = "uditapbzuditagscwxuqdflgzpbu´ßiaefnlmzeßtrubiadern"
)
// CreateToken creates a new token from username and name
func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"usr": user.Email,
"name": user.Name,
"exp": time.Now().Add(time.Hour * expiration).Unix(),
"id": user.ID,
})
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte(secret))
if err != nil {
return "", err
}
return t, nil
}
// VerifyToken verifys a given string-token
func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return []byte(secret), nil
})
if err != nil {
return nil, fmt.Errorf("parse jwt: %w", err)
}
claims, err := verifyToken(token)
if err != nil {
return nil, fmt.Errorf("verify jwt: %w", err)
}
tkn := &Token{
username: claims["usr"].(string),
name: claims["name"].(string),
expiry: claims["exp"].(float64),
id: uuid.MustParse(claims["id"].(string)),
}
return tkn, nil
}
func verifyToken(token *jwt.Token) (jwt.MapClaims, error) {
if !token.Valid {
return nil, fmt.Errorf("Token is not valid")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("Claims are not of Type MapClaims")
}
if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
return nil, fmt.Errorf("Claims have expired")
}
return claims, nil
}
func (t *Token) GetName() string {
return t.name
}
func (t *Token) GetUsername() string {
return t.username
}
func (t *Token) GetExpiry() float64 {
return t.expiry
}
func (t *Token) GetID() uuid.UUID {
return t.id
}

89
jwt/token-verifier.go Normal file
View File

@ -0,0 +1,89 @@
package jwt
import (
"fmt"
"time"
"git.javil.eu/jacob1123/budgeteer"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/dgrijalva/jwt-go"
"github.com/google/uuid"
)
// TokenVerifier verifies Tokens.
type TokenVerifier struct {
Expiration time.Duration
secret string
}
const DefaultExpiration = time.Hour * time.Duration(72)
func NewTokenVerifier(secret string) (*TokenVerifier, error) {
if secret == "" {
return nil, ErrEmptySecret
}
return &TokenVerifier{
Expiration: DefaultExpiration,
secret: secret,
}, nil
}
var (
ErrUnexpectedSigningMethod = fmt.Errorf("unexpected signing method")
ErrInvalidToken = fmt.Errorf("token is invalid")
ErrTokenExpired = fmt.Errorf("token has expired")
ErrEmptySecret = fmt.Errorf("secret is required")
)
// CreateToken creates a new token from username and name.
func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
if tv.secret == "" {
return "", ErrEmptySecret
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"usr": user.Email,
"name": user.Name,
"exp": time.Now().Add(tv.Expiration).Unix(),
"id": user.ID,
})
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte(tv.secret))
if err != nil {
return "", fmt.Errorf("create token: %w", err)
}
return t, nil
}
// VerifyToken verifies a given string-token.
func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) { //nolint:ireturn
if tv.secret == "" {
return nil, ErrEmptySecret
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("method '%v': %w", token.Header["alg"], ErrUnexpectedSigningMethod)
}
return []byte(tv.secret), nil
})
if err != nil {
return nil, fmt.Errorf("parse jwt: %w", err)
}
claims, err := verifyToken(token)
if err != nil {
return nil, fmt.Errorf("verify jwt: %w", err)
}
tkn := &Token{ //nolint:forcetypeassert
username: claims["usr"].(string),
name: claims["name"].(string),
expiry: claims["exp"].(float64),
id: uuid.MustParse(claims["id"].(string)),
}
return tkn, nil
}

49
jwt/token.go Normal file
View File

@ -0,0 +1,49 @@
package jwt
import (
"time"
"github.com/dgrijalva/jwt-go"
"github.com/google/uuid"
)
// Token contains everything to authenticate a user.
type Token struct {
username string
name string
expiry float64
id uuid.UUID
}
func verifyToken(token *jwt.Token) (jwt.MapClaims, error) {
if !token.Valid {
return nil, ErrInvalidToken
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, ErrInvalidToken
}
if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
return nil, ErrTokenExpired
}
return claims, nil
}
func (t *Token) GetName() string {
return t.name
}
func (t *Token) GetUsername() string {
return t.username
}
func (t *Token) GetExpiry() float64 {
return t.expiry
}
func (t *Token) GetID() uuid.UUID {
return t.id
}

View File

@ -1,11 +1,15 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
// source: accounts.sql // source: accounts.sql
package postgres package postgres
import ( import (
"context" "context"
"database/sql"
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -13,7 +17,7 @@ const createAccount = `-- name: CreateAccount :one
INSERT INTO accounts INSERT INTO accounts
(name, budget_id) (name, budget_id)
VALUES ($1, $2) VALUES ($1, $2)
RETURNING id, budget_id, name RETURNING id, budget_id, name, on_budget, is_open, last_reconciled
` `
type CreateAccountParams struct { type CreateAccountParams struct {
@ -24,25 +28,40 @@ type CreateAccountParams struct {
func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) { func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (Account, error) {
row := q.db.QueryRowContext(ctx, createAccount, arg.Name, arg.BudgetID) row := q.db.QueryRowContext(ctx, createAccount, arg.Name, arg.BudgetID)
var i Account var i Account
err := row.Scan(&i.ID, &i.BudgetID, &i.Name) err := row.Scan(
&i.ID,
&i.BudgetID,
&i.Name,
&i.OnBudget,
&i.IsOpen,
&i.LastReconciled,
)
return i, err return i, err
} }
const getAccount = `-- name: GetAccount :one const getAccount = `-- name: GetAccount :one
SELECT accounts.id, accounts.budget_id, accounts.name FROM accounts SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget, accounts.is_open, accounts.last_reconciled FROM accounts
WHERE accounts.id = $1 WHERE accounts.id = $1
` `
func (q *Queries) GetAccount(ctx context.Context, id uuid.UUID) (Account, error) { func (q *Queries) GetAccount(ctx context.Context, id uuid.UUID) (Account, error) {
row := q.db.QueryRowContext(ctx, getAccount, id) row := q.db.QueryRowContext(ctx, getAccount, id)
var i Account var i Account
err := row.Scan(&i.ID, &i.BudgetID, &i.Name) err := row.Scan(
&i.ID,
&i.BudgetID,
&i.Name,
&i.OnBudget,
&i.IsOpen,
&i.LastReconciled,
)
return i, err return i, err
} }
const getAccounts = `-- name: GetAccounts :many const getAccounts = `-- name: GetAccounts :many
SELECT accounts.id, accounts.budget_id, accounts.name FROM accounts SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget, accounts.is_open, accounts.last_reconciled FROM accounts
WHERE accounts.budget_id = $1 WHERE accounts.budget_id = $1
AND accounts.is_open = TRUE
ORDER BY accounts.name ORDER BY accounts.name
` `
@ -55,7 +74,14 @@ func (q *Queries) GetAccounts(ctx context.Context, budgetID uuid.UUID) ([]Accoun
var items []Account var items []Account
for rows.Next() { for rows.Next() {
var i Account var i Account
if err := rows.Scan(&i.ID, &i.BudgetID, &i.Name); err != nil { if err := rows.Scan(
&i.ID,
&i.BudgetID,
&i.Name,
&i.OnBudget,
&i.IsOpen,
&i.LastReconciled,
); err != nil {
return nil, err return nil, err
} }
items = append(items, i) items = append(items, i)
@ -70,19 +96,25 @@ func (q *Queries) GetAccounts(ctx context.Context, budgetID uuid.UUID) ([]Accoun
} }
const getAccountsWithBalance = `-- name: GetAccountsWithBalance :many const getAccountsWithBalance = `-- name: GetAccountsWithBalance :many
SELECT accounts.id, accounts.name, SUM(transactions.amount)::decimal(12,2) as balance SELECT accounts.id, accounts.name, accounts.on_budget, accounts.is_open, accounts.last_reconciled,
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW())::decimal(12,2) as working_balance,
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status IN ('Cleared', 'Reconciled'))::decimal(12,2) as cleared_balance,
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status = 'Reconciled')::decimal(12,2) as reconciled_balance
FROM accounts FROM accounts
LEFT JOIN transactions ON transactions.account_id = accounts.id
WHERE accounts.budget_id = $1 WHERE accounts.budget_id = $1
AND transactions.date < NOW() AND accounts.is_open = TRUE
GROUP BY accounts.id, accounts.name
ORDER BY accounts.name ORDER BY accounts.name
` `
type GetAccountsWithBalanceRow struct { type GetAccountsWithBalanceRow struct {
ID uuid.UUID ID uuid.UUID
Name string Name string
Balance Numeric OnBudget bool
IsOpen bool
LastReconciled sql.NullTime
WorkingBalance numeric.Numeric
ClearedBalance numeric.Numeric
ReconciledBalance numeric.Numeric
} }
func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID) ([]GetAccountsWithBalanceRow, error) { func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID) ([]GetAccountsWithBalanceRow, error) {
@ -94,7 +126,16 @@ func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID
var items []GetAccountsWithBalanceRow var items []GetAccountsWithBalanceRow
for rows.Next() { for rows.Next() {
var i GetAccountsWithBalanceRow var i GetAccountsWithBalanceRow
if err := rows.Scan(&i.ID, &i.Name, &i.Balance); err != nil { if err := rows.Scan(
&i.ID,
&i.Name,
&i.OnBudget,
&i.IsOpen,
&i.LastReconciled,
&i.WorkingBalance,
&i.ClearedBalance,
&i.ReconciledBalance,
); err != nil {
return nil, err return nil, err
} }
items = append(items, i) items = append(items, i)
@ -107,3 +148,97 @@ func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID
} }
return items, nil return items, nil
} }
const searchAccounts = `-- name: SearchAccounts :many
SELECT accounts.id, accounts.budget_id, accounts.name, 'account' as type FROM accounts
WHERE accounts.budget_id = $1
AND accounts.is_open = TRUE
AND accounts.name ILIKE $2
ORDER BY accounts.name
`
type SearchAccountsParams struct {
BudgetID uuid.UUID
Search string
}
type SearchAccountsRow struct {
ID uuid.UUID
BudgetID uuid.UUID
Name string
Type interface{}
}
func (q *Queries) SearchAccounts(ctx context.Context, arg SearchAccountsParams) ([]SearchAccountsRow, error) {
rows, err := q.db.QueryContext(ctx, searchAccounts, arg.BudgetID, arg.Search)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SearchAccountsRow
for rows.Next() {
var i SearchAccountsRow
if err := rows.Scan(
&i.ID,
&i.BudgetID,
&i.Name,
&i.Type,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const setLastReconciled = `-- name: SetLastReconciled :exec
UPDATE accounts
SET last_reconciled = NOW()
WHERE accounts.id = $1
`
func (q *Queries) SetLastReconciled(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, setLastReconciled, id)
return err
}
const updateAccount = `-- name: UpdateAccount :one
UPDATE accounts
SET name = $1,
on_budget = $2,
is_open = $3
WHERE accounts.id = $4
RETURNING id, budget_id, name, on_budget, is_open, last_reconciled
`
type UpdateAccountParams struct {
Name string
OnBudget bool
IsOpen bool
ID uuid.UUID
}
func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (Account, error) {
row := q.db.QueryRowContext(ctx, updateAccount,
arg.Name,
arg.OnBudget,
arg.IsOpen,
arg.ID,
)
var i Account
err := row.Scan(
&i.ID,
&i.BudgetID,
&i.Name,
&i.OnBudget,
&i.IsOpen,
&i.LastReconciled,
)
return i, err
}

View File

@ -1,4 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
// source: assignments.sql // source: assignments.sql
package postgres package postgres
@ -7,6 +9,7 @@ import (
"context" "context"
"time" "time"
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -16,12 +19,12 @@ INSERT INTO assignments (
) VALUES ( ) VALUES (
$1, $2, $3 $1, $2, $3
) )
RETURNING id, category_id, date, memo, amount RETURNING category_id, date, memo, amount
` `
type CreateAssignmentParams struct { type CreateAssignmentParams struct {
Date time.Time Date time.Time
Amount Numeric Amount numeric.Numeric
CategoryID uuid.UUID CategoryID uuid.UUID
} }
@ -29,7 +32,6 @@ func (q *Queries) CreateAssignment(ctx context.Context, arg CreateAssignmentPara
row := q.db.QueryRowContext(ctx, createAssignment, arg.Date, arg.Amount, arg.CategoryID) row := q.db.QueryRowContext(ctx, createAssignment, arg.Date, arg.Amount, arg.CategoryID)
var i Assignment var i Assignment
err := row.Scan( err := row.Scan(
&i.ID,
&i.CategoryID, &i.CategoryID,
&i.Date, &i.Date,
&i.Memo, &i.Memo,
@ -52,3 +54,118 @@ func (q *Queries) DeleteAllAssignments(ctx context.Context, budgetID uuid.UUID)
} }
return result.RowsAffected() return result.RowsAffected()
} }
const getAllAssignments = `-- name: GetAllAssignments :many
SELECT assignments.date, categories.name as category, category_groups.name as group, assignments.amount
FROM assignments
INNER JOIN categories ON categories.id = assignments.category_id
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
WHERE category_groups.budget_id = $1
`
type GetAllAssignmentsRow struct {
Date time.Time
Category string
Group string
Amount numeric.Numeric
}
func (q *Queries) GetAllAssignments(ctx context.Context, budgetID uuid.UUID) ([]GetAllAssignmentsRow, error) {
rows, err := q.db.QueryContext(ctx, getAllAssignments, budgetID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllAssignmentsRow
for rows.Next() {
var i GetAllAssignmentsRow
if err := rows.Scan(
&i.Date,
&i.Category,
&i.Group,
&i.Amount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAssignmentsByMonthAndCategory = `-- name: GetAssignmentsByMonthAndCategory :many
SELECT date, category_id, budget_id, amount
FROM assignments_by_month
WHERE assignments_by_month.budget_id = $1
`
func (q *Queries) GetAssignmentsByMonthAndCategory(ctx context.Context, budgetID uuid.UUID) ([]AssignmentsByMonth, error) {
rows, err := q.db.QueryContext(ctx, getAssignmentsByMonthAndCategory, budgetID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AssignmentsByMonth
for rows.Next() {
var i AssignmentsByMonth
if err := rows.Scan(
&i.Date,
&i.CategoryID,
&i.BudgetID,
&i.Amount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateAssignment = `-- name: UpdateAssignment :exec
INSERT INTO assignments (category_id, date, amount)
VALUES($1, $2, $3)
ON CONFLICT (category_id, date)
DO
UPDATE SET amount = $3
`
type UpdateAssignmentParams struct {
CategoryID uuid.UUID
Date time.Time
Amount numeric.Numeric
}
func (q *Queries) UpdateAssignment(ctx context.Context, arg UpdateAssignmentParams) error {
_, err := q.db.ExecContext(ctx, updateAssignment, arg.CategoryID, arg.Date, arg.Amount)
return err
}
const updateAssignmentWithDifference = `-- name: UpdateAssignmentWithDifference :exec
INSERT INTO assignments (category_id, date, amount)
VALUES($1, $2, $3)
ON CONFLICT (category_id, date)
DO
UPDATE SET amount = assignments.amount + $3
`
type UpdateAssignmentWithDifferenceParams struct {
CategoryID uuid.UUID
Date time.Time
Amount numeric.Numeric
}
func (q *Queries) UpdateAssignmentWithDifference(ctx context.Context, arg UpdateAssignmentWithDifferenceParams) error {
_, err := q.db.ExecContext(ctx, updateAssignmentWithDifference, arg.CategoryID, arg.Date, arg.Amount)
return err
}

View File

@ -1,42 +1,69 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
// source: budgets.sql // source: budgets.sql
package postgres package postgres
import ( import (
"context" "context"
"time"
"github.com/google/uuid" "github.com/google/uuid"
) )
const createBudget = `-- name: CreateBudget :one const createBudget = `-- name: CreateBudget :one
INSERT INTO budgets INSERT INTO budgets
(name, last_modification) (name, income_category_id, last_modification)
VALUES ($1, NOW()) VALUES ($1, $2, NOW())
RETURNING id, name, last_modification RETURNING id, name, last_modification, income_category_id
` `
func (q *Queries) CreateBudget(ctx context.Context, name string) (Budget, error) { type CreateBudgetParams struct {
row := q.db.QueryRowContext(ctx, createBudget, name) Name string
IncomeCategoryID uuid.UUID
}
func (q *Queries) CreateBudget(ctx context.Context, arg CreateBudgetParams) (Budget, error) {
row := q.db.QueryRowContext(ctx, createBudget, arg.Name, arg.IncomeCategoryID)
var i Budget var i Budget
err := row.Scan(&i.ID, &i.Name, &i.LastModification) err := row.Scan(
&i.ID,
&i.Name,
&i.LastModification,
&i.IncomeCategoryID,
)
return i, err return i, err
} }
const deleteBudget = `-- name: DeleteBudget :exec
DELETE FROM budgets WHERE id = $1
`
func (q *Queries) DeleteBudget(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteBudget, id)
return err
}
const getBudget = `-- name: GetBudget :one const getBudget = `-- name: GetBudget :one
SELECT id, name, last_modification FROM budgets SELECT id, name, last_modification, income_category_id FROM budgets
WHERE id = $1 WHERE id = $1
` `
func (q *Queries) GetBudget(ctx context.Context, id uuid.UUID) (Budget, error) { func (q *Queries) GetBudget(ctx context.Context, id uuid.UUID) (Budget, error) {
row := q.db.QueryRowContext(ctx, getBudget, id) row := q.db.QueryRowContext(ctx, getBudget, id)
var i Budget var i Budget
err := row.Scan(&i.ID, &i.Name, &i.LastModification) err := row.Scan(
&i.ID,
&i.Name,
&i.LastModification,
&i.IncomeCategoryID,
)
return i, err return i, err
} }
const getBudgetsForUser = `-- name: GetBudgetsForUser :many const getBudgetsForUser = `-- name: GetBudgetsForUser :many
SELECT budgets.id, budgets.name, budgets.last_modification FROM budgets SELECT budgets.id, budgets.name, budgets.last_modification, budgets.income_category_id FROM budgets
LEFT JOIN user_budgets ON budgets.id = user_budgets.budget_id LEFT JOIN user_budgets ON budgets.id = user_budgets.budget_id
WHERE user_budgets.user_id = $1 WHERE user_budgets.user_id = $1
` `
@ -50,7 +77,12 @@ func (q *Queries) GetBudgetsForUser(ctx context.Context, userID uuid.UUID) ([]Bu
var items []Budget var items []Budget
for rows.Next() { for rows.Next() {
var i Budget var i Budget
if err := rows.Scan(&i.ID, &i.Name, &i.LastModification); err != nil { if err := rows.Scan(
&i.ID,
&i.Name,
&i.LastModification,
&i.IncomeCategoryID,
); err != nil {
return nil, err return nil, err
} }
items = append(items, i) items = append(items, i)
@ -63,3 +95,42 @@ func (q *Queries) GetBudgetsForUser(ctx context.Context, userID uuid.UUID) ([]Bu
} }
return items, nil return items, nil
} }
const getFirstActivity = `-- name: GetFirstActivity :one
SELECT MIN(dates.min_date)::date as min_date
FROM (
SELECT MIN(assignments.date) as min_date
FROM assignments
INNER JOIN categories ON categories.id = assignments.category_id
INNER JOIN category_groups ON category_groups.id = categories.category_group_id
WHERE category_groups.budget_id = $1
UNION
SELECT MIN(transactions.date) as min_date
FROM transactions
INNER JOIN accounts ON accounts.id = transactions.account_id
WHERE accounts.budget_id = $1
) dates
`
func (q *Queries) GetFirstActivity(ctx context.Context, budgetID uuid.UUID) (time.Time, error) {
row := q.db.QueryRowContext(ctx, getFirstActivity, budgetID)
var min_date time.Time
err := row.Scan(&min_date)
return min_date, err
}
const setInflowCategory = `-- name: SetInflowCategory :exec
UPDATE budgets
SET income_category_id = $1
WHERE budgets.id = $2
`
type SetInflowCategoryParams struct {
IncomeCategoryID uuid.UUID
ID uuid.UUID
}
func (q *Queries) SetInflowCategory(ctx context.Context, arg SetInflowCategoryParams) error {
_, err := q.db.ExecContext(ctx, setInflowCategory, arg.IncomeCategoryID, arg.ID)
return err
}

View File

@ -2,39 +2,63 @@ package postgres
import ( import (
"context" "context"
"database/sql"
"fmt"
"github.com/google/uuid" "github.com/google/uuid"
) )
// Budget returns a budget for a given id. // NewBudget creates a budget and adds it to the current user.
func (s *Repository) Budget(id uuid.UUID) (*Budget, error) { func (s *Database) NewBudget(context context.Context, name string, userID uuid.UUID) (*Budget, error) {
budget, err := s.DB.GetBudget(context.Background(), id) tx, err := s.BeginTx(context, &sql.TxOptions{})
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("begin transaction: %w", err)
} }
return &budget, nil
}
func (s *Repository) BudgetsForUser(id uuid.UUID) ([]Budget, error) { transaction := s.WithTx(tx)
budgets, err := s.DB.GetBudgetsForUser(context.Background(), id) budget, err := transaction.CreateBudget(context, CreateBudgetParams{
Name: name,
IncomeCategoryID: uuid.New(),
})
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("create budget: %w", err)
}
return budgets, nil
}
func (s *Repository) NewBudget(name string, userID uuid.UUID) (*Budget, error) {
budget, err := s.DB.CreateBudget(context.Background(), name)
if err != nil {
return nil, err
} }
ub := LinkBudgetToUserParams{UserID: userID, BudgetID: budget.ID} ub := LinkBudgetToUserParams{UserID: userID, BudgetID: budget.ID}
_, err = s.DB.LinkBudgetToUser(context.Background(), ub) _, err = transaction.LinkBudgetToUser(context, ub)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("link budget to user: %w", err)
} }
group, err := transaction.CreateCategoryGroup(context, CreateCategoryGroupParams{
Name: "Inflow",
BudgetID: budget.ID,
})
if err != nil {
return nil, fmt.Errorf("create inflow category_group: %w", err)
}
cat, err := transaction.CreateCategory(context, CreateCategoryParams{
Name: "Ready to Assign",
CategoryGroupID: group.ID,
})
if err != nil {
return nil, fmt.Errorf("create ready to assign category: %w", err)
}
err = transaction.SetInflowCategory(context, SetInflowCategoryParams{
IncomeCategoryID: cat.ID,
ID: budget.ID,
})
if err != nil {
return nil, fmt.Errorf("set inflow category: %w", err)
}
err = tx.Commit()
if err != nil {
return nil, fmt.Errorf("commit: %w", err)
}
budget.IncomeCategoryID = cat.ID
return &budget, nil return &budget, nil
} }

View File

@ -1,11 +1,12 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
// source: categories.sql // source: categories.sql
package postgres package postgres
import ( import (
"context" "context"
"time"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -52,6 +53,7 @@ const getCategories = `-- name: GetCategories :many
SELECT categories.id, categories.category_group_id, categories.name, category_groups.name as group FROM categories SELECT categories.id, categories.category_group_id, categories.name, category_groups.name as group FROM categories
INNER JOIN category_groups ON categories.category_group_id = category_groups.id INNER JOIN category_groups ON categories.category_group_id = category_groups.id
WHERE category_groups.budget_id = $1 WHERE category_groups.budget_id = $1
ORDER BY category_groups.name, categories.name
` `
type GetCategoriesRow struct { type GetCategoriesRow struct {
@ -89,81 +91,6 @@ func (q *Queries) GetCategories(ctx context.Context, budgetID uuid.UUID) ([]GetC
return items, nil return items, nil
} }
const getCategoriesWithBalance = `-- name: GetCategoriesWithBalance :many
SELECT categories.id, categories.name, category_groups.name as group,
(COALESCE(
(
SELECT SUM(a_hist.amount)
FROM assignments a_hist
WHERE categories.id = a_hist.category_id
AND a_hist.date < $1
)
, 0)+COALESCE(
(
SELECT SUM(t_hist.amount)
FROM transactions t_hist
WHERE categories.id = t_hist.category_id
AND t_hist.date < $1
)
, 0))::decimal(12,2) as balance,
COALESCE(
(
SELECT SUM(t_this.amount)
FROM transactions t_this
WHERE categories.id = t_this.category_id
AND t_this.date BETWEEN $1 AND $2
)
, 0)::decimal(12,2) as activity
FROM categories
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
WHERE category_groups.budget_id = $3
GROUP BY categories.id, categories.name, category_groups.name
ORDER BY category_groups.name, categories.name
`
type GetCategoriesWithBalanceParams struct {
FromDate time.Time
ToDate time.Time
BudgetID uuid.UUID
}
type GetCategoriesWithBalanceRow struct {
ID uuid.UUID
Name string
Group string
Balance Numeric
Activity Numeric
}
func (q *Queries) GetCategoriesWithBalance(ctx context.Context, arg GetCategoriesWithBalanceParams) ([]GetCategoriesWithBalanceRow, error) {
rows, err := q.db.QueryContext(ctx, getCategoriesWithBalance, arg.FromDate, arg.ToDate, arg.BudgetID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetCategoriesWithBalanceRow
for rows.Next() {
var i GetCategoriesWithBalanceRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Group,
&i.Balance,
&i.Activity,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getCategoryGroups = `-- name: GetCategoryGroups :many const getCategoryGroups = `-- name: GetCategoryGroups :many
SELECT category_groups.id, category_groups.budget_id, category_groups.name FROM category_groups SELECT category_groups.id, category_groups.budget_id, category_groups.name FROM category_groups
WHERE category_groups.budget_id = $1 WHERE category_groups.budget_id = $1
@ -191,3 +118,47 @@ func (q *Queries) GetCategoryGroups(ctx context.Context, budgetID uuid.UUID) ([]
} }
return items, nil return items, nil
} }
const searchCategories = `-- name: SearchCategories :many
SELECT CONCAT(category_groups.name, ' : ', categories.name) as name, categories.id, 'category' as type
FROM categories
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
WHERE category_groups.budget_id = $1
AND categories.name ILIKE $2
AND category_groups.name != 'Hidden Categories'
ORDER BY category_groups.name, categories.name
`
type SearchCategoriesParams struct {
BudgetID uuid.UUID
Search string
}
type SearchCategoriesRow struct {
Name interface{}
ID uuid.UUID
Type interface{}
}
func (q *Queries) SearchCategories(ctx context.Context, arg SearchCategoriesParams) ([]SearchCategoriesRow, error) {
rows, err := q.db.QueryContext(ctx, searchCategories, arg.BudgetID, arg.Search)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SearchCategoriesRow
for rows.Next() {
var i SearchCategoriesRow
if err := rows.Scan(&i.Name, &i.ID, &i.Type); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -5,25 +5,32 @@ import (
"embed" "embed"
"fmt" "fmt"
_ "github.com/jackc/pgx/v4/stdlib" _ "github.com/jackc/pgx/v4/stdlib" // needed for pg connection
"github.com/pressly/goose/v3" "github.com/pressly/goose/v3"
) )
//go:embed schema/*.sql //go:embed schema/*.sql
var migrations embed.FS var migrations embed.FS
// Connect to a database type Database struct {
func Connect(server string, user string, password string, database string) (*Queries, *sql.DB, error) { *Queries
connString := fmt.Sprintf("postgres://%s:%s@%s/%s", user, password, server, database) *sql.DB
conn, err := sql.Open("pgx", connString) }
// Connect connects to a database.
func Connect(typ string, connString string) (*Database, error) {
conn, err := sql.Open(typ, connString)
if err != nil { if err != nil {
return nil, nil, err return nil, fmt.Errorf("open connection: %w", err)
} }
goose.SetBaseFS(migrations) goose.SetBaseFS(migrations)
if err = goose.Up(conn, "schema"); err != nil { if err = goose.Up(conn, "schema"); err != nil {
return nil, nil, err return nil, fmt.Errorf("migrate: %w", err)
} }
return New(conn), conn, nil return &Database{
New(conn),
conn,
}, nil
} }

View File

@ -0,0 +1,59 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
// source: cumultative-balances.sql
package postgres
import (
"context"
"time"
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
"github.com/google/uuid"
)
const getCumultativeBalances = `-- name: GetCumultativeBalances :many
SELECT COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id),
COALESCE(ass.amount, 0)::decimal(12,2) as assignments,
COALESCE(tra.amount, 0)::decimal(12,2) as transactions
FROM assignments_by_month as ass
FULL OUTER JOIN transactions_by_month as tra ON ass.date = tra.date AND ass.category_id = tra.category_id
WHERE COALESCE(ass.budget_id, tra.budget_id) = $1
ORDER BY COALESCE(ass.date, tra.date), COALESCE(ass.amount, tra.amount)
`
type GetCumultativeBalancesRow struct {
Date time.Time
CategoryID uuid.UUID
Assignments numeric.Numeric
Transactions numeric.Numeric
}
func (q *Queries) GetCumultativeBalances(ctx context.Context, budgetID uuid.UUID) ([]GetCumultativeBalancesRow, error) {
rows, err := q.db.QueryContext(ctx, getCumultativeBalances, budgetID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetCumultativeBalancesRow
for rows.Next() {
var i GetCumultativeBalancesRow
if err := rows.Scan(
&i.Date,
&i.CategoryID,
&i.Assignments,
&i.Transactions,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -1,4 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
package postgres package postgres

View File

@ -1,32 +1,66 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
package postgres package postgres
import ( import (
"database/sql" "database/sql"
"fmt"
"time" "time"
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
"github.com/google/uuid" "github.com/google/uuid"
) )
type TransactionStatus string
const (
TransactionStatusReconciled TransactionStatus = "Reconciled"
TransactionStatusCleared TransactionStatus = "Cleared"
TransactionStatusUncleared TransactionStatus = "Uncleared"
)
func (e *TransactionStatus) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = TransactionStatus(s)
case string:
*e = TransactionStatus(s)
default:
return fmt.Errorf("unsupported scan type for TransactionStatus: %T", src)
}
return nil
}
type Account struct { type Account struct {
ID uuid.UUID ID uuid.UUID
BudgetID uuid.UUID BudgetID uuid.UUID
Name string Name string
OnBudget bool
IsOpen bool
LastReconciled sql.NullTime
} }
type Assignment struct { type Assignment struct {
ID uuid.UUID
CategoryID uuid.UUID CategoryID uuid.UUID
Date time.Time Date time.Time
Memo sql.NullString Memo sql.NullString
Amount Numeric Amount numeric.Numeric
}
type AssignmentsByMonth struct {
Date time.Time
CategoryID uuid.UUID
BudgetID uuid.UUID
Amount int64
} }
type Budget struct { type Budget struct {
ID uuid.UUID ID uuid.UUID
Name string Name string
LastModification sql.NullTime LastModification sql.NullTime
IncomeCategoryID uuid.UUID
} }
type Category struct { type Category struct {
@ -41,6 +75,24 @@ type CategoryGroup struct {
Name string Name string
} }
type DisplayTransaction struct {
ID uuid.UUID
Date time.Time
Memo string
Amount numeric.Numeric
GroupID uuid.NullUUID
Status TransactionStatus
Account string
PayeeID uuid.NullUUID
CategoryID uuid.NullUUID
Payee string
CategoryGroup string
Category string
TransferAccount string
BudgetID uuid.UUID
AccountID uuid.UUID
}
type Payee struct { type Payee struct {
ID uuid.UUID ID uuid.UUID
BudgetID uuid.UUID BudgetID uuid.UUID
@ -51,10 +103,19 @@ type Transaction struct {
ID uuid.UUID ID uuid.UUID
Date time.Time Date time.Time
Memo string Memo string
Amount Numeric Amount numeric.Numeric
AccountID uuid.UUID AccountID uuid.UUID
CategoryID uuid.NullUUID CategoryID uuid.NullUUID
PayeeID uuid.NullUUID PayeeID uuid.NullUUID
GroupID uuid.NullUUID
Status TransactionStatus
}
type TransactionsByMonth struct {
Date time.Time
CategoryID uuid.NullUUID
BudgetID uuid.UUID
Amount int64
} }
type User struct { type User struct {

View File

@ -1,21 +0,0 @@
package postgres
import "github.com/jackc/pgtype"
type Numeric struct {
pgtype.Numeric
}
func (n Numeric) GetFloat64() float64 {
var balance float64
err := n.AssignTo(&balance)
if err != nil {
panic(err)
}
return balance
}
func (n Numeric) GetPositive() bool {
float := n.GetFloat64()
return float >= 0
}

272
postgres/numeric/numeric.go Normal file
View File

@ -0,0 +1,272 @@
package numeric
import (
"fmt"
"math/big"
"strings"
"unicode/utf8"
"github.com/jackc/pgtype"
)
type Numeric struct {
pgtype.Numeric
}
func Zero() Numeric {
return Numeric{pgtype.Numeric{Exp: 0, Int: big.NewInt(0), Status: pgtype.Present, NaN: false}}
}
func FromInt64(value int64) Numeric {
return Numeric{Numeric: pgtype.Numeric{Int: big.NewInt(value), Status: pgtype.Present}}
}
func FromInt64WithExp(value int64, exp int32) Numeric {
return Numeric{Numeric: pgtype.Numeric{Int: big.NewInt(value), Exp: exp, Status: pgtype.Present}}
}
func (n *Numeric) SetZero() {
n.Exp = 0
n.Int = big.NewInt(0)
n.Status = pgtype.Present
n.NaN = false
}
func (n Numeric) GetFloat64() float64 {
if n.Status != pgtype.Present {
return 0
}
var balance float64
err := n.AssignTo(&balance)
if err != nil {
panic(err)
}
return balance
}
func (n Numeric) IsPositive() bool {
if n.Status != pgtype.Present {
return true
}
float := n.GetFloat64()
return float >= 0
}
func (n Numeric) IsZero() bool {
if n.Status != pgtype.Present {
return true
}
float := n.GetFloat64()
return float == 0
}
func (n *Numeric) MatchExpI(exp int32) {
diffExp := n.Exp - exp
factor := big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(diffExp)), nil) //nolint:gomnd
n.Exp = exp
n.Int = big.NewInt(0).Mul(n.Int, factor)
}
func (n Numeric) MatchExp(exp int32) Numeric {
diffExp := n.Exp - exp
factor := big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(diffExp)), nil) //nolint:gomnd
return Numeric{pgtype.Numeric{
Exp: exp,
Int: big.NewInt(0).Mul(n.Int, factor),
Status: n.Status,
NaN: n.NaN,
}}
}
func (n *Numeric) SubI(other Numeric) *Numeric {
right := other
if n.Exp < other.Exp {
right = other.MatchExp(n.Exp)
} else if n.Exp > other.Exp {
n.MatchExpI(other.Exp)
}
if n.Exp == right.Exp {
n.Int = big.NewInt(0).Sub(n.Int, right.Int)
return n
}
panic("Cannot subtract with different exponents")
}
func (n Numeric) Sub(other Numeric) Numeric {
left := n
right := other
if n.Exp < other.Exp {
right = other.MatchExp(n.Exp)
} else if n.Exp > other.Exp {
left = n.MatchExp(other.Exp)
}
if left.Exp == right.Exp {
return Numeric{pgtype.Numeric{
Exp: left.Exp,
Int: big.NewInt(0).Sub(left.Int, right.Int),
}}
}
panic("Cannot subtract with different exponents")
}
func (n Numeric) Neg() Numeric {
return Numeric{pgtype.Numeric{Exp: n.Exp, Int: big.NewInt(-1 * n.Int.Int64()), Status: n.Status}}
}
func (n Numeric) Add(other Numeric) Numeric {
left := n
right := other
if n.Exp < other.Exp {
right = other.MatchExp(n.Exp)
} else if n.Exp > other.Exp {
left = n.MatchExp(other.Exp)
}
if left.Exp == right.Exp {
return Numeric{pgtype.Numeric{
Exp: left.Exp,
Int: big.NewInt(0).Add(left.Int, right.Int),
}}
}
panic("Cannot add with different exponents")
}
func (n *Numeric) AddI(other Numeric) *Numeric {
right := other
if n.Exp < other.Exp {
right = other.MatchExp(n.Exp)
} else if n.Exp > other.Exp {
n.MatchExpI(other.Exp)
}
if n.Exp == right.Exp {
n.Int = big.NewInt(0).Add(n.Int, right.Int)
return n
}
panic("Cannot add with different exponents")
}
func (n Numeric) String() string {
if n.Int == nil || n.Int.Int64() == 0 {
return "0"
}
s := fmt.Sprintf("%d", n.Int)
bytes := []byte(s)
exp := n.Exp
for exp > 0 {
bytes = append(bytes, byte('0'))
exp--
}
if exp == 0 {
return string(bytes)
}
length := int32(len(bytes))
var bytesWithSeparator []byte
exp = -exp
for length <= exp {
if n.Int.Int64() < 0 {
bytes = append([]byte{bytes[0], byte('0')}, bytes[1:]...)
} else {
bytes = append([]byte{byte('0')}, bytes...)
}
length++
}
split := length - exp
bytesWithSeparator = append(bytesWithSeparator, bytes[:split]...)
if split == 1 && n.Int.Int64() < 0 {
bytesWithSeparator = append(bytesWithSeparator, byte('0'))
}
bytesWithSeparator = append(bytesWithSeparator, byte('.'))
bytesWithSeparator = append(bytesWithSeparator, bytes[split:]...)
return string(bytesWithSeparator)
}
func (n Numeric) MarshalJSON() ([]byte, error) {
if n.Int == nil || n.Int.Int64() == 0 {
return []byte("0"), nil
}
s := fmt.Sprintf("%d", n.Int)
bytes := []byte(s)
exp := n.Exp
for exp > 0 {
bytes = append(bytes, byte('0'))
exp--
}
if exp == 0 {
return bytes, nil
}
length := int32(len(bytes))
var bytesWithSeparator []byte
exp = -exp
for length <= exp {
if n.Int.Int64() < 0 {
bytes = append([]byte{bytes[0], byte('0')}, bytes[1:]...)
} else {
bytes = append([]byte{byte('0')}, bytes...)
}
length++
}
split := length - exp
bytesWithSeparator = append(bytesWithSeparator, bytes[:split]...)
if split == 1 && n.Int.Int64() < 0 {
bytesWithSeparator = append(bytesWithSeparator, byte('0'))
}
bytesWithSeparator = append(bytesWithSeparator, byte('.'))
bytesWithSeparator = append(bytesWithSeparator, bytes[split:]...)
return bytesWithSeparator, nil
}
func MustParse(text string) Numeric {
num, err := Parse(text)
if err != nil {
panic(err)
}
return num
}
func Parse(text string) (Numeric, error) {
// Unify decimal separator
text = strings.Replace(text, ",", ".", 1)
num := Numeric{}
err := num.Set(text)
if err != nil {
return num, fmt.Errorf("parse numeric %s: %w", text, err)
}
return num, nil
}
func ParseCurrency(text string) (Numeric, error) {
// Remove trailing currency
text = trimLastChar(text)
return Parse(text)
}
func trimLastChar(s string) string {
r, size := utf8.DecodeLastRuneInString(s)
if r == utf8.RuneError && (size == 0 || size == 1) {
size = 0
}
return s[:len(s)-size]
}

View File

@ -0,0 +1,118 @@
package numeric_test
import (
"testing"
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
)
type TestCaseMarshalJSON struct {
Value numeric.Numeric
Result string
}
func TestMarshalJSON(t *testing.T) {
t.Parallel()
tests := []TestCaseMarshalJSON{
{numeric.Zero(), `0`},
{numeric.MustParse("1.23"), "1.23"},
{numeric.MustParse("1,24"), "1.24"},
{numeric.MustParse("1"), "1"},
{numeric.MustParse("10"), "10"},
{numeric.MustParse("100"), "100"},
{numeric.MustParse("1000"), "1000"},
{numeric.MustParse("0.1"), "0.1"},
{numeric.MustParse("0.01"), "0.01"},
{numeric.MustParse("0.001"), "0.001"},
{numeric.MustParse("0.0001"), "0.0001"},
{numeric.MustParse("-1"), "-1"},
{numeric.MustParse("-10"), "-10"},
{numeric.MustParse("-100"), "-100"},
{numeric.MustParse("-1000"), "-1000"},
{numeric.MustParse("-0.1"), "-0.1"},
{numeric.MustParse("-0.01"), "-0.01"},
{numeric.MustParse("-0.001"), "-0.001"},
{numeric.MustParse("-0.0001"), "-0.0001"},
{numeric.MustParse("123456789.12345"), "123456789.12345"},
{numeric.MustParse("123456789.12345"), "123456789.12345"},
{numeric.MustParse("-1.23"), "-1.23"},
{numeric.MustParse("-1,24"), "-1.24"},
{numeric.MustParse("-123456789.12345"), "-123456789.12345"},
}
for i := range tests {
test := tests[i]
t.Run(test.Result, func(t *testing.T) {
t.Parallel()
z := test.Value
result, err := z.MarshalJSON()
if err != nil {
t.Error(err)
return
}
if string(result) != test.Result {
t.Errorf("Expected %s, got %s", test.Result, string(result))
return
}
})
}
}
type TestCaseParse struct {
Result numeric.Numeric
Value string
}
func TestParse(t *testing.T) {
t.Parallel()
tests := []TestCaseParse{
{numeric.FromInt64WithExp(0, 0), `0`},
{numeric.FromInt64WithExp(1, 0), `1`},
{numeric.FromInt64WithExp(1, 1), `10`},
{numeric.FromInt64WithExp(1, 2), `100`},
{numeric.FromInt64WithExp(1, 3), `1000`},
{numeric.FromInt64WithExp(1, -1), `0.1`},
{numeric.FromInt64WithExp(1, -2), `0.01`},
{numeric.FromInt64WithExp(1, -3), `0.001`},
{numeric.FromInt64WithExp(1, -4), `0.0001`},
{numeric.FromInt64WithExp(-1, 0), `-1`},
{numeric.FromInt64WithExp(-1, 1), `-10`},
{numeric.FromInt64WithExp(-1, 2), `-100`},
{numeric.FromInt64WithExp(-1, 3), `-1000`},
{numeric.FromInt64WithExp(-1, -1), `-0.1`},
{numeric.FromInt64WithExp(-1, -2), `-0.01`},
{numeric.FromInt64WithExp(-1, -3), `-0.001`},
{numeric.FromInt64WithExp(-1, -4), `-0.0001`},
{numeric.FromInt64WithExp(123, -2), "1.23"},
{numeric.FromInt64WithExp(124, -2), "1,24"},
{numeric.FromInt64WithExp(12345678912345, -5), "123456789.12345"},
{numeric.FromInt64WithExp(0, 0), `-0`},
{numeric.FromInt64WithExp(-1, 0), `-1`},
{numeric.FromInt64WithExp(-1, 1), `-10`},
{numeric.FromInt64WithExp(-1, 2), `-100`},
{numeric.FromInt64WithExp(-123, -2), "-1.23"},
{numeric.FromInt64WithExp(-124, -2), "-1,24"},
{numeric.FromInt64WithExp(-12345678912345, -5), "-123456789.12345"},
}
for i := range tests {
test := tests[i]
t.Run(test.Value, func(t *testing.T) {
t.Parallel()
result, err := numeric.Parse(test.Value)
if err != nil {
t.Error(err)
return
}
if test.Result.Int.Int64() != result.Int.Int64() {
t.Errorf("Expected int %d, got %d", test.Result.Int, result.Int)
return
}
if test.Result.Exp != result.Exp {
t.Errorf("Expected exp %d, got %d", test.Result.Exp, result.Exp)
return
}
})
}
}

View File

@ -1,4 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
// source: payees.sql // source: payees.sql
package postgres package postgres
@ -31,6 +33,7 @@ func (q *Queries) CreatePayee(ctx context.Context, arg CreatePayeeParams) (Payee
const getPayees = `-- name: GetPayees :many const getPayees = `-- name: GetPayees :many
SELECT payees.id, payees.budget_id, payees.name FROM payees SELECT payees.id, payees.budget_id, payees.name FROM payees
WHERE payees.budget_id = $1 WHERE payees.budget_id = $1
ORDER BY name
` `
func (q *Queries) GetPayees(ctx context.Context, budgetID uuid.UUID) ([]Payee, error) { func (q *Queries) GetPayees(ctx context.Context, budgetID uuid.UUID) ([]Payee, error) {
@ -55,3 +58,50 @@ func (q *Queries) GetPayees(ctx context.Context, budgetID uuid.UUID) ([]Payee, e
} }
return items, nil return items, nil
} }
const searchPayees = `-- name: SearchPayees :many
SELECT payees.id, payees.budget_id, payees.name, 'payee' as type FROM payees
WHERE payees.budget_id = $1
AND payees.name ILIKE $2
ORDER BY payees.name
`
type SearchPayeesParams struct {
BudgetID uuid.UUID
Search string
}
type SearchPayeesRow struct {
ID uuid.UUID
BudgetID uuid.UUID
Name string
Type interface{}
}
func (q *Queries) SearchPayees(ctx context.Context, arg SearchPayeesParams) ([]SearchPayeesRow, error) {
rows, err := q.db.QueryContext(ctx, searchPayees, arg.BudgetID, arg.Search)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SearchPayeesRow
for rows.Next() {
var i SearchPayeesRow
if err := rows.Scan(
&i.ID,
&i.BudgetID,
&i.Name,
&i.Type,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@ -11,13 +11,35 @@ WHERE accounts.id = $1;
-- name: GetAccounts :many -- name: GetAccounts :many
SELECT accounts.* FROM accounts SELECT accounts.* FROM accounts
WHERE accounts.budget_id = $1 WHERE accounts.budget_id = $1
AND accounts.is_open = TRUE
ORDER BY accounts.name; ORDER BY accounts.name;
-- name: GetAccountsWithBalance :many -- name: GetAccountsWithBalance :many
SELECT accounts.id, accounts.name, SUM(transactions.amount)::decimal(12,2) as balance SELECT accounts.id, accounts.name, accounts.on_budget, accounts.is_open, accounts.last_reconciled,
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW())::decimal(12,2) as working_balance,
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status IN ('Cleared', 'Reconciled'))::decimal(12,2) as cleared_balance,
(SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status = 'Reconciled')::decimal(12,2) as reconciled_balance
FROM accounts FROM accounts
LEFT JOIN transactions ON transactions.account_id = accounts.id
WHERE accounts.budget_id = $1 WHERE accounts.budget_id = $1
AND transactions.date < NOW() AND accounts.is_open = TRUE
GROUP BY accounts.id, accounts.name
ORDER BY accounts.name; ORDER BY accounts.name;
-- name: SearchAccounts :many
SELECT accounts.id, accounts.budget_id, accounts.name, 'account' as type FROM accounts
WHERE accounts.budget_id = @budget_id
AND accounts.is_open = TRUE
AND accounts.name ILIKE @search
ORDER BY accounts.name;
-- name: UpdateAccount :one
UPDATE accounts
SET name = $1,
on_budget = $2,
is_open = $3
WHERE accounts.id = $4
RETURNING *;
-- name: SetLastReconciled :exec
UPDATE accounts
SET last_reconciled = NOW()
WHERE accounts.id = $1;

View File

@ -11,3 +11,29 @@ DELETE FROM assignments
USING categories USING categories
INNER JOIN category_groups ON categories.category_group_id = category_groups.id INNER JOIN category_groups ON categories.category_group_id = category_groups.id
WHERE categories.id = assignments.category_id AND category_groups.budget_id = @budget_id; WHERE categories.id = assignments.category_id AND category_groups.budget_id = @budget_id;
-- name: GetAssignmentsByMonthAndCategory :many
SELECT *
FROM assignments_by_month
WHERE assignments_by_month.budget_id = @budget_id;
-- name: GetAllAssignments :many
SELECT assignments.date, categories.name as category, category_groups.name as group, assignments.amount
FROM assignments
INNER JOIN categories ON categories.id = assignments.category_id
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
WHERE category_groups.budget_id = @budget_id;
-- name: UpdateAssignment :exec
INSERT INTO assignments (category_id, date, amount)
VALUES($1, $2, $3)
ON CONFLICT (category_id, date)
DO
UPDATE SET amount = $3;
-- name: UpdateAssignmentWithDifference :exec
INSERT INTO assignments (category_id, date, amount)
VALUES($1, $2, $3)
ON CONFLICT (category_id, date)
DO
UPDATE SET amount = assignments.amount + $3;

View File

@ -1,9 +1,14 @@
-- name: CreateBudget :one -- name: CreateBudget :one
INSERT INTO budgets INSERT INTO budgets
(name, last_modification) (name, income_category_id, last_modification)
VALUES ($1, NOW()) VALUES ($1, $2, NOW())
RETURNING *; RETURNING *;
-- name: SetInflowCategory :exec
UPDATE budgets
SET income_category_id = $1
WHERE budgets.id = $2;
-- name: GetBudgetsForUser :many -- name: GetBudgetsForUser :many
SELECT budgets.* FROM budgets SELECT budgets.* FROM budgets
LEFT JOIN user_budgets ON budgets.id = user_budgets.budget_id LEFT JOIN user_budgets ON budgets.id = user_budgets.budget_id
@ -12,3 +17,21 @@ WHERE user_budgets.user_id = $1;
-- name: GetBudget :one -- name: GetBudget :one
SELECT * FROM budgets SELECT * FROM budgets
WHERE id = $1; WHERE id = $1;
-- name: GetFirstActivity :one
SELECT MIN(dates.min_date)::date as min_date
FROM (
SELECT MIN(assignments.date) as min_date
FROM assignments
INNER JOIN categories ON categories.id = assignments.category_id
INNER JOIN category_groups ON category_groups.id = categories.category_group_id
WHERE category_groups.budget_id = @budget_id
UNION
SELECT MIN(transactions.date) as min_date
FROM transactions
INNER JOIN accounts ON accounts.id = transactions.account_id
WHERE accounts.budget_id = @budget_id
) dates;
-- name: DeleteBudget :exec
DELETE FROM budgets WHERE id = $1;

View File

@ -17,35 +17,15 @@ RETURNING *;
-- name: GetCategories :many -- name: GetCategories :many
SELECT categories.*, category_groups.name as group FROM categories SELECT categories.*, category_groups.name as group FROM categories
INNER JOIN category_groups ON categories.category_group_id = category_groups.id INNER JOIN category_groups ON categories.category_group_id = category_groups.id
WHERE category_groups.budget_id = $1; WHERE category_groups.budget_id = $1
ORDER BY category_groups.name, categories.name;
-- name: GetCategoriesWithBalance :many -- name: SearchCategories :many
SELECT categories.id, categories.name, category_groups.name as group, SELECT CONCAT(category_groups.name, ' : ', categories.name) as name, categories.id, 'category' as type
(COALESCE(
(
SELECT SUM(a_hist.amount)
FROM assignments a_hist
WHERE categories.id = a_hist.category_id
AND a_hist.date < @from_date
)
, 0)+COALESCE(
(
SELECT SUM(t_hist.amount)
FROM transactions t_hist
WHERE categories.id = t_hist.category_id
AND t_hist.date < @from_date
)
, 0))::decimal(12,2) as balance,
COALESCE(
(
SELECT SUM(t_this.amount)
FROM transactions t_this
WHERE categories.id = t_this.category_id
AND t_this.date BETWEEN @from_date AND @to_date
)
, 0)::decimal(12,2) as activity
FROM categories FROM categories
INNER JOIN category_groups ON categories.category_group_id = category_groups.id INNER JOIN category_groups ON categories.category_group_id = category_groups.id
WHERE category_groups.budget_id = @budget_id WHERE category_groups.budget_id = @budget_id
GROUP BY categories.id, categories.name, category_groups.name AND categories.name ILIKE @search
AND category_groups.name != 'Hidden Categories'
ORDER BY category_groups.name, categories.name; ORDER BY category_groups.name, categories.name;
--ORDER BY levenshtein(payees.name, $2);

View File

@ -0,0 +1,8 @@
-- name: GetCumultativeBalances :many
SELECT COALESCE(ass.date, tra.date), COALESCE(ass.category_id, tra.category_id),
COALESCE(ass.amount, 0)::decimal(12,2) as assignments,
COALESCE(tra.amount, 0)::decimal(12,2) as transactions
FROM assignments_by_month as ass
FULL OUTER JOIN transactions_by_month as tra ON ass.date = tra.date AND ass.category_id = tra.category_id
WHERE COALESCE(ass.budget_id, tra.budget_id) = @budget_id
ORDER BY COALESCE(ass.date, tra.date), COALESCE(ass.amount, tra.amount);

View File

@ -6,4 +6,12 @@ RETURNING *;
-- name: GetPayees :many -- name: GetPayees :many
SELECT payees.* FROM payees SELECT payees.* FROM payees
WHERE payees.budget_id = $1; WHERE payees.budget_id = $1
ORDER BY name;
-- name: SearchPayees :many
SELECT payees.*, 'payee' as type FROM payees
WHERE payees.budget_id = @budget_id
AND payees.name ILIKE @search
ORDER BY payees.name;
--ORDER BY levenshtein(payees.name, $2);

View File

@ -1,35 +1,73 @@
-- name: GetTransaction :one
SELECT * FROM display_transactions
WHERE id = $1;
-- name: CreateTransaction :one -- name: CreateTransaction :one
INSERT INTO transactions INSERT INTO transactions
(date, memo, amount, account_id, payee_id, category_id) (date, memo, amount, account_id, payee_id, category_id, group_id, status)
VALUES ($1, $2, $3, $4, $5, $6) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *; RETURNING id;
-- name: GetTransactionsForBudget :many -- name: UpdateTransaction :exec
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, UPDATE transactions
accounts.name as account, COALESCE(payees.name, '') as payee, COALESCE(category_groups.name, '') as category_group, COALESCE(categories.name, '') as category SET date = $1,
FROM transactions memo = $2,
INNER JOIN accounts ON accounts.id = transactions.account_id amount = $3,
LEFT JOIN payees ON payees.id = transactions.payee_id payee_id = $4,
LEFT JOIN categories ON categories.id = transactions.category_id category_id = $5
LEFT JOIN category_groups ON category_groups.id = categories.category_group_id WHERE id = $6;
WHERE accounts.budget_id = $1
ORDER BY transactions.date DESC -- name: SetTransactionReconciled :exec
LIMIT 200; UPDATE transactions
SET status = 'Reconciled'
WHERE id = $1;
-- name: DeleteTransaction :exec
DELETE FROM transactions
WHERE id = $1;
-- name: GetAllTransactionsForBudget :many
SELECT t.*
FROM display_transactions AS t
WHERE t.budget_id = $1;
-- name: GetTransactionsForAccount :many -- name: GetTransactionsForAccount :many
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, SELECT t.*
accounts.name as account, COALESCE(payees.name, '') as payee, COALESCE(category_groups.name, '') as category_group, COALESCE(categories.name, '') as category FROM display_transactions AS t
FROM transactions WHERE t.account_id = $1;
INNER JOIN accounts ON accounts.id = transactions.account_id
LEFT JOIN payees ON payees.id = transactions.payee_id
LEFT JOIN categories ON categories.id = transactions.category_id
LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
WHERE transactions.account_id = $1
ORDER BY transactions.date DESC
LIMIT 200;
-- name: DeleteAllTransactions :execrows -- name: DeleteAllTransactions :execrows
DELETE FROM transactions DELETE FROM transactions
USING accounts USING accounts
WHERE accounts.budget_id = @budget_id WHERE accounts.budget_id = @budget_id
AND accounts.id = transactions.account_id; AND accounts.id = transactions.account_id;
-- name: GetTransactionsByMonthAndCategory :many
SELECT *
FROM transactions_by_month
WHERE transactions_by_month.budget_id = @budget_id;
-- name: GetProblematicTransactions :many
SELECT transactions.*
FROM display_transactions AS transactions
LEFT JOIN accounts
ON transactions.account_id = accounts.id
LEFT JOIN transactions AS otherGroupTransaction
ON transactions.group_id = otherGroupTransaction.group_id
AND transactions.id != otherGroupTransaction.id
AND transactions.account_id != otherGroupTransaction.account_id
LEFT JOIn accounts AS otherGroupAccount
ON otherGroupTransaction.account_id = otherGroupAccount.id
WHERE transactions.category_id IS NULL
AND accounts.on_budget
AND (otherGroupAccount.id IS NULL OR NOT otherGroupAccount.on_budget)
AND accounts.budget_id = $1;
-- name: GetFilteredTransactions :many
SELECT transactions.*
FROM display_transactions AS transactions
WHERE (NOT @filter_category::boolean OR transactions.category_id = @category_id)
AND (NOT @filter_account::boolean OR transactions.account_id = @account_id)
AND (NOT @filter_payee::boolean OR transactions.payee_id = @payee_id)
AND transactions.date BETWEEN @from_date AND @to_date
AND transactions.budget_id = @budget_id;

View File

@ -1,17 +0,0 @@
package postgres
import "database/sql"
// Repository represents a PostgreSQL implementation of all ModelServices
type Repository struct {
DB *Queries
LegacyDB *sql.DB
}
func NewRepository(queries *Queries, db *sql.DB) (*Repository, error) {
repo := &Repository{
DB: queries,
LegacyDB: db,
}
return repo, nil
}

View File

@ -0,0 +1,9 @@
-- +goose Up
CREATE TABLE budgets (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
name text NOT NULL,
last_modification timestamp with time zone
);
-- +goose Down
DROP TABLE budgets;

View File

@ -0,0 +1,11 @@
-- +goose Up
CREATE TABLE users (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
email text NOT NULL,
name text NOT NULL,
password text NOT NULL,
last_login timestamp with time zone
);
-- +goose Down
DROP TABLE users;

View File

@ -0,0 +1,8 @@
-- +goose Up
CREATE TABLE user_budgets (
user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE,
budget_id uuid NOT NULL REFERENCES budgets (id) ON DELETE CASCADE
);
-- +goose Down
DROP TABLE user_budgets;

View File

@ -0,0 +1,10 @@
-- +goose Up
CREATE TABLE accounts (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
budget_id uuid NOT NULL REFERENCES budgets (id) ON DELETE CASCADE,
name varchar(50) NOT NULL,
on_budget boolean DEFAULT TRUE NOT NULL
);
-- +goose Down
DROP TABLE accounts;

View File

@ -0,0 +1,9 @@
-- +goose Up
CREATE TABLE payees (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
budget_id uuid NOT NULL REFERENCES budgets (id) ON DELETE CASCADE,
name varchar(50) NOT NULL
);
-- +goose Down
DROP TABLE payees;

View File

@ -0,0 +1,9 @@
-- +goose Up
CREATE TABLE category_groups (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
budget_id uuid NOT NULL REFERENCES budgets (id) ON DELETE CASCADE,
name varchar(50) NOT NULL
);
-- +goose Down
DROP TABLE category_groups;

View File

@ -0,0 +1,12 @@
-- +goose Up
CREATE TABLE categories (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
category_group_id uuid NOT NULL REFERENCES category_groups (id) ON DELETE CASCADE,
name varchar(50) NOT NULL
);
ALTER TABLE budgets ADD COLUMN
income_category_id uuid NOT NULL REFERENCES categories (id) DEFERRABLE INITIALLY DEFERRED;
-- +goose Down
ALTER TABLE budgets DROP COLUMN income_category_id;
DROP TABLE categories;

View File

@ -0,0 +1,16 @@
-- +goose Up
CREATE TABLE transactions (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
date date NOT NULL,
memo text NOT NULL,
amount decimal(12,2) NOT NULL,
account_id uuid NOT NULL REFERENCES accounts (id),
category_id uuid REFERENCES categories (id),
payee_id uuid REFERENCES payees (id)
);
ALTER TABLE "transactions" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id");
ALTER TABLE "transactions" ADD FOREIGN KEY ("payee_id") REFERENCES "payees" ("id");
ALTER TABLE "transactions" ADD FOREIGN KEY ("category_id") REFERENCES "categories" ("id");
-- +goose Down
DROP TABLE transactions;

View File

@ -0,0 +1,17 @@
-- +goose Up
CREATE VIEW transactions_by_month AS
SELECT date_trunc('month', transactions.date)::date as date, transactions.category_id, accounts.budget_id, SUM(amount) as amount
FROM transactions
INNER JOIN accounts ON accounts.id = transactions.account_id
GROUP BY date_trunc('month', transactions.date), transactions.category_id, accounts.budget_id;
CREATE VIEW assignments_by_month AS
SELECT date_trunc('month', assignments.date)::date as date, assignments.category_id, category_groups.budget_id, SUM(amount) as amount
FROM assignments
INNER JOIN categories ON categories.id = assignments.category_id
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
GROUP BY date_trunc('month', assignments.date), assignments.category_id, category_groups.budget_id;
-- +goose Down
DROP VIEW transactions_by_month;
DROP VIEW assignments_by_month;

View File

@ -0,0 +1,5 @@
-- +goose Up
ALTER TABLE transactions ADD COLUMN group_id uuid NULL;
-- +goose Down
ALTER TABLE transactions DROP COLUMN group_id;

View File

@ -0,0 +1,12 @@
-- +goose Up
CREATE TYPE transaction_status AS ENUM (
'Reconciled',
'Cleared',
'Uncleared'
);
ALTER TABLE transactions ADD COLUMN status transaction_status NOT NULL DEFAULT 'Uncleared';
-- +goose Down
ALTER TABLE transactions DROP COLUMN status;
DROP TYPE transaction_status;

View File

@ -0,0 +1,5 @@
-- +goose Up
CREATE EXTENSION IF NOT EXISTS "fuzzystrmatch";
-- +goose Down
DROP EXTENSION "fuzzystrmatch";

View File

@ -0,0 +1,25 @@
-- +goose Up
CREATE VIEW display_transactions AS
SELECT transactions.id, transactions.date, transactions.memo,
transactions.amount, transactions.group_id, transactions.status,
accounts.name as account, transactions.payee_id, transactions.category_id,
COALESCE(payees.name, '') as payee,
COALESCE(category_groups.name, '') as category_group,
COALESCE(categories.name, '') as category,
COALESCE((
SELECT CONCAT(otherAccounts.name)
FROM transactions otherTransactions
LEFT JOIN accounts otherAccounts ON otherAccounts.id = otherTransactions.account_id
WHERE otherTransactions.group_id = transactions.group_id
AND otherTransactions.id != transactions.id
), '')::text as transfer_account,
accounts.budget_id, transactions.account_id
FROM transactions
INNER JOIN accounts ON accounts.id = transactions.account_id
LEFT JOIN payees ON payees.id = transactions.payee_id
LEFT JOIN categories ON categories.id = transactions.category_id
LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
ORDER BY transactions.date DESC;
-- +goose Down
DROP VIEW display_transactions;

View File

@ -0,0 +1,5 @@
-- +goose Up
ALTER TABLE accounts ADD COLUMN is_open BOOLEAN NOT NULL DEFAULT TRUE;
-- +goose Down
ALTER TABLE accounts DROP COLUMN is_open;

View File

@ -0,0 +1,6 @@
-- +goose Up
ALTER TABLE assignments DROP id;
ALTER TABLE assignments ADD PRIMARY KEY (category_id, date);
-- +goose Down
ALTER TABLE assignments ADD COLUMN id uuid DEFAULT uuid_generate_v4() PRIMARY KEY;

View File

@ -0,0 +1,12 @@
-- +goose Up
ALTER TABLE accounts ADD COLUMN last_reconciled date NULL;
UPDATE accounts
SET last_reconciled = (
SELECT MAX(transactions.date)
FROM transactions
WHERE transactions.account_id = accounts.id
AND transactions.status = 'Reconciled'
);
-- +goose Down
ALTER TABLE accounts DROP COLUMN last_reconciled;

View File

@ -0,0 +1,13 @@
-- +goose Up
CREATE OR REPLACE VIEW transactions_by_month AS
SELECT date_trunc('month', transactions.date)::date as date, transactions.category_id, accounts.budget_id, SUM(amount) as amount
FROM transactions
INNER JOIN accounts ON accounts.id = transactions.account_id AND accounts.on_budget
GROUP BY date_trunc('month', transactions.date), transactions.category_id, accounts.budget_id;
-- +goose Down
CREATE OR REPLACE VIEW transactions_by_month AS
SELECT date_trunc('month', transactions.date)::date as date, transactions.category_id, accounts.budget_id, SUM(amount) as amount
FROM transactions
INNER JOIN accounts ON accounts.id = transactions.account_id
GROUP BY date_trunc('month', transactions.date), transactions.category_id, accounts.budget_id;

View File

@ -1,66 +0,0 @@
-- +goose Up
CREATE TABLE budgets (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
name text NOT NULL,
last_modification timestamp with time zone
);
CREATE TABLE users (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
email text NOT NULL,
name text NOT NULL,
password text NOT NULL,
last_login timestamp with time zone
);
CREATE TABLE user_budgets (
user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE,
budget_id uuid NOT NULL REFERENCES budgets (id) ON DELETE CASCADE
);
CREATE TABLE accounts (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
budget_id uuid NOT NULL REFERENCES budgets (id) ON DELETE CASCADE,
name varchar(50) NOT NULL
);
CREATE TABLE payees (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
budget_id uuid NOT NULL REFERENCES budgets (id) ON DELETE CASCADE,
name varchar(50) NOT NULL
);
CREATE TABLE category_groups (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
budget_id uuid NOT NULL REFERENCES budgets (id) ON DELETE CASCADE,
name varchar(50) NOT NULL
);
CREATE TABLE categories (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
category_group_id uuid NOT NULL REFERENCES category_groups (id) ON DELETE CASCADE,
name varchar(50) NOT NULL
);
CREATE TABLE transactions (
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
date date NOT NULL,
memo text NOT NULL,
amount decimal(12,2) NOT NULL,
account_id uuid NOT NULL REFERENCES accounts (id),
category_id uuid REFERENCES categories (id),
payee_id uuid REFERENCES payees (id)
);
ALTER TABLE "transactions" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id");
ALTER TABLE "transactions" ADD FOREIGN KEY ("payee_id") REFERENCES "payees" ("id");
ALTER TABLE "transactions" ADD FOREIGN KEY ("category_id") REFERENCES "categories" ("id");
-- +goose Down
DROP TABLE transactions;
DROP TABLE accounts;
DROP TABLE payees;
DROP TABLE categories;
DROP TABLE category_groups;
DROP TABLE user_budgets;
DROP TABLE budgets;
DROP TABLE users;

View File

@ -1,4 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
// source: transactions.sql // source: transactions.sql
package postgres package postgres
@ -7,26 +9,29 @@ import (
"context" "context"
"time" "time"
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
"github.com/google/uuid" "github.com/google/uuid"
) )
const createTransaction = `-- name: CreateTransaction :one const createTransaction = `-- name: CreateTransaction :one
INSERT INTO transactions INSERT INTO transactions
(date, memo, amount, account_id, payee_id, category_id) (date, memo, amount, account_id, payee_id, category_id, group_id, status)
VALUES ($1, $2, $3, $4, $5, $6) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, date, memo, amount, account_id, category_id, payee_id RETURNING id
` `
type CreateTransactionParams struct { type CreateTransactionParams struct {
Date time.Time Date time.Time
Memo string Memo string
Amount Numeric Amount numeric.Numeric
AccountID uuid.UUID AccountID uuid.UUID
PayeeID uuid.NullUUID PayeeID uuid.NullUUID
CategoryID uuid.NullUUID CategoryID uuid.NullUUID
GroupID uuid.NullUUID
Status TransactionStatus
} }
func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (Transaction, error) { func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (uuid.UUID, error) {
row := q.db.QueryRowContext(ctx, createTransaction, row := q.db.QueryRowContext(ctx, createTransaction,
arg.Date, arg.Date,
arg.Memo, arg.Memo,
@ -34,18 +39,12 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa
arg.AccountID, arg.AccountID,
arg.PayeeID, arg.PayeeID,
arg.CategoryID, arg.CategoryID,
arg.GroupID,
arg.Status,
) )
var i Transaction var id uuid.UUID
err := row.Scan( err := row.Scan(&id)
&i.ID, return id, err
&i.Date,
&i.Memo,
&i.Amount,
&i.AccountID,
&i.CategoryID,
&i.PayeeID,
)
return i, err
} }
const deleteAllTransactions = `-- name: DeleteAllTransactions :execrows const deleteAllTransactions = `-- name: DeleteAllTransactions :execrows
@ -63,48 +62,281 @@ func (q *Queries) DeleteAllTransactions(ctx context.Context, budgetID uuid.UUID)
return result.RowsAffected() return result.RowsAffected()
} }
const getTransactionsForAccount = `-- name: GetTransactionsForAccount :many const deleteTransaction = `-- name: DeleteTransaction :exec
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, DELETE FROM transactions
accounts.name as account, COALESCE(payees.name, '') as payee, COALESCE(category_groups.name, '') as category_group, COALESCE(categories.name, '') as category WHERE id = $1
FROM transactions
INNER JOIN accounts ON accounts.id = transactions.account_id
LEFT JOIN payees ON payees.id = transactions.payee_id
LEFT JOIN categories ON categories.id = transactions.category_id
LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
WHERE transactions.account_id = $1
ORDER BY transactions.date DESC
LIMIT 200
` `
type GetTransactionsForAccountRow struct { func (q *Queries) DeleteTransaction(ctx context.Context, id uuid.UUID) error {
ID uuid.UUID _, err := q.db.ExecContext(ctx, deleteTransaction, id)
Date time.Time return err
Memo string
Amount Numeric
Account string
Payee string
CategoryGroup string
Category string
} }
func (q *Queries) GetTransactionsForAccount(ctx context.Context, accountID uuid.UUID) ([]GetTransactionsForAccountRow, error) { const getAllTransactionsForBudget = `-- name: GetAllTransactionsForBudget :many
SELECT t.id, t.date, t.memo, t.amount, t.group_id, t.status, t.account, t.payee_id, t.category_id, t.payee, t.category_group, t.category, t.transfer_account, t.budget_id, t.account_id
FROM display_transactions AS t
WHERE t.budget_id = $1
`
func (q *Queries) GetAllTransactionsForBudget(ctx context.Context, budgetID uuid.UUID) ([]DisplayTransaction, error) {
rows, err := q.db.QueryContext(ctx, getAllTransactionsForBudget, budgetID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []DisplayTransaction
for rows.Next() {
var i DisplayTransaction
if err := rows.Scan(
&i.ID,
&i.Date,
&i.Memo,
&i.Amount,
&i.GroupID,
&i.Status,
&i.Account,
&i.PayeeID,
&i.CategoryID,
&i.Payee,
&i.CategoryGroup,
&i.Category,
&i.TransferAccount,
&i.BudgetID,
&i.AccountID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getFilteredTransactions = `-- name: GetFilteredTransactions :many
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, transactions.group_id, transactions.status, transactions.account, transactions.payee_id, transactions.category_id, transactions.payee, transactions.category_group, transactions.category, transactions.transfer_account, transactions.budget_id, transactions.account_id
FROM display_transactions AS transactions
WHERE (NOT $1::boolean OR transactions.category_id = $2)
AND (NOT $3::boolean OR transactions.account_id = $4)
AND (NOT $5::boolean OR transactions.payee_id = $6)
AND transactions.date BETWEEN $7 AND $8
AND transactions.budget_id = $9
`
type GetFilteredTransactionsParams struct {
FilterCategory bool
CategoryID uuid.NullUUID
FilterAccount bool
AccountID uuid.UUID
FilterPayee bool
PayeeID uuid.NullUUID
FromDate time.Time
ToDate time.Time
BudgetID uuid.UUID
}
func (q *Queries) GetFilteredTransactions(ctx context.Context, arg GetFilteredTransactionsParams) ([]DisplayTransaction, error) {
rows, err := q.db.QueryContext(ctx, getFilteredTransactions,
arg.FilterCategory,
arg.CategoryID,
arg.FilterAccount,
arg.AccountID,
arg.FilterPayee,
arg.PayeeID,
arg.FromDate,
arg.ToDate,
arg.BudgetID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []DisplayTransaction
for rows.Next() {
var i DisplayTransaction
if err := rows.Scan(
&i.ID,
&i.Date,
&i.Memo,
&i.Amount,
&i.GroupID,
&i.Status,
&i.Account,
&i.PayeeID,
&i.CategoryID,
&i.Payee,
&i.CategoryGroup,
&i.Category,
&i.TransferAccount,
&i.BudgetID,
&i.AccountID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getProblematicTransactions = `-- name: GetProblematicTransactions :many
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, transactions.group_id, transactions.status, transactions.account, transactions.payee_id, transactions.category_id, transactions.payee, transactions.category_group, transactions.category, transactions.transfer_account, transactions.budget_id, transactions.account_id
FROM display_transactions AS transactions
LEFT JOIN accounts
ON transactions.account_id = accounts.id
LEFT JOIN transactions AS otherGroupTransaction
ON transactions.group_id = otherGroupTransaction.group_id
AND transactions.id != otherGroupTransaction.id
AND transactions.account_id != otherGroupTransaction.account_id
LEFT JOIn accounts AS otherGroupAccount
ON otherGroupTransaction.account_id = otherGroupAccount.id
WHERE transactions.category_id IS NULL
AND accounts.on_budget
AND (otherGroupAccount.id IS NULL OR NOT otherGroupAccount.on_budget)
AND accounts.budget_id = $1
`
func (q *Queries) GetProblematicTransactions(ctx context.Context, budgetID uuid.UUID) ([]DisplayTransaction, error) {
rows, err := q.db.QueryContext(ctx, getProblematicTransactions, budgetID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []DisplayTransaction
for rows.Next() {
var i DisplayTransaction
if err := rows.Scan(
&i.ID,
&i.Date,
&i.Memo,
&i.Amount,
&i.GroupID,
&i.Status,
&i.Account,
&i.PayeeID,
&i.CategoryID,
&i.Payee,
&i.CategoryGroup,
&i.Category,
&i.TransferAccount,
&i.BudgetID,
&i.AccountID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTransaction = `-- name: GetTransaction :one
SELECT id, date, memo, amount, group_id, status, account, payee_id, category_id, payee, category_group, category, transfer_account, budget_id, account_id FROM display_transactions
WHERE id = $1
`
func (q *Queries) GetTransaction(ctx context.Context, id uuid.UUID) (DisplayTransaction, error) {
row := q.db.QueryRowContext(ctx, getTransaction, id)
var i DisplayTransaction
err := row.Scan(
&i.ID,
&i.Date,
&i.Memo,
&i.Amount,
&i.GroupID,
&i.Status,
&i.Account,
&i.PayeeID,
&i.CategoryID,
&i.Payee,
&i.CategoryGroup,
&i.Category,
&i.TransferAccount,
&i.BudgetID,
&i.AccountID,
)
return i, err
}
const getTransactionsByMonthAndCategory = `-- name: GetTransactionsByMonthAndCategory :many
SELECT date, category_id, budget_id, amount
FROM transactions_by_month
WHERE transactions_by_month.budget_id = $1
`
func (q *Queries) GetTransactionsByMonthAndCategory(ctx context.Context, budgetID uuid.UUID) ([]TransactionsByMonth, error) {
rows, err := q.db.QueryContext(ctx, getTransactionsByMonthAndCategory, budgetID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TransactionsByMonth
for rows.Next() {
var i TransactionsByMonth
if err := rows.Scan(
&i.Date,
&i.CategoryID,
&i.BudgetID,
&i.Amount,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTransactionsForAccount = `-- name: GetTransactionsForAccount :many
SELECT t.id, t.date, t.memo, t.amount, t.group_id, t.status, t.account, t.payee_id, t.category_id, t.payee, t.category_group, t.category, t.transfer_account, t.budget_id, t.account_id
FROM display_transactions AS t
WHERE t.account_id = $1
`
func (q *Queries) GetTransactionsForAccount(ctx context.Context, accountID uuid.UUID) ([]DisplayTransaction, error) {
rows, err := q.db.QueryContext(ctx, getTransactionsForAccount, accountID) rows, err := q.db.QueryContext(ctx, getTransactionsForAccount, accountID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []GetTransactionsForAccountRow var items []DisplayTransaction
for rows.Next() { for rows.Next() {
var i GetTransactionsForAccountRow var i DisplayTransaction
if err := rows.Scan( if err := rows.Scan(
&i.ID, &i.ID,
&i.Date, &i.Date,
&i.Memo, &i.Memo,
&i.Amount, &i.Amount,
&i.GroupID,
&i.Status,
&i.Account, &i.Account,
&i.PayeeID,
&i.CategoryID,
&i.Payee, &i.Payee,
&i.CategoryGroup, &i.CategoryGroup,
&i.Category, &i.Category,
&i.TransferAccount,
&i.BudgetID,
&i.AccountID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -119,58 +351,44 @@ func (q *Queries) GetTransactionsForAccount(ctx context.Context, accountID uuid.
return items, nil return items, nil
} }
const getTransactionsForBudget = `-- name: GetTransactionsForBudget :many const setTransactionReconciled = `-- name: SetTransactionReconciled :exec
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, UPDATE transactions
accounts.name as account, COALESCE(payees.name, '') as payee, COALESCE(category_groups.name, '') as category_group, COALESCE(categories.name, '') as category SET status = 'Reconciled'
FROM transactions WHERE id = $1
INNER JOIN accounts ON accounts.id = transactions.account_id
LEFT JOIN payees ON payees.id = transactions.payee_id
LEFT JOIN categories ON categories.id = transactions.category_id
LEFT JOIN category_groups ON category_groups.id = categories.category_group_id
WHERE accounts.budget_id = $1
ORDER BY transactions.date DESC
LIMIT 200
` `
type GetTransactionsForBudgetRow struct { func (q *Queries) SetTransactionReconciled(ctx context.Context, id uuid.UUID) error {
ID uuid.UUID _, err := q.db.ExecContext(ctx, setTransactionReconciled, id)
Date time.Time return err
Memo string
Amount Numeric
Account string
Payee string
CategoryGroup string
Category string
} }
func (q *Queries) GetTransactionsForBudget(ctx context.Context, budgetID uuid.UUID) ([]GetTransactionsForBudgetRow, error) { const updateTransaction = `-- name: UpdateTransaction :exec
rows, err := q.db.QueryContext(ctx, getTransactionsForBudget, budgetID) UPDATE transactions
if err != nil { SET date = $1,
return nil, err memo = $2,
} amount = $3,
defer rows.Close() payee_id = $4,
var items []GetTransactionsForBudgetRow category_id = $5
for rows.Next() { WHERE id = $6
var i GetTransactionsForBudgetRow `
if err := rows.Scan(
&i.ID, type UpdateTransactionParams struct {
&i.Date, Date time.Time
&i.Memo, Memo string
&i.Amount, Amount numeric.Numeric
&i.Account, PayeeID uuid.NullUUID
&i.Payee, CategoryID uuid.NullUUID
&i.CategoryGroup, ID uuid.UUID
&i.Category, }
); err != nil {
return nil, err func (q *Queries) UpdateTransaction(ctx context.Context, arg UpdateTransactionParams) error {
} _, err := q.db.ExecContext(ctx, updateTransaction,
items = append(items, i) arg.Date,
} arg.Memo,
if err := rows.Close(); err != nil { arg.Amount,
return nil, err arg.PayeeID,
} arg.CategoryID,
if err := rows.Err(); err != nil { arg.ID,
return nil, err )
} return err
return items, nil
} }

View File

@ -1,4 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
// source: user_budgets.sql // source: user_budgets.sql
package postgres package postgres

View File

@ -1,4 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
// source: users.sql // source: users.sql
package postgres package postgres

144
postgres/ynab-export.go Normal file
View File

@ -0,0 +1,144 @@
package postgres
import (
"context"
"encoding/csv"
"fmt"
"io"
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
"github.com/google/uuid"
)
type YNABExport struct {
queries *Queries
budgetID uuid.UUID
}
func NewYNABExport(context context.Context, queries *Queries, budgetID uuid.UUID) (*YNABExport, error) {
return &YNABExport{
queries: queries,
budgetID: budgetID,
}, nil
}
// ImportAssignments expects a TSV-file as exported by YNAB in the following format:
// "Month" "Category Group/Category" "Category Group" "Category" "Budgeted" "Activity" "Available"
// "Apr 2019" "Income: Next Month" "Income" "Next Month" 0,00€ 0,00€ 0,00€
//
// Activity and Available are not imported, since they are determined by the transactions and historic assignments.
func (ynab *YNABExport) ExportAssignments(context context.Context, w io.Writer) error {
csv := csv.NewWriter(w)
csv.Comma = '\t'
assignments, err := ynab.queries.GetAllAssignments(context, ynab.budgetID)
if err != nil {
return fmt.Errorf("load assignments: %w", err)
}
count := 0
for _, assignment := range assignments {
row := []string{
assignment.Date.Format("Jan 2006"),
assignment.Group + ": " + assignment.Category,
assignment.Group,
assignment.Category,
assignment.Amount.String() + "€",
numeric.Zero().String() + "€",
numeric.Zero().String() + "€",
}
err := csv.Write(row)
if err != nil {
return fmt.Errorf("write assignment: %w", err)
}
count++
}
csv.Flush()
fmt.Printf("Exported %d assignments\n", count)
return nil
}
// ImportTransactions expects a TSV-file as exported by YNAB in the following format:
// "Account" "Flag" "Date" "Payee" "Category Group/Category" "Category Group" "Category" "Memo" "Outflow" "Inflow" "Cleared"
// "Cash" "" "11.12.2021" "Transfer : Checking" "" "" "" "Brought to bank" 500,00€ 0,00€ "Cleared".
func (ynab *YNABExport) ExportTransactions(context context.Context, w io.Writer) error {
csv := csv.NewWriter(w)
csv.Comma = '\t'
transactions, err := ynab.queries.GetAllTransactionsForBudget(context, ynab.budgetID)
if err != nil {
return fmt.Errorf("load transactions: %w", err)
}
header := []string{
"Account",
"Flag",
"Date",
"Payee",
"Category Group/Category",
"Category Group",
"Category",
"Memo",
"Outflow",
"Inflow",
"Cleared",
}
err = csv.Write(header)
if err != nil {
return fmt.Errorf("write transaction: %w", err)
}
count := 0
for _, transaction := range transactions {
row := GetTransactionRow(transaction)
err := csv.Write(row)
if err != nil {
return fmt.Errorf("write transaction: %w", err)
}
count++
}
csv.Flush()
fmt.Printf("Exported %d transactions\n", count)
return nil
}
func GetTransactionRow(transaction DisplayTransaction) []string {
row := []string{
transaction.Account,
"", // Flag
transaction.Date.Format("02.01.2006"),
}
if transaction.TransferAccount != "" {
row = append(row, "Transfer : "+transaction.TransferAccount)
} else {
row = append(row, transaction.Payee)
}
if transaction.CategoryGroup != "" && transaction.Category != "" {
row = append(row,
transaction.CategoryGroup+": "+transaction.Category,
transaction.CategoryGroup,
transaction.Category)
} else {
row = append(row, "", "", "")
}
row = append(row, transaction.Memo)
if transaction.Amount.IsPositive() {
row = append(row, numeric.Zero().String()+"€", transaction.Amount.String()+"€")
} else {
row = append(row, transaction.Amount.String()[1:]+"€", numeric.Zero().String()+"€")
}
return append(row, string(transaction.Status))
}

396
postgres/ynab-import.go Normal file
View File

@ -0,0 +1,396 @@
package postgres
import (
"context"
"encoding/csv"
"fmt"
"io"
"strings"
"time"
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
"github.com/google/uuid"
)
type YNABImport struct {
accounts []Account
payees []Payee
categories []GetCategoriesRow
categoryGroups []CategoryGroup
queries *Queries
budgetID uuid.UUID
}
func NewYNABImport(context context.Context, queries *Queries, budgetID uuid.UUID) (*YNABImport, error) {
accounts, err := queries.GetAccounts(context, budgetID)
if err != nil {
return nil, err
}
payees, err := queries.GetPayees(context, budgetID)
if err != nil {
return nil, err
}
categories, err := queries.GetCategories(context, budgetID)
if err != nil {
return nil, err
}
categoryGroups, err := queries.GetCategoryGroups(context, budgetID)
if err != nil {
return nil, err
}
return &YNABImport{
accounts: accounts,
payees: payees,
categories: categories,
categoryGroups: categoryGroups,
queries: queries,
budgetID: budgetID,
}, nil
}
// ImportAssignments expects a TSV-file as exported by YNAB in the following format:
// "Month" "Category Group/Category" "Category Group" "Category" "Budgeted" "Activity" "Available"
// "Apr 2019" "Income: Next Month" "Income" "Next Month" 0,00€ 0,00€ 0,00€
//
// Activity and Available are not imported, since they are determined by the transactions and historic assignments.
func (ynab *YNABImport) ImportAssignments(context context.Context, r io.Reader) error {
csv := csv.NewReader(r)
csv.Comma = '\t'
csv.LazyQuotes = true
csvData, err := csv.ReadAll()
if err != nil {
return fmt.Errorf("read from tsv: %w", err)
}
count := 0
for _, record := range csvData[1:] {
dateString := record[0]
date, err := time.Parse("Jan 2006", dateString)
if err != nil {
return fmt.Errorf("parse date %s: %w", dateString, err)
}
categoryGroup, categoryName := record[2], record[3] // also in 1 joined by :
category, err := ynab.GetCategory(context, categoryGroup, categoryName)
if err != nil {
return fmt.Errorf("get category %s/%s: %w", categoryGroup, categoryName, err)
}
amountString := record[4]
amount, err := GetAmount(amountString, "0,00€")
if err != nil {
return fmt.Errorf("parse amount %s: %w", amountString, err)
}
if amount.Int.Int64() == 0 {
continue
}
assignment := UpdateAssignmentWithDifferenceParams{
Date: date,
CategoryID: category.UUID,
Amount: amount,
}
err = ynab.queries.UpdateAssignmentWithDifference(context, assignment)
if err != nil {
return fmt.Errorf("save assignment %v: %w", assignment, err)
}
count++
}
fmt.Printf("Imported %d assignments\n", count)
return nil
}
type Transfer struct {
CreateTransactionParams
TransferToAccount *Account
FromAccount string
ToAccount string
}
// ImportTransactions expects a TSV-file as exported by YNAB in the following format:
// "Account" "Flag" "Date" "Payee" "Category Group/Category" "Category Group" "Category" "Memo" "Outflow" "Inflow" "Cleared"
// "Cash" "" "11.12.2021" "Transfer : Checking" "" "" "" "Brought to bank" 500,00€ 0,00€ "Cleared".
func (ynab *YNABImport) ImportTransactions(context context.Context, r io.Reader) error {
csv := csv.NewReader(r)
csv.Comma = '\t'
csv.LazyQuotes = true
csvData, err := csv.ReadAll()
if err != nil {
return fmt.Errorf("read from tsv: %w", err)
}
var openTransfers []Transfer
count := 0
for _, record := range csvData[1:] {
transaction, err := ynab.GetTransaction(context, record)
if err != nil {
return err
}
payeeName := record[3]
// Transaction is a transfer
if strings.HasPrefix(payeeName, "Transfer : ") {
err = ynab.ImportTransferTransaction(context, payeeName, transaction.CreateTransactionParams,
&openTransfers, transaction.Account, transaction.Amount)
} else {
err = ynab.ImportRegularTransaction(context, payeeName, transaction.CreateTransactionParams)
}
if err != nil {
return err
}
count++
}
for _, openTransfer := range openTransfers {
fmt.Printf("Saving unmatched transfer from %s to %s on %s over %f as regular transaction\n",
openTransfer.FromAccount, openTransfer.ToAccount, openTransfer.Date, openTransfer.Amount.GetFloat64())
_, err = ynab.queries.CreateTransaction(context, openTransfer.CreateTransactionParams)
if err != nil {
return fmt.Errorf("save transaction %v: %w", openTransfer.CreateTransactionParams, err)
}
}
fmt.Printf("Imported %d transactions\n", count)
return nil
}
type NewTransaction struct {
CreateTransactionParams
Account *Account
}
func (ynab *YNABImport) GetTransaction(context context.Context, record []string) (NewTransaction, error) {
accountName := record[0]
account, err := ynab.GetAccount(context, accountName)
if err != nil {
return NewTransaction{}, fmt.Errorf("get account %s: %w", accountName, err)
}
// flag := record[1]
dateString := record[2]
date, err := time.Parse("02.01.2006", dateString)
if err != nil {
return NewTransaction{}, fmt.Errorf("parse date %s: %w", dateString, err)
}
categoryGroup, categoryName := record[5], record[6] // also in 4 joined by :
category, err := ynab.GetCategory(context, categoryGroup, categoryName)
if err != nil {
return NewTransaction{}, fmt.Errorf("get category %s/%s: %w", categoryGroup, categoryName, err)
}
memo := record[7]
outflow := record[8]
inflow := record[9]
amount, err := GetAmount(inflow, outflow)
if err != nil {
return NewTransaction{}, fmt.Errorf("parse amount from (%s/%s): %w", inflow, outflow, err)
}
statusEnum := TransactionStatusUncleared
status := record[10]
switch status {
case "Cleared":
statusEnum = TransactionStatusCleared
case "Reconciled":
statusEnum = TransactionStatusReconciled
case "Uncleared":
}
return NewTransaction{
CreateTransactionParams: CreateTransactionParams{
Date: date,
Memo: memo,
AccountID: account.ID,
CategoryID: category,
Amount: amount,
Status: statusEnum,
},
Account: account,
}, nil
}
func (ynab *YNABImport) ImportRegularTransaction(context context.Context, payeeName string,
transaction CreateTransactionParams,
) error {
payeeID, err := ynab.GetPayee(context, payeeName)
if err != nil {
return fmt.Errorf("get payee %s: %w", payeeName, err)
}
transaction.PayeeID = payeeID
_, err = ynab.queries.CreateTransaction(context, transaction)
if err != nil {
return fmt.Errorf("save transaction %v: %w", transaction, err)
}
return nil
}
func (ynab *YNABImport) ImportTransferTransaction(context context.Context, payeeName string,
transaction CreateTransactionParams, openTransfers *[]Transfer,
account *Account, amount numeric.Numeric,
) error {
transferToAccountName := payeeName[11:]
transferToAccount, err := ynab.GetAccount(context, transferToAccountName)
if err != nil {
return fmt.Errorf("get transfer account %s: %w", transferToAccountName, err)
}
transfer := Transfer{
transaction,
transferToAccount,
account.Name,
transferToAccountName,
}
found := false
for i, openTransfer := range *openTransfers {
if openTransfer.TransferToAccount.ID != transfer.AccountID {
continue
}
if openTransfer.AccountID != transfer.TransferToAccount.ID {
continue
}
if openTransfer.Amount.GetFloat64() != -1*transfer.Amount.GetFloat64() {
continue
}
fmt.Printf("Matched transfers from %s to %s over %f\n", account.Name, transferToAccount.Name, amount.GetFloat64())
transfers := *openTransfers
transfers[i] = transfers[len(transfers)-1]
*openTransfers = transfers[:len(transfers)-1]
found = true
groupID := uuid.New()
transfer.GroupID = uuid.NullUUID{UUID: groupID, Valid: true}
openTransfer.GroupID = uuid.NullUUID{UUID: groupID, Valid: true}
_, err = ynab.queries.CreateTransaction(context, transfer.CreateTransactionParams)
if err != nil {
return fmt.Errorf("save transaction %v: %w", transfer.CreateTransactionParams, err)
}
_, err = ynab.queries.CreateTransaction(context, openTransfer.CreateTransactionParams)
if err != nil {
return fmt.Errorf("save transaction %v: %w", openTransfer.CreateTransactionParams, err)
}
break
}
if !found {
*openTransfers = append(*openTransfers, transfer)
}
return nil
}
func GetAmount(inflow string, outflow string) (numeric.Numeric, error) {
in, err := numeric.ParseCurrency(inflow)
if err != nil {
return in, fmt.Errorf("parse inflow: %w", err)
}
if !in.IsZero() {
return in, nil
}
// if inflow is zero, use outflow
out, err := numeric.ParseCurrency("-" + outflow)
if err != nil {
return out, fmt.Errorf("parse outflow: %w", err)
}
return out, nil
}
func (ynab *YNABImport) GetAccount(context context.Context, name string) (*Account, error) {
for _, acc := range ynab.accounts {
if acc.Name == name {
return &acc, nil
}
}
account, err := ynab.queries.CreateAccount(context, CreateAccountParams{Name: name, BudgetID: ynab.budgetID})
if err != nil {
return nil, err
}
ynab.accounts = append(ynab.accounts, account)
return &account, nil
}
func (ynab *YNABImport) GetPayee(context context.Context, name string) (uuid.NullUUID, error) {
if name == "" {
return uuid.NullUUID{}, nil
}
for _, pay := range ynab.payees {
if pay.Name == name {
return uuid.NullUUID{UUID: pay.ID, Valid: true}, nil
}
}
payee, err := ynab.queries.CreatePayee(context, CreatePayeeParams{Name: name, BudgetID: ynab.budgetID})
if err != nil {
return uuid.NullUUID{}, err
}
ynab.payees = append(ynab.payees, payee)
return uuid.NullUUID{UUID: payee.ID, Valid: true}, nil
}
func (ynab *YNABImport) GetCategory(context context.Context, group string, name string) (uuid.NullUUID, error) { //nolint
if group == "" || name == "" {
return uuid.NullUUID{}, nil
}
for _, category := range ynab.categories {
if category.Name == name && category.Group == group {
return uuid.NullUUID{UUID: category.ID, Valid: true}, nil
}
}
var categoryGroup CategoryGroup
for _, existingGroup := range ynab.categoryGroups {
if existingGroup.Name == group {
categoryGroup = existingGroup
}
}
if categoryGroup.Name == "" {
newGroup := CreateCategoryGroupParams{Name: group, BudgetID: ynab.budgetID}
var err error
categoryGroup, err = ynab.queries.CreateCategoryGroup(context, newGroup)
if err != nil {
return uuid.NullUUID{}, err
}
ynab.categoryGroups = append(ynab.categoryGroups, categoryGroup)
}
newCategory := CreateCategoryParams{Name: name, CategoryGroupID: categoryGroup.ID}
category, err := ynab.queries.CreateCategory(context, newCategory)
if err != nil {
return uuid.NullUUID{}, err
}
getCategory := GetCategoriesRow{
ID: category.ID,
CategoryGroupID: category.CategoryGroupID,
Name: category.Name,
Group: categoryGroup.Name,
}
ynab.categories = append(ynab.categories, getCategory)
return uuid.NullUUID{UUID: category.ID, Valid: true}, nil
}

135
server/account.go Normal file
View File

@ -0,0 +1,135 @@
package server
import (
"net/http"
"time"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
)
type FilterTransactionsRequest struct {
CategoryID string `json:"categoryId"`
PayeeID string `json:"payeeId"`
AccountID string `json:"accountId"`
FromDate time.Time `json:"fromDate"`
ToDate time.Time `json:"toDate"`
}
func (h *Handler) filteredTransactions(c echo.Context) error {
budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
var request FilterTransactionsRequest
err = c.Bind(&request)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
params := postgres.GetFilteredTransactionsParams{
BudgetID: budgetUUID,
FromDate: request.FromDate,
ToDate: request.ToDate,
}
params.CategoryID, params.FilterCategory = parseEmptyUUID(request.CategoryID)
accountID, filterAccount := parseEmptyUUID(request.AccountID)
params.AccountID, params.FilterAccount = accountID.UUID, filterAccount
params.PayeeID, params.FilterPayee = parseEmptyUUID(request.PayeeID)
transactions, err := h.Service.GetFilteredTransactions(c.Request().Context(), params)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err)
}
return c.JSON(http.StatusOK, TransactionsResponse{nil, transactions})
}
func parseEmptyUUID(value string) (uuid.NullUUID, bool) {
if value == "" {
return uuid.NullUUID{}, false
}
val, err := uuid.Parse(value)
if err != nil {
return uuid.NullUUID{}, false
}
return uuid.NullUUID{UUID: val, Valid: true}, true
}
func (h *Handler) problematicTransactions(c echo.Context) error {
budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
transactions, err := h.Service.GetProblematicTransactions(c.Request().Context(), budgetUUID)
if err != nil {
return err
}
return c.JSON(http.StatusOK, TransactionsResponse{nil, transactions})
}
func (h *Handler) transactionsForAccount(c echo.Context) error {
accountID := c.Param("accountid")
accountUUID, err := uuid.Parse(accountID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
account, err := h.Service.GetAccount(c.Request().Context(), accountUUID)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err)
}
transactions, err := h.Service.GetTransactionsForAccount(c.Request().Context(), accountUUID)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err)
}
return c.JSON(http.StatusOK, TransactionsResponse{&account, transactions})
}
type TransactionsResponse struct {
Account *postgres.Account
Transactions []postgres.DisplayTransaction
}
type EditAccountRequest struct {
Name string `json:"name"`
OnBudget bool `json:"onBudget"`
IsOpen bool `json:"isOpen"`
}
func (h *Handler) editAccount(c echo.Context) error {
accountID := c.Param("accountid")
accountUUID, err := uuid.Parse(accountID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
var request EditAccountRequest
err = c.Bind(&request)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
updateParams := postgres.UpdateAccountParams{
Name: request.Name,
OnBudget: request.OnBudget,
IsOpen: request.IsOpen,
ID: accountUUID,
}
account, err := h.Service.UpdateAccount(c.Request().Context(), updateParams)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err)
}
return h.getBudget(c, account.BudgetID)
}

71
server/account_test.go Normal file
View File

@ -0,0 +1,71 @@
package server
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.javil.eu/jacob1123/budgeteer/bcrypt"
"git.javil.eu/jacob1123/budgeteer/jwt"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/labstack/echo/v4"
)
func TestRegisterUser(t *testing.T) {
t.Parallel()
database, err := postgres.Connect("pgtx", cfg.DatabaseConnection)
if err != nil {
t.Errorf("connect to DB: %v", err)
return
}
tokenVerifier, _ := jwt.NewTokenVerifier(cfg.SessionSecret)
h := Handler{
Service: database,
TokenVerifier: tokenVerifier,
CredentialsVerifier: &bcrypt.Verifier{},
}
recorder := httptest.NewRecorder()
engine := echo.New()
h.LoadRoutes(engine)
t.Run("RegisterUser", func(t *testing.T) {
t.Parallel()
request, err := http.NewRequestWithContext(context.Background(),
http.MethodPost,
"/api/v1/user/register",
strings.NewReader(`{"password":"pass","email":"info@example.com","name":"Test"}`))
request.Header.Set("Content-Type", "application/json")
context := engine.NewContext(request, recorder)
if err != nil {
t.Errorf("error creating request: %s", err)
return
}
err = h.registerPost(context)
if err != nil {
t.Error(err.Error())
t.Error("Error registering")
return
}
if recorder.Code != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", recorder.Code, http.StatusOK)
}
var response LoginResponse
err = json.NewDecoder(recorder.Body).Decode(&response)
if err != nil {
t.Error(err.Error())
t.Error("Error registering")
}
if len(response.Token) == 0 {
t.Error("Did not get a token")
}
})
}

69
server/admin.go Normal file
View File

@ -0,0 +1,69 @@
package server
import (
"fmt"
"net/http"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/pressly/goose/v3"
)
func (h *Handler) clearDatabase(c echo.Context) error {
if err := goose.Reset(h.Service.DB, "schema"); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err)
}
if err := goose.Up(h.Service.DB, "schema"); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err)
}
return nil
}
func (h *Handler) deleteBudget(c echo.Context) error {
budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
err = h.clearBudgetData(c, budgetUUID)
if err != nil {
return err
}
err = h.Service.DeleteBudget(c.Request().Context(), budgetUUID)
if err != nil {
return err
}
return nil
}
func (h *Handler) clearBudgetData(c echo.Context, budgetUUID uuid.UUID) error {
rows, err := h.Service.DeleteAllAssignments(c.Request().Context(), budgetUUID)
if err != nil {
return err
}
fmt.Printf("Deleted %d assignments\n", rows)
rows, err = h.Service.DeleteAllTransactions(c.Request().Context(), budgetUUID)
if err != nil {
return err
}
fmt.Printf("Deleted %d transactions\n", rows)
return nil
}
func (h *Handler) clearBudget(c echo.Context) error {
budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
return h.clearBudgetData(c, budgetUUID)
}

87
server/autocomplete.go Normal file
View File

@ -0,0 +1,87 @@
package server
import (
"net/http"
"strings"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
)
func (h *Handler) autocompleteAccounts(c echo.Context) error {
budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "budgetid missing from URL")
}
query := c.Request().URL.Query().Get("s")
searchParams := postgres.SearchAccountsParams{
BudgetID: budgetUUID,
Search: "%" + query + "%",
}
categories, err := h.Service.SearchAccounts(c.Request().Context(), searchParams)
if err != nil {
return err
}
return c.JSON(http.StatusOK, categories)
}
func (h *Handler) autocompleteCategories(c echo.Context) error {
budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "budgetid missing from URL")
}
query := c.Request().URL.Query().Get("s")
searchParams := postgres.SearchCategoriesParams{
BudgetID: budgetUUID,
Search: "%" + query + "%",
}
categories, err := h.Service.SearchCategories(c.Request().Context(), searchParams)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err)
}
return c.JSON(http.StatusOK, categories)
}
func (h *Handler) autocompletePayee(c echo.Context) error {
budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "budgetid missing from URL")
}
query := c.Request().URL.Query().Get("s")
transferPrefix := "Transfer"
if strings.HasPrefix(query, transferPrefix) {
searchParams := postgres.SearchAccountsParams{
BudgetID: budgetUUID,
Search: "%" + strings.Trim(query[len(transferPrefix):], " \t\n:") + "%",
}
accounts, err := h.Service.SearchAccounts(c.Request().Context(), searchParams)
if err != nil {
return err
}
return c.JSON(http.StatusOK, accounts)
}
searchParams := postgres.SearchPayeesParams{
BudgetID: budgetUUID,
Search: query + "%",
}
payees, err := h.Service.SearchPayees(c.Request().Context(), searchParams)
if err != nil {
return err
}
return c.JSON(http.StatusOK, payees)
}

30
server/budget.go Normal file
View File

@ -0,0 +1,30 @@
package server
import (
"net/http"
"github.com/labstack/echo/v4"
)
type newBudgetInformation struct {
Name string `json:"name"`
}
func (h *Handler) newBudget(c echo.Context) error {
var newBudget newBudgetInformation
if err := c.Bind(&newBudget); err != nil {
return echo.NewHTTPError(http.StatusNotAcceptable, err)
}
if newBudget.Name == "" {
return echo.NewHTTPError(http.StatusBadRequest, "budget name is required")
}
userID := MustGetToken(c).GetID()
budget, err := h.Service.NewBudget(c.Request().Context(), newBudget.Name, userID)
if err != nil {
return err
}
return c.JSON(http.StatusOK, budget)
}

182
server/budgeting.go Normal file
View File

@ -0,0 +1,182 @@
package server
import (
"context"
"fmt"
"net/http"
"git.javil.eu/jacob1123/budgeteer/postgres"
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
)
type CategoryWithBalance struct {
*postgres.GetCategoriesRow
AvailableLastMonth numeric.Numeric
Activity numeric.Numeric
Assigned numeric.Numeric
}
func NewCategoryWithBalance(category *postgres.GetCategoriesRow) CategoryWithBalance {
return CategoryWithBalance{
GetCategoriesRow: category,
AvailableLastMonth: numeric.Zero(),
Activity: numeric.Zero(),
Assigned: numeric.Zero(),
}
}
func (h *Handler) budgetingForMonth(c echo.Context) error {
budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "budgetid missing from URL")
}
budget, err := h.Service.GetBudget(c.Request().Context(), budgetUUID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
month, err := getDate(c)
if err != nil {
return c.Redirect(http.StatusTemporaryRedirect, "/budget/"+budget.ID.String())
}
data, err := h.getBudgetingViewForMonth(c.Request().Context(), budget, month)
if err != nil {
return err
}
return c.JSON(http.StatusOK, data)
}
func (h *Handler) getBudgetingViewForMonth(ctx context.Context, budget postgres.Budget, month Month) (BudgetingForMonthResponse, error) {
categories, err := h.Service.GetCategories(ctx, budget.ID)
if err != nil {
return BudgetingForMonthResponse{}, fmt.Errorf("error loading categories: %w", err)
}
cumultativeBalances, err := h.Service.GetCumultativeBalances(ctx, budget.ID)
if err != nil {
return BudgetingForMonthResponse{}, fmt.Errorf("error loading balances: %w", err)
}
categoriesWithBalance, moneyUsed, overspentLastMonth := h.calculateBalances(budget, month, categories, cumultativeBalances)
availableBalance := h.getAvailableBalance(budget, month, moneyUsed, cumultativeBalances)
data := BudgetingForMonthResponse{categoriesWithBalance, availableBalance, overspentLastMonth}
return data, nil
}
type BudgetingForMonthResponse struct {
Categories []CategoryWithBalance
AvailableBalance numeric.Numeric
OverspentLastMonth numeric.Numeric
}
func (*Handler) getAvailableBalance(budget postgres.Budget, month Month,
moneyUsed numeric.Numeric, cumultativeBalances []postgres.GetCumultativeBalancesRow,
) numeric.Numeric {
availableBalance := moneyUsed
for _, bal := range cumultativeBalances {
if bal.CategoryID != budget.IncomeCategoryID {
continue
}
if month.InFuture(bal.Date) {
continue
}
availableBalance.AddI(bal.Transactions)
availableBalance.AddI(bal.Assignments) // should be zero, but who knows
}
return availableBalance
}
type BudgetingResponse struct {
Accounts []postgres.GetAccountsWithBalanceRow
Budget postgres.Budget
}
func (h *Handler) budget(c echo.Context) error {
budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "budgetid missing from URL")
}
return h.getBudget(c, budgetUUID)
}
func (h *Handler) getBudget(c echo.Context, budgetUUID uuid.UUID) error {
budget, err := h.Service.GetBudget(c.Request().Context(), budgetUUID)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err)
}
accounts, err := h.Service.GetAccountsWithBalance(c.Request().Context(), budgetUUID)
if err != nil {
return err
}
data := BudgetingResponse{accounts, budget}
return c.JSON(http.StatusOK, data)
}
func (h *Handler) calculateBalances(budget postgres.Budget, month Month,
categories []postgres.GetCategoriesRow, cumultativeBalances []postgres.GetCumultativeBalancesRow,
) ([]CategoryWithBalance, numeric.Numeric, numeric.Numeric) {
categoriesWithBalance := []CategoryWithBalance{}
moneyUsed := numeric.Zero()
overspentLastMonth := numeric.Zero()
categories = append(categories, postgres.GetCategoriesRow{
Group: "Income",
Name: "No Category",
ID: uuid.UUID{},
})
for i := range categories {
cat := &categories[i]
if cat.ID == budget.IncomeCategoryID {
continue
}
categoryWithBalance := NewCategoryWithBalance(cat)
for _, bal := range cumultativeBalances {
if bal.CategoryID != cat.ID {
continue
}
// skip everything in the future
if month.InFuture(bal.Date) {
continue
}
moneyUsed.SubI(bal.Assignments)
if month.InPresent(bal.Date) {
categoryWithBalance.Activity = bal.Transactions
categoryWithBalance.Assigned = bal.Assignments
continue
}
categoryWithBalance.AvailableLastMonth.AddI(bal.Assignments)
categoryWithBalance.AvailableLastMonth.AddI(bal.Transactions)
if !categoryWithBalance.AvailableLastMonth.IsPositive() {
moneyUsed.AddI(categoryWithBalance.AvailableLastMonth)
if month.Previous().InPresent(bal.Date) {
overspentLastMonth.AddI(categoryWithBalance.AvailableLastMonth)
}
categoryWithBalance.AvailableLastMonth = numeric.Zero()
}
}
categoriesWithBalance = append(categoriesWithBalance, categoryWithBalance)
}
return categoriesWithBalance, moneyUsed, overspentLastMonth
}

52
server/category.go Normal file
View File

@ -0,0 +1,52 @@
package server
import (
"fmt"
"net/http"
"git.javil.eu/jacob1123/budgeteer/postgres"
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
)
type SetCategoryAssignmentRequest struct {
Assigned float64
}
func (h *Handler) setCategoryAssignment(c echo.Context) error {
categoryID := c.Param("categoryid")
categoryUUID, err := uuid.Parse(categoryID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "categoryid missing from URL")
}
var request SetCategoryAssignmentRequest
err = c.Bind(&request)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid payload: %w", err))
}
date, err := getDate(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("date invalid: %w", err))
}
var amount numeric.Numeric
err = amount.Set(request.Assigned)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("parse amount: %w", err))
}
updateArgs := postgres.UpdateAssignmentParams{
CategoryID: categoryUUID,
Date: date.FirstOfMonth(),
Amount: amount,
}
err = h.Service.UpdateAssignment(c.Request().Context(), updateArgs)
if err != nil {
return fmt.Errorf("update assignment: %w", err)
}
return nil
}

128
server/http.go Normal file
View File

@ -0,0 +1,128 @@
package server
import (
"errors"
"io"
"io/fs"
"net/http"
"path"
"strings"
"git.javil.eu/jacob1123/budgeteer"
"git.javil.eu/jacob1123/budgeteer/bcrypt"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// Handler handles incoming requests.
type Handler struct {
Service *postgres.Database
TokenVerifier budgeteer.TokenVerifier
CredentialsVerifier *bcrypt.Verifier
StaticFS http.FileSystem
}
// Serve starts the http server.
func (h *Handler) Serve() {
router := echo.New()
h.LoadRoutes(router)
if err := router.Start(":1323"); err != nil {
panic(err)
}
}
// LoadRoutes initializes all the routes.
func (h *Handler) LoadRoutes(router *echo.Echo) {
router.Use(enableCachingForStaticFiles())
router.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Filesystem: h.StaticFS,
HTML5: true,
}))
api := router.Group("/api/v1")
anonymous := api.Group("/user")
anonymous.POST("/login", h.loginPost)
anonymous.POST("/register", h.registerPost)
authenticated := api.Group("")
{
authenticated.Use(h.verifyLoginWithForbidden)
authenticated.GET("/account/:accountid/transactions", h.transactionsForAccount)
authenticated.POST("/account/:accountid/reconcile", h.reconcileTransactions)
authenticated.POST("/account/:accountid", h.editAccount)
authenticated.GET("/admin/clear-database", h.clearDatabase)
budget := authenticated.Group("/budget")
budget.POST("/new", h.newBudget)
budget.GET("/:budgetid", h.budget)
budget.GET("/:budgetid/:year/:month", h.budgetingForMonth)
budget.POST("/:budgetid/category/:categoryid/:year/:month", h.setCategoryAssignment)
budget.GET("/:budgetid/autocomplete/payees", h.autocompletePayee)
budget.GET("/:budgetid/autocomplete/accounts", h.autocompleteAccounts)
budget.GET("/:budgetid/autocomplete/categories", h.autocompleteCategories)
budget.GET("/:budgetid/problematic-transactions", h.problematicTransactions)
budget.POST("/:budgetid/filtered-transactions", h.filteredTransactions)
budget.DELETE("/:budgetid", h.deleteBudget)
budget.POST("/:budgetid/import/ynab", h.importYNAB)
budget.POST("/:budgetid/export/ynab/transactions", h.exportYNABTransactions)
budget.POST("/:budgetid/export/ynab/assignments", h.exportYNABAssignments)
budget.POST("/:budgetid/settings/clear", h.clearBudget)
transaction := authenticated.Group("/transaction")
transaction.POST("/new", h.newTransaction)
transaction.POST("/:transactionid", h.updateTransaction)
}
}
func (h *Handler) ServeStatic(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
h.ServeStaticFile(c, c.Path())
return nil
}
}
func (h *Handler) ServeStaticFile(c echo.Context, fullPath string) {
file, err := h.StaticFS.Open(fullPath)
if errors.Is(err, fs.ErrNotExist) {
h.ServeStaticFile(c, path.Join("/", "/index.html"))
return
}
if err != nil {
c.Error(err)
return
}
stat, err := file.Stat()
if err != nil {
c.Error(err)
return
}
if stat.IsDir() {
h.ServeStaticFile(c, path.Join(fullPath, "index.html"))
return
}
if file, ok := file.(io.ReadSeeker); ok {
http.ServeContent(c.Response().Writer, c.Request(), stat.Name(), stat.ModTime(), file)
} else {
panic("File does not implement ReadSeeker")
}
}
func enableCachingForStaticFiles() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if strings.HasPrefix(c.Path(), "/static/") {
c.Response().Header().Set("Cache-Control", "max-age=86400")
}
return next(c)
}
}
}

37
server/json-date.go Normal file
View File

@ -0,0 +1,37 @@
package server
import (
"encoding/json"
"fmt"
"strings"
"time"
)
type JSONDate time.Time
// UnmarshalJSON parses the JSONDate from a JSON input.
func (j *JSONDate) UnmarshalJSON(b []byte) error {
s := strings.Trim(string(b), "\"")
t, err := time.Parse("2006-01-02", s)
if err != nil {
return fmt.Errorf("parse date: %w", err)
}
*j = JSONDate(t)
return nil
}
// MarshalJSON converts the JSONDate to a JSON in ISO format.
func (j JSONDate) MarshalJSON() ([]byte, error) {
result, err := json.Marshal(time.Time(j))
if err != nil {
return nil, fmt.Errorf("marshal date: %w", err)
}
return result, nil
}
// Format formats the time using the regular time.Time mechanics..
func (j JSONDate) Format(s string) string {
t := time.Time(j)
return t.Format(s)
}

305
server/main_test.go Normal file
View File

@ -0,0 +1,305 @@
package server
import (
"context"
"encoding/json"
"fmt"
"io/fs"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"git.javil.eu/jacob1123/budgeteer/bcrypt"
"git.javil.eu/jacob1123/budgeteer/config"
"git.javil.eu/jacob1123/budgeteer/jwt"
"git.javil.eu/jacob1123/budgeteer/postgres"
"git.javil.eu/jacob1123/budgeteer/web"
txdb "github.com/DATA-DOG/go-txdb"
)
var cfg = config.Config{ //nolint:gochecknoglobals
DatabaseConnection: "postgres://budgeteer:budgeteer@db:5432/budgeteer_test",
SessionSecret: "this_is_my_demo_secret_for_unit_tests",
}
func init() { //nolint:gochecknoinits
_, err := postgres.Connect("pgx", cfg.DatabaseConnection)
if err != nil {
panic("failed connecting to DB for migrations: " + err.Error())
}
txdb.Register("pgtx", "pgx", cfg.DatabaseConnection)
}
func TestMain(t *testing.T) {
t.Parallel()
queries, err := postgres.Connect("pgtx", cfg.DatabaseConnection)
if err != nil {
t.Errorf("connect to DB: %v", err)
}
static, err := fs.Sub(web.Static, "dist")
if err != nil {
panic("couldn't open static files")
}
tokenVerifier, err := jwt.NewTokenVerifier(cfg.SessionSecret)
if err != nil {
panic(fmt.Errorf("couldn't create token verifier: %w", err))
}
handler := &Handler{
Service: queries,
TokenVerifier: tokenVerifier,
CredentialsVerifier: &bcrypt.Verifier{},
StaticFS: http.FS(static),
}
ctx := context.Background()
createUserParams := postgres.CreateUserParams{
Email: "test@example.com",
Name: "test@example.com",
Password: "this is my dumb password",
}
user, err := handler.Service.CreateUser(ctx, createUserParams)
if err != nil {
fmt.Println(err)
t.Fail()
return
}
budget, err := handler.Service.NewBudget(ctx, "My nice Budget", user.ID)
if err != nil {
fmt.Println(err)
t.Fail()
return
}
handler.DoYNABImport(ctx, t, budget)
loc := time.Now().Location()
AssertCategoriesAndAvailableEqual(ctx, t, loc, handler, budget)
AssertAccountsEqual(ctx, t, handler, budget)
}
func AssertAccountsEqual(ctx context.Context, t *testing.T, handler *Handler, budget *postgres.Budget) {
t.Helper()
t.Run("account balances", func(t *testing.T) {
t.Parallel()
resultDir := "../testdata/production-export/results"
files, err := os.ReadDir(resultDir)
if err != nil {
t.Errorf("could not load results: %s", err)
return
}
for _, file := range files {
if file.IsDir() {
continue
}
name := file.Name()[0 : len(file.Name())-5]
if name != "accounts" {
continue
}
testCaseFile := filepath.Join(resultDir, file.Name())
handler.CheckAccountBalance(ctx, t, testCaseFile, budget)
}
})
}
func (h Handler) CheckAccountBalance(ctx context.Context, t *testing.T, testCaseFile string, budget *postgres.Budget) {
t.Helper()
accounts, err := h.Service.GetAccountsWithBalance(ctx, budget.ID)
if err != nil {
t.Errorf("get accounts: %s", err)
return
}
testDataFile, err := os.Open(testCaseFile)
if err != nil {
t.Errorf("could not load category test data: %s", err)
return
}
var testData map[string]float64
dec := json.NewDecoder(testDataFile)
err = dec.Decode(&testData)
if err != nil {
t.Errorf("could not decode category test data: %s", err)
return
}
for accountName, accountBalance := range testData {
found := false
for _, account := range accounts {
if account.Name == accountName {
assertEqual(t, accountBalance, account.WorkingBalance.GetFloat64(), "balance for "+accountName)
found = true
}
}
if !found {
t.Errorf("account " + accountName + " was not found in result")
}
}
}
func AssertCategoriesAndAvailableEqual(ctx context.Context, t *testing.T, loc *time.Location, handler *Handler, budget *postgres.Budget) {
t.Helper()
t.Run("Categories and available balance", func(t *testing.T) {
t.Parallel()
resultDir := "../testdata/production-export/results"
files, err := os.ReadDir(resultDir)
if err != nil {
t.Errorf("could not load results: %s", err)
return
}
for _, file := range files {
if file.IsDir() {
continue
}
name := file.Name()[0 : len(file.Name())-5]
parts := strings.Split(name, "-")
if len(parts) != 2 {
continue
}
year, _ := strconv.Atoi(parts[0])
month, _ := strconv.Atoi(parts[1])
testCaseFile := filepath.Join(resultDir, file.Name())
handler.CheckAvailableBalance(ctx, t, testCaseFile, budget, Month{year, month})
}
})
}
type TestData struct {
AvailableBalance float64
Categories map[string]CategoryTestData
}
type CategoryTestData struct {
Available float64
Activity float64
Assigned float64
}
func (h Handler) CheckAvailableBalance(ctx context.Context, t *testing.T, testCaseFile string, budget *postgres.Budget, month Month) {
t.Helper()
t.Run(month.String(), func(t *testing.T) {
t.Parallel()
data, err := h.getBudgetingViewForMonth(ctx, *budget, month)
if err != nil {
t.Errorf("prepare budgeting: %s", err)
return
}
testDataFile, err := os.Open(testCaseFile)
if err != nil {
t.Errorf("could not load category test data: %s", err)
return
}
var testData TestData
dec := json.NewDecoder(testDataFile)
err = dec.Decode(&testData)
if err != nil {
t.Errorf("could not decode category test data: %s", err)
return
}
assertEqual(t, testData.AvailableBalance, data.AvailableBalance.GetFloat64(), "available balance")
for categoryName, categoryTestData := range testData.Categories {
found := false
for _, category := range data.Categories {
name := category.Group + " : " + category.Name
if name == categoryName {
assertEqual(t, categoryTestData.Activity, category.Activity.GetFloat64(), "activity for "+categoryName)
assertEqual(t, categoryTestData.Assigned, category.Assigned.GetFloat64(), "assigned for "+categoryName)
available := category.AvailableLastMonth
available.AddI(category.Activity).AddI(category.Assigned)
assertEqual(t, categoryTestData.Available, available.GetFloat64(), "available for "+categoryName)
found = true
}
}
if !found {
t.Errorf("category " + categoryName + " was not found in result")
}
}
})
}
func AssertEqualBool(t *testing.T, expected, actual bool, message string) {
t.Helper()
if expected == actual {
return
}
t.Errorf("%s: expected %v, got %v", message, expected, actual)
}
func assertEqual(t *testing.T, expected, actual float64, message string) {
t.Helper()
if expected == actual {
return
}
t.Errorf("%s: expected %f, got %f", message, expected, actual)
}
func (h Handler) DoYNABImport(ctx context.Context, t *testing.T, budget *postgres.Budget) {
t.Helper()
budgetID := budget.ID
ynab, err := postgres.NewYNABImport(ctx, h.Service.Queries, budgetID)
if err != nil {
fmt.Println(err)
t.Fail()
return
}
transactions, err := os.Open("../testdata/production-export/Register.tsv")
if err != nil {
fmt.Println(err)
t.Fail()
return
}
assignments, err := os.Open("../testdata/production-export/Budget.tsv")
if err != nil {
fmt.Println(err)
t.Fail()
return
}
err = ynab.ImportTransactions(ctx, transactions)
if err != nil {
fmt.Println(err)
t.Fail()
return
}
err = ynab.ImportAssignments(ctx, assignments)
if err != nil {
fmt.Println(err)
t.Fail()
return
}
}

71
server/month.go Normal file
View File

@ -0,0 +1,71 @@
package server
import (
"fmt"
"time"
)
type Month struct {
Year int
Month int
}
func NewFromTime(date time.Time) Month {
return Month{date.Year(), int(date.Month())}
}
func (m Month) String() string {
return fmt.Sprintf("%d-%d", m.Year, m.Month)
}
func (m Month) FirstOfMonth() time.Time {
return time.Date(m.Year, time.Month(m.Month), 1, 0, 0, 0, 0, time.Now().Location())
}
func (m Month) Previous() Month {
if m.Month == int(time.January) {
return Month{Year: m.Year - 1, Month: int(time.December)}
}
return Month{Year: m.Year, Month: m.Month - 1}
}
func (m Month) Next() Month {
if m.Month == int(time.December) {
return Month{Year: m.Year + 1, Month: int(time.January)}
}
return Month{Year: m.Year, Month: m.Month + 1}
}
func (m Month) InFuture(date time.Time) bool {
if m.Year < date.Year() {
return true
}
if m.Year > date.Year() {
return false
}
return time.Month(m.Month) < date.Month()
}
func (m Month) InPast(date time.Time) bool {
if m.Year > date.Year() {
return true
}
if m.Year < date.Year() {
return false
}
return time.Month(m.Month) > date.Month()
}
func (m Month) InPresent(date time.Time) bool {
if date.Year() != m.Year {
return false
}
return date.Month() == time.Month(m.Month)
}

46
server/month_test.go Normal file
View File

@ -0,0 +1,46 @@
package server_test
import (
"testing"
"time"
"git.javil.eu/jacob1123/budgeteer/server"
)
type TestCaseCompare struct {
Value server.Month
Date time.Time
InPast bool
InPresent bool
InFuture bool
}
func TestComparisons(t *testing.T) {
t.Parallel()
loc := time.Now().Location()
tests := []TestCaseCompare{
{server.Month{2022, 2}, time.Date(2022, 3, 1, 0, 0, 0, 0, loc), false, false, true},
{server.Month{2022, 3}, time.Date(2022, 3, 1, 0, 0, 0, 0, loc), false, true, false},
{server.Month{2022, 4}, time.Date(2022, 3, 1, 0, 0, 0, 0, loc), true, false, false},
{server.Month{2022, 2}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), false, false, true},
{server.Month{2022, 3}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), false, true, false},
{server.Month{2022, 4}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), true, false, false},
{server.Month{2021, 2}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), false, false, true},
{server.Month{2021, 3}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), false, false, true},
{server.Month{2021, 4}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), false, false, true},
{server.Month{2023, 2}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), true, false, false},
{server.Month{2023, 3}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), true, false, false},
{server.Month{2023, 4}, time.Date(2022, 3, 31, 0, 0, 0, 0, loc), true, false, false},
{server.Month{2021, 11}, time.Date(2021, 12, 1, 0, 0, 0, 0, loc), false, false, true},
}
for i := range tests { //nolint:paralleltest
test := tests[i]
t.Run(test.Date.Format("2006-01-02")+" is in of "+test.Value.String(), func(t *testing.T) {
t.Parallel()
server.AssertEqualBool(t, test.InPast, test.Value.InPast(test.Date), "in past")
server.AssertEqualBool(t, test.InPresent, test.Value.InPresent(test.Date), "in present")
server.AssertEqualBool(t, test.InFuture, test.Value.InFuture(test.Date), "in future")
})
}
}

101
server/reconcile.go Normal file
View File

@ -0,0 +1,101 @@
package server
import (
"database/sql"
"fmt"
"net/http"
"time"
"git.javil.eu/jacob1123/budgeteer/postgres"
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
)
type ReconcileTransactionsRequest struct {
TransactionIDs []uuid.UUID `json:"transactionIds"`
ReconcilationTransactionAmount string `json:"reconciliationTransactionAmount"`
}
type ReconcileTransactionsResponse struct {
Message string
ReconciliationTransaction *postgres.DisplayTransaction
}
func (h *Handler) reconcileTransactions(c echo.Context) error {
accountID := c.Param("accountid")
accountUUID, err := uuid.Parse(accountID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
var request ReconcileTransactionsRequest
err = c.Bind(&request)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("parse request: %w", err))
}
var amount numeric.Numeric
err = amount.Set(request.ReconcilationTransactionAmount)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("parse request: %w", err))
}
tx, err := h.Service.BeginTx(c.Request().Context(), &sql.TxOptions{})
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
db := h.Service.WithTx(tx)
for _, transactionID := range request.TransactionIDs {
err := db.SetTransactionReconciled(c.Request().Context(), transactionID)
if err != nil {
return fmt.Errorf("update transaction: %w", err)
}
}
reconciliationTransaction, err := h.CreateReconcilationTransaction(amount, accountUUID, db, c)
if err != nil {
return fmt.Errorf("insert new transaction: %w", err)
}
err = h.Service.SetLastReconciled(c.Request().Context(), accountUUID)
if err != nil {
return fmt.Errorf("set last reconciled: %w", err)
}
err = tx.Commit()
if err != nil {
return fmt.Errorf("commit: %w", err)
}
return c.JSON(http.StatusOK, ReconcileTransactionsResponse{
Message: fmt.Sprintf("Set status for %d transactions", len(request.TransactionIDs)),
ReconciliationTransaction: reconciliationTransaction,
})
}
func (*Handler) CreateReconcilationTransaction(amount numeric.Numeric, accountUUID uuid.UUID, db *postgres.Queries, c echo.Context) (*postgres.DisplayTransaction, error) {
if amount.IsZero() {
return nil, nil //nolint: nilnil
}
createTransaction := postgres.CreateTransactionParams{
Date: time.Now(),
Memo: "Reconciliation Transaction",
Amount: amount,
AccountID: accountUUID,
Status: "Reconciled",
}
transactionUUID, err := db.CreateTransaction(c.Request().Context(), createTransaction)
if err != nil {
return nil, fmt.Errorf("insert new transaction: %w", err)
}
transaction, err := db.GetTransaction(c.Request().Context(), transactionUUID)
if err != nil {
return nil, fmt.Errorf("get created transaction: %w", err)
}
return &transaction, nil
}

156
server/session.go Normal file
View File

@ -0,0 +1,156 @@
package server
import (
"context"
"fmt"
"net/http"
"git.javil.eu/jacob1123/budgeteer"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
)
const (
HeaderName = "Authorization"
Bearer = "Bearer "
ParamName = "token"
)
func MustGetToken(c echo.Context) budgeteer.Token { //nolint:ireturn
token := c.Get(ParamName)
if token, ok := token.(budgeteer.Token); ok {
return token
}
panic("Token is not a valid Token")
}
func (h *Handler) verifyLogin(c echo.Context) (budgeteer.Token, error) { //nolint:ireturn
tokenString := c.Request().Header.Get(HeaderName)
if len(tokenString) <= len(Bearer) {
return nil, echo.NewHTTPError(http.StatusUnauthorized, "no authorization header supplied")
}
tokenString = tokenString[7:]
token, err := h.TokenVerifier.VerifyToken(tokenString)
if err != nil {
return nil, fmt.Errorf("verify token '%s': %w", tokenString, err)
}
return token, nil
}
func (h *Handler) verifyLoginWithForbidden(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token, err := h.verifyLogin(c)
if err != nil {
// c.Header("WWW-Authenticate", "Bearer")
return echo.NewHTTPError(http.StatusForbidden, err)
}
c.Set(ParamName, token)
return next(c)
}
}
type loginInformation struct {
Password string `json:"password"`
User string `json:"user"`
}
func (h *Handler) loginPost(c echo.Context) error {
var login loginInformation
err := c.Bind(&login)
if err != nil {
return fmt.Errorf("parse payload: %w", err)
}
user, err := h.Service.GetUserByUsername(c.Request().Context(), login.User)
if err != nil {
return err
}
if err = h.CredentialsVerifier.Verify(login.Password, user.Password); err != nil {
return fmt.Errorf("verify password: %w", err)
}
token, err := h.TokenVerifier.CreateToken(&user)
if err != nil {
return fmt.Errorf("create token: %w", err)
}
go h.UpdateLastLogin(user.ID)
budgets, err := h.Service.GetBudgetsForUser(c.Request().Context(), user.ID)
if err != nil {
return err
}
return c.JSON(http.StatusOK, LoginResponse{token, user, budgets})
}
type LoginResponse struct {
Token string
User postgres.User
Budgets []postgres.Budget
}
type registerInformation struct {
Password string `json:"password"`
Email string `json:"email"`
Name string `json:"name"`
}
func (h *Handler) registerPost(c echo.Context) error {
var register registerInformation
err := c.Bind(&register)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "error parsing body")
}
if register.Email == "" || register.Password == "" || register.Name == "" {
return echo.NewHTTPError(http.StatusBadRequest, "e-mail, password and name are required")
}
_, err = h.Service.GetUserByUsername(c.Request().Context(), register.Email)
if err == nil {
return echo.NewHTTPError(http.StatusBadRequest, "email is already taken")
}
hash, err := h.CredentialsVerifier.Hash(register.Password)
if err != nil {
return fmt.Errorf("hash password: %w", err)
}
createUser := postgres.CreateUserParams{
Name: register.Name,
Password: hash,
Email: register.Email,
}
user, err := h.Service.CreateUser(c.Request().Context(), createUser)
if err != nil {
return err
}
token, err := h.TokenVerifier.CreateToken(&user)
if err != nil {
return fmt.Errorf("create token: %w", err)
}
go h.UpdateLastLogin(user.ID)
budgets, err := h.Service.GetBudgetsForUser(c.Request().Context(), user.ID)
if err != nil {
return err
}
return c.JSON(http.StatusOK, LoginResponse{token, user, budgets})
}
func (h *Handler) UpdateLastLogin(userID uuid.UUID) {
_, err := h.Service.UpdateLastLogin(context.Background(), userID)
if err != nil {
fmt.Printf("Error updating last login: %s", err)
}
}

166
server/transaction.go Normal file
View File

@ -0,0 +1,166 @@
package server
import (
"context"
"fmt"
"net/http"
"time"
"git.javil.eu/jacob1123/budgeteer/postgres"
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
)
type NewTransactionPayload struct {
Date JSONDate `json:"date"`
Payee struct {
ID uuid.NullUUID
Name string
Type string
} `json:"payee"`
CategoryID uuid.NullUUID `json:"categoryId"`
Memo string `json:"memo"`
Amount string `json:"amount"`
BudgetID uuid.UUID `json:"budgetId"`
AccountID uuid.UUID `json:"accountId"`
State string `json:"state"`
}
func (h *Handler) updateTransaction(c echo.Context) error {
var payload NewTransactionPayload
err := c.Bind(&payload)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
amount, err := numeric.Parse(payload.Amount)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("amount: %w", err))
}
transactionID := c.Param("transactionid")
transactionUUID, err := uuid.Parse(transactionID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "transactionid missing from URL")
}
return h.UpdateTransaction(payload, amount, transactionUUID, c)
}
func (h *Handler) newTransaction(c echo.Context) error {
var payload NewTransactionPayload
err := c.Bind(&payload)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
amount, err := numeric.Parse(payload.Amount)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("amount: %w", err))
}
newTransaction := postgres.CreateTransactionParams{
Memo: payload.Memo,
Date: time.Time(payload.Date),
Amount: amount,
Status: postgres.TransactionStatus(payload.State),
CategoryID: payload.CategoryID,
AccountID: payload.AccountID,
}
if payload.Payee.Type == "account" {
groupID, err := h.CreateTransferForOtherAccount(newTransaction, amount, payload, c)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err)
}
newTransaction.GroupID = groupID
} else {
payeeID, err := GetPayeeID(c.Request().Context(), payload, h)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("create payee: %w", err))
}
newTransaction.PayeeID = payeeID
}
transactionUUID, err := h.Service.CreateTransaction(c.Request().Context(), newTransaction)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("create transaction: %w", err))
}
transaction, err := h.Service.GetTransaction(c.Request().Context(), transactionUUID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("get transaction: %w", err))
}
return c.JSON(http.StatusOK, transaction)
}
func (h *Handler) UpdateTransaction(payload NewTransactionPayload, amount numeric.Numeric, transactionUUID uuid.UUID, c echo.Context) error {
if amount.IsZero() {
err := h.Service.DeleteTransaction(c.Request().Context(), transactionUUID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("delete transaction: %w", err))
}
return nil
}
editTransaction := postgres.UpdateTransactionParams{
Memo: payload.Memo,
Date: time.Time(payload.Date),
Amount: amount,
PayeeID: payload.Payee.ID,
CategoryID: payload.CategoryID,
ID: transactionUUID,
}
err := h.Service.UpdateTransaction(c.Request().Context(), editTransaction)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("edit transaction: %w", err))
}
transaction, err := h.Service.GetTransaction(c.Request().Context(), transactionUUID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("get transaction: %w", err))
}
return c.JSON(http.StatusOK, transaction)
}
func (h *Handler) CreateTransferForOtherAccount(newTransaction postgres.CreateTransactionParams, amount numeric.Numeric, payload NewTransactionPayload, c echo.Context) (uuid.NullUUID, error) {
newTransaction.GroupID = uuid.NullUUID{UUID: uuid.New(), Valid: true}
newTransaction.Amount = amount.Neg()
newTransaction.AccountID = payload.Payee.ID.UUID
// transfer does not need category. Either it's account is off-budget or no category was supplied.
newTransaction.CategoryID = uuid.NullUUID{}
_, err := h.Service.CreateTransaction(c.Request().Context(), newTransaction)
if err != nil {
return uuid.NullUUID{}, fmt.Errorf("create transfer transaction: %w", err)
}
return newTransaction.GroupID, nil
}
func GetPayeeID(context context.Context, payload NewTransactionPayload, h *Handler) (uuid.NullUUID, error) {
payeeID := payload.Payee.ID
if payeeID.Valid {
return payeeID, nil
}
if payload.Payee.Name == "" {
return uuid.NullUUID{}, nil
}
newPayee := postgres.CreatePayeeParams{
Name: payload.Payee.Name,
BudgetID: payload.BudgetID,
}
payee, err := h.Service.CreatePayee(context, newPayee)
if err != nil {
return uuid.NullUUID{}, fmt.Errorf("create payee: %w", err)
}
return uuid.NullUUID{UUID: payee.ID, Valid: true}, nil
}

37
server/util.go Normal file
View File

@ -0,0 +1,37 @@
package server
import (
"fmt"
"strconv"
"time"
"github.com/labstack/echo/v4"
)
func getDate(c echo.Context) (Month, error) {
var year, month int
yearString := c.Param("year")
monthString := c.Param("month")
if yearString == "" && monthString == "" {
return getFirstOfMonthTime(time.Now()), nil
}
year, err := strconv.Atoi(yearString)
if err != nil {
return Month{}, fmt.Errorf("parse year: %w", err)
}
month, err = strconv.Atoi(monthString)
if err != nil {
return Month{}, fmt.Errorf("parse month: %w", err)
}
return Month{year, month}, nil
}
func getFirstOfMonthTime(date time.Time) Month {
var monthM time.Month
year, monthM, _ := date.Date()
month := int(monthM)
return Month{year, month}
}

107
server/ynab-import.go Normal file
View File

@ -0,0 +1,107 @@
package server
import (
"fmt"
"net/http"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
)
func (h *Handler) importYNAB(c echo.Context) error {
budgetID := c.Param("budgetid")
if budgetID == "" {
return echo.NewHTTPError(http.StatusBadRequest, "no budget_id specified")
}
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
ynab, err := postgres.NewYNABImport(c.Request().Context(), h.Service.Queries, budgetUUID)
if err != nil {
return err
}
transactionsFile, err := c.FormFile("transactions")
if err != nil {
return fmt.Errorf("get transactions: %w", err)
}
transactions, err := transactionsFile.Open()
if err != nil {
return fmt.Errorf("open transactions: %w", err)
}
err = ynab.ImportTransactions(c.Request().Context(), transactions)
if err != nil {
return err
}
assignmentsFile, err := c.FormFile("assignments")
if err != nil {
return fmt.Errorf("get assignments: %w", err)
}
assignments, err := assignmentsFile.Open()
if err != nil {
return fmt.Errorf("open assignments: %w", err)
}
err = ynab.ImportAssignments(c.Request().Context(), assignments)
if err != nil {
return err
}
return nil
}
func (h *Handler) exportYNABTransactions(c echo.Context) error {
budgetID := c.Param("budgetid")
if budgetID == "" {
return echo.NewHTTPError(http.StatusBadRequest, "no budget_id specified")
}
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
ynab, err := postgres.NewYNABExport(c.Request().Context(), h.Service.Queries, budgetUUID)
if err != nil {
return err
}
err = ynab.ExportTransactions(c.Request().Context(), c.Response().Writer)
if err != nil {
return err
}
return nil
}
func (h *Handler) exportYNABAssignments(c echo.Context) error {
budgetID := c.Param("budgetid")
if budgetID == "" {
return echo.NewHTTPError(http.StatusBadRequest, "no budget_id specified")
}
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
}
ynab, err := postgres.NewYNABExport(c.Request().Context(), h.Service.Queries, budgetUUID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err)
}
err = ynab.ExportAssignments(c.Request().Context(), c.Response().Writer)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err)
}
return nil
}

View File

@ -7,9 +7,11 @@ packages:
queries: "postgres/queries/" queries: "postgres/queries/"
overrides: overrides:
- go_type: - go_type:
type: "Numeric" import: "git.javil.eu/jacob1123/budgeteer/postgres/numeric"
type: Numeric
db_type: "pg_catalog.numeric" db_type: "pg_catalog.numeric"
- go_type: - go_type:
type: "Numeric" import: "git.javil.eu/jacob1123/budgeteer/postgres/numeric"
type: Numeric
db_type: "pg_catalog.numeric" db_type: "pg_catalog.numeric"
nullable: true nullable: true

1
testdata Submodule

@ -0,0 +1 @@
Subproject commit 8de369b17a81f2e6ed079374ab35f868f259f9c1

Some files were not shown because too many files have changed in this diff Show More