@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,819 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import { TasenorElement, AccountAddress, Asset, AssetExchange, AssetTransfer, AssetTransferReason, AssetType, Currency, Language, TransactionDescription, TransactionApplyResults, debug, realNegative, AccountNumber, realPositive, ProcessConfig, ImportStateText, TextFileLine, SegmentId, NO_SEGMENT, num, ImportSegment, Directions, ImportAnswers, ImportConfig, BalanceSummaryEntry, less, mergeTags, log } from '@dataplug/tasenor-common'
3
+ import { TransferAnalyzer } from './TransferAnalyzer'
4
+ import hash from 'object-hash'
5
+ import { TransactionUI } from './TransactionUI'
6
+ import { TransactionRules } from './TransactionRules'
7
+ import { isTransactionImportConnector, TransactionImportConnector } from './TransactionImportConnector'
8
+ import { TextFileProcessHandler } from './TextFileProcessHandler'
9
+ import { Process, ProcessFile } from '../process'
10
+ import { BadState, InvalidFile, NotImplemented, SystemError } from '../error'
11
+ import clone from 'clone'
12
+
13
+ /**
14
+ * Core functionality for all transaction import handlers.
15
+ */
16
+ export class TransactionImportHandler extends TextFileProcessHandler {
17
+
18
+ public UI: TransactionUI
19
+ public rules: TransactionRules
20
+ private analyzer: TransferAnalyzer | null
21
+
22
+ constructor(name: string) {
23
+ super(name)
24
+ this.UI = new TransactionUI(this)
25
+ this.rules = new TransactionRules(this)
26
+ }
27
+
28
+ /**
29
+ * By default, we don't support multifile.
30
+ * @param file
31
+ * @returns
32
+ */
33
+ canAppend(file: ProcessFile): boolean {
34
+ return false
35
+ }
36
+
37
+ /**
38
+ * Get a single account balance.
39
+ * @param addr
40
+ */
41
+ getBalance(addr: AccountAddress): number {
42
+ if (!this.analyzer) {
43
+ throw new Error(`Cannot access balance for ${addr} when no analyzer instantiated.`)
44
+ }
45
+ return this.analyzer.getBalance(addr)
46
+ }
47
+
48
+ /**
49
+ * Get the translation for the text to the currently configured language.
50
+ * @param text
51
+ * @returns
52
+ */
53
+ async getTranslation(text: string, language: Language | undefined): Promise<string> {
54
+ if (!language) {
55
+ throw new SystemError('Language is compulsory setting for importing, if there are unknowns to ask from UI.')
56
+ }
57
+ return this.system.getTranslation(text, language)
58
+ }
59
+
60
+ /**
61
+ * Get the account having matching asset in their code.
62
+ * @param asset
63
+ * @returns
64
+ */
65
+ getAccountCanditates(addr: AccountAddress, config: ProcessConfig): Promise<AccountNumber[]> {
66
+ return (this.system.connector as unknown as TransactionImportConnector).getAccountCanditates(addr, config)
67
+ }
68
+
69
+ /**
70
+ * Construct grouping for the line data with columns defined using sub class that can generate unique ID per transaction.
71
+ * @param state
72
+ */
73
+ async groupingById(state: ImportStateText<'segmented'>): Promise<ImportStateText<'segmented'>> {
74
+ state.segments = {}
75
+ for (const fileName of Object.keys(state.files)) {
76
+ // Collect segments from lines.
77
+ for (let n = 0; n < state.files[fileName].lines.length; n++) {
78
+ const line: TextFileLine = state.files[fileName].lines[n]
79
+ if (!line.columns || Object.keys(line.columns).length === 0) {
80
+ continue
81
+ }
82
+ const id: SegmentId | typeof NO_SEGMENT = this.segmentId(line)
83
+ if (!id || !state.segments) {
84
+ throw new InvalidFile(`The segment ID for ${JSON.stringify(line)} was not found by ${this.constructor.name}.`)
85
+ }
86
+ if (id === NO_SEGMENT) {
87
+ continue
88
+ }
89
+ state.segments[id] = state.segments[id] || { id, time: undefined, lines: [] }
90
+ state.segments[id].lines.push({ number: n, file: fileName })
91
+ line.segmentId = id
92
+ }
93
+
94
+ // Calculate time stamps for each segment.
95
+ if (!state.segments) {
96
+ throw new InvalidFile('This cannot happen.')
97
+ }
98
+ Object.values(state.segments).forEach((segment: ImportSegment) => {
99
+ const stamps: Set<number> = new Set()
100
+ segment.lines.forEach(segmentLine => {
101
+ const line = state.files[segmentLine.file].lines[segmentLine.number]
102
+ const time = this.time(line)
103
+ if (time) {
104
+ stamps.add(time.getTime())
105
+ }
106
+ })
107
+ if (stamps.size === 0) {
108
+ throw new InvalidFile(`Was not able to find timestamps for lines ${JSON.stringify(segment.lines)}.`)
109
+ }
110
+ if (stamps.size > 1) {
111
+ throw new InvalidFile(`Found more than one (${stamps.size}) canditate for timestamp (${[...stamps]}) from lines ${JSON.stringify(segment.lines)}.`)
112
+ }
113
+ segment.time = new Date([...stamps][0])
114
+ })
115
+ }
116
+
117
+ return state
118
+ }
119
+
120
+ /**
121
+ * Default parser for file data.
122
+ */
123
+ async parse(state: ImportStateText<'initial'>, config: ProcessConfig = {}): Promise<ImportStateText<'segmented'>> {
124
+ switch (this.importOptions.parser) {
125
+ case 'csv':
126
+ if (this.importOptions.csv === undefined) {
127
+ throw new SystemError('No CSV options defined.')
128
+ }
129
+ return this.parseCSV(state, this.importOptions.csv)
130
+ case 'custom':
131
+ if (this.importOptions.custom === undefined) {
132
+ throw new SystemError('No custom options defined.')
133
+ }
134
+ return this.parseCustom(state, this.importOptions.custom)
135
+ default:
136
+ throw new SystemError(`Parser '${this.importOptions.parser}' is not implemented.`)
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Default segmentation is parsing CSV and then grouping by segment ID constructed for each line.
142
+ * @param state
143
+ * @param files
144
+ * @returns
145
+ */
146
+ async segmentationCSV(process: Process, state: ImportStateText<'initial'>, files: ProcessFile[]): Promise<ImportStateText<'segmented'>> {
147
+ const parsed = await this.parse(state, process.config)
148
+ const newState = await this.groupingById(parsed)
149
+ this.debugSegmentation(newState)
150
+ return newState
151
+ }
152
+
153
+ /**
154
+ * Hook to do some post proccessing for segmentation process. Collects standard fields.
155
+ * @param state
156
+ * @returns
157
+ */
158
+ async segmentationPostProcess(state: ImportStateText<'segmented'>): Promise<ImportStateText<'segmented'>> {
159
+ const shared: Record<SegmentId, Record<string, string>> = {}
160
+
161
+ for (const fileName of Object.keys(state.files)) {
162
+ // Build standard fields.
163
+ const { textField, totalAmountField } = this.importOptions
164
+ for (let n = 0; n < state.files[fileName].lines.length; n++) {
165
+ const { columns, segmentId } = state.files[fileName].lines[n]
166
+ for (const name of this.importOptions.requiredFields) {
167
+ if (columns[name] === undefined) {
168
+ columns[name] = ''
169
+ }
170
+ }
171
+ for (const name of this.importOptions.numericFields) {
172
+ if (columns[name] !== undefined) {
173
+ // TODO: We need to allow numeric values as well. Might need some syntax fixing here and there.
174
+ columns[name] = (columns[name] === '' ? 0 : num(columns[name])) as unknown as string
175
+ }
176
+ }
177
+ if (this.importOptions.sharedFields) {
178
+ for (const name of this.importOptions.sharedFields) {
179
+ if (columns[name] !== undefined) {
180
+ shared[segmentId as SegmentId] = shared[segmentId as SegmentId] || {}
181
+ if (shared[segmentId as SegmentId][name] === undefined) {
182
+ shared[segmentId as SegmentId][name] = columns[name]
183
+ } else {
184
+ throw new Error(`No handling implemented when shared field '${name}' is found from more than one line (${JSON.stringify(shared[segmentId as SegmentId][name])} and ${JSON.stringify(columns[name])}).`)
185
+ }
186
+ }
187
+ }
188
+ }
189
+ if (textField) {
190
+ columns._textField = columns[textField]
191
+ }
192
+ if (totalAmountField) {
193
+ columns._totalAmountField = columns[totalAmountField]
194
+ }
195
+ }
196
+ }
197
+
198
+ // Add shared fields.
199
+ for (const fileName of Object.keys(state.files)) {
200
+ for (let n = 0; n < state.files[fileName].lines.length; n++) {
201
+ const { columns, segmentId } = state.files[fileName].lines[n]
202
+ if (shared[segmentId as SegmentId]) {
203
+ Object.assign(columns, shared[segmentId as SegmentId])
204
+ }
205
+ }
206
+ }
207
+
208
+ return state
209
+ }
210
+
211
+ async segmentation(process: Process, state: ImportStateText<'initial'>, files: ProcessFile[]): Promise<ImportStateText<'segmented'>> {
212
+ const result = await this.segmentationPostProcess(await this.segmentationCSV(process, state, files))
213
+ return result
214
+ }
215
+
216
+ /**
217
+ * Helper to dump segmentation results.
218
+ */
219
+ debugSegmentation(state: ImportStateText<'segmented'>) {
220
+ if (state.files) {
221
+ Object.keys(state.files).forEach(fileName => {
222
+ debug('SEGMENTATION', `Segmentation of ${fileName}`)
223
+ debug('SEGMENTATION', state.files[fileName].lines.filter(line => Object.keys(line.columns).length > 0))
224
+ })
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Construct a hash for a text line usable as unique segment ID.
230
+ * @param line
231
+ */
232
+ hash(line: TextFileLine, columns: string[] | undefined = undefined): SegmentId {
233
+ // Trim spaces away before calculating hash.
234
+ if (columns === undefined) {
235
+ columns = Object.keys(line.columns)
236
+ }
237
+
238
+ const obj = columns.map(c => [c, line.columns[c]]).filter(entry => entry[1] !== undefined).reduce((prev, cur) => ({ ...prev, [cur[0]]: `${cur[1]}`.trim() }), {})
239
+
240
+ return hash.sha1(obj) as SegmentId
241
+ }
242
+
243
+ /**
244
+ * Segmentation by ID can use this function to group lines by their ID. By default the hash is used.
245
+ * @param line
246
+ */
247
+ segmentId(line: TextFileLine, columns: string[] | undefined = undefined): SegmentId | typeof NO_SEGMENT {
248
+ if (columns === undefined) {
249
+ columns = Object.keys(line.columns)
250
+ }
251
+ if (line.columns && Object.keys(columns).length) {
252
+ return this.hash(line, columns)
253
+ }
254
+ return NO_SEGMENT
255
+ }
256
+
257
+ /**
258
+ * Find out the timestamp from the line data if any.
259
+ * @param line
260
+ */
261
+ time(line: TextFileLine): Date | undefined {
262
+ throw new NotImplemented(`Import class ${this.constructor.name} does not implement time().`)
263
+ }
264
+
265
+ /**
266
+ * Default classification constructs lines belonging to each segment and asks subclass to classify them.
267
+ *
268
+ * @param state
269
+ * @param files
270
+ * @returns
271
+ */
272
+ async classification(process: Process, state: ImportStateText<'segmented'>, files: ProcessFile[]): Promise<ImportStateText<'classified'>> {
273
+ const newState: ImportStateText<'classified'> = {
274
+ stage: 'classified',
275
+ files: state.files,
276
+ segments: state.segments,
277
+ result: {}
278
+ }
279
+
280
+ if (state.segments) {
281
+ // Handle segments by date.
282
+ for (const segment of this.sortSegments(state.segments)) {
283
+ const lines = segment.lines.map(fileRef => state.files[fileRef.file].lines[fileRef.number])
284
+ const result = await this.classifyLines(lines, process.config, state.segments[segment.id])
285
+ if (newState.result) { // Needed for compiler.
286
+ newState.result[segment.id] = [result]
287
+ }
288
+ }
289
+ }
290
+
291
+ this.debugClassification(newState)
292
+
293
+ return newState
294
+ }
295
+
296
+ /**
297
+ * Helper to dump classification results.
298
+ */
299
+ debugClassification(state: ImportStateText<'classified'>) {
300
+ if (state.result) {
301
+ Object.keys(state.result).forEach(segmentId => {
302
+ if (state.result && state.result[segmentId]) {
303
+ debug('CLASSIFICATION', `Classification of ${segmentId}`)
304
+ debug('CLASSIFICATION', state.result[segmentId])
305
+ }
306
+ })
307
+ }
308
+ }
309
+
310
+ /**
311
+ * By default, use rules to classify.
312
+ * @param lines
313
+ */
314
+ async classifyLines(lines: TextFileLine[], config: ProcessConfig, segment: ImportSegment): Promise<TransactionDescription> {
315
+ return await this.rules.classifyLines(lines, config, segment)
316
+ }
317
+
318
+ /**
319
+ * Collect lines related to the segment.
320
+ * @param state
321
+ * @param segmentId
322
+ */
323
+ getLines(state: ImportStateText<'classified'>, segmentId: SegmentId): TextFileLine[] | null {
324
+ if (state.segments && state.segments[segmentId]) {
325
+ const segment = state.segments[segmentId]
326
+ const lines: TextFileLine[] = segment.lines.map(line => state.files[line.file].lines[line.number])
327
+ return lines
328
+ }
329
+ return null
330
+ }
331
+
332
+ /**
333
+ * Check if all accounts are configured and if not, construct query UI for it.
334
+ * @param state
335
+ * @returns
336
+ */
337
+ async needInputForAnalysis(state: ImportStateText<'classified'>, config: ProcessConfig): Promise<Directions | false> {
338
+
339
+ if (!state.result || !state.segments) {
340
+ return false
341
+ }
342
+
343
+ const missing = new Set<AccountAddress>()
344
+ // Use fresh analyzer to avoid messing stock bookkeeping.
345
+ const analyzer = new TransferAnalyzer(this, config, state)
346
+
347
+ for (const [segmentId, result] of Object.entries(state.result)) {
348
+ const segment: ImportSegment = state.segments[segmentId]
349
+ const items: TransactionDescription[] = result as TransactionDescription[]
350
+
351
+ // Check if we have accounts.
352
+ for (const transfer of items) {
353
+ for (const acc of await analyzer.collectAccounts(segment, transfer, { findMissing: true }) as AccountAddress[]) {
354
+ missing.add(acc)
355
+ }
356
+ }
357
+
358
+ // Find out if some of the missing accounts are actual defined as UI query or perhaps already answered.
359
+ for (const address of missing) {
360
+ if (config.answers) {
361
+ const answers = config.answers as Record<SegmentId, Record<string, unknown>>
362
+ if ((segmentId in answers) &&
363
+ (`account.${address}` in answers[segmentId]) &&
364
+ (answers[segmentId][`account.${address}`] !== undefined)) {
365
+
366
+ missing.delete(address)
367
+ continue
368
+ }
369
+ }
370
+ const [reason, type, asset] = address.split('.')
371
+ const query = await analyzer.getAccountQuery(reason as AssetTransferReason, type as AssetType, asset as Asset)
372
+ const lines = this.getLines(state, segmentId)
373
+ if (!lines) {
374
+ throw new Error(`Failed to collect lines for segment ${segmentId}.`)
375
+ }
376
+ if (query) {
377
+ const description = await this.UI.describeLines(lines, config.language as Language)
378
+ const question = await this.UI.query(`answer.${segmentId}.account.${address}`, query, [], config.language as Language)
379
+ return new Directions({
380
+ type: 'ui',
381
+ element: {
382
+ type: 'flat',
383
+ elements: [description, question]
384
+ }
385
+ })
386
+ }
387
+ }
388
+ }
389
+
390
+ if (!missing.size) {
391
+ return false
392
+ }
393
+
394
+ log(`Need to configure some accounts: ${[...missing].join(', ')}`)
395
+
396
+ return this.directionsForMissingAccounts(missing, config)
397
+ }
398
+
399
+ /**
400
+ * Study configured accounts and missing accounts and construct appropriate UI query for accounts.
401
+ * @param missing
402
+ * @param config
403
+ * @returns
404
+ */
405
+ async directionsForMissingAccounts(missing: Set<AccountAddress>, config: ProcessConfig): Promise<Directions | false> {
406
+ // Collect account settings from config.
407
+ const configured: string[] = Object.keys(config).filter(key => /^account\.\w+\.\w+\./.test(key))
408
+
409
+ // Get reason + type pair grouping and accounts for each group.
410
+ const pairs: Record<string, Set<AccountAddress>> = {}
411
+ for (const address of configured) {
412
+ const [, reason, type, asset] = address.split('.')
413
+ if (asset !== '*') {
414
+ pairs[`${reason}.${type}`] = pairs[`${reason}.${type}`] || new Set()
415
+ pairs[`${reason}.${type}`].add(`${reason}.${type}.${asset}` as AccountAddress)
416
+ }
417
+ }
418
+ for (const address of missing) {
419
+ const [reason, type, asset] = address.split('.')
420
+ pairs[`${reason}.${type}`] = pairs[`${reason}.${type}`] || new Set()
421
+ pairs[`${reason}.${type}`].add(`${reason}.${type}.${asset}` as AccountAddress)
422
+ }
423
+
424
+ // Check groups and construct query either for single account or grouped accounts.
425
+ const elements: TasenorElement[] = []
426
+ for (const addresses of Object.values(pairs)) {
427
+ if (addresses.size === 1) {
428
+ if (missing.has([...addresses][0])) {
429
+ elements.push(await this.UI.account(config, [...addresses][0]))
430
+ }
431
+ } else {
432
+ let count = 0
433
+ for (const address of addresses) {
434
+ if (missing.has(address)) count++
435
+ }
436
+ if (count) {
437
+ elements.push(await this.UI.accountGroup(config, [...addresses]))
438
+ }
439
+ }
440
+ }
441
+
442
+ if (elements.length === 0) {
443
+ return false
444
+ }
445
+
446
+ elements.push(await this.UI.submit('Continue', 1, config.language as Language))
447
+
448
+ return new Directions({
449
+ type: 'ui',
450
+ element: {
451
+ type: 'flat',
452
+ elements
453
+ }
454
+ })
455
+
456
+ }
457
+
458
+ /**
459
+ * Insert custom segments based on answer collection, if necessary.
460
+ */
461
+ async createCustomSegments(state: ImportStateText<'classified'>, config: ImportConfig): Promise<ImportStateText<'classified'>> {
462
+ const newState = clone(state)
463
+ if (!newState.result) {
464
+ newState.result = {}
465
+ }
466
+ if (!newState.segments) {
467
+ newState.segments = {}
468
+ }
469
+
470
+ if ('answers' in config && '' in (config.answers as Record<string, unknown>)) {
471
+
472
+ const answers = config.answers as ImportAnswers
473
+ const renamed = await this.getTranslation('note-renamed', config.language as Language)
474
+ const oldName = await this.getTranslation('note-old-name', config.language as Language)
475
+ const newName = await this.getTranslation('note-new-name', config.language as Language)
476
+
477
+ if ('' in answers && answers['']) {
478
+ for (const rename of answers['']['asset-renaming'] || []) {
479
+ const transfers: AssetTransfer[] = [
480
+ {
481
+ reason: 'trade',
482
+ type: rename.type,
483
+ asset: rename.old,
484
+ data: {
485
+ notes: [renamed, oldName]
486
+ }
487
+ },
488
+ {
489
+ reason: 'trade',
490
+ type: rename.type,
491
+ asset: rename.new,
492
+ data: {
493
+ notes: [renamed, newName]
494
+ }
495
+ }
496
+ ]
497
+
498
+ const segment: ImportSegment = {
499
+ id: `rename-${rename.type}-${rename.old}-${rename.new}`,
500
+ time: new Date(`${rename.date}T00:00:00.000Z`),
501
+ lines: []
502
+ }
503
+
504
+ const td: TransactionDescription = {
505
+ type: 'transfers',
506
+ transfers
507
+ }
508
+
509
+ newState.segments[segment.id] = segment
510
+ newState.result[segment.id] = [td]
511
+ }
512
+ }
513
+ }
514
+
515
+ return newState
516
+ }
517
+
518
+ /**
519
+ * Sort the segments by their date.
520
+ * @param segments
521
+ * @returns
522
+ */
523
+ sortSegments(segments: Record<string, ImportSegment>): ImportSegment[] {
524
+ const time = (entry): number => {
525
+ return (typeof entry.time === 'string') ? new Date(entry.time).getTime() : entry.time.getTime()
526
+ }
527
+
528
+ return Object.values(segments).sort((a, b) => time(a) - time(b))
529
+ }
530
+
531
+ /**
532
+ * Convert transfers to the actual transactions with account numbers.
533
+ * @param state
534
+ * @param files
535
+ */
536
+ async analysis(process: Process, state: ImportStateText<'classified'>, files: ProcessFile[], config: ProcessConfig): Promise<ImportStateText<'analyzed'>> {
537
+ // Insert custom segments to the state.
538
+ state = await this.createCustomSegments(state, config as ImportConfig)
539
+
540
+ this.analyzer = new TransferAnalyzer(this, config, state)
541
+
542
+ if (state.result && state.segments) {
543
+
544
+ // Sort segments by timestamp and find the first and the last.
545
+ const segments = this.sortSegments(state.segments)
546
+
547
+ let firstTimeStamp: Date | undefined
548
+
549
+ if (segments.length) {
550
+ // Look for the first and last valid time stamp.
551
+ const confStartDate = config.firstDate ? new Date(`${config.firstDate}T00:00:00.000Z`) : null
552
+ for (let i = 0; i < segments.length; i++) {
553
+ const segmentTime = typeof segments[i].time === 'string' ? new Date(segments[i].time) : segments[i].time
554
+ if (!confStartDate || segmentTime >= confStartDate) {
555
+ firstTimeStamp = segmentTime
556
+ break
557
+ }
558
+ }
559
+ if (!firstTimeStamp) {
560
+ throw new Error(`Unable to find any valid time stamps after ${confStartDate}.`)
561
+ }
562
+ await this.analyzer.initialize(firstTimeStamp)
563
+ }
564
+
565
+ // Prepare loan account information.
566
+ const debtAccounts: Record<AccountNumber, BalanceSummaryEntry> = {}
567
+ this.analyzer.getBalances().filter(balance => balance.mayTakeLoan).forEach(balance => {
568
+ debtAccounts[balance.account] = balance
569
+ })
570
+
571
+ // Analyze each segment in chronological order.
572
+ for (const segment of segments) {
573
+ const txDesc: TransactionDescription[] = state.result[segment.id] as TransactionDescription[]
574
+ if (!txDesc) {
575
+ throw new BadState(`Cannot find results for segment ${segment.id} during analysis (${JSON.stringify(segment)})`)
576
+ }
577
+ for (let i = 0; i < txDesc.length; i++) {
578
+ txDesc[i] = await this.analyze(txDesc[i], segment, config, state, debtAccounts)
579
+ }
580
+ }
581
+ }
582
+
583
+ const newState: ImportStateText<'analyzed'> = {
584
+ ...state,
585
+ stage: 'analyzed'
586
+ }
587
+
588
+ this.debugAnalysis(newState)
589
+
590
+ return newState
591
+ }
592
+
593
+ /**
594
+ * Analyze and construct transaction details from a transaction description.
595
+ * @param txs
596
+ */
597
+ async analyze(txs: TransactionDescription, segment: ImportSegment, config: ProcessConfig, state: ImportStateText<'classified'>, debtAccounts: Record<AccountNumber, BalanceSummaryEntry>): Promise<TransactionDescription> {
598
+ if (!this.analyzer) {
599
+ throw new SystemError('Calling analyze() without setting up analyzer.')
600
+ }
601
+ let result: TransactionDescription
602
+ switch (txs.type) {
603
+ case 'transfers':
604
+ result = await this.analyzer.analyze(txs, segment, config)
605
+ return await this.checkForLoan(result, debtAccounts)
606
+ default:
607
+ throw new NotImplemented(`Cannot analyze yet type '${txs.type}' in ${this.constructor.name}.`)
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Check if the resulting transactions needs to be recorded to loan account.
613
+ */
614
+ async checkForLoan(result: TransactionDescription, debtAccounts: Record<AccountNumber, BalanceSummaryEntry>): Promise<TransactionDescription> {
615
+
616
+ if (!this.analyzer) throw new Error('No analyzer. Internal error.')
617
+
618
+ for (const tx of result.transactions || []) {
619
+ for (const entry of tx.entries) {
620
+ if (entry.account in debtAccounts) {
621
+ // Find loan account if defined.
622
+ const balance = debtAccounts[entry.account]
623
+ const [loanReason, loanType, loanAsset] = balance.debtAddress.split('.')
624
+ const loanAccount = await this.analyzer.getAccount(loanReason as AssetTransferReason, loanType as AssetType, loanAsset as Asset)
625
+ if (balance.account === loanAccount) {
626
+ continue
627
+ }
628
+ const accountBalance = this.analyzer.getBalance(balance.address) || 0
629
+ const debtBalance = this.analyzer.getBalance(balance.debtAddress) || 0
630
+ // Take more loan.
631
+ if (realNegative(accountBalance) && realNegative(entry.amount)) {
632
+ this.analyzer.revertBalance(entry)
633
+ const originalBalance = this.analyzer.getBalance(balance.address) || 0
634
+ // Only partial loan needed.
635
+ if (realPositive(originalBalance)) {
636
+ const loanEntry = {
637
+ account: loanAccount || '0' as AccountNumber,
638
+ amount: -(-entry.amount - originalBalance),
639
+ description: entry.description
640
+ }
641
+ entry.amount = -originalBalance
642
+
643
+ // Add tags if any.
644
+ loanEntry.description = mergeTags(loanEntry.description, await this.analyzer.getTagsForAddr(balance.debtAddress) || [])
645
+
646
+ tx.entries.push(loanEntry)
647
+
648
+ this.analyzer.applyBalance(entry)
649
+ this.analyzer.applyBalance(loanEntry)
650
+ } else {
651
+ // Full loan needed.
652
+ entry.account = loanAccount || '0' as AccountNumber
653
+ this.analyzer.applyBalance(entry)
654
+ }
655
+ }
656
+
657
+ // Pay back loan.
658
+ if (realNegative(debtBalance) && realPositive(entry.amount)) {
659
+ this.analyzer.revertBalance(entry)
660
+ // Getting more than full payment.
661
+ if (less(-debtBalance, entry.amount)) {
662
+ const loanEntry = {
663
+ account: loanAccount || '0' as AccountNumber,
664
+ amount: -debtBalance,
665
+ description: entry.description
666
+ }
667
+
668
+ entry.amount -= -debtBalance
669
+
670
+ // Add tags if any.
671
+ loanEntry.description = mergeTags(loanEntry.description, await this.analyzer.getTagsForAddr(balance.debtAddress) || [])
672
+
673
+ tx.entries.push(loanEntry)
674
+ this.analyzer.applyBalance(entry)
675
+ this.analyzer.applyBalance(loanEntry)
676
+ } else {
677
+ // Partial payment.
678
+ entry.account = loanAccount || '0' as AccountNumber
679
+ this.analyzer.applyBalance(entry)
680
+ }
681
+ }
682
+ }
683
+ }
684
+ }
685
+ return result
686
+ }
687
+
688
+ /**
689
+ * Dump analysis results.
690
+ * @param state
691
+ */
692
+ debugAnalysis(state: ImportStateText<'analyzed'>) {
693
+ if (state.result !== undefined) {
694
+ Object.keys(state.result).forEach(segmentId => {
695
+ debug('ANALYSIS', `Analyzed ${segmentId}`)
696
+ if (state.result && segmentId in state.result) {
697
+ for (const result of (state.result[segmentId] as TransactionDescription[])) {
698
+ debug('ANALYSIS', result.transfers)
699
+ }
700
+ }
701
+ })
702
+ }
703
+ }
704
+
705
+ /**
706
+ * Apply the result using the connector.
707
+ * @param state
708
+ * @param files
709
+ * @returns
710
+ */
711
+ async execution(process: Process, state: ImportStateText<'analyzed'>, files: ProcessFile[]): Promise<ImportStateText<'executed'>> {
712
+
713
+ const output = new TransactionApplyResults()
714
+
715
+ if (state.result) {
716
+ // Initialize all execution results.
717
+ for (const segmentId of Object.keys(state.result)) {
718
+ const result: TransactionDescription[] = state.result[segmentId] as TransactionDescription[]
719
+ for (const res of result) {
720
+ if (res.transactions) {
721
+ for (const tx of res.transactions) {
722
+ if (!tx.executionResult) {
723
+ tx.executionResult = 'not done'
724
+ }
725
+ }
726
+ }
727
+ }
728
+ }
729
+
730
+ // Apply everything segment by segment.
731
+ for (const segmentId of Object.keys(state.result)) {
732
+ debug('EXECUTION', `Execution of segment ${segmentId}`)
733
+ const result: TransactionDescription[] = state.result[segmentId] as TransactionDescription[]
734
+ for (const res of result) {
735
+ debug('EXECUTION', res.transactions)
736
+ const hasOld = await this.system.connector.resultExists(process.id, res)
737
+ if (hasOld) {
738
+ const allow = await this.UI.getBoolean(process.config, 'allowIdenticalTx', 'Allow creation of identical transactions that has been already created.')
739
+ if (!allow) {
740
+ for (const tx of res.transactions || []) {
741
+ tx.executionResult = 'duplicate'
742
+ output.duplicate(tx)
743
+ }
744
+ continue
745
+ }
746
+ log(`Duplicate transaction created ${JSON.stringify(res.transactions)} since allowed in settings.`)
747
+ }
748
+ const applied = await this.system.connector.applyResult(process.id, res)
749
+ output.add(applied)
750
+ }
751
+ }
752
+ }
753
+
754
+ // Remove stock data.
755
+ this.analyzer = null
756
+
757
+ return {
758
+ ...state,
759
+ output: output.toJSON(),
760
+ stage: 'executed'
761
+ }
762
+ }
763
+
764
+ /**
765
+ * Ask VAT from connector.
766
+ * @param time
767
+ * @param reason
768
+ * @param asset
769
+ * @param currency
770
+ */
771
+ async getVAT(time: Date, transfer: AssetTransfer, currency: Currency): Promise<null | number> {
772
+ const connector: TransactionImportConnector = this.system.connector as unknown as TransactionImportConnector
773
+ return connector.getVAT(time, transfer, currency)
774
+ }
775
+
776
+ /**
777
+ * Find the rate in the default currency for the asset.
778
+ * If there is information about rates inside the files, this function could be overridden and
779
+ * used for digging actual values. Those values can be collected during parse() call.
780
+ * @param time
781
+ * @param type
782
+ * @param asset
783
+ */
784
+ async getRate(time: Date, type: AssetType, asset: Asset, currency: Currency, exchange: AssetExchange): Promise<number> {
785
+ if (!isTransactionImportConnector(this.system.connector)) {
786
+ throw new SystemError('Connector used is not a transaction import connector.')
787
+ }
788
+ return this.system.connector.getRate(time, type, asset, currency, exchange)
789
+ }
790
+
791
+ /**
792
+ * Remove transactions created.
793
+ */
794
+ async rollback(process: Process, state: ImportStateText<'executed'>): Promise<ImportStateText<'rolledback'>> {
795
+ const success = await this.system.connector.rollback(process.id)
796
+ if (!success) {
797
+ throw new SystemError('Rollback failed.')
798
+ }
799
+
800
+ if (state.result) {
801
+ for (const segmentId of Object.keys(state.result)) {
802
+ const result: TransactionDescription[] = state.result[segmentId] as TransactionDescription[]
803
+ for (const res of result) {
804
+ if (res.transactions) {
805
+ for (const tx of res.transactions) {
806
+ tx.executionResult = 'reverted'
807
+ }
808
+ }
809
+ }
810
+ }
811
+ }
812
+
813
+ return {
814
+ ...state,
815
+ stage: 'rolledback'
816
+ }
817
+ }
818
+
819
+ }