@actual-app/core 26.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/.swcrc +11 -0
  2. package/bin/build-browser +40 -0
  3. package/bin/copy-migrations +9 -0
  4. package/db.sqlite +0 -0
  5. package/default-db.sqlite +0 -0
  6. package/migrations/.force-copy-windows +0 -0
  7. package/migrations/1548957970627_remove-db-version.sql +5 -0
  8. package/migrations/1550601598648_payees.sql +23 -0
  9. package/migrations/1555786194328_remove_category_group_unique.sql +25 -0
  10. package/migrations/1561751833510_indexes.sql +7 -0
  11. package/migrations/1567699552727_budget.sql +38 -0
  12. package/migrations/1582384163573_cleared.sql +6 -0
  13. package/migrations/1597756566448_rules.sql +10 -0
  14. package/migrations/1608652596043_parent_field.sql +13 -0
  15. package/migrations/1608652596044_trans_views.sql +56 -0
  16. package/migrations/1612625548236_optimize.sql +7 -0
  17. package/migrations/1614782639336_trans_views2.sql +33 -0
  18. package/migrations/1615745967948_meta.sql +10 -0
  19. package/migrations/1616167010796_accounts_order.sql +5 -0
  20. package/migrations/1618975177358_schedules.sql +28 -0
  21. package/migrations/1632571489012_remove_cache.js +136 -0
  22. package/migrations/1679728867040_rules_conditions.sql +5 -0
  23. package/migrations/1681115033845_add_schedule_name.sql +5 -0
  24. package/migrations/1682974838138_remove_payee_rules.sql +5 -0
  25. package/migrations/1685007876842_add_category_hidden.sql +6 -0
  26. package/migrations/1686139660866_remove_account_type.sql +5 -0
  27. package/migrations/1688749527273_transaction_filters.sql +10 -0
  28. package/migrations/1688841238000_add_account_type.sql +5 -0
  29. package/migrations/1691233396000_add_schedule_next_date_tombstone.sql +5 -0
  30. package/migrations/1694438752000_add_goal_targets.sql +7 -0
  31. package/migrations/1697046240000_add_reconciled.sql +5 -0
  32. package/migrations/1704572023730_add_account_sync_source.sql +5 -0
  33. package/migrations/1704572023731_add_missing_goCardless_sync_source.sql +9 -0
  34. package/migrations/1707267033000_reports.sql +28 -0
  35. package/migrations/1712784523000_unhide_input_group.sql +8 -0
  36. package/migrations/1716359441000_include_current.sql +5 -0
  37. package/migrations/1720310586000_link_transfer_schedules.sql +19 -0
  38. package/migrations/1720664867241_add_payee_favorite.sql +5 -0
  39. package/migrations/1720665000000_goal_context.sql +6 -0
  40. package/migrations/1722717601000_reports_move_selected_categories.js +55 -0
  41. package/migrations/1722804019000_create_dashboard_table.js +69 -0
  42. package/migrations/1723665565000_prefs.js +59 -0
  43. package/migrations/1730744182000_fix_dashboard_table.sql +7 -0
  44. package/migrations/1736640000000_custom_report_sorting.sql +7 -0
  45. package/migrations/1737158400000_add_learn_categories_to_payees.sql +5 -0
  46. package/migrations/1738491452000_sorting_rename.sql +13 -0
  47. package/migrations/1739139550000_bank_sync_page.sql +7 -0
  48. package/migrations/1740506588539_add_last_reconciled_at.sql +5 -0
  49. package/migrations/1745425408000_update_budgetType_pref.sql +7 -0
  50. package/migrations/1749799110000_add_tags.sql +10 -0
  51. package/migrations/1749799110001_tags_tombstone.sql +5 -0
  52. package/migrations/1754611200000_add_category_template_settings.sql +5 -0
  53. package/migrations/1759260219000_add_trim_interval_report_setting.sql +6 -0
  54. package/migrations/1759842823172_add_isGlobal_to_preferences.sql +1 -0
  55. package/migrations/1762178745667_rename_csv_skip_lines_pref.sql +8 -0
  56. package/migrations/1765518577215_multiple_dashboards.js +30 -0
  57. package/migrations/1768872504000_add_payee_locations.sql +21 -0
  58. package/package.json +128 -0
  59. package/src/mocks/arbitrary-schema.ts +162 -0
  60. package/src/mocks/budget.ts +901 -0
  61. package/src/mocks/files/8859-1.qfx +63 -0
  62. package/src/mocks/files/best.data-ever$.QFX +124 -0
  63. package/src/mocks/files/big.data.QiF +91 -0
  64. package/src/mocks/files/budgets/.commit-to-git +0 -0
  65. package/src/mocks/files/camt/camt.053.payee-memo.xml +127 -0
  66. package/src/mocks/files/camt/camt.053.xml +463 -0
  67. package/src/mocks/files/credit-card.ofx +11 -0
  68. package/src/mocks/files/data-multi-decimal.ofx +64 -0
  69. package/src/mocks/files/data-payee-memo.ofx +75 -0
  70. package/src/mocks/files/data-payee-memo.qif +17 -0
  71. package/src/mocks/files/data.ofx +124 -0
  72. package/src/mocks/files/data.qfx +124 -0
  73. package/src/mocks/files/data.qif +91 -0
  74. package/src/mocks/files/default-budget-template/db.sqlite +0 -0
  75. package/src/mocks/files/default-budget-template/metadata.json +6 -0
  76. package/src/mocks/files/html-vals.qfx +17 -0
  77. package/src/mocks/index.ts +221 -0
  78. package/src/mocks/migrations/1508717984291_up_add-poop.sql +13 -0
  79. package/src/mocks/migrations/1508718036311_up_modify-poop.sql +2 -0
  80. package/src/mocks/migrations/1508727787513_remove-is_income.sql +15 -0
  81. package/src/mocks/random.ts +16 -0
  82. package/src/mocks/setup.ts +180 -0
  83. package/src/mocks/spreadsheet.ts +101 -0
  84. package/src/mocks/util.ts +82 -0
  85. package/src/platform/client/connection/README.md +3 -0
  86. package/src/platform/client/connection/__mocks__/index.ts +67 -0
  87. package/src/platform/client/connection/index-types.ts +95 -0
  88. package/src/platform/client/connection/index.browser.ts +213 -0
  89. package/src/platform/client/connection/index.ts +155 -0
  90. package/src/platform/client/undo/index.ts +59 -0
  91. package/src/platform/exceptions/__mocks__/index.ts +7 -0
  92. package/src/platform/exceptions/index.ts +9 -0
  93. package/src/platform/server/asyncStorage/__mocks__/index.ts +50 -0
  94. package/src/platform/server/asyncStorage/index-types.ts +35 -0
  95. package/src/platform/server/asyncStorage/index.api.ts +2 -0
  96. package/src/platform/server/asyncStorage/index.electron.ts +88 -0
  97. package/src/platform/server/asyncStorage/index.ts +126 -0
  98. package/src/platform/server/connection/README.md +3 -0
  99. package/src/platform/server/connection/__mocks__/index.ts +15 -0
  100. package/src/platform/server/connection/index-types.ts +20 -0
  101. package/src/platform/server/connection/index.api.ts +13 -0
  102. package/src/platform/server/connection/index.electron.ts +102 -0
  103. package/src/platform/server/connection/index.ts +154 -0
  104. package/src/platform/server/fetch/__mocks__/index.ts +3 -0
  105. package/src/platform/server/fetch/index.api.ts +1 -0
  106. package/src/platform/server/fetch/index.electron.ts +18 -0
  107. package/src/platform/server/fetch/index.ts +20 -0
  108. package/src/platform/server/fs/index.api.ts +198 -0
  109. package/src/platform/server/fs/index.electron.ts +208 -0
  110. package/src/platform/server/fs/index.test.ts +117 -0
  111. package/src/platform/server/fs/index.ts +416 -0
  112. package/src/platform/server/fs/path-join.api.ts +1 -0
  113. package/src/platform/server/fs/path-join.electron.ts +1 -0
  114. package/src/platform/server/fs/path-join.ts +97 -0
  115. package/src/platform/server/fs/shared.ts +33 -0
  116. package/src/platform/server/indexeddb/index.ts +115 -0
  117. package/src/platform/server/log/index.ts +43 -0
  118. package/src/platform/server/sqlite/index.api.ts +2 -0
  119. package/src/platform/server/sqlite/index.electron.ts +134 -0
  120. package/src/platform/server/sqlite/index.test.ts +108 -0
  121. package/src/platform/server/sqlite/index.ts +241 -0
  122. package/src/platform/server/sqlite/normalise.ts +9 -0
  123. package/src/platform/server/sqlite/unicodeLike.test.ts +58 -0
  124. package/src/platform/server/sqlite/unicodeLike.ts +31 -0
  125. package/src/server/__mocks__/post.ts +9 -0
  126. package/src/server/__snapshots__/main.test.ts.snap +199 -0
  127. package/src/server/__snapshots__/sheet.test.ts.snap +9 -0
  128. package/src/server/accounts/__snapshots__/sync.test.ts.snap +136 -0
  129. package/src/server/accounts/app-bank-sync.test.ts +136 -0
  130. package/src/server/accounts/app.ts +1294 -0
  131. package/src/server/accounts/link.ts +25 -0
  132. package/src/server/accounts/payees.ts +36 -0
  133. package/src/server/accounts/sync.test.ts +679 -0
  134. package/src/server/accounts/sync.ts +1168 -0
  135. package/src/server/accounts/title/index.ts +60 -0
  136. package/src/server/accounts/title/lower-case.ts +93 -0
  137. package/src/server/accounts/title/specials.ts +21 -0
  138. package/src/server/admin/app.ts +241 -0
  139. package/src/server/api-models.ts +244 -0
  140. package/src/server/api.test.ts +36 -0
  141. package/src/server/api.ts +1030 -0
  142. package/src/server/app.ts +91 -0
  143. package/src/server/aql/compiler.test.ts +966 -0
  144. package/src/server/aql/compiler.ts +1222 -0
  145. package/src/server/aql/exec.test.ts +289 -0
  146. package/src/server/aql/exec.ts +128 -0
  147. package/src/server/aql/index.ts +41 -0
  148. package/src/server/aql/schema/executors.test.ts +420 -0
  149. package/src/server/aql/schema/executors.ts +345 -0
  150. package/src/server/aql/schema/index.test.ts +67 -0
  151. package/src/server/aql/schema/index.ts +409 -0
  152. package/src/server/aql/schema-helpers.test.ts +242 -0
  153. package/src/server/aql/schema-helpers.ts +208 -0
  154. package/src/server/aql/views.test.ts +62 -0
  155. package/src/server/aql/views.ts +57 -0
  156. package/src/server/auth/app.ts +387 -0
  157. package/src/server/bench.ts +29 -0
  158. package/src/server/budget/actions.ts +686 -0
  159. package/src/server/budget/app.ts +469 -0
  160. package/src/server/budget/base.test.ts +340 -0
  161. package/src/server/budget/base.ts +339 -0
  162. package/src/server/budget/category-template-context.test.ts +1658 -0
  163. package/src/server/budget/category-template-context.ts +862 -0
  164. package/src/server/budget/cleanup-template.pegjs +27 -0
  165. package/src/server/budget/cleanup-template.ts +408 -0
  166. package/src/server/budget/envelope.ts +403 -0
  167. package/src/server/budget/goal-template.pegjs +110 -0
  168. package/src/server/budget/goal-template.ts +309 -0
  169. package/src/server/budget/report.ts +308 -0
  170. package/src/server/budget/schedule-template.test.ts +184 -0
  171. package/src/server/budget/schedule-template.ts +351 -0
  172. package/src/server/budget/statements.ts +60 -0
  173. package/src/server/budget/template-notes.test.ts +393 -0
  174. package/src/server/budget/template-notes.ts +323 -0
  175. package/src/server/budget/util.ts +25 -0
  176. package/src/server/budgetfiles/__snapshots__/backups.test.ts.snap +101 -0
  177. package/src/server/budgetfiles/app.ts +672 -0
  178. package/src/server/budgetfiles/backups.test.ts +79 -0
  179. package/src/server/budgetfiles/backups.ts +251 -0
  180. package/src/server/cloud-storage.ts +467 -0
  181. package/src/server/dashboard/app.ts +373 -0
  182. package/src/server/db/__snapshots__/index.test.ts.snap +271 -0
  183. package/src/server/db/index.test.ts +300 -0
  184. package/src/server/db/index.ts +855 -0
  185. package/src/server/db/mappings.ts +59 -0
  186. package/src/server/db/sort.ts +58 -0
  187. package/src/server/db/types/index.ts +342 -0
  188. package/src/server/db/util.ts +36 -0
  189. package/src/server/encryption/app.ts +133 -0
  190. package/src/server/encryption/encryption-internals.api.ts +2 -0
  191. package/src/server/encryption/encryption-internals.electron.ts +89 -0
  192. package/src/server/encryption/encryption-internals.ts +109 -0
  193. package/src/server/encryption/encryption.test.ts +19 -0
  194. package/src/server/encryption/index.test.ts +19 -0
  195. package/src/server/encryption/index.ts +89 -0
  196. package/src/server/errors.ts +110 -0
  197. package/src/server/filters/app.ts +191 -0
  198. package/src/server/importers/actual.ts +49 -0
  199. package/src/server/importers/index.ts +58 -0
  200. package/src/server/importers/ynab4-types.ts +163 -0
  201. package/src/server/importers/ynab4.ts +470 -0
  202. package/src/server/importers/ynab5-types.ts +290 -0
  203. package/src/server/importers/ynab5.ts +1193 -0
  204. package/src/server/main-app.ts +25 -0
  205. package/src/server/main.test.ts +392 -0
  206. package/src/server/main.ts +336 -0
  207. package/src/server/migrate/__snapshots__/migrations.test.ts.snap +17 -0
  208. package/src/server/migrate/cli.ts +100 -0
  209. package/src/server/migrate/migrations.test.ts +81 -0
  210. package/src/server/migrate/migrations.ts +192 -0
  211. package/src/server/models.ts +184 -0
  212. package/src/server/mutators.ts +139 -0
  213. package/src/server/notes/app.ts +18 -0
  214. package/src/server/payees/app.ts +351 -0
  215. package/src/server/polyfills.ts +26 -0
  216. package/src/server/post.ts +219 -0
  217. package/src/server/preferences/app.ts +249 -0
  218. package/src/server/prefs.ts +91 -0
  219. package/src/server/reports/app.ts +187 -0
  220. package/src/server/rules/action.ts +344 -0
  221. package/src/server/rules/app.ts +193 -0
  222. package/src/server/rules/condition.ts +436 -0
  223. package/src/server/rules/customFunctions.ts +61 -0
  224. package/src/server/rules/formula-action.test.ts +175 -0
  225. package/src/server/rules/handlebars-helpers.ts +131 -0
  226. package/src/server/rules/index.test.ts +1095 -0
  227. package/src/server/rules/index.ts +22 -0
  228. package/src/server/rules/rule-indexer.ts +89 -0
  229. package/src/server/rules/rule-utils.ts +274 -0
  230. package/src/server/rules/rule.ts +193 -0
  231. package/src/server/schedules/app.test.ts +502 -0
  232. package/src/server/schedules/app.ts +644 -0
  233. package/src/server/schedules/find-schedules.ts +391 -0
  234. package/src/server/server-config.ts +59 -0
  235. package/src/server/sheet.test.ts +101 -0
  236. package/src/server/sheet.ts +280 -0
  237. package/src/server/spreadsheet/__snapshots__/spreadsheet.test.ts.snap +5 -0
  238. package/src/server/spreadsheet/app.ts +54 -0
  239. package/src/server/spreadsheet/globals.ts +13 -0
  240. package/src/server/spreadsheet/graph-data-structure.ts +165 -0
  241. package/src/server/spreadsheet/scratch +60 -0
  242. package/src/server/spreadsheet/spreadsheet.test.ts +191 -0
  243. package/src/server/spreadsheet/spreadsheet.ts +523 -0
  244. package/src/server/spreadsheet/util.ts +15 -0
  245. package/src/server/sql/init.sql +88 -0
  246. package/src/server/sync/__snapshots__/sync.test.ts.snap +31 -0
  247. package/src/server/sync/app.ts +29 -0
  248. package/src/server/sync/encoder.ts +129 -0
  249. package/src/server/sync/index.ts +820 -0
  250. package/src/server/sync/make-test-message.ts +19 -0
  251. package/src/server/sync/migrate.test.ts +169 -0
  252. package/src/server/sync/migrate.ts +48 -0
  253. package/src/server/sync/repair.ts +39 -0
  254. package/src/server/sync/reset.ts +91 -0
  255. package/src/server/sync/sync.property.test.ts +385 -0
  256. package/src/server/sync/sync.test.ts +349 -0
  257. package/src/server/sync/utils.ts +3 -0
  258. package/src/server/tags/app.ts +101 -0
  259. package/src/server/tests/mockData.json +9352 -0
  260. package/src/server/tests/mockSyncServer.ts +119 -0
  261. package/src/server/tools/app.ts +152 -0
  262. package/src/server/transactions/__snapshots__/transaction-rules.test.ts.snap +173 -0
  263. package/src/server/transactions/__snapshots__/transfer.test.ts.snap +655 -0
  264. package/src/server/transactions/app.ts +136 -0
  265. package/src/server/transactions/export/export-to-csv.ts +132 -0
  266. package/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap +1582 -0
  267. package/src/server/transactions/import/ofx2json.test.ts +33 -0
  268. package/src/server/transactions/import/ofx2json.ts +157 -0
  269. package/src/server/transactions/import/parse-file.test.ts +224 -0
  270. package/src/server/transactions/import/parse-file.ts +286 -0
  271. package/src/server/transactions/import/qif2json.ts +110 -0
  272. package/src/server/transactions/import/xmlcamt2json.ts +168 -0
  273. package/src/server/transactions/index.ts +196 -0
  274. package/src/server/transactions/merge.test.ts +370 -0
  275. package/src/server/transactions/merge.ts +139 -0
  276. package/src/server/transactions/transaction-rules.test.ts +994 -0
  277. package/src/server/transactions/transaction-rules.ts +1038 -0
  278. package/src/server/transactions/transfer.test.ts +221 -0
  279. package/src/server/transactions/transfer.ts +173 -0
  280. package/src/server/undo.ts +271 -0
  281. package/src/server/update.ts +37 -0
  282. package/src/server/util/budget-name.ts +61 -0
  283. package/src/server/util/custom-sync-mapping.ts +48 -0
  284. package/src/server/util/rschedule.ts +9 -0
  285. package/src/shared/__mocks__/platform.ts +7 -0
  286. package/src/shared/__snapshots__/months.test.ts.snap +21 -0
  287. package/src/shared/arithmetic.test.ts +112 -0
  288. package/src/shared/arithmetic.ts +170 -0
  289. package/src/shared/async.test.ts +135 -0
  290. package/src/shared/async.ts +76 -0
  291. package/src/shared/constants.ts +5 -0
  292. package/src/shared/currencies.ts +70 -0
  293. package/src/shared/dashboard.ts +260 -0
  294. package/src/shared/environment.ts +18 -0
  295. package/src/shared/errors.ts +195 -0
  296. package/src/shared/locale.ts +27 -0
  297. package/src/shared/location-utils.test.ts +69 -0
  298. package/src/shared/location-utils.ts +49 -0
  299. package/src/shared/months.test.ts +5 -0
  300. package/src/shared/months.ts +485 -0
  301. package/src/shared/normalisation.ts +6 -0
  302. package/src/shared/platform.electron.ts +21 -0
  303. package/src/shared/platform.ts +20 -0
  304. package/src/shared/query.ts +176 -0
  305. package/src/shared/rules.test.ts +56 -0
  306. package/src/shared/rules.ts +371 -0
  307. package/src/shared/schedules.test.ts +570 -0
  308. package/src/shared/schedules.ts +560 -0
  309. package/src/shared/test-helpers.ts +156 -0
  310. package/src/shared/transactions.test.ts +275 -0
  311. package/src/shared/transactions.ts +433 -0
  312. package/src/shared/transfer.test.ts +75 -0
  313. package/src/shared/transfer.ts +16 -0
  314. package/src/shared/user.ts +4 -0
  315. package/src/shared/util.test.ts +240 -0
  316. package/src/shared/util.ts +633 -0
  317. package/src/types/api-handlers.ts +287 -0
  318. package/src/types/budget.ts +8 -0
  319. package/src/types/file.ts +47 -0
  320. package/src/types/handlers.ts +46 -0
  321. package/src/types/models/account.ts +24 -0
  322. package/src/types/models/bank-sync.ts +23 -0
  323. package/src/types/models/bank.ts +6 -0
  324. package/src/types/models/category-group.ts +11 -0
  325. package/src/types/models/category.ts +13 -0
  326. package/src/types/models/dashboard.ts +199 -0
  327. package/src/types/models/gocardless.ts +84 -0
  328. package/src/types/models/import-transaction.ts +56 -0
  329. package/src/types/models/index.ts +23 -0
  330. package/src/types/models/nearby-payee.ts +7 -0
  331. package/src/types/models/note.ts +4 -0
  332. package/src/types/models/openid.ts +8 -0
  333. package/src/types/models/payee-location.ts +8 -0
  334. package/src/types/models/payee.ts +10 -0
  335. package/src/types/models/pluggyai.ts +19 -0
  336. package/src/types/models/reports.ts +144 -0
  337. package/src/types/models/rule.ts +174 -0
  338. package/src/types/models/schedule.ts +49 -0
  339. package/src/types/models/simplefin.ts +28 -0
  340. package/src/types/models/tags.ts +6 -0
  341. package/src/types/models/templates.ts +135 -0
  342. package/src/types/models/transaction-filter.ts +9 -0
  343. package/src/types/models/transaction.ts +39 -0
  344. package/src/types/models/user-access.ts +10 -0
  345. package/src/types/models/user.ts +25 -0
  346. package/src/types/prefs.ts +167 -0
  347. package/src/types/server-events.ts +86 -0
  348. package/src/types/server-handlers.ts +27 -0
  349. package/src/types/util.ts +26 -0
  350. package/tsconfig.json +34 -0
  351. package/typings/pegjs.ts +1 -0
  352. package/typings/process-worker.ts +12 -0
  353. package/typings/vite-plugin-peggy-loader.ts +1 -0
  354. package/typings/window.ts +62 -0
  355. package/vite.config.ts +109 -0
  356. package/vite.desktop.config.ts +59 -0
  357. package/vitest.config.ts +43 -0
  358. package/vitest.web.config.ts +38 -0
