@tasenor/common-node 1.9.16

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 (326) hide show
  1. package/.eslintrc.js +4 -0
  2. package/LICENSE +21 -0
  3. package/dist/tasenor-common-node/src/cli.d.ts +81 -0
  4. package/dist/tasenor-common-node/src/cli.js +242 -0
  5. package/dist/tasenor-common-node/src/cli.js.map +1 -0
  6. package/dist/tasenor-common-node/src/commands/account.d.ts +12 -0
  7. package/dist/tasenor-common-node/src/commands/account.js +58 -0
  8. package/dist/tasenor-common-node/src/commands/account.js.map +1 -0
  9. package/dist/tasenor-common-node/src/commands/balance.d.ts +11 -0
  10. package/dist/tasenor-common-node/src/commands/balance.js +117 -0
  11. package/dist/tasenor-common-node/src/commands/balance.js.map +1 -0
  12. package/dist/tasenor-common-node/src/commands/db.d.ts +14 -0
  13. package/dist/tasenor-common-node/src/commands/db.js +69 -0
  14. package/dist/tasenor-common-node/src/commands/db.js.map +1 -0
  15. package/dist/tasenor-common-node/src/commands/entry.d.ts +13 -0
  16. package/dist/tasenor-common-node/src/commands/entry.js +106 -0
  17. package/dist/tasenor-common-node/src/commands/entry.js.map +1 -0
  18. package/dist/tasenor-common-node/src/commands/import.d.ts +17 -0
  19. package/dist/tasenor-common-node/src/commands/import.js +140 -0
  20. package/dist/tasenor-common-node/src/commands/import.js.map +1 -0
  21. package/dist/tasenor-common-node/src/commands/importer.d.ts +13 -0
  22. package/dist/tasenor-common-node/src/commands/importer.js +71 -0
  23. package/dist/tasenor-common-node/src/commands/importer.js.map +1 -0
  24. package/dist/tasenor-common-node/src/commands/index.d.ts +191 -0
  25. package/dist/tasenor-common-node/src/commands/index.js +482 -0
  26. package/dist/tasenor-common-node/src/commands/index.js.map +1 -0
  27. package/dist/tasenor-common-node/src/commands/period.d.ts +12 -0
  28. package/dist/tasenor-common-node/src/commands/period.js +48 -0
  29. package/dist/tasenor-common-node/src/commands/period.js.map +1 -0
  30. package/dist/tasenor-common-node/src/commands/plugin.d.ts +15 -0
  31. package/dist/tasenor-common-node/src/commands/plugin.js +78 -0
  32. package/dist/tasenor-common-node/src/commands/plugin.js.map +1 -0
  33. package/dist/tasenor-common-node/src/commands/report.d.ts +11 -0
  34. package/dist/tasenor-common-node/src/commands/report.js +96 -0
  35. package/dist/tasenor-common-node/src/commands/report.js.map +1 -0
  36. package/dist/tasenor-common-node/src/commands/settings.d.ts +10 -0
  37. package/dist/tasenor-common-node/src/commands/settings.js +64 -0
  38. package/dist/tasenor-common-node/src/commands/settings.js.map +1 -0
  39. package/dist/tasenor-common-node/src/commands/stock.d.ts +8 -0
  40. package/dist/tasenor-common-node/src/commands/stock.js +73 -0
  41. package/dist/tasenor-common-node/src/commands/stock.js.map +1 -0
  42. package/dist/tasenor-common-node/src/commands/tag.d.ts +13 -0
  43. package/dist/tasenor-common-node/src/commands/tag.js +89 -0
  44. package/dist/tasenor-common-node/src/commands/tag.js.map +1 -0
  45. package/dist/tasenor-common-node/src/commands/tx.d.ts +12 -0
  46. package/dist/tasenor-common-node/src/commands/tx.js +81 -0
  47. package/dist/tasenor-common-node/src/commands/tx.js.map +1 -0
  48. package/dist/tasenor-common-node/src/commands/user.d.ts +12 -0
  49. package/dist/tasenor-common-node/src/commands/user.js +52 -0
  50. package/dist/tasenor-common-node/src/commands/user.js.map +1 -0
  51. package/dist/tasenor-common-node/src/database/BookkeeperImporter.d.ts +77 -0
  52. package/dist/tasenor-common-node/src/database/BookkeeperImporter.js +343 -0
  53. package/dist/tasenor-common-node/src/database/BookkeeperImporter.js.map +1 -0
  54. package/dist/tasenor-common-node/src/database/DB.d.ts +51 -0
  55. package/dist/tasenor-common-node/src/database/DB.js +354 -0
  56. package/dist/tasenor-common-node/src/database/DB.js.map +1 -0
  57. package/dist/tasenor-common-node/src/database/index.d.ts +7 -0
  58. package/dist/tasenor-common-node/src/database/index.js +8 -0
  59. package/dist/tasenor-common-node/src/database/index.js.map +1 -0
  60. package/dist/tasenor-common-node/src/doccer.d.ts +29 -0
  61. package/dist/tasenor-common-node/src/doccer.js +30 -0
  62. package/dist/tasenor-common-node/src/doccer.js.map +1 -0
  63. package/dist/tasenor-common-node/src/error.d.ts +30 -0
  64. package/dist/tasenor-common-node/src/error.js +35 -0
  65. package/dist/tasenor-common-node/src/error.js.map +1 -0
  66. package/dist/tasenor-common-node/src/export/Exporter.d.ts +69 -0
  67. package/dist/tasenor-common-node/src/export/Exporter.js +123 -0
  68. package/dist/tasenor-common-node/src/export/Exporter.js.map +1 -0
  69. package/dist/tasenor-common-node/src/export/TasenorExporter.d.ts +55 -0
  70. package/dist/tasenor-common-node/src/export/TasenorExporter.js +135 -0
  71. package/dist/tasenor-common-node/src/export/TasenorExporter.js.map +1 -0
  72. package/dist/tasenor-common-node/src/export/TilitinExporter.d.ts +71 -0
  73. package/dist/tasenor-common-node/src/export/TilitinExporter.js +290 -0
  74. package/dist/tasenor-common-node/src/export/TilitinExporter.js.map +1 -0
  75. package/dist/tasenor-common-node/src/export/index.d.ts +8 -0
  76. package/dist/tasenor-common-node/src/export/index.js +9 -0
  77. package/dist/tasenor-common-node/src/export/index.js.map +1 -0
  78. package/dist/tasenor-common-node/src/import/TextFileProcessHandler.d.ts +104 -0
  79. package/dist/tasenor-common-node/src/import/TextFileProcessHandler.js +354 -0
  80. package/dist/tasenor-common-node/src/import/TextFileProcessHandler.js.map +1 -0
  81. package/dist/tasenor-common-node/src/import/TransactionImportConnector.d.ts +38 -0
  82. package/dist/tasenor-common-node/src/import/TransactionImportConnector.js +27 -0
  83. package/dist/tasenor-common-node/src/import/TransactionImportConnector.js.map +1 -0
  84. package/dist/tasenor-common-node/src/import/TransactionImportHandler.d.ts +173 -0
  85. package/dist/tasenor-common-node/src/import/TransactionImportHandler.js +733 -0
  86. package/dist/tasenor-common-node/src/import/TransactionImportHandler.js.map +1 -0
  87. package/dist/tasenor-common-node/src/import/TransactionRules.d.ts +238 -0
  88. package/dist/tasenor-common-node/src/import/TransactionRules.js +522 -0
  89. package/dist/tasenor-common-node/src/import/TransactionRules.js.map +1 -0
  90. package/dist/tasenor-common-node/src/import/TransactionUI.d.ts +181 -0
  91. package/dist/tasenor-common-node/src/import/TransactionUI.js +482 -0
  92. package/dist/tasenor-common-node/src/import/TransactionUI.js.map +1 -0
  93. package/dist/tasenor-common-node/src/import/TransferAnalyzer.d.ts +324 -0
  94. package/dist/tasenor-common-node/src/import/TransferAnalyzer.js +1379 -0
  95. package/dist/tasenor-common-node/src/import/TransferAnalyzer.js.map +1 -0
  96. package/dist/tasenor-common-node/src/import/index.d.ts +11 -0
  97. package/dist/tasenor-common-node/src/import/index.js +12 -0
  98. package/dist/tasenor-common-node/src/import/index.js.map +1 -0
  99. package/dist/tasenor-common-node/src/index.d.ts +12 -0
  100. package/dist/tasenor-common-node/src/index.js +13 -0
  101. package/dist/tasenor-common-node/src/index.js.map +1 -0
  102. package/dist/tasenor-common-node/src/net/crypto.d.ts +33 -0
  103. package/dist/tasenor-common-node/src/net/crypto.js +63 -0
  104. package/dist/tasenor-common-node/src/net/crypto.js.map +1 -0
  105. package/dist/tasenor-common-node/src/net/git.d.ts +49 -0
  106. package/dist/tasenor-common-node/src/net/git.js +137 -0
  107. package/dist/tasenor-common-node/src/net/git.js.map +1 -0
  108. package/dist/tasenor-common-node/src/net/index.d.ts +10 -0
  109. package/dist/tasenor-common-node/src/net/index.js +11 -0
  110. package/dist/tasenor-common-node/src/net/index.js.map +1 -0
  111. package/dist/tasenor-common-node/src/net/middleware.d.ts +61 -0
  112. package/dist/tasenor-common-node/src/net/middleware.js +220 -0
  113. package/dist/tasenor-common-node/src/net/middleware.js.map +1 -0
  114. package/dist/tasenor-common-node/src/net/tokens.d.ts +50 -0
  115. package/dist/tasenor-common-node/src/net/tokens.js +141 -0
  116. package/dist/tasenor-common-node/src/net/tokens.js.map +1 -0
  117. package/dist/tasenor-common-node/src/net/vault.d.ts +67 -0
  118. package/dist/tasenor-common-node/src/net/vault.js +145 -0
  119. package/dist/tasenor-common-node/src/net/vault.js.map +1 -0
  120. package/dist/tasenor-common-node/src/plugins/BackendPlugin.d.ts +91 -0
  121. package/dist/tasenor-common-node/src/plugins/BackendPlugin.js +165 -0
  122. package/dist/tasenor-common-node/src/plugins/BackendPlugin.js.map +1 -0
  123. package/dist/tasenor-common-node/src/plugins/DataPlugin.d.ts +13 -0
  124. package/dist/tasenor-common-node/src/plugins/DataPlugin.js +26 -0
  125. package/dist/tasenor-common-node/src/plugins/DataPlugin.js.map +1 -0
  126. package/dist/tasenor-common-node/src/plugins/ImportPlugin.d.ts +188 -0
  127. package/dist/tasenor-common-node/src/plugins/ImportPlugin.js +204 -0
  128. package/dist/tasenor-common-node/src/plugins/ImportPlugin.js.map +1 -0
  129. package/dist/tasenor-common-node/src/plugins/ReportPlugin.d.ts +132 -0
  130. package/dist/tasenor-common-node/src/plugins/ReportPlugin.js +393 -0
  131. package/dist/tasenor-common-node/src/plugins/ReportPlugin.js.map +1 -0
  132. package/dist/tasenor-common-node/src/plugins/SchemePlugin.d.ts +34 -0
  133. package/dist/tasenor-common-node/src/plugins/SchemePlugin.js +47 -0
  134. package/dist/tasenor-common-node/src/plugins/SchemePlugin.js.map +1 -0
  135. package/dist/tasenor-common-node/src/plugins/ServicePlugin.d.ts +80 -0
  136. package/dist/tasenor-common-node/src/plugins/ServicePlugin.js +168 -0
  137. package/dist/tasenor-common-node/src/plugins/ServicePlugin.js.map +1 -0
  138. package/dist/tasenor-common-node/src/plugins/ToolPlugin.d.ts +27 -0
  139. package/dist/tasenor-common-node/src/plugins/ToolPlugin.js +37 -0
  140. package/dist/tasenor-common-node/src/plugins/ToolPlugin.js.map +1 -0
  141. package/dist/tasenor-common-node/src/plugins/index.d.ts +13 -0
  142. package/dist/tasenor-common-node/src/plugins/index.js +14 -0
  143. package/dist/tasenor-common-node/src/plugins/index.js.map +1 -0
  144. package/dist/tasenor-common-node/src/plugins/plugins.d.ts +101 -0
  145. package/dist/tasenor-common-node/src/plugins/plugins.js +292 -0
  146. package/dist/tasenor-common-node/src/plugins/plugins.js.map +1 -0
  147. package/dist/tasenor-common-node/src/process/Process.d.ts +108 -0
  148. package/dist/tasenor-common-node/src/process/Process.js +335 -0
  149. package/dist/tasenor-common-node/src/process/Process.js.map +1 -0
  150. package/dist/tasenor-common-node/src/process/ProcessConnector.d.ts +24 -0
  151. package/dist/tasenor-common-node/src/process/ProcessConnector.js +28 -0
  152. package/dist/tasenor-common-node/src/process/ProcessConnector.js.map +1 -0
  153. package/dist/tasenor-common-node/src/process/ProcessFile.d.ts +69 -0
  154. package/dist/tasenor-common-node/src/process/ProcessFile.js +145 -0
  155. package/dist/tasenor-common-node/src/process/ProcessFile.js.map +1 -0
  156. package/dist/tasenor-common-node/src/process/ProcessHandler.d.ts +60 -0
  157. package/dist/tasenor-common-node/src/process/ProcessHandler.js +73 -0
  158. package/dist/tasenor-common-node/src/process/ProcessHandler.js.map +1 -0
  159. package/dist/tasenor-common-node/src/process/ProcessStep.d.ts +52 -0
  160. package/dist/tasenor-common-node/src/process/ProcessStep.js +78 -0
  161. package/dist/tasenor-common-node/src/process/ProcessStep.js.map +1 -0
  162. package/dist/tasenor-common-node/src/process/ProcessingSystem.d.ts +60 -0
  163. package/dist/tasenor-common-node/src/process/ProcessingSystem.js +182 -0
  164. package/dist/tasenor-common-node/src/process/ProcessingSystem.js.map +1 -0
  165. package/dist/tasenor-common-node/src/process/index.d.ts +11 -0
  166. package/dist/tasenor-common-node/src/process/index.js +12 -0
  167. package/dist/tasenor-common-node/src/process/index.js.map +1 -0
  168. package/dist/tasenor-common-node/src/reports/conversions.d.ts +8 -0
  169. package/dist/tasenor-common-node/src/reports/conversions.js +47 -0
  170. package/dist/tasenor-common-node/src/reports/conversions.js.map +1 -0
  171. package/dist/tasenor-common-node/src/reports/index.d.ts +6 -0
  172. package/dist/tasenor-common-node/src/reports/index.js +7 -0
  173. package/dist/tasenor-common-node/src/reports/index.js.map +1 -0
  174. package/dist/tasenor-common-node/src/server/ISPDemoServer.d.ts +43 -0
  175. package/dist/tasenor-common-node/src/server/ISPDemoServer.js +112 -0
  176. package/dist/tasenor-common-node/src/server/ISPDemoServer.js.map +1 -0
  177. package/dist/tasenor-common-node/src/server/api.d.ts +15 -0
  178. package/dist/tasenor-common-node/src/server/api.js +27 -0
  179. package/dist/tasenor-common-node/src/server/api.js.map +1 -0
  180. package/dist/tasenor-common-node/src/server/index.d.ts +7 -0
  181. package/dist/tasenor-common-node/src/server/index.js +8 -0
  182. package/dist/tasenor-common-node/src/server/index.js.map +1 -0
  183. package/dist/tasenor-common-node/src/server/router.d.ts +5 -0
  184. package/dist/tasenor-common-node/src/server/router.js +37 -0
  185. package/dist/tasenor-common-node/src/server/router.js.map +1 -0
  186. package/dist/tasenor-common-node/src/system.d.ts +27 -0
  187. package/dist/tasenor-common-node/src/system.js +95 -0
  188. package/dist/tasenor-common-node/src/system.js.map +1 -0
  189. package/dist/tasenor-common-node/src/testing/ProcessingSystemMock.d.ts +21 -0
  190. package/dist/tasenor-common-node/src/testing/ProcessingSystemMock.js +33 -0
  191. package/dist/tasenor-common-node/src/testing/ProcessingSystemMock.js.map +1 -0
  192. package/dist/tasenor-common-node/src/testing/UnitTestImportConnector.d.ts +24 -0
  193. package/dist/tasenor-common-node/src/testing/UnitTestImportConnector.js +68 -0
  194. package/dist/tasenor-common-node/src/testing/UnitTestImportConnector.js.map +1 -0
  195. package/dist/tasenor-common-node/src/testing/UnitTester.d.ts +64 -0
  196. package/dist/tasenor-common-node/src/testing/UnitTester.js +199 -0
  197. package/dist/tasenor-common-node/src/testing/UnitTester.js.map +1 -0
  198. package/dist/tasenor-common-node/src/testing/index.d.ts +4 -0
  199. package/dist/tasenor-common-node/src/testing/index.js +5 -0
  200. package/dist/tasenor-common-node/src/testing/index.js.map +1 -0
  201. package/dist/tasenor-common-node/src/testing/test-handlers.d.ts +13 -0
  202. package/dist/tasenor-common-node/src/testing/test-handlers.js +52 -0
  203. package/dist/tasenor-common-node/src/testing/test-handlers.js.map +1 -0
  204. package/dist/tasenor-common-node/tests/TransactionRules.spec.d.ts +1 -0
  205. package/dist/tasenor-common-node/tests/TransactionRules.spec.js +64 -0
  206. package/dist/tasenor-common-node/tests/TransactionRules.spec.js.map +1 -0
  207. package/dist/tasenor-common-node/tests/TransferAnalyzer-account-address.spec.d.ts +1 -0
  208. package/dist/tasenor-common-node/tests/TransferAnalyzer-account-address.spec.js +80 -0
  209. package/dist/tasenor-common-node/tests/TransferAnalyzer-account-address.spec.js.map +1 -0
  210. package/dist/tasenor-common-node/tests/TransferAnalyzer-buying-and-selling.spec.d.ts +1 -0
  211. package/dist/tasenor-common-node/tests/TransferAnalyzer-buying-and-selling.spec.js +342 -0
  212. package/dist/tasenor-common-node/tests/TransferAnalyzer-buying-and-selling.spec.js.map +1 -0
  213. package/dist/tasenor-common-node/tests/TransferAnalyzer-loans.spec.d.ts +1 -0
  214. package/dist/tasenor-common-node/tests/TransferAnalyzer-loans.spec.js +174 -0
  215. package/dist/tasenor-common-node/tests/TransferAnalyzer-loans.spec.js.map +1 -0
  216. package/dist/tasenor-common-node/tests/TransferAnalyzer-multiple-null-amounts.spec.d.ts +1 -0
  217. package/dist/tasenor-common-node/tests/TransferAnalyzer-multiple-null-amounts.spec.js +175 -0
  218. package/dist/tasenor-common-node/tests/TransferAnalyzer-multiple-null-amounts.spec.js.map +1 -0
  219. package/dist/tasenor-common-node/tests/password.spec.d.ts +1 -0
  220. package/dist/tasenor-common-node/tests/password.spec.js +8 -0
  221. package/dist/tasenor-common-node/tests/password.spec.js.map +1 -0
  222. package/dist/tasenor-common-node/tests/tokens.spec.d.ts +1 -0
  223. package/dist/tasenor-common-node/tests/tokens.spec.js +49 -0
  224. package/dist/tasenor-common-node/tests/tokens.spec.js.map +1 -0
  225. package/dist/tasenor-common-node/tests/vault.spec.d.ts +1 -0
  226. package/dist/tasenor-common-node/tests/vault.spec.js +19 -0
  227. package/dist/tasenor-common-node/tests/vault.spec.js.map +1 -0
  228. package/dist/tasenor-common-plugins/src/CoinbaseImport/backend/CoinbaseHandler.d.ts +11 -0
  229. package/dist/tasenor-common-plugins/src/CoinbaseImport/backend/CoinbaseHandler.js +30 -0
  230. package/dist/tasenor-common-plugins/src/CoinbaseImport/backend/CoinbaseHandler.js.map +1 -0
  231. package/dist/tasenor-common-plugins/src/IncomeAndExpenses/backend/index.d.ts +5 -0
  232. package/dist/tasenor-common-plugins/src/IncomeAndExpenses/backend/index.js +350 -0
  233. package/dist/tasenor-common-plugins/src/IncomeAndExpenses/backend/index.js.map +1 -0
  234. package/dist/tasenor-common-plugins/src/KrakenImport/backend/KrakenHandler.d.ts +23 -0
  235. package/dist/tasenor-common-plugins/src/KrakenImport/backend/KrakenHandler.js +83 -0
  236. package/dist/tasenor-common-plugins/src/KrakenImport/backend/KrakenHandler.js.map +1 -0
  237. package/dist/tasenor-common-plugins/src/LynxImport/backend/LynxHandler.d.ts +28 -0
  238. package/dist/tasenor-common-plugins/src/LynxImport/backend/LynxHandler.js +340 -0
  239. package/dist/tasenor-common-plugins/src/LynxImport/backend/LynxHandler.js.map +1 -0
  240. package/dist/tasenor-common-plugins/src/NordeaImport/backend/NordeaHandler.d.ts +11 -0
  241. package/dist/tasenor-common-plugins/src/NordeaImport/backend/NordeaHandler.js +39 -0
  242. package/dist/tasenor-common-plugins/src/NordeaImport/backend/NordeaHandler.js.map +1 -0
  243. package/dist/tasenor-common-plugins/src/NordnetImport/backend/NordnetHandler.d.ts +17 -0
  244. package/dist/tasenor-common-plugins/src/NordnetImport/backend/NordnetHandler.js +66 -0
  245. package/dist/tasenor-common-plugins/src/NordnetImport/backend/NordnetHandler.js.map +1 -0
  246. package/dist/tasenor-common-plugins/src/TITOImport/backend/TITOHandler.d.ts +13 -0
  247. package/dist/tasenor-common-plugins/src/TITOImport/backend/TITOHandler.js +241 -0
  248. package/dist/tasenor-common-plugins/src/TITOImport/backend/TITOHandler.js.map +1 -0
  249. package/jest.config.js +1 -0
  250. package/package.json +62 -0
  251. package/src/cli.ts +267 -0
  252. package/src/commands/account.ts +69 -0
  253. package/src/commands/balance.ts +131 -0
  254. package/src/commands/db.ts +84 -0
  255. package/src/commands/entry.ts +117 -0
  256. package/src/commands/import.ts +160 -0
  257. package/src/commands/importer.ts +84 -0
  258. package/src/commands/index.ts +534 -0
  259. package/src/commands/period.ts +59 -0
  260. package/src/commands/plugin.ts +95 -0
  261. package/src/commands/report.ts +113 -0
  262. package/src/commands/settings.ts +75 -0
  263. package/src/commands/stock.ts +80 -0
  264. package/src/commands/tag.ts +102 -0
  265. package/src/commands/tx.ts +93 -0
  266. package/src/commands/user.ts +65 -0
  267. package/src/database/BookkeeperImporter.ts +358 -0
  268. package/src/database/DB.ts +396 -0
  269. package/src/database/index.ts +7 -0
  270. package/src/doccer.ts +29 -0
  271. package/src/error.ts +32 -0
  272. package/src/export/Exporter.ts +136 -0
  273. package/src/export/TasenorExporter.ts +144 -0
  274. package/src/export/TilitinExporter.ts +302 -0
  275. package/src/export/index.ts +8 -0
  276. package/src/import/TextFileProcessHandler.ts +384 -0
  277. package/src/import/TransactionImportConnector.ts +65 -0
  278. package/src/import/TransactionImportHandler.ts +819 -0
  279. package/src/import/TransactionRules.ts +570 -0
  280. package/src/import/TransactionUI.ts +520 -0
  281. package/src/import/TransferAnalyzer.ts +1450 -0
  282. package/src/import/index.ts +11 -0
  283. package/src/index.ts +12 -0
  284. package/src/net/crypto.ts +69 -0
  285. package/src/net/git.ts +151 -0
  286. package/src/net/index.ts +10 -0
  287. package/src/net/middleware.ts +261 -0
  288. package/src/net/tokens.ts +140 -0
  289. package/src/net/vault.ts +161 -0
  290. package/src/plugins/BackendPlugin.ts +188 -0
  291. package/src/plugins/DataPlugin.ts +29 -0
  292. package/src/plugins/ImportPlugin.ts +211 -0
  293. package/src/plugins/ReportPlugin.ts +443 -0
  294. package/src/plugins/SchemePlugin.ts +56 -0
  295. package/src/plugins/ServicePlugin.ts +188 -0
  296. package/src/plugins/ToolPlugin.ts +44 -0
  297. package/src/plugins/index.ts +13 -0
  298. package/src/plugins/plugins.ts +345 -0
  299. package/src/process/Process.ts +368 -0
  300. package/src/process/ProcessConnector.ts +45 -0
  301. package/src/process/ProcessFile.ts +169 -0
  302. package/src/process/ProcessHandler.ts +94 -0
  303. package/src/process/ProcessStep.ts +100 -0
  304. package/src/process/ProcessingSystem.ts +202 -0
  305. package/src/process/index.ts +11 -0
  306. package/src/reports/conversions.ts +52 -0
  307. package/src/reports/index.ts +6 -0
  308. package/src/server/ISPDemoServer.ts +122 -0
  309. package/src/server/api.ts +37 -0
  310. package/src/server/index.ts +7 -0
  311. package/src/server/router.ts +60 -0
  312. package/src/system.ts +96 -0
  313. package/src/testing/ProcessingSystemMock.ts +45 -0
  314. package/src/testing/UnitTestImportConnector.ts +86 -0
  315. package/src/testing/UnitTester.ts +231 -0
  316. package/src/testing/index.ts +4 -0
  317. package/src/testing/test-handlers.ts +55 -0
  318. package/tests/TransactionRules.spec.ts +73 -0
  319. package/tests/TransferAnalyzer-account-address.spec.ts +87 -0
  320. package/tests/TransferAnalyzer-buying-and-selling.spec.ts +354 -0
  321. package/tests/TransferAnalyzer-loans.spec.ts +197 -0
  322. package/tests/TransferAnalyzer-multiple-null-amounts.spec.ts +181 -0
  323. package/tests/password.spec.ts +8 -0
  324. package/tests/tokens.spec.ts +52 -0
  325. package/tests/vault.spec.ts +20 -0
  326. package/tsconfig.json +13 -0
