@tasenor/common-plugins 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 (114) hide show
  1. package/.eslintrc.js +4 -0
  2. package/.turbo/turbo-fix.log +4 -0
  3. package/.turbo/turbo-lint.log +4 -0
  4. package/.turbo/turbo-version.log +5 -0
  5. package/LICENSE +21 -0
  6. package/README.md +15 -0
  7. package/data/FinnishBalanceSheetReport.mjs +9 -0
  8. package/data/FinnishBalanceSheetReportInvestment.mjs +11 -0
  9. package/data/FinnishBalanceSheetReportLite.mjs +9 -0
  10. package/data/FinnishIncomeStatementReport.mjs +9 -0
  11. package/data/FinnishIncomeStatementReportInvestment.mjs +11 -0
  12. package/data/FinnishIncomeStatementReportLite.mjs +9 -0
  13. package/data/FinnishInvestmentCompany.mjs +5 -0
  14. package/data/FinnishLimitedCompanyComplete.mjs +5 -0
  15. package/data/FinnishLimitedCompanyLite.mjs +9 -0
  16. package/data/IncomeAndExpenses.mjs +64 -0
  17. package/data/README.md +8 -0
  18. package/data/VATFinland.mjs +22 -0
  19. package/data/bin/build_all +11 -0
  20. package/data/lib/utils.mjs +314 -0
  21. package/data/src/Assets Tree - Definitions.tsv +101 -0
  22. package/data/src/Expense Tree - Definitions.tsv +99 -0
  23. package/data/src/Finland VAT - Definitions.tsv +22 -0
  24. package/data/src/FinnishBalanceSheetReport - balance-sheet-detailed-fi.tsv +215 -0
  25. package/data/src/FinnishBalanceSheetReport - balance-sheet-fi.tsv +93 -0
  26. package/data/src/FinnishBalanceSheetReportInvestment - balance-sheet-detailed-fi.tsv +100 -0
  27. package/data/src/FinnishBalanceSheetReportInvestment - balance-sheet-fi.tsv +52 -0
  28. package/data/src/FinnishBalanceSheetReportLite - balance-sheet-en.tsv +21 -0
  29. package/data/src/FinnishBalanceSheetReportLite - balance-sheet-fi.tsv +21 -0
  30. package/data/src/FinnishBalanceSheetReportLite - balance-sheet-lite-en.tsv +21 -0
  31. package/data/src/FinnishBalanceSheetReportLite - balance-sheet-lite-fi.tsv +21 -0
  32. package/data/src/FinnishIncomeStatementReport - income-statement-detailed-fi.tsv +118 -0
  33. package/data/src/FinnishIncomeStatementReport - income-statement-fi.tsv +45 -0
  34. package/data/src/FinnishIncomeStatementReportInvestment - income-statement-detailed-fi.tsv +96 -0
  35. package/data/src/FinnishIncomeStatementReportInvestment - income-statement-fi.tsv +39 -0
  36. package/data/src/FinnishIncomeStatementReportLite - income-statement-lite-en.tsv +23 -0
  37. package/data/src/FinnishIncomeStatementReportLite - income-statement-lite-fi.tsv +23 -0
  38. package/data/src/FinnishInvestmentCompany - fi-EUR.tsv +722 -0
  39. package/data/src/FinnishLimitedCompanyComplete - fi-EUR.tsv +1086 -0
  40. package/data/src/FinnishLimitedCompanyLite - en-EUR.tsv +97 -0
  41. package/data/src/FinnishLimitedCompanyLite - fi-EUR.tsv +99 -0
  42. package/data/src/Income Tree - Definitions.tsv +60 -0
  43. package/data/src/Tax Types - Definitions.tsv +11 -0
  44. package/package.json +51 -0
  45. package/src/CoinAPI/backend/index.ts +102 -0
  46. package/src/CoinbaseImport/backend/CoinbaseHandler.ts +35 -0
  47. package/src/CoinbaseImport/backend/index.ts +24 -0
  48. package/src/CoinbaseImport/backend/rules.json +64 -0
  49. package/src/DocumentCleaner/ui/index.tsx +165 -0
  50. package/src/Euro/ui/index.tsx +27 -0
  51. package/src/Finnish/ui/finnish.json +341 -0
  52. package/src/Finnish/ui/index.tsx +54 -0
  53. package/src/FinnishBalanceSheetReport/backend/balance-sheet-detailed-fi.tsv +215 -0
  54. package/src/FinnishBalanceSheetReport/backend/balance-sheet-fi.tsv +93 -0
  55. package/src/FinnishBalanceSheetReport/backend/index.ts +107 -0
  56. package/src/FinnishBalanceSheetReportInvestment/backend/balance-sheet-investment-detailed-fi.tsv +100 -0
  57. package/src/FinnishBalanceSheetReportInvestment/backend/balance-sheet-investment-fi.tsv +52 -0
  58. package/src/FinnishBalanceSheetReportInvestment/backend/index.ts +107 -0
  59. package/src/FinnishBalanceSheetReportLite/backend/balance-sheet-lite-en.tsv +21 -0
  60. package/src/FinnishBalanceSheetReportLite/backend/balance-sheet-lite-fi.tsv +21 -0
  61. package/src/FinnishBalanceSheetReportLite/backend/index.ts +121 -0
  62. package/src/FinnishIncomeStatementReport/backend/income-statement-detailed-fi.tsv +118 -0
  63. package/src/FinnishIncomeStatementReport/backend/income-statement-fi.tsv +45 -0
  64. package/src/FinnishIncomeStatementReport/backend/index.ts +212 -0
  65. package/src/FinnishIncomeStatementReportInvestment/backend/income-statement-detailed-fi.tsv +118 -0
  66. package/src/FinnishIncomeStatementReportInvestment/backend/income-statement-fi.tsv +45 -0
  67. package/src/FinnishIncomeStatementReportInvestment/backend/income-statement-investment-detailed-fi.tsv +96 -0
  68. package/src/FinnishIncomeStatementReportInvestment/backend/income-statement-investment-fi.tsv +39 -0
  69. package/src/FinnishIncomeStatementReportInvestment/backend/index.ts +212 -0
  70. package/src/FinnishIncomeStatementReportLite/backend/income-statement-lite-en.tsv +23 -0
  71. package/src/FinnishIncomeStatementReportLite/backend/income-statement-lite-fi.tsv +23 -0
  72. package/src/FinnishIncomeStatementReportLite/backend/index.ts +210 -0
  73. package/src/FinnishInvestmentCompany/backend/fi-EUR.tsv +722 -0
  74. package/src/FinnishInvestmentCompany/backend/index.ts +46 -0
  75. package/src/FinnishInvestmentCompany/ui/index.tsx +26 -0
  76. package/src/FinnishLimitedCompanyComplete/backend/fi-EUR.tsv +1086 -0
  77. package/src/FinnishLimitedCompanyComplete/backend/index.ts +46 -0
  78. package/src/FinnishLimitedCompanyComplete/ui/index.tsx +26 -0
  79. package/src/FinnishLimitedCompanyLite/backend/en-EUR.tsv +97 -0
  80. package/src/FinnishLimitedCompanyLite/backend/fi-EUR.tsv +99 -0
  81. package/src/FinnishLimitedCompanyLite/backend/index.ts +53 -0
  82. package/src/FinnishLimitedCompanyLite/ui/index.tsx +28 -0
  83. package/src/GitBackup/backend/index.ts +109 -0
  84. package/src/GitBackup/ui/index.tsx +127 -0
  85. package/src/IncomeAndExpenses/backend/assetCodes.json +126 -0
  86. package/src/IncomeAndExpenses/backend/expense.json +190 -0
  87. package/src/IncomeAndExpenses/backend/income.json +120 -0
  88. package/src/IncomeAndExpenses/backend/index.ts +354 -0
  89. package/src/IncomeAndExpenses/backend/taxTypes.json +12 -0
  90. package/src/JournalReport/backend/index.ts +157 -0
  91. package/src/KrakenImport/backend/KrakenHandler.ts +88 -0
  92. package/src/KrakenImport/backend/index.ts +24 -0
  93. package/src/KrakenImport/backend/rules.json +52 -0
  94. package/src/LedgerReport/backend/index.ts +161 -0
  95. package/src/LynxImport/backend/LynxHandler.ts +389 -0
  96. package/src/LynxImport/backend/index.ts +24 -0
  97. package/src/LynxImport/backend/rules.json +412 -0
  98. package/src/NordeaImport/backend/NordeaHandler.ts +44 -0
  99. package/src/NordeaImport/backend/index.ts +24 -0
  100. package/src/NordeaImport/backend/rules.json +4 -0
  101. package/src/NordnetImport/backend/NordnetHandler.ts +78 -0
  102. package/src/NordnetImport/backend/index.ts +24 -0
  103. package/src/NordnetImport/backend/rules.json +271 -0
  104. package/src/Rand/ui/index.tsx +27 -0
  105. package/src/RapidAPI/backend/index.ts +133 -0
  106. package/src/TITOImport/backend/TITOHandler.ts +268 -0
  107. package/src/TITOImport/backend/index.ts +24 -0
  108. package/src/TITOImport/backend/rules.json +4 -0
  109. package/src/TagEditor/ui/index.tsx +510 -0
  110. package/src/USDollar/ui/index.tsx +27 -0
  111. package/src/VAT/ui/index.tsx +572 -0
  112. package/src/VATFinland/backend/index.ts +22 -0
  113. package/src/VATFinland/backend/vat.json +23 -0
  114. package/tsconfig.json +13 -0