@@ -0,0 +1,58 @@
1
+ // @ts-strict-ignore
2
+ import { logger } from '../../platform/server/log';
3
+ import { handlers } from '../main';
4
+
5
+ import { importActual } from './actual';
6
+ import * as YNAB4 from './ynab4';
7
+ import * as YNAB5 from './ynab5';
8
+
9
+ export type ImportableBudgetType = 'ynab4' | 'ynab5' | 'actual';
10
+
11
+ type Importer = {
12
+ parseFile(buffer: Buffer): unknown;
13
+ getBudgetName(filepath: string, data: unknown): string | null;
14
+ doImport(data: unknown): Promise<void>;
15
+ };
16
+
17
+ const importers: Record<Exclude<ImportableBudgetType, 'actual'>, Importer> = {
18
+ ynab4: YNAB4,
19
+ ynab5: YNAB5,
20
+ };
21
+
22
+ export async function handleBudgetImport(
23
+ type: ImportableBudgetType,
24
+ filepath: string,
25
+ buffer: Buffer,
26
+ ) {
27
+ if (type === 'actual') {
28
+ return importActual(filepath, buffer);
29
+ }
30
+ const importer = importers[type];
31
+ try {
32
+ let data;
33
+ let budgetName: string;
34
+ try {
35
+ data = importer.parseFile(buffer);
36
+ budgetName = importer.getBudgetName(filepath, data);
37
+ } catch (e) {
38
+ logger.error('failed to parse file', e);
39
+ }
40
+ if (!budgetName) {
41
+ return { error: 'not-' + type };
42
+ }
43
+
44
+ try {
45
+ await handlers['api/start-import']({ budgetName });
46
+ } catch (e) {
47
+ logger.error('failed to start import', e);
48
+ return { error: 'unknown' };
49
+ }
50
+ await importer.doImport(data);
51
+ } catch (e) {
52
+ await handlers['api/abort-import']();
53
+ logger.error('failed to run import', e);
54
+ return { error: 'unknown' };
55
+ }
56
+
57
+ await handlers['api/finish-import']();
58
+ }
@@ -0,0 +1,163 @@
1
+ export type YFull = {
2
+ masterCategories: MasterCategory[];
3
+ payees: Payee[];
4
+ monthlyBudgets: MonthlyBudget[];
5
+ fileMetaData: FileMetaData;
6
+ transactions: Transaction[];
7
+ scheduledTransactions: ScheduledTransaction[];
8
+ // accountMappings: [];
9
+ budgetMetaData: BudgetMetaData;
10
+ accounts: Account[];
11
+ };
12
+
13
+ export type MasterCategory = {
14
+ entityType: string;
15
+ expanded: boolean;
16
+ note?: string;
17
+ name: string;
18
+ type: string;
19
+ deleteable: boolean;
20
+ subCategories?: SubCategory[];
21
+ entityVersion: string;
22
+ entityId: string;
23
+ sortableIndex: number;
24
+
25
+ // speculative
26
+ isTombstone?: boolean;
27
+ };
28
+
29
+ export type SubCategory = {
30
+ entityType: string;
31
+ name: string;
32
+ note?: string;
33
+ type: string;
34
+ // cachedBalance: null;
35
+ masterCategoryId: string;
36
+ entityVersion: string;
37
+ entityId: string;
38
+ sortableIndex: number;
39
+
40
+ // speculative
41
+ isTombstone?: boolean;
42
+ };
43
+
44
+ export type Payee = {
45
+ entityType: string;
46
+ autoFillCategoryId?: string;
47
+ autoFillAmount: number;
48
+ name: string;
49
+ renameConditions?: RenameCondition[];
50
+ autoFillMemo?: string;
51
+ targetAccountId?: string;
52
+ // locations: null;
53
+ enabled: boolean;
54
+ entityVersion: string;
55
+ entityId: string;
56
+
57
+ // speculative
58
+ isTombstone?: boolean;
59
+ };
60
+
61
+ export type RenameCondition = {
62
+ entityType: string;
63
+ parentPayeeId: string;
64
+ operator: string;
65
+ operand: string;
66
+ entityVersion: string;
67
+ entityId: string;
68
+ };
69
+
70
+ export type MonthlyBudget = {
71
+ entityType: string;
72
+ monthlySubCategoryBudgets: MonthlySubCategoryBudget[];
73
+ month: string;
74
+ entityVersion: string;
75
+ entityId: string;
76
+ };
77
+
78
+ export type MonthlySubCategoryBudget = {
79
+ entityType: string;
80
+ categoryId: string;
81
+ budgeted: number;
82
+ overspendingHandling?: string | undefined | null;
83
+ entityVersion: string;
84
+ entityId: string;
85
+ parentMonthlyBudgetId: string;
86
+
87
+ // speculative
88
+ isTombstone?: boolean;
89
+ };
90
+
91
+ export type FileMetaData = {
92
+ entityType: string;
93
+ budgetDataVersion: string;
94
+ currentKnowledge: string;
95
+ };
96
+
97
+ export type Transaction = {
98
+ entityType: string;
99
+ entityId: string;
100
+ categoryId: string;
101
+ payeeId: string;
102
+ amount: number;
103
+ date: string;
104
+ accountId: string;
105
+ entityVersion: string;
106
+ cleared: string;
107
+ accepted: boolean;
108
+ isTombstone?: boolean;
109
+ memo?: string;
110
+ dateEnteredFromSchedule?: string;
111
+ // speculative:
112
+ subTransactions?: SubTransaction[];
113
+ transferTransactionId?: string;
114
+ targetAccountId?: string;
115
+ };
116
+
117
+ // speculative, not in the test data
118
+ export type SubTransaction = Omit<Transaction, 'subTransactions'>;
119
+
120
+ export type ScheduledTransaction = {
121
+ entityType: string;
122
+ entityId: string;
123
+ categoryId: string;
124
+ payeeId: string;
125
+ amount: number;
126
+ date: string;
127
+ isTombstone?: boolean;
128
+ accountId: string;
129
+ entityVersion: string;
130
+ memo: string;
131
+ twiceAMonthStartDay: number;
132
+ cleared: string;
133
+ frequency: string;
134
+ accepted: boolean;
135
+ };
136
+
137
+ export type BudgetMetaData = {
138
+ entityType: string;
139
+ strictBudget: string;
140
+ currencyISOSymbol?: string;
141
+ entityVersion: string;
142
+ currencyLocale: string;
143
+ budgetType: string;
144
+ dateLocale: string;
145
+ entityId: string;
146
+ };
147
+
148
+ export type Account = {
149
+ entityType: string;
150
+ // lastReconciledDate: null;
151
+ lastEnteredCheckNumber: number;
152
+ lastReconciledBalance: number;
153
+ accountType: string;
154
+ hidden: boolean;
155
+ sortableIndex: number;
156
+ onBudget: boolean;
157
+ accountName: string;
158
+ entityVersion: string;
159
+ entityId: string;
160
+
161
+ // speculative
162
+ isTombstone?: boolean;
163
+ };
@@ -0,0 +1,470 @@
1
+ // @ts-strict-ignore
2
+ import AdmZip from 'adm-zip';
3
+ import normalizePathSep from 'slash';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+
6
+ import { logger } from '../../platform/server/log';
7
+ import * as monthUtils from '../../shared/months';
8
+ import { amountToInteger, groupBy, sortByKey } from '../../shared/util';
9
+ import { send } from '../main-app';
10
+
11
+ import type * as YNAB4 from './ynab4-types';
12
+
13
+ // Importer
14
+
15
+ async function importAccounts(
16
+ data: YNAB4.YFull,
17
+ entityIdMap: Map<string, string>,
18
+ ) {
19
+ const accounts = sortByKey(data.accounts, 'sortableIndex');
20
+
21
+ return Promise.all(
22
+ accounts.map(async account => {
23
+ if (!account.isTombstone) {
24
+ const id = await send('api/account-create', {
25
+ account: {
26
+ name: account.accountName,
27
+ offbudget: account.onBudget ? false : true,
28
+ closed: account.hidden ? true : false,
29
+ },
30
+ });
31
+ entityIdMap.set(account.entityId, id);
32
+ }
33
+ }),
34
+ );
35
+ }
36
+
37
+ async function importCategories(
38
+ data: YNAB4.YFull,
39
+ entityIdMap: Map<string, string>,
40
+ ) {
41
+ const masterCategories = sortByKey(data.masterCategories, 'sortableIndex');
42
+
43
+ await Promise.all(
44
+ masterCategories.map(async masterCategory => {
45
+ if (
46
+ masterCategory.type === 'OUTFLOW' &&
47
+ !masterCategory.isTombstone &&
48
+ masterCategory.subCategories &&
49
+ masterCategory.subCategories.some(cat => !cat.isTombstone)
50
+ ) {
51
+ const id = await send('api/category-group-create', {
52
+ group: {
53
+ name: masterCategory.name,
54
+ is_income: false,
55
+ },
56
+ });
57
+ entityIdMap.set(masterCategory.entityId, id);
58
+ if (masterCategory.note) {
59
+ void send('notes-save', {
60
+ id,
61
+ note: masterCategory.note,
62
+ });
63
+ }
64
+
65
+ if (masterCategory.subCategories) {
66
+ const subCategories = sortByKey(
67
+ masterCategory.subCategories,
68
+ 'sortableIndex',
69
+ );
70
+ subCategories.reverse();
71
+
72
+ // This can't be done in parallel because sort order depends
73
+ // on insertion order
74
+ for (const category of subCategories) {
75
+ if (!category.isTombstone) {
76
+ let categoryName = category.name;
77
+
78
+ // Hidden categories have the parent category entity id
79
+ // appended to the end of the sub category name.
80
+ // The format is 'MasterCategory ` SubCategory ` entityId'.
81
+ // Remove the id to shorten the name.
82
+ if (masterCategory.name === 'Hidden Categories') {
83
+ const categoryNameParts = categoryName.split(' ` ');
84
+
85
+ // Remove the last part, which is the entityId.
86
+ categoryNameParts.pop();
87
+
88
+ // Join the remaining parts with a slash between them.
89
+ categoryName = categoryNameParts.join('/').trim();
90
+ }
91
+
92
+ const id = await send('api/category-create', {
93
+ category: {
94
+ name: categoryName,
95
+ group_id: entityIdMap.get(category.masterCategoryId),
96
+ },
97
+ });
98
+ entityIdMap.set(category.entityId, id);
99
+ if (category.note) {
100
+ void send('notes-save', {
101
+ id,
102
+ note: category.note,
103
+ });
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }),
110
+ );
111
+ }
112
+
113
+ async function importPayees(
114
+ data: YNAB4.YFull,
115
+ entityIdMap: Map<string, string>,
116
+ ) {
117
+ for (const payee of data.payees) {
118
+ if (!payee.isTombstone) {
119
+ const id = await send('api/payee-create', {
120
+ payee: {
121
+ name: payee.name,
122
+ transfer_acct: entityIdMap.get(payee.targetAccountId) || null,
123
+ },
124
+ });
125
+
126
+ // TODO: import payee rules
127
+
128
+ entityIdMap.set(payee.entityId, id);
129
+ }
130
+ }
131
+ }
132
+
133
+ async function importTransactions(
134
+ data: YNAB4.YFull,
135
+ entityIdMap: Map<string, string>,
136
+ ) {
137
+ const categories = await send('api/categories-get', {
138
+ grouped: false,
139
+ });
140
+ const incomeCategoryId: string = categories.find(
141
+ cat => cat.name === 'Income',
142
+ ).id;
143
+ const accounts = await send('api/accounts-get');
144
+ const payees = await send('api/payees-get');
145
+
146
+ function getCategory(id: string) {
147
+ if (id == null || id === 'Category/__Split__') {
148
+ return null;
149
+ } else if (
150
+ id === 'Category/__ImmediateIncome__' ||
151
+ id === 'Category/__DeferredIncome__'
152
+ ) {
153
+ return incomeCategoryId;
154
+ }
155
+ return entityIdMap.get(id);
156
+ }
157
+
158
+ function isOffBudget(acctId: string) {
159
+ const acct = accounts.find(acct => acct.id === acctId);
160
+ if (!acct) {
161
+ throw new Error('Could not find account for transaction when importing');
162
+ }
163
+ return acct.offbudget;
164
+ }
165
+
166
+ // Go ahead and generate ids for all of the transactions so we can
167
+ // reliably resolve transfers
168
+ for (const transaction of data.transactions) {
169
+ entityIdMap.set(transaction.entityId, uuidv4());
170
+
171
+ if (transaction.subTransactions) {
172
+ for (const subTransaction of transaction.subTransactions) {
173
+ entityIdMap.set(subTransaction.entityId, uuidv4());
174
+ }
175
+ }
176
+ }
177
+
178
+ const transactionsGrouped = groupBy(data.transactions, 'accountId');
179
+
180
+ await Promise.all(
181
+ [...transactionsGrouped.keys()].map(async accountId => {
182
+ const transactions = transactionsGrouped.get(accountId);
183
+
184
+ const toImport = transactions
185
+ .map(transaction => {
186
+ if (transaction.isTombstone) {
187
+ return null;
188
+ }
189
+
190
+ const id = entityIdMap.get(transaction.entityId);
191
+
192
+ function transferProperties(t: YNAB4.SubTransaction) {
193
+ const transferId = entityIdMap.get(t.transferTransactionId) || null;
194
+
195
+ let payee = null;
196
+ let imported_payee = null;
197
+ if (transferId) {
198
+ payee = payees.find(
199
+ p => p.transfer_acct === entityIdMap.get(t.targetAccountId),
200
+ ).id;
201
+ } else {
202
+ payee = entityIdMap.get(t.payeeId);
203
+ imported_payee = data.payees.find(
204
+ p => p.entityId === t.payeeId,
205
+ )?.name;
206
+ }
207
+
208
+ return {
209
+ transfer_id: transferId,
210
+ payee,
211
+ imported_payee,
212
+ };
213
+ }
214
+
215
+ const newTransaction = {
216
+ id,
217
+ amount: amountToInteger(transaction.amount),
218
+ category: isOffBudget(entityIdMap.get(accountId))
219
+ ? null
220
+ : getCategory(transaction.categoryId),
221
+ date: transaction.date,
222
+ notes: transaction.memo || null,
223
+ cleared:
224
+ transaction.cleared === 'Cleared' ||
225
+ transaction.cleared === 'Reconciled',
226
+ reconciled: transaction.cleared === 'Reconciled',
227
+ ...transferProperties(transaction),
228
+
229
+ subtransactions:
230
+ transaction.subTransactions &&
231
+ transaction.subTransactions
232
+ .filter(st => !st.isTombstone)
233
+ .map(t => {
234
+ return {
235
+ id: entityIdMap.get(t.entityId),
236
+ amount: amountToInteger(t.amount),
237
+ category: getCategory(t.categoryId),
238
+ notes: t.memo || null,
239
+ ...transferProperties(t),
240
+ };
241
+ }),
242
+ };
243
+
244
+ return newTransaction;
245
+ })
246
+ .filter(x => x);
247
+
248
+ await send('api/transactions-add', {
249
+ accountId: entityIdMap.get(accountId),
250
+ transactions: toImport,
251
+ learnCategories: true,
252
+ runTransfers: false,
253
+ });
254
+ }),
255
+ );
256
+ }
257
+
258
+ function fillInBudgets(
259
+ data: YNAB4.YFull,
260
+ categoryBudgets: YNAB4.MonthlySubCategoryBudget[],
261
+ ) {
262
+ // YNAB only contains entries for categories that have been actually
263
+ // budgeted. That would be fine except that we need to set the
264
+ // "carryover" flag on each month when carrying debt across months.
265
+ // To make sure our system has a chance to set this flag on each
266
+ // category, make sure a budget exists for every category of every
267
+ // month.
268
+ const budgets: {
269
+ budgeted: number;
270
+ categoryId: string;
271
+ overspendingHandling?: string;
272
+ }[] = [...categoryBudgets];
273
+ data.masterCategories.forEach(masterCategory => {
274
+ if (masterCategory.subCategories) {
275
+ masterCategory.subCategories.forEach(category => {
276
+ if (!budgets.find(b => b.categoryId === category.entityId)) {
277
+ budgets.push({
278
+ budgeted: 0,
279
+ categoryId: category.entityId,
280
+ });
281
+ }
282
+ });
283
+ }
284
+ });
285
+ return budgets;
286
+ }
287
+
288
+ async function importBudgets(
289
+ data: YNAB4.YFull,
290
+ entityIdMap: Map<string, string>,
291
+ ) {
292
+ const budgets = sortByKey(data.monthlyBudgets, 'month');
293
+
294
+ await send('api/batch-budget-start');
295
+ try {
296
+ for (const budget of budgets) {
297
+ const filled = fillInBudgets(
298
+ data,
299
+ budget.monthlySubCategoryBudgets.filter(b => !b.isTombstone),
300
+ );
301
+
302
+ await Promise.all(
303
+ filled.map(async catBudget => {
304
+ const amount = amountToInteger(catBudget.budgeted);
305
+ const catId = entityIdMap.get(catBudget.categoryId);
306
+ const month = monthUtils.monthFromDate(budget.month);
307
+ if (!catId) {
308
+ return;
309
+ }
310
+
311
+ await send('api/budget-set-amount', {
312
+ month,
313
+ categoryId: catId,
314
+ amount,
315
+ });
316
+
317
+ if (catBudget.overspendingHandling === 'AffectsBuffer') {
318
+ await send('api/budget-set-carryover', {
319
+ month,
320
+ categoryId: catId,
321
+ flag: false,
322
+ });
323
+ } else if (catBudget.overspendingHandling === 'Confined') {
324
+ await send('api/budget-set-carryover', {
325
+ month,
326
+ categoryId: catId,
327
+ flag: true,
328
+ });
329
+ }
330
+ }),
331
+ );
332
+ }
333
+ } finally {
334
+ await send('api/batch-budget-end');
335
+ }
336
+ }
337
+
338
+ function estimateRecentness(str: string) {
339
+ // The "recentness" is the total amount of changes that this device
340
+ // is aware of, which is estimated by summing up all of the version
341
+ // numbers that its aware of. This works because version numbers are
342
+ // increasing integers.
343
+ return str.split(',').reduce((total, version) => {
344
+ const [_, number] = version.split('-');
345
+ return total + parseInt(number);
346
+ }, 0);
347
+ }
348
+
349
+ function findLatestDevice(zipped: AdmZip, entries: AdmZip.IZipEntry[]): string {
350
+ let devices = entries
351
+ .map(entry => {
352
+ const contents = zipped.readFile(entry).toString('utf8');
353
+
354
+ let data;
355
+ try {
356
+ data = JSON.parse(contents);
357
+ } catch {
358
+ return null;
359
+ }
360
+
361
+ if (data.hasFullKnowledge) {
362
+ return {
363
+ deviceGUID: data.deviceGUID,
364
+ shortName: data.shortDeviceId,
365
+ recentness: estimateRecentness(data.knowledge),
366
+ };
367
+ }
368
+
369
+ return null;
370
+ })
371
+ .filter(x => x);
372
+
373
+ devices = sortByKey(devices, 'recentness');
374
+ return devices[devices.length - 1].deviceGUID;
375
+ }
376
+
377
+ export async function doImport(data: YNAB4.YFull) {
378
+ const entityIdMap = new Map<string, string>();
379
+
380
+ logger.log('Importing Accounts...');
381
+ await importAccounts(data, entityIdMap);
382
+
383
+ logger.log('Importing Categories...');
384
+ await importCategories(data, entityIdMap);
385
+
386
+ logger.log('Importing Payees...');
387
+ await importPayees(data, entityIdMap);
388
+
389
+ logger.log('Importing Transactions...');
390
+ await importTransactions(data, entityIdMap);
391
+
392
+ logger.log('Importing Budgets...');
393
+ await importBudgets(data, entityIdMap);
394
+
395
+ logger.log('Setting up...');
396
+ }
397
+
398
+ export function getBudgetName(filepath) {
399
+ let unixFilepath = normalizePathSep(filepath);
400
+
401
+ if (!/\.zip/.test(unixFilepath)) {
402
+ return null;
403
+ }
404
+
405
+ unixFilepath = unixFilepath.replace(/\.zip$/, '').replace(/.ynab4$/, '');
406
+
407
+ // Most budgets are named like "Budget~51938D82.ynab4" but sometimes
408
+ // they are only "Budget.ynab4". We only want to grab the name
409
+ // before the ~ if it exists.
410
+ const m = unixFilepath.match(/([^/~]+)[^/]*$/);
411
+ if (!m) {
412
+ return null;
413
+ }
414
+ return m[1];
415
+ }
416
+
417
+ function getFile(entries: AdmZip.IZipEntry[], path: string) {
418
+ const files = entries.filter(e => e.entryName === path);
419
+ if (files.length === 0) {
420
+ throw new Error('Could not find file: ' + path);
421
+ }
422
+ if (files.length >= 2) {
423
+ throw new Error('File name matches multiple files: ' + path);
424
+ }
425
+ return files[0];
426
+ }
427
+
428
+ function join(...paths: string[]): string {
429
+ return paths.slice(1).reduce(
430
+ (full, path) => {
431
+ return full + '/' + path.replace(/^\//, '');
432
+ },
433
+ paths[0].replace(/\/$/, ''),
434
+ );
435
+ }
436
+
437
+ export function parseFile(buffer: Buffer): YNAB4.YFull {
438
+ const zipped = new AdmZip(buffer);
439
+ const entries = zipped.getEntries();
440
+
441
+ let root = '';
442
+ const dirMatch = entries[0].entryName.match(/([^/]*\.ynab4)/);
443
+ if (dirMatch) {
444
+ root = dirMatch[1] + '/';
445
+ }
446
+
447
+ const metaStr = zipped.readFile(getFile(entries, root + 'Budget.ymeta'));
448
+ const meta = JSON.parse(metaStr.toString('utf8'));
449
+ const budgetPath = join(root, meta.relativeDataFolderName);
450
+
451
+ const deviceFiles = entries.filter(e =>
452
+ e.entryName.startsWith(join(budgetPath, 'devices')),
453
+ );
454
+ const deviceGUID = findLatestDevice(zipped, deviceFiles);
455
+
456
+ const yfullPath = join(budgetPath, deviceGUID, 'Budget.yfull');
457
+ let contents;
458
+ try {
459
+ contents = zipped.readFile(getFile(entries, yfullPath)).toString('utf8');
460
+ } catch (e) {
461
+ logger.log(e);
462
+ throw new Error('Error reading Budget.yfull file');
463
+ }
464
+
465
+ try {
466
+ return JSON.parse(contents);
467
+ } catch {
468
+ throw new Error('Error parsing Budget.yfull file');
469
+ }
470
+ }