@@ -0,0 +1,1450 @@
1
+ import clone from 'clone'
2
+ import merge from 'merge'
3
+ import { sprintf } from 'sprintf-js'
4
+ import {
5
+ AccountNumber, Asset, AccountAddress, AssetExchange, AssetTransfer, AssetTransferReason, AssetType,
6
+ Currency, Language, StockValueData, TradeableAsset, Transaction, TransactionDescription, TransactionKind,
7
+ TransactionLine, StockBookkeeping, UIQuery, Tag, VATTarget, IncomeSource, ExpenseSink, isCurrency, TransferNote, isAssetTransferReason, isAssetType, ZERO_CENTS, less, warning, BalanceBookkeeping, realNegative, AdditionalTransferInfo, CryptoCurrency, ImportSegment, ImportStateText, ProcessConfig, SegmentId, TextFileLine
8
+ } from '@dataplug/tasenor-common'
9
+ import { TransactionImportHandler } from './TransactionImportHandler'
10
+ import { isTransactionImportConnector, TransactionImportConnector } from './TransactionImportConnector'
11
+ import { TransactionUI } from './TransactionUI'
12
+ import { BadState, InvalidFile, NotImplemented, SystemError } from '../error'
13
+
14
+ /**
15
+ * Check that two set of symbols are the same.
16
+ * @param s1
17
+ * @param s2
18
+ * @returns
19
+ */
20
+ function setEqual<T>(s1: Set<T>, s2:Set<T>): boolean {
21
+ if (s1.size !== s2.size) {
22
+ return false
23
+ }
24
+ return new Set([...s1, ...s2]).size === s1.size
25
+ }
26
+
27
+ /**
28
+ * Ensure that an array of symbols is equal to the set of symbols.
29
+ * @param s1
30
+ * @param s2
31
+ * @returns
32
+ */
33
+ function setEqualArray<T>(s1: Set<T>, s2:T[]): boolean {
34
+ return setEqual(s1, new Set(s2))
35
+ }
36
+
37
+ /**
38
+ * Helper to build string presentation of a number.
39
+ * @param value Numeric value.
40
+ * @param digits How many digits to have.
41
+ * @param sign If true, add always sign.
42
+ * @returns
43
+ */
44
+ function num(value: number, digits: number|null = null, sign = false): string {
45
+ let result: string
46
+ if (digits !== null) {
47
+ result = sprintf(`%.${digits}f`, value)
48
+ } else {
49
+ result = `${value}`
50
+ }
51
+ if (sign && value >= 0) {
52
+ result = `+${result}`
53
+ }
54
+ return result
55
+ }
56
+
57
+ /**
58
+ * ## Transfer Analysis
59
+ *
60
+ * ### Transfer
61
+ *
62
+ * The structure desribing a single part of a transfer is the following:
63
+ * ```json
64
+ * {
65
+ * "if": "<optional condition>",
66
+ * "reason": "<reason>",
67
+ * "type": "<type>",
68
+ * "asset": "<asset>",
69
+ * "amount": "<amount>",
70
+ * "text": "<optional description>"
71
+ * "questions": {
72
+ * },
73
+ * "data": {
74
+ * <optional additional data>
75
+ * }
76
+ * }
77
+ * ```
78
+ * Note that every expression is a string. Value to evaluate to `null` has to be given as a string `"null"`.
79
+ *
80
+ * #### Reason
81
+ *
82
+ * The reason component describes the fundamental general background causing the transaction. For example reason
83
+ * can be `expense` or `income`. Some spesific cases are taken seprately due to their nature, which may need
84
+ * different handling. For example `fee` or `dividend` are special cases of those.
85
+ *
86
+ * For more details {@link AssetTransferReason}.
87
+ *
88
+ * #### Type
89
+ *
90
+ * The type of transfer describes either concrete asset class (`stock`, `currency`, `cryptocurrency`) or sometimes
91
+ * more abstract thing like for example `statement` which represent transfer counterpart in the report.
92
+ *
93
+ * For more details {@link AssetType}.
94
+ *
95
+ * #### Asset
96
+ *
97
+ * Asset is the code for denoting the asset itself like currency code or stock ticker. It is also used for
98
+ * other purposes like code denoting income, expense or tax type.
99
+ *
100
+ * Definition is here {@link Asset}.
101
+ *
102
+ * #### Amount
103
+ *
104
+ * The amount is the number of units of the asset transferred. Typically it is measured in default currency but
105
+ * it could be also other currency or crypto currency. These are converted to the default currency during the
106
+ * processing either by using rates information in the transfer itself or by calling external service to find
107
+ * out the value at the transaction date.
108
+ *
109
+ * Special value `null` can be used to denote amount that must be calculated based on the remainder value of
110
+ * all the other transfer parts added together.
111
+ *
112
+ * #### Text
113
+ *
114
+ * By default the explanation is constructed automatically. If one wants to override the description, then the
115
+ * `text` field can be used.
116
+ *
117
+ * #### Questions
118
+ *
119
+ * Questions defined in setup (See {@link TransactionRules}) can be used in a transfer. When we want to determine
120
+ * some aspect of the transaction by using from the user, we can define additional variables mapping the variable
121
+ * names to the question names. Once the questions are answered, the variables are filled with the answers.
122
+ *
123
+ * For example the following sets the variable `type` based on the selection given
124
+ * ```json
125
+ * "questions": {
126
+ * "type": "Computer purchase"
127
+ * }
128
+ * ```
129
+ *
130
+ * #### Other Fields
131
+ *
132
+ * In addition it may have some special fields:
133
+ * - `if` When this arbitrary expression is given, it is evaluated and if not `true`, entry is skipped.
134
+ * - `data` This field can have optional informative fields of interest displayed by UI. (See {@link AdditionalTransferInfo}.)
135
+ */
136
+ export class TransferAnalyzer {
137
+
138
+ private handler: TransactionImportHandler
139
+ private config: ProcessConfig
140
+ private stocks: Record<AccountNumber, StockBookkeeping>
141
+ private state: ImportStateText<'classified'>
142
+ private balances: BalanceBookkeeping
143
+
144
+ constructor(handler: TransactionImportHandler, config: ProcessConfig, state: ImportStateText<'classified'>) {
145
+ this.handler = handler
146
+ this.config = config
147
+ this.state = state
148
+ this.stocks = {}
149
+ this.balances = new BalanceBookkeeping()
150
+ }
151
+
152
+ get UI(): TransactionUI {
153
+ return this.handler.UI
154
+ }
155
+
156
+ /**
157
+ * Read the initial balance.
158
+ */
159
+ async initialize(time: Date): Promise<void> {
160
+ await (this.handler.system.connector as unknown as TransactionImportConnector).initializeBalances(time, this.balances, this.config)
161
+ }
162
+
163
+ /**
164
+ * Get the summary of the balances.
165
+ */
166
+ getBalances() {
167
+ return this.balances.summary()
168
+ }
169
+
170
+ /**
171
+ * Get a single account balance.
172
+ * @param addr
173
+ */
174
+ getBalance(addr: AccountAddress) {
175
+ return this.balances.get(addr)
176
+ }
177
+
178
+ /**
179
+ * Update balance.
180
+ * @param txEntry
181
+ * @param name
182
+ * @returns
183
+ */
184
+ applyBalance(txEntry: TransactionLine): number {
185
+ return this.balances.apply(txEntry)
186
+ }
187
+
188
+ /**
189
+ * Revert balance.
190
+ * @param txEntry
191
+ * @param name
192
+ * @returns
193
+ */
194
+ revertBalance(txEntry: TransactionLine): number {
195
+ return this.balances.revert(txEntry)
196
+ }
197
+
198
+ /**
199
+ * Get the value from the system configuration.
200
+ */
201
+ getConfig(name: string, def: unknown = undefined): unknown {
202
+ if (!this.config[name]) {
203
+ if (def !== undefined) {
204
+ return def
205
+ }
206
+ throw new SystemError(`A variable ${name} is not configured for transfer analyser.`)
207
+ }
208
+ return this.config[name]
209
+ }
210
+
211
+ /**
212
+ * Translate a text.
213
+ * @param text
214
+ * @param language
215
+ * @returns
216
+ */
217
+ async getTranslation(text: string): Promise<string> {
218
+ return this.handler.getTranslation(text, this.getConfig('language') as Language)
219
+ }
220
+
221
+ /**
222
+ * Collect lines related to the segment.
223
+ * @param segmentId
224
+ */
225
+ getLines(segmentId: SegmentId): TextFileLine[] | null {
226
+ return this.handler.getLines(this.state, segmentId)
227
+ }
228
+
229
+ /**
230
+ * Analyse transfers and collect accounts needed.
231
+ * @param transfers
232
+ * @param options.findMissing If given, list missing accounts by their reason and type instead of throwing error.
233
+ * @returns Accounts or list of missing.
234
+ */
235
+ async collectAccounts(segment: ImportSegment, transfers: TransactionDescription, options: { findMissing?: boolean } = { findMissing: false }): Promise<Record<string, AccountNumber> | AccountAddress[]> {
236
+
237
+ const missing: AccountAddress[] = []
238
+ const accounts: Record<string, AccountNumber> = {}
239
+
240
+ // Gather accounts.
241
+ for (const transfer of transfers.transfers) {
242
+ // Get normal account.
243
+ const account = await this.getAccount(transfer.reason, transfer.type, transfer.asset, segment.id)
244
+ if (account === undefined) {
245
+ if (!options.findMissing) {
246
+ throw new BadState(`Unable to find an account number for ${transfer.reason}.${transfer.type}.${transfer.asset}.`)
247
+ }
248
+ missing.push(`${transfer.reason}.${transfer.type}.${transfer.asset}` as AccountAddress)
249
+ continue
250
+ }
251
+ accounts[`${transfer.reason}.${transfer.type}.${transfer.asset}`] = account as AccountNumber
252
+
253
+ // Check short selling accounts.
254
+ if (transfer.reason === 'trade' && transfer.type === 'stock' && this.getConfig('allowShortSelling', false)) {
255
+ const account = await this.getAccount('trade', 'short', transfer.asset, segment.id)
256
+ if (account === undefined) {
257
+ if (!options.findMissing) {
258
+ throw new BadState(`Unable to find an account number for trade.short.${transfer.asset}.`)
259
+ }
260
+ missing.push(`trade.short.${transfer.asset}` as AccountAddress)
261
+ } else {
262
+ accounts[`trade.short.${transfer.asset}`] = account as AccountNumber
263
+ }
264
+ continue
265
+ }
266
+ }
267
+
268
+ return options.findMissing ? missing : accounts
269
+ }
270
+
271
+ /**
272
+ * Collect some important values needed from transfer and resolve what kind of transfer we have.
273
+ *
274
+ * The following values are resolved:
275
+ * * `kind` - Kind of transfer recognized.
276
+ * * `exchange` - Name of the imporeter.
277
+ * * `name` - Name of the target asset for statement, if relevant.
278
+ * * `takeAmount` - Amount affecting the asset.
279
+ * * `takeAsset` - The name of the asset.
280
+ */
281
+ async collectOtherValues(transfers: TransactionDescription, values: Record<string, string|number>): Promise<Record<string, string|number>> {
282
+ const currency = this.getConfig('currency') as Currency
283
+
284
+ // Collect non-fee reasons.
285
+ const primaryReasons = new Set(transfers.transfers
286
+ .filter(t => !['fee'].includes(t.reason))
287
+ .map(t => t.reason)
288
+ )
289
+ // Collect non-fee assets.
290
+ const primaryAssets = new Set(transfers.transfers
291
+ .filter(t => !['fee'].includes(t.reason))
292
+ .map(t => t.type)
293
+ )
294
+
295
+ // Verify that both transfer reasons and assets are exactly the required.
296
+ function weHave(reasons: string[], assets: string[]): boolean {
297
+ return setEqualArray(primaryReasons, reasons) && setEqualArray(primaryAssets, assets)
298
+ }
299
+ // Filter entries that have the given reason and any of types and asset if given.
300
+ function entriesHaving(reason: AssetTransferReason, type: AssetType | AssetType[], asset: Asset | null = null) {
301
+ if (typeof (type) === 'string') {
302
+ type = [type]
303
+ }
304
+ return transfers.transfers.filter(
305
+ t => t.reason === reason &&
306
+ type.includes(t.type) &&
307
+ (asset === null || t.asset === asset))
308
+ }
309
+
310
+ // Ensure there is exactly one entry with the given specifications. Throw an error otherwise.
311
+ function shouldHaveOne(reason: AssetTransferReason, type: AssetType | AssetType[], asset: Asset | null = null): AssetTransfer {
312
+ const entries = entriesHaving(reason, type, asset)
313
+ if (entries.length < 1) {
314
+ throw new InvalidFile(`Dit not find entries matching ${reason}.${type}.${asset} from ${JSON.stringify(transfers)}`)
315
+ }
316
+ if (entries.length > 1) {
317
+ throw new InvalidFile(`Found too many entries matching ${reason}.${type}.${asset}: ${JSON.stringify(entries)}`)
318
+ }
319
+ return entries[0]
320
+ }
321
+
322
+ // Set up basic values.
323
+ values.currency = currency
324
+ values.exchange = this.handler.name.replace(/Import$/, '')
325
+ transfers.transfers.forEach(transfer => {
326
+ if (transfer.data) {
327
+ Object.assign(values, transfer.data)
328
+ }
329
+ })
330
+
331
+ // Find the kind.
332
+ let kind: TransactionKind
333
+
334
+ if (transfers.transfers.length === 0) {
335
+ kind = 'none'
336
+ } else if (weHave(['trade'], ['currency', 'crypto']) || weHave(['trade'], ['currency', 'stock'])) {
337
+ const moneyEntry = shouldHaveOne('trade', 'currency')
338
+ if (moneyEntry.amount === undefined) {
339
+ throw new SystemError(`Invalid trade transfer amount undefined in ${JSON.stringify(moneyEntry)}.`)
340
+ }
341
+ kind = moneyEntry.amount < 0 ? 'buy' : 'sell'
342
+ const tradeableEntry = shouldHaveOne('trade', ['crypto', 'stock'])
343
+ if (tradeableEntry.amount === undefined) {
344
+ throw new SystemError(`Invalid buy/sell transfer amount undefined in ${JSON.stringify(tradeableEntry)}.`)
345
+ }
346
+ // TODO: We should get rid of this and handle it in asset valuation always.
347
+ values.takeAmount = num(tradeableEntry.amount, null, true)
348
+ values.takeAsset = tradeableEntry.asset
349
+ } else if (weHave(['trade'], ['crypto']) || weHave(['trade'], ['stock'])) {
350
+ kind = 'trade'
351
+ } else if (weHave(['trade'], ['currency', 'short'])) {
352
+ if (!values.kind) throw new BadState(`Kind is not defined in values for short trade ${JSON.stringify(transfers.transfers)}.`)
353
+ // Kind has been set.
354
+ kind = values.kind as TransactionKind
355
+ } else if (weHave(['forex'], ['currency']) || weHave(['forex', 'income'], ['currency', 'statement']) || weHave(['forex', 'expense'], ['currency', 'statement'])) {
356
+ kind = 'forex'
357
+ const myEntry = transfers.transfers.filter(a => a.reason === 'forex' && a.type === 'currency' && a.asset === currency)
358
+ if (myEntry.length === 0) {
359
+ throw new SystemError(`Cannot find transfer of currency ${currency} from ${JSON.stringify(myEntry)}.`)
360
+ }
361
+ if (myEntry.length > 1) {
362
+ throw new SystemError(`Too many transfers of currency ${currency} in ${JSON.stringify(myEntry)}.`)
363
+ }
364
+ if (myEntry[0].amount === undefined) {
365
+ throw new SystemError(`Invalid forex transfer amount undefined in ${JSON.stringify(myEntry)}.`)
366
+ }
367
+ const otherEntry = transfers.transfers.filter(a => a.reason === 'forex' && a.type === 'currency' && a.asset !== currency)
368
+ if (myEntry.length === 0) {
369
+ throw new SystemError(`Cannot find transfer of currency not ${currency} from ${JSON.stringify(myEntry)}.`)
370
+ }
371
+ if (myEntry.length > 1) {
372
+ throw new SystemError(`Too many transfers of currency not ${currency} in ${JSON.stringify(myEntry)}.`)
373
+ }
374
+ if (otherEntry[0].amount === undefined) {
375
+ throw new SystemError(`Invalid forex transfer amount undefined in ${JSON.stringify(otherEntry)}.`)
376
+ }
377
+ // TODO: We should get rid of this and handle it in asset valuation always.
378
+ values.takeAsset = myEntry[0].amount < 0 ? otherEntry[0].asset : myEntry[0].asset
379
+ values.giveAsset = myEntry[0].amount < 0 ? myEntry[0].asset : otherEntry[0].asset
380
+ } else if (weHave(['dividend', 'income'], ['currency', 'statement']) || weHave(['tax', 'dividend', 'income'], ['currency', 'statement'])) {
381
+ kind = 'dividend'
382
+ } else if (weHave(['income'], ['currency', 'statement']) || weHave(['income', 'tax'], ['currency', 'statement'])) {
383
+ kind = 'income'
384
+ const statementEntry = shouldHaveOne('income', 'statement')
385
+ values.name = await this.getTranslation(`income-${statementEntry.asset}`)
386
+ } else if (weHave(['income'], ['account'])) {
387
+ kind = 'income'
388
+ const texts = transfers.transfers.filter(tr => tr.type === 'account' && tr.data && tr.data.text !== undefined).map(tr => tr.data?.text)
389
+ if (!texts.length) {
390
+ throw new SystemError(`If transfer uses direct 'account' type, one of the parts must have text defined in data: ${JSON.stringify(transfers.transfers)}`)
391
+ }
392
+ values.name = texts.join(' ')
393
+ } else if (weHave(['investment'], ['currency', 'statement'])) {
394
+ kind = 'investment'
395
+ const statementEntry = shouldHaveOne('investment', 'statement')
396
+ values.name = await this.getTranslation(`income-${statementEntry.asset}`)
397
+ } else if (weHave(['expense'], ['currency', 'statement']) || weHave(['expense', 'tax'], ['currency', 'statement'])) {
398
+ kind = 'expense'
399
+ const statementEntry = shouldHaveOne('expense', 'statement')
400
+ values.name = await this.getTranslation(`expense-${statementEntry.asset}`)
401
+ } else if (weHave(['expense'], ['account'])) {
402
+ kind = 'expense'
403
+ const texts = transfers.transfers.filter(tr => tr.type === 'account' && tr.data && tr.data.text !== undefined).map(tr => tr.data?.text)
404
+ if (!texts.length) {
405
+ throw new SystemError(`If transfer uses direct 'account' type, one of the parts must have text defined in data: ${JSON.stringify(transfers.transfers)}`)
406
+ }
407
+ values.name = texts.join(' ')
408
+ } else if (weHave(['distribution'], ['currency', 'statement'])) {
409
+ kind = 'distribution'
410
+ const statementEntry = shouldHaveOne('distribution', 'statement')
411
+ values.name = await this.getTranslation(`expense-${statementEntry.asset}`)
412
+ } else if (weHave(['tax'], ['currency', 'statement'])) {
413
+ kind = 'tax'
414
+ const statementEntry = shouldHaveOne('tax', 'statement')
415
+ values.name = await this.getTranslation(`tax-${statementEntry.asset}`)
416
+ } else if (weHave(['deposit'], ['currency', 'external'])) {
417
+ kind = 'deposit'
418
+ const moneyEntry = shouldHaveOne('deposit', 'currency', currency)
419
+ if (moneyEntry.amount === undefined) {
420
+ throw new SystemError(`Invalid deposit transfer amount undefined in ${JSON.stringify(moneyEntry)}.`)
421
+ }
422
+ } else if (weHave(['withdrawal'], ['currency', 'external'])) {
423
+ kind = 'withdrawal'
424
+ const moneyEntry = shouldHaveOne('withdrawal', 'currency', currency)
425
+ if (moneyEntry.amount === undefined) {
426
+ throw new SystemError(`Invalid withdrawal transfer amount undefined in ${JSON.stringify(moneyEntry)}.`)
427
+ }
428
+ } else if (weHave(['transfer'], ['currency', 'external'])) {
429
+ kind = 'transfer'
430
+ const moneyEntry = shouldHaveOne('transfer', 'currency', currency)
431
+ if (moneyEntry.amount === undefined) {
432
+ throw new SystemError(`Invalid transfer amount undefined in ${JSON.stringify(moneyEntry)}.`)
433
+ }
434
+ const externalEntry = shouldHaveOne('transfer', 'external')
435
+ values.service = externalEntry.asset
436
+ } else if (weHave(['correction'], ['currency', 'statement']) || weHave(['tax', 'correction'], ['currency', 'statement']) || weHave(['tax', 'correction'], ['statement'])) {
437
+ kind = 'correction'
438
+ const assets = transfers.transfers.filter(t => t.reason !== 'tax' && t.type === 'statement').reduce((prev, cur) => prev.add(cur.asset), new Set())
439
+ if (assets.size > 1) {
440
+ throw new SystemError(`Mixed asset ${[...assets].join(' and ')} corrections not supported in ${JSON.stringify(transfers.transfers)}`)
441
+ }
442
+ if (!assets.size) {
443
+ throw new SystemError(`Cannot find any statement types in ${JSON.stringify(transfers.transfers)}`)
444
+ }
445
+ const assetName = assets.values().next().value
446
+ if (/^INCOME/.test(assetName)) {
447
+ values.name = await this.getTranslation(`income-${assetName}`)
448
+ } else {
449
+ values.name = await this.getTranslation(`expense-${assetName}`)
450
+ }
451
+ } else {
452
+ console.log('Failing transfers:')
453
+ console.dir(transfers, { depth: null })
454
+ throw new NotImplemented(`Analyzer does not handle combination '${[...primaryReasons]}' and '${[...primaryAssets]}' yet.`)
455
+ }
456
+
457
+ values.kind = kind
458
+
459
+ return values
460
+ }
461
+
462
+ /**
463
+ * Helper to set values in data field.
464
+ * @param transfer
465
+ * @param values
466
+ */
467
+ setData(transfer, values): void {
468
+ if (!transfer.data) {
469
+ transfer.data = {}
470
+ }
471
+ Object.assign(transfer.data, values)
472
+ }
473
+
474
+ /**
475
+ * Helper to set rate in data field.
476
+ * @param transfer
477
+ * @param asset
478
+ * @param rate
479
+ */
480
+ setRate(transfer, asset, rate): void {
481
+ if (!transfer.data) {
482
+ transfer.data = {}
483
+ }
484
+ if (!transfer.data.rates) {
485
+ transfer.data.rates = {}
486
+ }
487
+ transfer.data.rates[asset] = rate
488
+ }
489
+
490
+ /**
491
+ * Helper to either get asset rate from data directly or ask from elsewhere.
492
+ * @param time
493
+ * @param transfer
494
+ * @param type
495
+ * @param asset
496
+ */
497
+ async getRate(time: Date, transfer: AssetTransfer, type: AssetType, asset: Asset): Promise<number> {
498
+ if (transfer.data && transfer.data.rates && transfer.data.rates[asset] !== undefined) {
499
+ if (transfer.data.rates[asset] !== undefined) {
500
+ const rate: number | undefined = transfer.data.rates[asset]
501
+ // Compiler fooling.
502
+ if (rate !== undefined) {
503
+ return rate
504
+ }
505
+ }
506
+ }
507
+ return await this.getRateAt(time, type, asset)
508
+ }
509
+
510
+ /**
511
+ * Check if rate needs to be fetched and updates it, if needed. Calculate the value.
512
+ * @param time
513
+ * @param transfer
514
+ * @param type
515
+ * @param asset
516
+ */
517
+ async setValue(time: Date, transfer: AssetTransfer, type: AssetType, asset: Asset, amount: number | null = null): Promise<void> {
518
+ const currency = this.getConfig('currency') as Currency
519
+ if (amount === null) {
520
+ // If there is no amount in transfer, postpone resolving value.
521
+ if (transfer.amount === null || transfer.amount === undefined) {
522
+ return
523
+ }
524
+ amount = transfer.amount
525
+ }
526
+
527
+ if ((type === 'currency' && asset === currency) || type === 'account') {
528
+ transfer.value = Math.round((amount || 0) * 100)
529
+ } else {
530
+ const rate = await this.getRate(time, transfer, type, asset)
531
+ transfer.value = Math.round(rate * (amount || 0) * 100)
532
+ this.setRate(transfer, asset, rate)
533
+ if (type === 'currency' && isCurrency(transfer.asset)) {
534
+ this.setData(transfer, {
535
+ currency: transfer.asset,
536
+ currencyValue: Math.round((amount || 0) * 100)
537
+ })
538
+ }
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Find local currency entries and valueate them trivially.
544
+ * @param time
545
+ * @param transfers
546
+ */
547
+ async fillInLocalCurrencies(time: Date, transfers: TransactionDescription) {
548
+ // Main currency of the database.
549
+ const currency = this.getConfig('currency') as Currency
550
+
551
+ // Fill in trivial values for local currency assets.
552
+ for (const transfer of transfers.transfers) {
553
+ if ((transfer.type === 'account' || (transfer.type === 'currency' && transfer.asset === currency)) && transfer.amount !== null) {
554
+ await this.setValue(time, transfer, transfer.type, transfer.asset)
555
+ }
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Find currency entries and valueate them.
561
+ * @param time
562
+ * @param transfers
563
+ */
564
+ async fillInCurrencies(time: Date, transfers: TransactionDescription) {
565
+ for (const transfer of transfers.transfers) {
566
+ if (transfer.value) continue
567
+ if (transfer.amount === null) continue
568
+ if (transfer.type === 'currency' && isCurrency(transfer.asset)) {
569
+ await this.setValue(time, transfer, transfer.type, transfer.asset)
570
+ } else if (transfer.data && transfer.data.currency && isCurrency(transfer.data.currency) && transfer.data.currencyValue) {
571
+ await this.setValue(time, transfer, 'currency', transfer.data.currency, transfer.data.currencyValue / 100)
572
+ } else if (transfer.type === 'currency') {
573
+ throw new SystemError(`Cannot determine currency in ${JSON.stringify(transfer)}.`)
574
+ }
575
+ }
576
+ }
577
+
578
+ /**
579
+ * Check and fill the last unknown value, if only one left.
580
+ * @param canDeduct - If set to false, just check and do not fill.
581
+ * @returns
582
+ */
583
+ fillLastMissing(transfers: AssetTransfer[], canDeduct: boolean): boolean {
584
+ // Single transfer has to define the value itself.
585
+ if (transfers.length === 1) {
586
+ return transfers[0].value !== null && transfers[0].value !== undefined
587
+ }
588
+
589
+ let total = 0
590
+ let unknown: AssetTransfer | null = null
591
+ for (const transfer of transfers) {
592
+ if (transfer.value === null || transfer.value === undefined) {
593
+ if (unknown === null && canDeduct) {
594
+ // First unknown is okay.
595
+ unknown = transfer
596
+ } else {
597
+ // More than one is too much. Give up.
598
+ return false
599
+ }
600
+ } else {
601
+ // Collect total.
602
+ total += transfer.value
603
+ }
604
+ }
605
+ // Weather or not we have any unknowns, we done.
606
+ // Note that validity of total === 0 is checked elsewhere.
607
+ if (unknown) {
608
+ unknown.value = -total
609
+ // Put the amount also, if missing and it matches the value.
610
+ if ((unknown.reason === 'income' || unknown.reason === 'expense') && unknown.type === 'statement' && unknown.amount === null) {
611
+ unknown.amount = -(total / 100)
612
+ }
613
+ }
614
+ return true
615
+ }
616
+
617
+ /**
618
+ * Look for all missing asset values and fill them in as system currency to transfer list and values list.
619
+ *
620
+ * May fill the following values:
621
+ *
622
+ * * `giveAmount` - Amount used the other given away if any.
623
+ * * `giveAsset` - Name of the other asset given away if any.
624
+ * * `takeAmount` - Amount the other asset received if any.
625
+ * * `takeAsset` - Name of the other asset received if any.
626
+ * * `data.currency` - The original currency used, if different than default.
627
+ * * `data.currencyValue` - Value in original currency used, if different than default.
628
+ *
629
+ * For transfers, the following values may be filled:
630
+ *
631
+ * * `value` - Value in the system default currency.
632
+ * * `rates` - Asset value rates vs. the system currency used in conversion.
633
+ *
634
+ * @param transfers
635
+ * @param values
636
+ * @param segment
637
+ * @param config
638
+ */
639
+ async calculateAssetValues(transfers: TransactionDescription, segment: ImportSegment): Promise<Record<string, string|number>> {
640
+ const values: Record<string, string|number> = {}
641
+
642
+ // Check if we are allowed to fill in nulls, i.e anything but trading our assets.
643
+ const hasNonCurrencyTrades = transfers.transfers.some(t => t.reason === 'trade' && t.type !== 'account' && t.type !== 'currency' && t.amount && t.amount < 0)
644
+ const needFullScan = transfers.transfers.every(t => t.value !== undefined)
645
+ let closingShortPosition = false
646
+ let canDeduct = !hasNonCurrencyTrades
647
+
648
+ // Check if we are closing short positions.
649
+ for (const transfer of transfers.transfers) {
650
+ if (transfer.reason === 'trade' && transfer.type === 'stock') {
651
+ const transferAmount = transfer.amount || 0
652
+ const { amount } = await this.getStock(segment.time, transfer.type, transfer.asset)
653
+ if (amount < 0 && transferAmount > 0) {
654
+ closingShortPosition = true
655
+ canDeduct = false
656
+ break
657
+ }
658
+ }
659
+ }
660
+
661
+ // Validate transfers.
662
+ for (const transfer of transfers.transfers) {
663
+ // Allow explicit currency data.
664
+ if (transfer.data && transfer.data.currency !== undefined && transfer.data.currencyValue !== undefined) {
665
+ continue
666
+ }
667
+ // Check invalid amount.
668
+ if (transfer.amount === undefined) {
669
+ throw new SystemError(`Invalid transfer amount undefined in ${JSON.stringify(transfer)}. Please use amount="null" to denote value that needs to be calculated.`)
670
+ }
671
+ // Check invalid reason.
672
+ if (!isAssetTransferReason(transfer.reason)) {
673
+ throw new SystemError(`Invalid transfer reason ${JSON.stringify(transfer.reason)} in ${JSON.stringify(transfer)}.`)
674
+ }
675
+ // Check invalid type.
676
+ if (!isAssetType(transfer.type)) {
677
+ throw new SystemError(`Invalid transfer type ${JSON.stringify(transfer.type)} in ${JSON.stringify(transfer)}.`)
678
+ }
679
+ // TODO: Check if rates are strings and convert. Or even better, throw an error and find the source?
680
+ }
681
+
682
+ // Calculate currency values.
683
+ await this.fillInLocalCurrencies(segment.time, transfers)
684
+ if (!needFullScan && this.fillLastMissing(transfers.transfers, canDeduct)) return values
685
+
686
+ await this.fillInCurrencies(segment.time, transfers)
687
+ if (!needFullScan && this.fillLastMissing(transfers.transfers, canDeduct)) return values
688
+
689
+ // Fill in currency values hidden inside data for some special cases.
690
+ for (const transfer of transfers.transfers) {
691
+ if (transfer.value === undefined && transfer.reason === 'tax') {
692
+ const taxCurrency = transfer?.data?.currency
693
+ if (!taxCurrency) {
694
+ throw new SystemError(`A currency must be defined in data for ${transfer.reason} transfers in ${JSON.stringify(transfer)}.`)
695
+ }
696
+ await this.setValue(segment.time, transfer, 'currency', taxCurrency)
697
+ }
698
+ }
699
+ if (!needFullScan && this.fillLastMissing(transfers.transfers, canDeduct)) return values
700
+
701
+ // Fill in fees and dividends in whatever unit they have been determined.
702
+ for (const transfer of transfers.transfers) {
703
+ if (transfer.value === undefined && (transfer.reason === 'fee' || transfer.reason === 'dividend')) {
704
+ await this.setValue(segment.time, transfer, transfer.type, transfer.asset)
705
+ }
706
+ }
707
+ if (!needFullScan && this.fillLastMissing(transfers.transfers, canDeduct)) return values
708
+
709
+ // Handle trades.
710
+ for (const transfer of transfers.transfers) {
711
+ if (transfer.reason === 'trade') {
712
+ const transferAmount = transfer.amount || 0
713
+ // SELLING or GIVING
714
+ if (transferAmount <= 0) {
715
+ // If value is already calculated, use it.
716
+ if (transfer.value !== undefined) {
717
+ transfer.value = Math.round(transfer.value || 0)
718
+ } else {
719
+ const { value, amount } = await this.getStock(segment.time, transfer.type, transfer.asset)
720
+ if (less(amount, -transferAmount)) {
721
+ // We hit this on first iteration. After that it should be sorted out.
722
+ // Ot at least pass through to the short selling question.
723
+ const renamed = await this.UI.askedRenamingOrThrow(this.config, segment, transfer.type, transfer.asset)
724
+ if (renamed === true) {
725
+ throw new SystemError(`Something went wrong. Asset ${transfer.type} ${transfer.asset} has been renamed but we did not encounter actual transaction for the renaming.`)
726
+ }
727
+ }
728
+ if (less(amount, -transferAmount)) {
729
+ // Handle short selling.
730
+ const shortOk = await this.UI.getBoolean(this.config, 'allowShortSelling', 'Do we allow short selling of assets?')
731
+ if (!shortOk) {
732
+ throw new SystemError(`We have ${amount} assets ${transfer.asset} in stock for trading on ${segment.time} when ${-transferAmount} needed.`)
733
+ }
734
+ if (amount > 0) {
735
+ throw new NotImplemented(`Cannot handle mix of short selling and normal selling ${transferAmount} ${transfer.asset} on ${segment.time} and having ${amount}.`)
736
+ }
737
+ transfer.type = 'short'
738
+ values.kind = 'short-sell'
739
+ transfer.value = -transfers.transfers.filter(t => t.value && t.value > 0 && t.type === 'currency').reduce((prev, cur) => prev + ((cur && cur.value) || 0), 0)
740
+ } else {
741
+ // Normal selling is valued by the value of the asset in our stock.
742
+ transfer.value = Math.round(transferAmount * (value / amount))
743
+ if (!transfer.value) {
744
+ throw new SystemError(`Asset ${transfer.type} ${transfer.asset} have no value left when trading on ${segment.time}.`)
745
+ }
746
+ }
747
+ }
748
+ values.giveAmount = num(transferAmount, null, true)
749
+ values.giveAsset = transfer.asset
750
+ } else {
751
+ // BUYING or RECEIVING
752
+ if (closingShortPosition) {
753
+ const { value, amount } = await this.getStock(segment.time, transfer.type, transfer.asset)
754
+ // Valuation comes from the short selling price.
755
+ transfer.value = Math.round(transferAmount * (value / amount))
756
+ transfer.type = 'short'
757
+ values.kind = 'short-buy'
758
+ } else {
759
+ // Get the value for stuff traded in from the rate.
760
+ if (transfer.value === undefined) {
761
+ const rate = await this.getRate(segment.time, transfer, transfer.type, transfer.asset)
762
+ transfer.value = Math.round(rate * transferAmount * 100)
763
+ this.setRate(transfer, transfer.asset, rate)
764
+ } else {
765
+ transfer.value = Math.round(transfer.value || 0)
766
+ }
767
+ }
768
+ values.takeAmount = num(transferAmount, null, true)
769
+ values.takeAsset = transfer.asset
770
+ }
771
+ }
772
+ }
773
+
774
+ // Try to sort out some more complex cases.
775
+ if (canDeduct) {
776
+ await this.handleMultipleMissingValues(transfers)
777
+ }
778
+
779
+ if (!this.fillLastMissing(transfers.transfers, canDeduct)) {
780
+ throw new SystemError(`Unable to determine valuation in ${JSON.stringify(transfers)}.`)
781
+ }
782
+
783
+ return values
784
+ }
785
+
786
+ /**
787
+ * Try some heuristics if we can map transfers so that can solve multiple missing valuations.
788
+ * @param transfers
789
+ */
790
+ async handleMultipleMissingValues(transfers: TransactionDescription): Promise<void> {
791
+ // console.dir(transfers, { depth: null })
792
+ // Transfers that have missing valuation.
793
+ const missing: AssetTransfer[] = []
794
+ // Those that has valuation sorted by reason-type-pair.
795
+ const byType: Record<string, AssetTransfer[]> = {}
796
+ for (const transfer of transfers.transfers) {
797
+ if (transfer.amount === null) {
798
+ missing.push(transfer)
799
+ } else {
800
+ const key = `${transfer.reason}.${transfer.type}`
801
+ byType[key] = byType[key] || []
802
+ byType[key].push(transfer)
803
+ }
804
+ }
805
+ // console.dir(byType, { depth: null })
806
+
807
+ const n = missing.length
808
+
809
+ if (n < 2) {
810
+ return
811
+ }
812
+
813
+ // Select type combinations we can solve.
814
+ const keys = Object.keys(byType)
815
+
816
+ if (setEqualArray(new Set(['income.statement', 'tax.statement']), keys)) {
817
+ // Check if it looks that there are mismatching chains.
818
+ for (const key of keys) {
819
+ if (byType[key].length > n) {
820
+ warning(`Trying to resolve more than one missing value, but probably leads to fail, since we got ${byType[key].length} entries for ${key} while expecting ${n}.`)
821
+ }
822
+ }
823
+
824
+ // Collect slices by taking one of each. One from missing and one from all others.
825
+ // Then resolve the missing one. Skip if some chain is shorter than others.
826
+ for (let i = 0; i < n; i++) {
827
+ const slice = [missing[i]]
828
+ for (const key of keys) {
829
+ if (byType[key][i]) {
830
+ slice.push(byType[key][i])
831
+ }
832
+ }
833
+ this.fillLastMissing(slice, true)
834
+ }
835
+ return
836
+ }
837
+
838
+ throw new NotImplemented(`Not able yet to calculate missing values for ${keys.join(' and ')}`)
839
+ }
840
+
841
+ /**
842
+ * Analyze transfer and construct the corresponding transaction structure.
843
+ * @param transfers
844
+ * @returns
845
+ */
846
+ async analyze(transfers: TransactionDescription, segment: ImportSegment, config: ProcessConfig): Promise<TransactionDescription> {
847
+ // Add new configurations in.
848
+ merge.recursive(this.config, config)
849
+ // Calculate some indicators and find settings.
850
+ transfers = clone(transfers)
851
+
852
+ // Collect accounts we are going to need.
853
+ const accounts: Record<string, AccountNumber> = await this.collectAccounts(segment, transfers) as Record<string, AccountNumber>
854
+
855
+ // Tune fees, if we have some and total needs adjustments.
856
+ let feeIsMissingFromTotal = false
857
+ const hasFees = transfers.transfers.filter(t => t.reason === 'fee').length > 0
858
+ if (hasFees) {
859
+ const nonFees = new Set(transfers.transfers.filter(t => t.reason !== 'fee' && !(t.reason === 'income' && t.asset.indexOf('PROFIT') >= 0) && !(t.reason === 'expense' && t.asset.indexOf('LOSS') >= 0)).map(t => t.reason))
860
+ if (nonFees.size > 1) {
861
+ throw new Error(`Too many non-fees (${[...nonFees].join(' and ')}) to determine actual transfer reasoning ${JSON.stringify(transfers.transfers)}.`)
862
+ }
863
+ const nonFee = [...nonFees][0]
864
+ const feeTypes = new Set(transfers.transfers.filter(t => t.reason === 'fee').map(t => t.type))
865
+ if (feeTypes.size > 1) {
866
+ throw new Error(`Too many fee types (${[...feeTypes].join(' and ')}) to determine actual fee type ${JSON.stringify(transfers.transfers)}.`)
867
+ }
868
+ const feeType = [...feeTypes][0]
869
+ let variable: string
870
+ if (nonFee === 'trade') {
871
+ switch (feeType) {
872
+ case 'currency':
873
+ variable = 'isTradeFeePartOfTotal'
874
+ break
875
+ case 'crypto':
876
+ variable = 'isCryptoTradeFeePartOfTotal'
877
+ break
878
+ default:
879
+ throw new NotImplemented(`Cannot handle fee type '${feeType}' yet.`)
880
+ }
881
+ } else if (nonFee === 'withdrawal') {
882
+ variable = 'isWithdrawalFeePartOfTotal'
883
+ } else if (nonFee === 'forex') {
884
+ variable = 'isForexFeePartOfTotal'
885
+ } else {
886
+ throw new Error(`Handling non-fee '${nonFee}' not implemented.`)
887
+ }
888
+ feeIsMissingFromTotal = !await this.UI.getBoolean(config, variable, 'Is transaction fee of type {type} already included in the {reason} total?'.replace('{type}', `${feeType}`).replace('{reason}', await this.getTranslation(`reason-${nonFee}`)))
889
+
890
+ // Adjust asset transfers by the fee paid as asset itself, when they are missing from transfer total.
891
+ if (feeIsMissingFromTotal) {
892
+ for (const fee of transfers.transfers.filter(t => t.reason === 'fee')) {
893
+ const assetTransfers = transfers.transfers.filter(t => t.type === fee.type && t.asset === fee.asset && ['trade', 'forex', 'withdrawal'].includes(t.reason))
894
+ if (assetTransfers.length < 1) {
895
+ throw new SystemError(`Cannot find any assets to adjust for ${fee.asset} fee in ${JSON.stringify(transfers.transfers)}`)
896
+ }
897
+ if (assetTransfers[0].amount === undefined || fee.amount === undefined) {
898
+ throw new SystemError(`Unable to adjust fee assets for ${fee.asset} fee in ${JSON.stringify(transfers.transfers)}`)
899
+ }
900
+ assetTransfers[0].amount -= fee.amount
901
+ }
902
+ }
903
+ }
904
+
905
+ // Fill in amounts for renamed assets.
906
+ if (transfers.transfers.length > 0 && 'notes' in (transfers.transfers[0].data || {})) {
907
+ const data = transfers.transfers[0].data as AdditionalTransferInfo
908
+ const renamed = await this.getTranslation('note-renamed')
909
+ if ((data?.notes || []).includes(renamed)) {
910
+
911
+ const oldName = await this.getTranslation('note-old-name')
912
+ const newName = await this.getTranslation('note-new-name')
913
+
914
+ const oldTr = transfers.transfers.find(t => (t.data?.notes || []).includes(oldName))
915
+ const newTr = transfers.transfers.find(t => (t.data?.notes || []).includes(newName))
916
+
917
+ if (!oldTr) {
918
+ throw new SystemError(`Cannot find old name '${oldName}' from transfer notes in renaming ${JSON.stringify(transfers.transfers)}.`)
919
+ }
920
+ if (!newTr) {
921
+ throw new SystemError(`Cannot find new name '${newName}' from transfer notes in renaming ${JSON.stringify(transfers.transfers)}.`)
922
+ }
923
+
924
+ const { value, amount } = await this.getStock(segment.time, oldTr.type, oldTr.asset)
925
+
926
+ oldTr.value = -value
927
+ oldTr.amount = -amount
928
+ newTr.value = +value
929
+ newTr.amount = +amount
930
+ }
931
+ }
932
+
933
+ // Resolve values in the system currency as cents for each line.
934
+ const assetValues = await this.calculateAssetValues(transfers, segment)
935
+
936
+ // Resolve what kind of transfers we have.
937
+ const values = await this.collectOtherValues(transfers, assetValues)
938
+ const kind: TransactionKind = values.kind as TransactionKind
939
+
940
+ // Calculate stock change values.
941
+ const feesToDeduct: Record<string, number> = {}
942
+ const valueToDeduct: Record<string, number> = {}
943
+ if (hasFees) {
944
+ for (const transfer of transfers.transfers) {
945
+ if (transfer.type === 'crypto' || transfer.type === 'stock' || transfer.type === 'short') {
946
+ if (transfer.reason === 'fee') {
947
+ feesToDeduct[transfer.asset] = (feesToDeduct[transfer.asset] || 0) + (transfer.amount || 0)
948
+ valueToDeduct[transfer.asset] = (valueToDeduct[transfer.asset] || 0) + (transfer.value || 0)
949
+ }
950
+ }
951
+ }
952
+ }
953
+
954
+ for (const transfer of transfers.transfers) {
955
+ const change: Partial<Record<Asset, StockValueData>> = {}
956
+ if (transfer.type === 'crypto' || transfer.type === 'stock' || transfer.type === 'short') {
957
+ if (transfer.reason !== 'fee') {
958
+ if (transfer.value === undefined) {
959
+ throw Error(`Encountered invalid transfer value undefined for ${JSON.stringify(transfer)}.`)
960
+ }
961
+ if (transfer.amount === undefined) {
962
+ throw Error(`Encountered invalid transfer amount undefined for ${JSON.stringify(transfer)}.`)
963
+ }
964
+ // Fees will reduce the same account than used in the transfers.
965
+ // They have been added to the stock transfer already, so can be ignored here.
966
+ change[transfer.asset as string] = {
967
+ value: transfer.value || 0,
968
+ amount: transfer.amount || 0
969
+ }
970
+ const data: AdditionalTransferInfo = { stock: { change } }
971
+ if (feesToDeduct[transfer.asset]) {
972
+ change[transfer.asset as string].amount -= feesToDeduct[transfer.asset]
973
+ change[transfer.asset as string].value -= valueToDeduct[transfer.asset]
974
+ data.feeAmount = feesToDeduct[transfer.asset]
975
+ data.feeCurrency = transfer.asset as Currency | CryptoCurrency
976
+ delete feesToDeduct[transfer.asset]
977
+ }
978
+ this.setData(transfer, data)
979
+ const type = transfer.type === 'short' ? 'stock' : transfer.type
980
+ await this.changeStock(segment.time, type, transfer.asset, transfer.amount, transfer.value)
981
+ }
982
+ }
983
+ }
984
+
985
+ if (Object.keys(feesToDeduct).length) {
986
+ throw new Error(`There was no matching transfer to deduct ${Object.keys(feesToDeduct).join(' and ')} in ${JSON.stringify(transfers.transfers)}.`)
987
+ }
988
+
989
+ // Verify that we have values set and calculate total.
990
+ // Collect missing values.
991
+ let total = 0
992
+ for (const transfer of transfers.transfers) {
993
+ if (transfer.value === undefined) {
994
+ throw new SystemError(`Failed to determine value of transfer ${JSON.stringify(transfer)}.`)
995
+ }
996
+ total += transfer.value
997
+ }
998
+
999
+ // Set up profit and losses.
1000
+ if (kind === 'trade' || kind === 'sell' || kind === 'short-buy') {
1001
+ if (total) {
1002
+ const soldAsset: AssetTransfer[] = (kind === 'short-buy'
1003
+ ? transfers.transfers.filter(t => t.reason === 'trade' && t.value && t.value > 0)
1004
+ : transfers.transfers.filter(t => t.reason === 'trade' && t.value && t.value < 0))
1005
+ if (soldAsset.length !== 1) {
1006
+ throw new BadState(`Did not found unique asset that was sold from ${JSON.stringify(transfers.transfers)}`)
1007
+ }
1008
+ let reason: AssetTransferReason
1009
+ let asset: VATTarget
1010
+ if (total > 0) {
1011
+ reason = 'income'
1012
+ if (kind === 'short-buy') {
1013
+ asset = 'TRADE_PROFIT_SHORT'
1014
+ } else {
1015
+ asset = `TRADE_PROFIT_${soldAsset[0].type.toUpperCase()}` as IncomeSource
1016
+ }
1017
+ } else {
1018
+ reason = 'expense'
1019
+ if (kind === 'short-buy') {
1020
+ asset = 'TRADE_LOSS_SHORT'
1021
+ } else {
1022
+ asset = `TRADE_LOSS_${soldAsset[0].type.toUpperCase()}` as ExpenseSink
1023
+ }
1024
+ }
1025
+ const gains: AssetTransfer = {
1026
+ reason,
1027
+ asset,
1028
+ amount: -total / 100,
1029
+ type: 'statement',
1030
+ value: -total
1031
+ }
1032
+ // Pass notes to profit/loss.
1033
+ if (soldAsset[0].data && soldAsset[0].data.notes) {
1034
+ gains.data = {
1035
+ notes: soldAsset[0].data.notes
1036
+ }
1037
+ }
1038
+ // Resolve account. If not known, ask.
1039
+ const account = await this.getAccount(gains.reason, gains.type, gains.asset, segment.id)
1040
+ const address: AccountAddress = `${gains.reason}.${gains.type}.${gains.asset}` as AccountAddress
1041
+ if (account) {
1042
+ accounts[address] = account
1043
+ } else {
1044
+ await this.UI.throwGetAccount(config, address)
1045
+ }
1046
+ transfers.transfers.push(gains)
1047
+ total = 0
1048
+ }
1049
+ }
1050
+
1051
+ // At this point, total should be in order.
1052
+ if (Math.abs(total) > ZERO_CENTS) {
1053
+ throw new SystemError(`Total should be zero but got ${total} from ${JSON.stringify(transfers.transfers)}.`)
1054
+ }
1055
+
1056
+ // Add more info where we can.
1057
+ this.fillCurrencies(transfers)
1058
+
1059
+ /*
1060
+ * Put together transaction parts
1061
+ * ------------------------------
1062
+ */
1063
+ const tx = await this.createTransaction(transfers, kind, values, accounts, segment)
1064
+ transfers.transactions = [tx]
1065
+
1066
+ // Helper to stop the process when need to check analysis final state during developing.
1067
+ // throw new Error('Stop')
1068
+ return transfers
1069
+ }
1070
+
1071
+ /**
1072
+ * Construct a transaction based on the data collected.
1073
+ * @param transfers
1074
+ * @param kind
1075
+ * @param values
1076
+ * @param accounts
1077
+ * @param segment
1078
+ * @returns
1079
+ */
1080
+ async createTransaction(transfers: TransactionDescription, kind: TransactionKind, values: Record<string, string | number>, accounts: Record<string, AccountNumber>, segment: ImportSegment): Promise<Transaction> {
1081
+
1082
+ const tx: Transaction = {
1083
+ date: segment.time,
1084
+ segmentId: segment.id,
1085
+ entries: []
1086
+ }
1087
+
1088
+ // Clone execution result if already given.
1089
+ const executionResult = transfers.transactions?.length ? transfers.transactions[0].executionResult : undefined
1090
+ if (executionResult) {
1091
+ tx.executionResult = executionResult
1092
+ }
1093
+
1094
+ let lastText
1095
+ for (let i = 0; i < transfers.transfers.length; i++) {
1096
+ // Figure out text.
1097
+ const transfer = transfers.transfers[i]
1098
+ const data = transfer.data || {}
1099
+ if (transfer.text) lastText = transfer.text
1100
+ let description = lastText
1101
+ if (!description) description = await this.constructText(kind, { ...values, ...data }, transfers)
1102
+ if (!description) {
1103
+ throw new SystemError(`Failed to construct description for ${JSON.stringify(transfer)}.`)
1104
+ }
1105
+
1106
+ // Add notes.
1107
+ if (transfer.data && transfer.data.notes) {
1108
+ const notes: TransferNote[] = []
1109
+ for (const note of transfer.data.notes) {
1110
+ if (note && `${note}`.trim()) {
1111
+ const translatedNote = await this.getTranslation(`note-${note}`)
1112
+ if (translatedNote !== `note-${note}`) {
1113
+ notes.push(translatedNote)
1114
+ } else {
1115
+ notes.push(note)
1116
+ }
1117
+ }
1118
+ }
1119
+ if (notes.length) {
1120
+ description += ` (${notes.join(', ')})`
1121
+ }
1122
+ }
1123
+
1124
+ // Set the account and amount.
1125
+ let txEntry: TransactionLine = {
1126
+ account: accounts[`${transfer.reason}.${transfer.type}.${transfer.asset}`],
1127
+ amount: transfer.value === undefined ? 0 : transfer.value,
1128
+ description
1129
+ }
1130
+ if (!txEntry.account) {
1131
+ throw new SystemError(`Cannot find account ${transfer.reason}.${transfer.type}.${transfer.asset} for entry ${JSON.stringify(txEntry)}`)
1132
+ }
1133
+
1134
+ // Update balance and check for negative currency account if it needs to be configured.
1135
+ const total = this.applyBalance(txEntry)
1136
+ if (this.balances.mayTakeLoan(transfer.reason, transfer.type, transfer.asset) && realNegative(total)) {
1137
+ const addr: AccountAddress = `${transfer.reason}.${transfer.type}.${transfer.asset}` as AccountAddress
1138
+ const debtAddr: AccountAddress = this.balances.debtAddress(addr)
1139
+ const debtAccount = this.getConfig(`account.${debtAddr}`, null)
1140
+ if (debtAccount === null) {
1141
+ await this.UI.throwDebtAccount(this.config, txEntry.account, addr)
1142
+ }
1143
+ }
1144
+
1145
+ // Add data and rates.
1146
+ if (transfer.data) {
1147
+ txEntry.data = clone(transfer.data)
1148
+ }
1149
+
1150
+ // Check if there are deposits or withdrawals and make sure they are added.
1151
+ const { reason, type } = transfer
1152
+ if (type === 'external') {
1153
+ if (reason === 'deposit') {
1154
+ const recordDeposits = await this.UI.getBoolean(this.config, 'recordDeposits', 'Deposits tend to appear in two import sources. Do you want to record deposits in this import?')
1155
+ if (!recordDeposits) {
1156
+ tx.executionResult = 'ignored'
1157
+ }
1158
+ } else if (reason === 'withdrawal') {
1159
+ const recordWithdrawals = await this.UI.getBoolean(this.config, 'recordWithdrawals', 'Withdrawals tend to appear in two import sources. Do you want to record withdrawals in this import?')
1160
+ if (!recordWithdrawals) {
1161
+ tx.executionResult = 'ignored'
1162
+ }
1163
+ }
1164
+ }
1165
+
1166
+ // Post-process and add it to the list.
1167
+ txEntry = await this.postProcessTags(txEntry, transfer, segment)
1168
+ tx.entries.push(txEntry)
1169
+ }
1170
+
1171
+ return tx
1172
+ }
1173
+
1174
+ /**
1175
+ * Handle tags for one transaction line.
1176
+ * @param tx
1177
+ * @param segment
1178
+ * @param config
1179
+ */
1180
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1181
+ async postProcessTags(tx: TransactionLine, transfer: AssetTransfer, segment: ImportSegment): Promise<TransactionLine> {
1182
+ // Find out tags.
1183
+ let tags
1184
+ // Check from transfer.
1185
+ if (!('tags' in transfer)) {
1186
+ // Find from config and ask if not yet given.
1187
+ tags = await this.getTags(transfer.reason, transfer.type, transfer.asset)
1188
+ } else {
1189
+ tags = transfer.tags
1190
+ }
1191
+ if (tags) {
1192
+ // Handle also string notation '[A][B]'
1193
+ if (typeof tags === 'string' && /^(\[[a-zA-Z0-9]+\])+$/.test(tags)) {
1194
+ tags = tags.substr(1, tags.length - 2).split('][')
1195
+ }
1196
+ if (!(tags instanceof Array)) {
1197
+ throw new BadState(`Invalid tags ${JSON.stringify(tags)}`)
1198
+ }
1199
+ tx.description = `[${tags.filter(t => !!t).join('][')}] ${tx.description}`
1200
+ }
1201
+ return tx
1202
+ }
1203
+
1204
+ /**
1205
+ * Get the specific account from the settings. Checks also more generic '<reason>.<type>.*' version if the exact not found.
1206
+ * @param reason
1207
+ * @param type
1208
+ * @param asset
1209
+ * @returns
1210
+ */
1211
+ async getAccount(reason: AssetTransferReason, type: AssetType, asset: Asset, segmentId?: SegmentId): Promise<undefined | AccountNumber> {
1212
+ const account = this.getConfig(`account.${reason}.${type}.${asset}`, null)
1213
+ if (typeof (account) === 'string') {
1214
+ return account as AccountNumber
1215
+ }
1216
+ const generic = this.getConfig(`account.${reason}.${type}.*`, null)
1217
+ if (typeof (generic) === 'string') {
1218
+ return generic as AccountNumber
1219
+ }
1220
+ if (/^[0-9]+$/.test(asset)) {
1221
+ return asset as AccountNumber
1222
+ }
1223
+
1224
+ if (!segmentId) {
1225
+ return undefined
1226
+ }
1227
+ // If we know segment ID, we could try to check if there is question about the account answered.
1228
+ const answers: Record<SegmentId, Record<AccountAddress, AccountNumber>> = this.getConfig('answers', {}) as Record<SegmentId, Record<AccountAddress, AccountNumber>>
1229
+
1230
+ if (answers[segmentId] && answers[segmentId][`account.${reason}.${type}.${asset}`]) {
1231
+ return answers[segmentId][`account.${reason}.${type}.${asset}`]
1232
+ }
1233
+ }
1234
+
1235
+ /**
1236
+ * Get tags for the transfer if defined in configuration.
1237
+ * @param reason
1238
+ * @param type
1239
+ * @param asset
1240
+ * @returns
1241
+ */
1242
+ async getTags(reason: AssetTransferReason, type: AssetType, asset: Asset): Promise<Tag[] | undefined> {
1243
+ for (const variable of [`tags.${reason}.${type}.${asset}`, `tags.${reason}.${type}.*`, `tags.${reason}.*.*`, 'tags.*.*.*']) {
1244
+ const tags = this.getConfig(variable, null)
1245
+ if (tags !== null) {
1246
+ if (tags instanceof Array) {
1247
+ return tags
1248
+ }
1249
+ throw new BadState(`Bad tags configured ${JSON.stringify(tags)} for tags.${reason}.${type}.${asset}`)
1250
+ }
1251
+ }
1252
+ }
1253
+
1254
+ /**
1255
+ * Similar to getTags() but use account address.
1256
+ * @param addr
1257
+ */
1258
+ async getTagsForAddr(addr: AccountAddress) {
1259
+ const [reason, type, asset] = addr.split('.')
1260
+ return this.getTags(reason as AssetTransferReason, type as AssetType, asset as Asset)
1261
+ }
1262
+
1263
+ /**
1264
+ * Get the UI query for account from the settings if defined.
1265
+ * @param reason
1266
+ * @param type
1267
+ * @param asset
1268
+ * @returns
1269
+ */
1270
+ async getAccountQuery(reason: AssetTransferReason, type: AssetType, asset: Asset): Promise<undefined | UIQuery> {
1271
+ const account = this.getConfig(`account.${reason}.${type}.${asset}`, null)
1272
+ if (typeof (account) === 'object' && account !== null) {
1273
+ return account as UIQuery
1274
+ }
1275
+ }
1276
+
1277
+ /**
1278
+ * Builder for text descriptions.
1279
+ * @param template
1280
+ * @param values
1281
+ */
1282
+ async constructText(kind: TransactionKind, values: Record<string, unknown>, original: TransactionDescription): Promise<string> {
1283
+
1284
+ const template = `import-text-${kind}`
1285
+
1286
+ // Fetch the text modifiers.
1287
+ const prefix: string = this.getConfig('transaction.prefix', '') as string
1288
+
1289
+ // Collect translations to one string.
1290
+ let text: string = await this.getTranslation(template)
1291
+ if (text === template) {
1292
+ throw new BadState(`Not able to find translation for '${template}'.`)
1293
+ }
1294
+ text = `${prefix}${text}`
1295
+
1296
+ // Substitute values into the string.
1297
+ while (true) {
1298
+ const match = /(\{([a-zA-Z0-9]+)\})/.exec(text)
1299
+ if (!match) break
1300
+ if (values[match[2]] === undefined) {
1301
+ throw new BadState(`Not able to find value '${match[2]}' needed by '${text}' from ${JSON.stringify(original)}`)
1302
+ }
1303
+ const value = `${values[match[2]]}`
1304
+ text = text.replace(match[1], value)
1305
+ }
1306
+ return text
1307
+ }
1308
+
1309
+ /**
1310
+ * Find the rate in the default currency for the asset.
1311
+ * @param time
1312
+ * @param type
1313
+ * @param asset
1314
+ */
1315
+ async getRateAt(time: Date, type: AssetType, asset: Asset): Promise<number> {
1316
+
1317
+ const exchange = this.handler.name as AssetExchange
1318
+ const currency = this.getConfig('currency') as Currency
1319
+ if ((type === 'currency' && asset === currency) || type === 'account') {
1320
+ return 1.0
1321
+ }
1322
+ if (!exchange && type === 'crypto') {
1323
+ throw new Error(`Exchange is compulsory setting in cryptocurrency import. Cannot determine rate for ${asset} at ${time}.`)
1324
+ }
1325
+ if (!isTransactionImportConnector(this.handler.system.connector)) {
1326
+ throw new SystemError('Connector used is not a transaction import connector.')
1327
+ }
1328
+ return this.handler.getRate(time, type, asset, currency, exchange)
1329
+ }
1330
+
1331
+ /**
1332
+ * Find the amount of asset owned at the spesific time.
1333
+ * @param time
1334
+ * @param type
1335
+ * @param asset
1336
+ * @returns
1337
+ */
1338
+ async getStock(time: Date, type: AssetType, asset: Asset): Promise<StockValueData> {
1339
+ if (!isTransactionImportConnector(this.handler.system.connector)) {
1340
+ throw new SystemError('Connector used is not a transaction import connector.')
1341
+ }
1342
+ const account: AccountNumber = await this.getAccount('trade', type, asset) as AccountNumber
1343
+ if (!account) {
1344
+ throw new Error(`Unable to find account for ${type} ${asset}.`)
1345
+ }
1346
+ // If no records yet, fetch it using the connector.
1347
+ if (!this.stocks[account]) {
1348
+ this.stocks[account] = new StockBookkeeping(`Account ${account}`)
1349
+ }
1350
+ if (!this.stocks[account].has(type, asset)) {
1351
+ const { value, amount } = await this.handler.system.connector.getStock(time, account, asset as TradeableAsset)
1352
+ this.stocks[account].set(time, type, asset, amount, value)
1353
+ return { value, amount }
1354
+ }
1355
+ const ret = this.stocks[account].get(time, type, asset)
1356
+ return ret
1357
+ }
1358
+
1359
+ /**
1360
+ * Update internal stock bookkeeping.
1361
+ * @param time
1362
+ * @param type
1363
+ * @param asset
1364
+ * @param amount
1365
+ * @param value
1366
+ */
1367
+ async changeStock(time: Date, type: AssetType, asset: Asset, amount: number, value: number): Promise<void> {
1368
+ // Force reading the stock initial status.
1369
+ await this.getStock(time, type, asset)
1370
+
1371
+ const account = await this.getAccount('trade', type, asset)
1372
+ if (!account) {
1373
+ throw new Error(`Unable to find account for ${type} ${asset}.`)
1374
+ }
1375
+ if (!this.stocks[account]) {
1376
+ this.stocks[account] = new StockBookkeeping(`Account ${account}`)
1377
+ }
1378
+ await this.stocks[account].change(time, type, asset, amount, value)
1379
+ }
1380
+
1381
+ /**
1382
+ * Get the average price of the asset at the specific time.
1383
+ * @param time
1384
+ * @param type
1385
+ * @param asset
1386
+ * @returns
1387
+ */
1388
+ async getAverage(time: Date, type: AssetType, asset: Asset): Promise<number> {
1389
+ const { amount, value } = await this.getStock(time, type, asset)
1390
+ return value / amount
1391
+ }
1392
+
1393
+ /**
1394
+ * Detect currencies and their rates and fill in data where we can.
1395
+ * @param transfers
1396
+ */
1397
+ fillCurrencies(transfers: TransactionDescription): void {
1398
+ const rates: Partial<Record<Currency, number>> = {}
1399
+ const explicitCurrencies: Set<Currency> = new Set()
1400
+
1401
+ // Helper to warn different rates.
1402
+ const setRate = (currency: Currency, rate: number): void => {
1403
+ if (rates[currency] !== undefined && Math.abs(rate - (rates[currency] || 0)) > 0.1) {
1404
+ warning(`Found two different rates ${rates[currency]} and ${rate} for ${currency} on ${JSON.stringify(transfers.transfers)}.`)
1405
+ }
1406
+ rates[currency] = rate
1407
+ }
1408
+
1409
+ // Scan for rates.
1410
+ transfers.transfers.forEach(transfer => {
1411
+ if (transfer.data && transfer.data.currency && transfer.data.currencyValue && transfer.value !== undefined) {
1412
+ setRate(transfer.data.currency, transfer.value / transfer.data.currencyValue)
1413
+ explicitCurrencies.add(transfer.data.currency)
1414
+ }
1415
+ if (transfer.data && transfer.data.rates !== undefined) {
1416
+ Object.keys(transfer.data.rates).forEach(currency => {
1417
+ if (transfer.data !== undefined && transfer.data.rates !== undefined && transfer.data.rates[currency] !== undefined) {
1418
+ setRate(currency as Currency, parseFloat(transfer.data.rates[currency]))
1419
+ }
1420
+ })
1421
+ }
1422
+ })
1423
+ if (Object.keys(rates).length === 0) {
1424
+ return
1425
+ }
1426
+
1427
+ // Fill in rates.
1428
+ transfers.transfers.forEach(transfer => {
1429
+ transfer.data = transfer.data || {}
1430
+ transfer.data.rates = transfer.data.rates || {}
1431
+ Object.assign(transfer.data.rates, rates)
1432
+ })
1433
+
1434
+ // If unique, calculate value for every entry.
1435
+ if (explicitCurrencies.size !== 1) {
1436
+ return
1437
+ }
1438
+ const currency: Currency = [...explicitCurrencies][0]
1439
+ transfers.transfers.forEach(transfer => {
1440
+ if (transfer.data && transfer.data.currency === undefined && transfer.data.currencyValue === undefined && transfer.value !== undefined) {
1441
+ if (rates[currency] !== undefined) {
1442
+ transfer.data = transfer.data || {}
1443
+ transfer.data.currency = currency
1444
+ transfer.data.currencyValue = Math.round(transfer.value / (rates[currency] || 0))
1445
+ }
1446
+ }
1447
+ })
1448
+
1449
+ }
1450
+ }