@@ -0,0 +1,52 @@
1
+ {
2
+ "rules": [
3
+ {
4
+ "name": "Trade",
5
+ "filter": "type == 'trade'",
6
+ "result": [
7
+ {
8
+ "reason": "'trade'",
9
+ "type": "isCurrency(ticker) ? 'currency' : 'crypto'",
10
+ "asset": "ticker",
11
+ "amount": "amount"
12
+ },
13
+ {
14
+ "reason": "'fee'",
15
+ "type": "isCurrency(ticker) ? 'currency' : 'crypto'",
16
+ "asset": "ticker",
17
+ "amount": "fee",
18
+ "data": {
19
+ "feeAmount": "fee",
20
+ "feeCurrency": "ticker"
21
+ },
22
+ "if": "fee"
23
+ }
24
+ ]
25
+ },
26
+ {
27
+ "name": "Withdrawal",
28
+ "filter": "type == 'withdrawal'",
29
+ "result": [
30
+ {
31
+ "reason": "'withdrawal'",
32
+ "type": "isCurrency(ticker) ? 'currency' : 'crypto'",
33
+ "asset": "ticker",
34
+ "amount": "amount"
35
+ },
36
+ {
37
+ "reason": "'fee'",
38
+ "type": "isCurrency(ticker) ? 'currency' : 'crypto'",
39
+ "asset": "ticker",
40
+ "amount": "fee",
41
+ "if": "fee"
42
+ },
43
+ {
44
+ "reason": "'withdrawal'",
45
+ "type": "'external'",
46
+ "asset": "ticker",
47
+ "amount": "amount"
48
+ }
49
+ ]
50
+ }
51
+ ]
52
+ }
@@ -0,0 +1,161 @@
1
+ import { ReportPlugin } from '@dataplug/tasenor-common-node'
2
+ import { Language, PluginCode, ReportColumnDefinition, ReportFormat, ReportID, ReportItem, ReportOptions, Version } from '@dataplug/tasenor-common'
3
+
4
+ class LedgerReport extends ReportPlugin {
5
+ constructor() {
6
+ super('general-ledger' as ReportID)
7
+
8
+ this.code = 'LedgerReport'as PluginCode
9
+ this.title = 'Ledger Report'
10
+ this.version = '1.0.13' as Version
11
+ this.icon = '<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><path d="M0,0h24v24H0V0z" fill="none"/></g><g><g><path d="M14,5H5v14h14v-9h-5V5z M8,17c-0.55,0-1-0.45-1-1s0.45-1,1-1s1,0.45,1,1S8.55,17,8,17z M8,13 c-0.55,0-1-0.45-1-1s0.45-1,1-1s1,0.45,1,1S8.55,13,8,13z M8,9C7.45,9,7,8.55,7,8s0.45-1,1-1s1,0.45,1,1S8.55,9,8,9z" opacity=".3"/><circle cx="8" cy="8" r="1"/><path d="M15,3H5C3.9,3,3.01,3.9,3.01,5L3,19c0,1.1,0.89,2,1.99,2H19c1.1,0,2-0.9,2-2V9L15,3z M19,19H5V5h9v5h5V19z"/><circle cx="8" cy="12" r="1"/><circle cx="8" cy="16" r="1"/></g></g></svg>'
12
+ this.releaseDate = '2022-03-05'
13
+ this.use = 'backend'
14
+ this.type = 'report'
15
+ this.description = 'General purpose ledger report listing all entries in each account. Each account has also running total for balance.'
16
+
17
+ this.languages = {
18
+ en: {
19
+ 'report-general-ledger': 'General Ledger'
20
+ },
21
+ fi: {
22
+ 'report-general-ledger': 'Pääkirja'
23
+ }
24
+ }
25
+ }
26
+
27
+ forceOptions() {
28
+ return {
29
+ negateAssetAndProfit: false,
30
+ addPreviousPeriod: false
31
+ }
32
+ }
33
+
34
+ getLanguages(): Language[] {
35
+ return ['fi', 'en']
36
+ }
37
+
38
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
39
+ getReportStructure(id: ReportID, lang: Language) : ReportFormat | undefined {
40
+ return '' as ReportFormat
41
+ }
42
+
43
+ getReportOptions(): ReportOptions {
44
+ return {
45
+ compact: 'boolean:true'
46
+ }
47
+ }
48
+
49
+ async getColumns(): Promise<ReportColumnDefinition[]> {
50
+ return [{
51
+ type: 'id',
52
+ name: 'account',
53
+ title: '{column-account-number}'
54
+ }, {
55
+ type: 'name',
56
+ name: 'name',
57
+ title: '{column-name-or-date}'
58
+ }, {
59
+ type: 'numeric',
60
+ name: 'debit',
61
+ title: '{column-debit}'
62
+ }, {
63
+ type: 'numeric',
64
+ name: 'credit',
65
+ title: '{column-credit}'
66
+ }, {
67
+ type: 'numeric',
68
+ name: 'balance',
69
+ title: '{column-balance}'
70
+ }]
71
+ }
72
+
73
+ preProcess(id, entries, options) {
74
+ // Pre-process entries by their account number.
75
+ const accounts = {}
76
+ const accountNames = {}
77
+ entries.forEach((entry) => {
78
+ let data
79
+ if (entry.number in accounts) {
80
+ data = accounts[entry.number]
81
+ } else {
82
+ data = []
83
+ }
84
+ data.push({
85
+ name: entry.name,
86
+ number: entry.number,
87
+ documentId: entry.documentId,
88
+ description: entry.description,
89
+ date: entry.date,
90
+ amounts: {
91
+ debit: entry.amount >= 0 ? entry.amount : null,
92
+ credit: entry.amount < 0 ? -entry.amount : null,
93
+ balance: null
94
+ }
95
+ })
96
+
97
+ accounts[entry.number] = data
98
+ accountNames[entry.number] = entry.name
99
+ })
100
+
101
+ const accountNumbers = Object.keys(accounts).sort()
102
+ const data: ReportItem[] = []
103
+ accountNumbers.forEach((number) => {
104
+ const lines = accounts[number]
105
+ data.push({
106
+ tab: 0,
107
+ bold: true,
108
+ id: number,
109
+ name: accountNames[number]
110
+ })
111
+ let total = 0
112
+ lines.forEach((line) => {
113
+ total += line.amounts.debit
114
+ total -= line.amounts.credit
115
+ line.amounts.balance = total
116
+ if (options.compact) {
117
+ data.push({
118
+ tab: 0,
119
+ needLocalization: true,
120
+ id: `#${line.documentId}`,
121
+ name: `{${this.time2str(line.date)}} ${line.description.replace(/^(\[.+?\])+\s*/g, '')}`,
122
+ amounts: line.amounts
123
+ })
124
+ } else {
125
+ data.push({
126
+ tab: 0,
127
+ needLocalization: true,
128
+ id: `#${line.documentId}`,
129
+ name: `{${this.time2str(line.date)}}`
130
+ })
131
+ data.push({
132
+ tab: 0,
133
+ useRemainingColumns: true,
134
+ italic: true,
135
+ name: `${line.description.replace(/^(\[.+?\])+\s*/g, '')}`
136
+ })
137
+ data.push({
138
+ tab: 0,
139
+ name: '',
140
+ amounts: line.amounts
141
+ })
142
+ }
143
+ })
144
+ data.push({
145
+ tab: 0,
146
+ name: '',
147
+ bold: true,
148
+ bigger: true,
149
+ amounts: {
150
+ debit: '',
151
+ credit: '',
152
+ balance: total
153
+ }
154
+ })
155
+ })
156
+
157
+ return data
158
+ }
159
+ }
160
+
161
+ export default LedgerReport
@@ -0,0 +1,389 @@
1
+ import { ImportStateText, NO_SEGMENT, ProcessConfig, warning, month, Language, ucfirst } from '@dataplug/tasenor-common'
2
+ import { InvalidFile, Process, ProcessFile, TransactionImportHandler } from '@dataplug/tasenor-common-node'
3
+
4
+ // Matcher for ticker and ISIN code.
5
+ const TICKER_ISIN_REGEX = /^\s*([-A-Za-z0-9 .]+)\(([A-Z0-9]+)\)/
6
+
7
+ // If set, pick only transactions concerning this ticker.
8
+ const DEBUG_TICKER = null
9
+ // If set, pick only transactions from this section in CSV file:
10
+ // 'Deposits & Withdrawals', 'Interest', 'Trades', 'Corporate Actions', 'Dividends', 'Withholding Tax', 'Fees'.
11
+ const DEBUG_TYPE = null
12
+
13
+ /**
14
+ * Import implementation for Lynx CSV format.
15
+ */
16
+ export class LynxHandler extends TransactionImportHandler {
17
+
18
+ constructor() {
19
+ super('LynxImport')
20
+ this.importOptions = {
21
+ parser: 'custom',
22
+ requiredFields: ['Amount', 'Quantity'],
23
+ numericFields: ['Amount', 'Quantity', 'Proceeds', 'T. Price', 'MTM in Currency', 'Comm/Fee', 'PerAsset', 'Value'],
24
+ textField: null,
25
+ totalAmountField: null,
26
+ }
27
+ }
28
+
29
+ canHandle(file: ProcessFile): boolean {
30
+ const header = 'Statement,Header,Field Name,Field Value\n'
31
+ const str = file.decode()
32
+ return str.charCodeAt(0) === 0xfeff &&
33
+ str.substr(1, header.length) === header
34
+ }
35
+
36
+ /**
37
+ * Special pre-processing for Lynx semi CSV-format. Collect headers and map content of interest to columns.
38
+ * @param state
39
+ * @returns
40
+ */
41
+ async parse(state: ImportStateText<'initial'>, config: ProcessConfig): Promise<ImportStateText<'segmented'>> {
42
+
43
+ const language = config.language as Language
44
+
45
+ const sectionsOfInterest = new Set([
46
+ 'Fees',
47
+ 'Interest',
48
+ 'Trades',
49
+ 'Deposits & Withdrawals',
50
+ 'Withholding Tax',
51
+ 'Dividends',
52
+ 'Corporate Actions'
53
+ ])
54
+
55
+ // Map headers of different sections to the data content and costruct columns.
56
+ for (const fileName of Object.keys(state.files)) {
57
+ const file = state.files[fileName]
58
+ let headers: string[] = []
59
+ for (let n = 0; n < file.lines.length; n++) {
60
+ // Some info lines has bad quotes, so ignore errors.
61
+ const columns = await this.parseCsvLine(file.lines[n].text, { columnSeparator: ',', skipErrors: true })
62
+
63
+ if (!columns || !sectionsOfInterest.has(columns[0])) {
64
+ continue
65
+ }
66
+ // Pick headers.
67
+ if (columns[1] === 'Header') {
68
+ headers = columns
69
+ continue
70
+ }
71
+ // Skip summaries and notes.
72
+ if (columns[1] === 'Total' || columns[1] === 'SubTotal' || columns[1] === 'Notes') {
73
+ continue
74
+ }
75
+ if (columns[2] && columns[2].startsWith('Total')) {
76
+ continue
77
+ }
78
+
79
+ // Skip dividend section with `Code` to avoid duplicate dividends.
80
+ if (headers[0] === 'Dividends' && headers[6] === 'Code') {
81
+ continue
82
+ }
83
+
84
+ if (DEBUG_TICKER) {
85
+ if (columns[0] === 'Deposits & Withdrawals' || columns[0] === 'Interest') continue
86
+ if (columns[0] === 'Trades' && columns[5] !== DEBUG_TICKER) continue
87
+ if (columns[0] === 'Corporate Actions' && !columns[6].startsWith(DEBUG_TICKER)) continue
88
+ if (columns[0] === 'Dividends' && !columns[4].startsWith(DEBUG_TICKER)) continue
89
+ if (columns[0] === 'Withholding Tax' && !columns[4].startsWith(DEBUG_TICKER)) continue
90
+ }
91
+ if (DEBUG_TYPE) {
92
+ if (columns[0] !== DEBUG_TYPE) continue
93
+ }
94
+
95
+ type LynxValues = {
96
+ Type: string
97
+ Subtype: string
98
+ Description: string
99
+ ISIN?: string
100
+ Ticker?: string
101
+ Amount?: string
102
+ PerAsset?: string
103
+ Currency?: string
104
+ Action?: string
105
+ SubAction?: string
106
+ Ratio?: string
107
+ Quantity?: string
108
+ Month?: string
109
+ Year?: string
110
+ TickerInText?: string
111
+ RatioDecimal?: string
112
+ }
113
+
114
+ const values: LynxValues = { Type: headers[0], Subtype: '', Description: '' }
115
+ for (let i = 2; i < columns.length; i++) {
116
+ values[headers[i] || `Column${i}`] = columns[i]
117
+ }
118
+
119
+ // Add ISIN and Ticker for dividends and tax
120
+ if (['Dividends', 'Withholding Tax'].includes(values.Type)) {
121
+ const match = TICKER_ISIN_REGEX.exec(values.Description)
122
+ if (!match) {
123
+ throw new InvalidFile(`Failed to extract ticker and ISIN from a line '${values.Description}'.`)
124
+ }
125
+ values.Ticker = match[1].trim()
126
+ values.ISIN = match[2]
127
+ // Parse more fields from dividend.
128
+ if (values.Type === 'Dividends') {
129
+ let text = values.Description.replace(TICKER_ISIN_REGEX, '').trim()
130
+ text = text.replace(values.Ticker, '')
131
+ text = text.replace(/\(\s*\)/g, '')
132
+ text = text.replace(/\s\s+/g, ' ')
133
+ let perAsset = /\b([0-9.]+)\s+per\s+share\b/i.exec(text)
134
+ if (!perAsset) {
135
+ const regex = new RegExp(`${values.Currency}\\s+([0-9.]+)\\s`)
136
+ perAsset = regex.exec(text)
137
+ }
138
+ if (perAsset) {
139
+ values.PerAsset = perAsset[1]
140
+ } else {
141
+ warning(`Failed to find per share dividend from a line '${values.Description}'.`)
142
+ values.PerAsset = undefined
143
+ }
144
+ }
145
+ }
146
+
147
+ // Add ISIN and Ticker for corporate actions and try to extract some info.
148
+ if (values.Type === 'Corporate Actions') {
149
+ let match = TICKER_ISIN_REGEX.exec(values.Description)
150
+ if (!match) {
151
+ throw new InvalidFile(`Failed to extract ticker and ISIN from a line '${values.Description}'.`)
152
+ }
153
+ values.Ticker = match[1].trim()
154
+ values.ISIN = match[2]
155
+ let text = values.Description.replace(TICKER_ISIN_REGEX, '').trim()
156
+ if (text.startsWith('CUSIP/ISIN Change to ')) {
157
+ values.Action = 'Renaming'
158
+ text = text.substring(21)
159
+ } else {
160
+ match = /^(\w+)/.exec(text)
161
+ if (!match) {
162
+ throw new InvalidFile(`Cannot extract corporate action from a line '${values.Description}'.`)
163
+ }
164
+ values.Action = ucfirst(match[1])
165
+ text = text.replace(/^\w+/, '').trim()
166
+ }
167
+ match = /^\((\w+)\)/.exec(text)
168
+ values.SubAction = match ? ucfirst(match[1]) : ''
169
+ text = text.replace(/^\((\w+)\)/, '').trim()
170
+ match = /\b(\d+)\s+for\s+(\d+)\s/i.exec(text)
171
+ if (match) {
172
+ values.Ratio = `${parseInt(match[1])}/${parseInt(match[2])}`
173
+ values.RatioDecimal = `${parseInt(match[1]) / parseInt(match[2])}`
174
+ } else {
175
+ values.Ratio = ''
176
+ values.RatioDecimal = ''
177
+ }
178
+ match = /.*\(([A-Z0-9 .]+),\s+.*?\)/.exec(text)
179
+ values.TickerInText = match ? match[1] : ''
180
+ }
181
+
182
+ // Rename some currency dependent fields.
183
+ if (values[`Comm in ${config.currency}`] !== undefined) {
184
+ values['Comm in Currency'] = values[`Comm in ${config.currency}`]
185
+ delete values[`Comm in ${config.currency}`]
186
+ }
187
+ if (values[`MTM in ${config.currency}`] !== undefined) {
188
+ values['MTM in Currency'] = values[`MTM in ${config.currency}`]
189
+ delete values[`MTM in ${config.currency}`]
190
+ }
191
+
192
+ // Trim the quantity's confusing desimal point.
193
+ if (values.Quantity && values.Type === 'Trades') {
194
+ values.Quantity = values.Quantity.replace(/,(\d\d\d)$/g, '$1')
195
+ }
196
+
197
+ // Fix Date/Time.
198
+ if (values['Date/Time']) {
199
+ values['Date/Time'] = values['Date/Time'].replace(',', '')
200
+ }
201
+
202
+ // Pick interest period.
203
+ if (values.Type === 'Interest') {
204
+ const re = /\s([A-Z][a-z][a-z])-(\d\d\d\d)$/.exec(values.Description)
205
+ values.Month = re ? ucfirst(await this.getTranslation(month(re[1]) || '', language)) : ''
206
+ values.Year = re ? re[2] : ''
207
+ }
208
+
209
+ // Ensure at least empty `Action` for easier processing in rules.
210
+ values.Action = values.Action || ''
211
+
212
+ file.lines[n].columns = values
213
+ }
214
+ }
215
+
216
+ const newState: ImportStateText<'segmented'> = {
217
+ ...state as ImportStateText<'initial'>,
218
+ stage: 'segmented'
219
+ }
220
+ return newState
221
+ }
222
+
223
+ /**
224
+ * Resolve date string for column data extracted.
225
+ * @param columns
226
+ */
227
+ date(columns: Record<string, string>): string {
228
+ // Mapping from types to date fields.
229
+ const dateField = {
230
+ Fees: 'Date',
231
+ Interest: 'Date',
232
+ Dividends: 'Date',
233
+ Trades: 'Date/Time',
234
+ 'Corporate Actions': 'Date/Time',
235
+ 'Deposits & Withdrawals': 'Settle Date',
236
+ 'Withholding Tax': 'Date'
237
+ }
238
+
239
+ let date = columns[dateField[columns.Type]]
240
+ if (!date) {
241
+ throw new InvalidFile(`Unable to determine timestamp from ${JSON.stringify(columns)}.`)
242
+ }
243
+ if (date.length < 12) {
244
+ date += 'T00:00:00Z'
245
+ }
246
+
247
+ return date
248
+ }
249
+
250
+ /**
251
+ * Pair dividends and witholding taxes. Other lines are kept as they are.
252
+ * @param process
253
+ * @param state
254
+ * @param files
255
+ * @returns
256
+ */
257
+ async segmentation(process: Process, state: ImportStateText<'initial'>): Promise<ImportStateText<'segmented'>> {
258
+
259
+ // Parse to get column content.
260
+ const parsed: ImportStateText<'segmented'> = await this.parse(state, process.config)
261
+ parsed.segments = {}
262
+
263
+ // Collect dividend line numbers per ISIN + date combination.
264
+ const actionLines: Record<string, number[]> = {}
265
+ const dividendSegments: Record<string, string> = {}
266
+
267
+ for (const fileName of Object.keys(parsed.files)) {
268
+ const file = parsed.files[fileName]
269
+ for (let n = 0; n < file.lines.length; n++) {
270
+ const { Type, ISIN } = file.lines[n].columns
271
+
272
+ if (Type === 'Dividends') {
273
+
274
+ const date = this.date(file.lines[n].columns)
275
+ const key = `${date} ${ISIN}`
276
+ if (dividendSegments[key] === undefined) {
277
+ const segmentId = this.segmentId(file.lines[n])
278
+ if (segmentId === NO_SEGMENT) {
279
+ throw new InvalidFile(`Failed to find segment for ${JSON.stringify(file.lines[n].columns)}`)
280
+ }
281
+ dividendSegments[key] = segmentId
282
+ }
283
+ file.lines[n].segmentId = dividendSegments[key]
284
+ const segmentId = dividendSegments[key]
285
+ parsed.segments[segmentId] = parsed.segments[segmentId] || { id: segmentId, time: new Date(date), lines: [] }
286
+ parsed.segments[segmentId].lines.push({ number: n, file: fileName })
287
+
288
+ } else if (Type === 'Corporate Actions') {
289
+
290
+ const Date = this.date(file.lines[n].columns)
291
+ const key = `${Date} ${ISIN}`
292
+ actionLines[key] = actionLines[key] || []
293
+ actionLines[key].push(n)
294
+ }
295
+ }
296
+ }
297
+
298
+ // Go through withholding taxes and share the segment ID with the dividend and the tax.
299
+ // Recognize tax adjustments and collect them for further processing.
300
+ // For other lines, get own segment ID.
301
+ const taxFixSegments: Record<string, string> = {}
302
+ for (const fileName of Object.keys(parsed.files)) {
303
+ const file = parsed.files[fileName]
304
+ for (let n = 0; n < file.lines.length; n++) {
305
+
306
+ const { Type, ISIN, Description } = file.lines[n].columns
307
+
308
+ if (!Type) {
309
+ continue
310
+ }
311
+
312
+ // Get the timestamp.
313
+ const date = this.date(file.lines[n].columns)
314
+ const time = new Date(date)
315
+
316
+ if (Type === 'Withholding Tax') {
317
+
318
+ // Group withholding tax entries with the corresponding dividend, if possible.
319
+ const key = `${date} ${ISIN}`
320
+ let segmentId
321
+ if (dividendSegments[key] === undefined) {
322
+ // Sometimes there are corrections afterwards. Let us then just adjust withholding taxes.
323
+ segmentId = taxFixSegments[key] || this.segmentId(file.lines[n])
324
+ if (segmentId === NO_SEGMENT) {
325
+ throw new InvalidFile(`Failed to find segment for ${JSON.stringify(file.lines[n].columns)}`)
326
+ }
327
+ taxFixSegments[key] = segmentId
328
+ file.lines[n].columns.Subtype = 'Adjustment'
329
+ } else {
330
+ segmentId = dividendSegments[key]
331
+ file.lines[n].columns.Subtype = 'Tax'
332
+ }
333
+ file.lines[n].segmentId = segmentId
334
+ parsed.segments[segmentId] = parsed.segments[segmentId] || { id: segmentId, time, lines: [] }
335
+ parsed.segments[segmentId].lines.push({ number: n, file: fileName })
336
+
337
+ } else if (Type === 'Corporate Actions') {
338
+
339
+ const key = `${date} ${ISIN}`
340
+ if (!actionLines[key]) {
341
+ throw new InvalidFile(`Unable to find matching corporate action for a line '${Description}.`)
342
+ }
343
+ // Use the first segmentId for all lines.
344
+ let segmentId
345
+ for (const line of actionLines[key]) {
346
+ if (file.lines[line].segmentId) {
347
+ continue
348
+ }
349
+ if (!segmentId) {
350
+ segmentId = this.segmentId(file.lines[n])
351
+ parsed.segments[segmentId] = { id: segmentId, time, lines: [] }
352
+ }
353
+ file.lines[line].segmentId = segmentId
354
+ parsed.segments[segmentId].lines.push({ number: line, file: fileName })
355
+ }
356
+
357
+ } else if (Type === 'Deposits & Withdrawals') {
358
+
359
+ // Determine subtype for the line.
360
+ if (/Electronic Fund Transfer/.test(Description)) {
361
+ file.lines[n].columns.Subtype = 'Deposit'
362
+ } else if (/Disbursement Initiated by/.test(Description)) {
363
+ file.lines[n].columns.Subtype = 'Withdrawal'
364
+ } else if (/^Adjustment:/.test(Description)) {
365
+ file.lines[n].columns.Subtype = 'Adjustment'
366
+ } else {
367
+ warning(`Unable to determine subtype for Deposits & Withdrawals '${Description}'.`)
368
+ }
369
+
370
+ }
371
+
372
+ // For those not yet assigned, just record a single line segment.
373
+ if (!file.lines[n].segmentId) {
374
+ const segmentId = this.segmentId(file.lines[n])
375
+ if (segmentId !== NO_SEGMENT) {
376
+ file.lines[n].segmentId = segmentId
377
+ parsed.segments[segmentId] = parsed.segments[segmentId] || { id: segmentId, time, lines: [] }
378
+ parsed.segments[segmentId].lines.push({ number: n, file: fileName })
379
+ }
380
+ }
381
+ }
382
+ }
383
+
384
+ const final = await this.segmentationPostProcess(parsed)
385
+ this.debugSegmentation(final)
386
+
387
+ return final
388
+ }
389
+ }
@@ -0,0 +1,24 @@
1
+ import { ImportPlugin } from '@dataplug/tasenor-common-node'
2
+ import { PluginCode, Version } from '@dataplug/tasenor-common'
3
+ import { LynxHandler } from './LynxHandler'
4
+
5
+ class LynxImportPlugin extends ImportPlugin {
6
+
7
+ constructor() {
8
+ super(new LynxHandler())
9
+
10
+ this.code = 'LynxImport' as PluginCode
11
+ this.title = 'Import for Lynx'
12
+ this.version = '1.0.36' as Version
13
+ this.icon = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M5 6.99h3V14h2V6.99h3L9 3zM14 10v7.01h-3L15 21l4-3.99h-3V10z"/></svg>'
14
+ this.releaseDate = '2023-05-03'
15
+ this.use = 'backend'
16
+ this.type = 'import'
17
+ this.description = 'Import plugin for importing transaction data in CSV format provided by Lynx.'
18
+
19
+ this.languages = this.getLanguages()
20
+ }
21
+
22
+ }
23
+
24
+ export default LynxImportPlugin