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