@jaskrowo/xtb2sure 0.1.0 → 0.2.2

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/README.md CHANGED
@@ -11,7 +11,7 @@ Convert XTB `.xlsx` exports into Sure-compatible CSV files.
11
11
 
12
12
  ## Requirements
13
13
 
14
- - Node.js 24
14
+ - Node.js 24+
15
15
 
16
16
  ## Install
17
17
 
@@ -22,7 +22,7 @@ npm install
22
22
  ## Usage
23
23
 
24
24
  ```bash
25
- node --experimental-transform-types ./src/cli.ts convert <input.xlsx> --currency <CODE> --account "<ACCOUNT_NAME>"
25
+ node ./src/cli.ts convert <input.xlsx> --currency <CODE> --account "<ACCOUNT_NAME>"
26
26
  ```
27
27
 
28
28
  Options:
@@ -37,11 +37,11 @@ Options:
37
37
  Examples:
38
38
 
39
39
  ```bash
40
- node --experimental-transform-types ./src/cli.ts convert sample_data/IKE/input.xlsx --currency USD --account "XTB IKE"
40
+ node ./src/cli.ts convert sample_data/IKE/input.xlsx --currency USD --account "XTB IKE"
41
41
  ```
42
42
 
43
43
  ```bash
44
- node --experimental-transform-types ./src/cli.ts convert sample_data/PLN/input.xlsx --currency PLN --account "XTB PLN" --out-dir ./out
44
+ node ./src/cli.ts convert sample_data/PLN/input.xlsx --currency PLN --account "XTB PLN" --out-dir ./out
45
45
 
46
46
  Or via npm script:
47
47
 
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "../src/cli.ts";
package/package.json CHANGED
@@ -1,30 +1,38 @@
1
1
  {
2
2
  "name": "@jaskrowo/xtb2sure",
3
- "version": "0.1.0",
3
+ "version": "0.2.2",
4
4
  "description": "Convert XTB .xlsx exports into Sure-compatible CSV files",
5
5
  "main": "src/index.ts",
6
6
  "engines": {
7
- "node": "24"
7
+ "node": ">=24"
8
8
  },
9
9
  "type": "module",
10
10
  "license": "MIT",
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/domeq/xtb2sure.git"
17
+ },
18
+ "files": [
19
+ "src",
20
+ "bin"
21
+ ],
14
22
  "bin": {
15
- "xtb2sure": "./src/cli.ts"
23
+ "xtb2sure": "./bin/xtb2sure.js"
16
24
  },
17
25
  "scripts": {
18
- "dev": "node --experimental-transform-types --watch src/index.ts",
19
- "convert": "node --experimental-transform-types ./src/cli.ts convert",
26
+ "dev": "node --watch src/index.ts",
27
+ "convert": "node ./src/cli.ts convert",
20
28
  "format": "biome format --write .",
21
29
  "knip": "knip",
22
30
  "lint-fix": "biome check --write .",
23
31
  "lint": "biome check .",
24
32
  "prepare": "husky",
25
33
  "release": "release-it",
26
- "start": "node --experimental-transform-types src/index.ts",
27
- "test": "node --experimental-transform-types --test",
34
+ "start": "node src/index.ts",
35
+ "test": "node --test",
28
36
  "typecheck": "tsc --noEmit"
29
37
  },
30
38
  "lint-staged": {
package/src/cli.ts CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env -S node --experimental-transform-types
1
+ #!/usr/bin/env node
2
2
 
3
3
  import process from "node:process";
4
4
  import { Command, CommanderError } from "commander";
package/.editorconfig DELETED
@@ -1,30 +0,0 @@
1
- # EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs.
2
- # Requires EditorConfig JetBrains Plugin - http://github.com/editorconfig/editorconfig-jetbrains
3
-
4
- # Set this file as the topmost .editorconfig
5
- # (multiple files can be used, and are applied starting from current document location)
6
- root = true
7
-
8
- # Use bracketed regexp to target specific file types or file locations
9
- [*.{ts,js,tsx,json,yml,yaml}]
10
-
11
- # Use hard or soft tabs ["tab", "space"]
12
- indent_style = space
13
-
14
- # Size of a single indent [an integer, "tab"]
15
- indent_size = tab
16
-
17
- # Number of columns representing a tab character [an integer]
18
- tab_width = 4
19
-
20
- # Line breaks representation ["lf", "cr", "crlf"]
21
- end_of_line = lf
22
-
23
- # ["latin1", "utf-8", "utf-16be", "utf-16le"]
24
- charset = utf-8
25
-
26
- # Remove any whitespace characters preceding newline characters ["true", "false"]
27
- trim_trailing_whitespace = false
28
-
29
- # Ensure file ends with a newline when saving ["true", "false"]
30
- insert_final_newline = true
@@ -1,15 +0,0 @@
1
- name: Runtime & application setup
2
- description: Sets right node version, installs dependencies
3
-
4
- runs:
5
- using: 'composite'
6
- steps:
7
- - name: Set correct node version
8
- uses: actions/setup-node@v7
9
- with:
10
- node-version-file: package.json
11
- cache: 'npm'
12
- registry-url: 'https://registry.npmjs.org'
13
- - name: Install dependencies
14
- run: npm ci --quiet --no-audit --no-fund --ignore-scripts
15
- shell: bash
@@ -1,18 +0,0 @@
1
- name: Validate PR title
2
- description: Runs commitlint on the PR title
3
-
4
- runs:
5
- using: composite
6
- steps:
7
- - name: Set correct node version
8
- uses: actions/setup-node@v7
9
- with:
10
- node-version: 24.x
11
- - name: Install commitlint
12
- run: npm install -g @commitlint/cli @commitlint/config-conventional
13
- shell: bash
14
- - name: Validate PR title
15
- env:
16
- PR_TITLE: ${{ github.event.pull_request.title }}
17
- run: echo "$PR_TITLE" | npx commitlint
18
- shell: bash
@@ -1,3 +0,0 @@
1
- # Summary
2
-
3
- ## Related issue(s)
@@ -1,51 +0,0 @@
1
- name: Main branch
2
-
3
- on:
4
- workflow_dispatch:
5
- push:
6
- branches:
7
- - main
8
- merge_group:
9
-
10
- permissions:
11
- id-token: write
12
- contents: read
13
-
14
- env:
15
- TZ: ${{ vars.TZ }}
16
- FORCE_COLOR: 1
17
-
18
- jobs:
19
- validate:
20
- name: Validate
21
- uses: ./.github/workflows/validate.yml
22
- secrets: inherit
23
-
24
- npm:
25
- name: NPM audit
26
- uses: ./.github/workflows/npm-audit.yml
27
- secrets: inherit
28
-
29
- release:
30
- name: Release
31
- needs: [validate]
32
- runs-on: ubuntu-latest
33
- concurrency: ${{ github.workflow }}-release
34
- permissions:
35
- contents: write
36
- id-token: write
37
- steps:
38
- - uses: actions/checkout@v7
39
- with:
40
- fetch-depth: 0
41
- token: ${{ secrets.RELEASE_TOKEN }}
42
- - name: Select correct node version, install deps
43
- uses: ./.github/actions/setup
44
- - name: Configure git
45
- run: |
46
- git config user.name "domeq"
47
- git config user.email "iskra.wojtek@gmail.com"
48
- - name: Create a release
49
- env:
50
- GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
51
- run: npm run release -- --ci
@@ -1,23 +0,0 @@
1
- name: NPM
2
-
3
- on:
4
- workflow_call:
5
-
6
- env:
7
- TZ: Europe/Oslo
8
-
9
- jobs:
10
- audit:
11
- name: Audit
12
- runs-on: ubuntu-latest
13
- timeout-minutes: 15
14
- steps:
15
- - uses: actions/checkout@v7
16
- - name: Set correct node version
17
- uses: actions/setup-node@v7
18
- with:
19
- node-version-file: package.json
20
- cache: npm
21
- registry-url: 'https://registry.npmjs.org'
22
- - name: NPM Audit
23
- run: npm audit --omit=dev --audit-level=high
@@ -1,20 +0,0 @@
1
- name: PR
2
-
3
- on:
4
- workflow_dispatch:
5
- pull_request:
6
-
7
- permissions:
8
- id-token: write
9
- contents: read
10
- pull-requests: write
11
-
12
- env:
13
- TZ: ${{ vars.TZ }}
14
- FORCE_COLOR: 1
15
-
16
- jobs:
17
- validate:
18
- name: Validate
19
- uses: ./.github/workflows/validate.yml
20
- secrets: inherit
@@ -1,60 +0,0 @@
1
- name: Validate
2
-
3
- permissions:
4
- id-token: write
5
- contents: read
6
-
7
- on:
8
- workflow_call:
9
-
10
- env:
11
- TZ: ${{ vars.TZ }}
12
-
13
- jobs:
14
- validate_pr_title:
15
- runs-on: ubuntu-latest
16
- name: Validate PR title
17
- if: github.event.pull_request != null
18
- timeout-minutes: 5
19
- steps:
20
- - uses: actions/checkout@v7
21
- - name: Validate PR title
22
- uses: ./.github/actions/validate-pr-title
23
- linters:
24
- name: Linters
25
- runs-on: ubuntu-latest
26
- timeout-minutes: 15
27
- steps:
28
- - uses: actions/checkout@v7
29
- - uses: ./.github/actions/setup
30
- - name: Lint
31
- run: npm run lint
32
- typecheck:
33
- name: Typecheck
34
- runs-on: ubuntu-latest
35
- timeout-minutes: 15
36
- steps:
37
- - uses: actions/checkout@v7
38
- - uses: ./.github/actions/setup
39
- - name: Typecheck
40
- run: npm run typecheck
41
- knip:
42
- name: Knip
43
- runs-on: ubuntu-latest
44
- timeout-minutes: 15
45
- steps:
46
- - uses: actions/checkout@v7
47
- - uses: ./.github/actions/setup
48
- - name: Knip
49
- run: npm run knip
50
- - name: Knip (production mode)
51
- run: npm run knip -- --production
52
- tests:
53
- name: Tests
54
- runs-on: ubuntu-latest
55
- timeout-minutes: 15
56
- steps:
57
- - uses: actions/checkout@v7
58
- - uses: ./.github/actions/setup
59
- - name: Test
60
- run: npm test
package/.husky/commit-msg DELETED
@@ -1 +0,0 @@
1
- npx --no -- commitlint --edit ${1}
package/.husky/pre-commit DELETED
@@ -1 +0,0 @@
1
- npx lint-staged
package/.release-it.cjs DELETED
@@ -1,41 +0,0 @@
1
- module.exports = {
2
- git: {
3
- // biome-ignore lint/suspicious/noTemplateCurlyInString: release-it placeholders
4
- commitMessage: "chore: release ${version} \n\n${changelog}",
5
- requireBranch: "main",
6
- requireCleanWorkingDir: true,
7
- // biome-ignore lint/suspicious/noTemplateCurlyInString: release-it placeholders
8
- tagName: "v${version}",
9
- },
10
- github: {
11
- release: true,
12
- tokenRef: "GITHUB_TOKEN",
13
- },
14
- npm: {
15
- publish: true,
16
- skipChecks: true,
17
- },
18
- plugins: {
19
- "@release-it/conventional-changelog": {
20
- infile: "CHANGELOG.md",
21
- preset: {
22
- name: "conventionalcommits",
23
- types: [
24
- { section: "Features", type: "feat" },
25
- { section: "Features", type: "feature" },
26
- { section: "Bug Fixes", type: "fix" },
27
- { section: "Performance Improvements", type: "perf" },
28
- { section: "Reverts", type: "revert" },
29
- { section: "Documentation", type: "docs" },
30
- { section: "Styles", type: "style" },
31
- { section: "Miscellaneous Chores", type: "chore" },
32
- { section: "Code Refactoring", type: "refactor" },
33
- { section: "Tests", type: "test" },
34
- { section: "Build System", type: "build" },
35
- { section: "Continuous Integration", type: "ci" },
36
- { section: "Configuration changes", type: "config" },
37
- ],
38
- },
39
- },
40
- },
41
- };
@@ -1,23 +0,0 @@
1
- # Code of Conduct
2
-
3
- ## Our Pledge
4
-
5
- We are committed to providing a friendly, safe, and welcoming environment for everyone.
6
-
7
- ## Our Standards
8
-
9
- Examples of positive behavior:
10
-
11
- - Being respectful and constructive
12
- - Accepting and giving feedback gracefully
13
- - Prioritizing collaboration and empathy
14
-
15
- Examples of unacceptable behavior:
16
-
17
- - Harassment, discrimination, or abusive language
18
- - Personal attacks or trolling
19
- - Publishing private information without consent
20
-
21
- ## Enforcement
22
-
23
- Project maintainers are responsible for clarifying and enforcing this code of conduct.
package/CONTRIBUTING.md DELETED
@@ -1,26 +0,0 @@
1
- # Contributing
2
-
3
- Thanks for your interest in contributing to `xtb2sure`.
4
-
5
- ## Setup
6
-
7
- 1. Install Node.js 22+.
8
- 2. Install dependencies:
9
-
10
- ```bash
11
- npm install
12
- ```
13
-
14
- ## Validate changes
15
-
16
- Run the test suite before opening a pull request:
17
-
18
- ```bash
19
- npm test
20
- ```
21
-
22
- ## Guidelines
23
-
24
- - Keep behavior deterministic and fixture-driven.
25
- - Add tests for every functional change.
26
- - Prefer small, focused pull requests.
package/SECURITY.md DELETED
@@ -1,11 +0,0 @@
1
- # Security Policy
2
-
3
- If you discover a security issue, please report it privately to the maintainers.
4
-
5
- Please include:
6
-
7
- - A clear description of the issue
8
- - Steps to reproduce
9
- - Potential impact
10
-
11
- Do not open public issues for security vulnerabilities.
package/biome.json DELETED
@@ -1,20 +0,0 @@
1
- {
2
- "$schema": "https://biomejs.dev/schemas/2.2.6/schema.json",
3
- "vcs": {
4
- "enabled": true,
5
- "clientKind": "git",
6
- "useIgnoreFile": true
7
- },
8
- "formatter": {
9
- "enabled": true,
10
- "indentStyle": "space",
11
- "indentWidth": 4,
12
- "lineWidth": 120
13
- },
14
- "linter": {
15
- "enabled": true,
16
- "rules": {
17
- "recommended": true
18
- }
19
- }
20
- }
@@ -1,12 +0,0 @@
1
- module.exports = {
2
- extends: ["@commitlint/config-conventional"],
3
- rules: {
4
- "body-max-line-length": [2, "always", Infinity],
5
- "footer-max-line-length": [2, "always", Infinity],
6
- "type-enum": [
7
- 2,
8
- "always",
9
- ["build", "chore", "ci", "config", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"],
10
- ],
11
- },
12
- };
@@ -1,11 +0,0 @@
1
- const NAME = process.env.DD_SERVICE || "xtb2sure";
2
- const ENVIRONMENT = process.env.NODE_ENV || "development";
3
- const VERSION = process.env.npm_package_version || "dev";
4
-
5
- export default {
6
- application: {
7
- env: ENVIRONMENT,
8
- name: NAME,
9
- version: VERSION,
10
- },
11
- };
package/docs/PRD.md DELETED
@@ -1,141 +0,0 @@
1
- # xtb2sure PRD
2
-
3
- ## Goal
4
-
5
- Build an open-source TypeScript CLI that converts XTB `.xlsx` exports into two Sure-compatible CSV files:
6
-
7
- 1. transactions CSV (for Sure Transactions import)
8
- 2. investments CSV (for Sure Trades import)
9
-
10
- Target runtime is current Node LTS, executed directly via Node strip-types.
11
-
12
- ## Confirmed Decisions
13
-
14
- - Input format: `.xlsx`
15
- - Source data: only `Cash Operations` sheet
16
- - Output: two CSVs per input file
17
- - Date output format: `YYYY-MM-DD`
18
- - Currency: required CLI parameter
19
- - Account: account-level label provided by CLI (e.g. `XTB PLN`, `XTB IKE`)
20
- - Include tax rows (`Free funds interest tax`, `Withholding tax`)
21
- - Ignore summary `Total` row
22
- - Trade ticker uses XTB input as-is (e.g. `EIMI.UK`, `IUSQ.DE`)
23
-
24
- ## Input Schema (XTB Cash Operations)
25
-
26
- Relevant columns:
27
- - `Type`
28
- - `Instrument`
29
- - `Ticker`
30
- - `Time`
31
- - `Amount`
32
- - `ID`
33
- - `Comment`
34
- - `Product`
35
- - `Position ID`
36
-
37
- ## Output A: Transactions CSV
38
-
39
- Header:
40
- - `date,amount,name,account,notes`
41
-
42
- Includes operation types:
43
- - `Deposit`
44
- - `IKE deposit`
45
- - `IKZE deposit`
46
- - `Free funds interest`
47
- - `Free funds interest tax`
48
- - `Withholding tax`
49
-
50
- Mapping:
51
- - `date` from `Time` (Excel serial UTC) -> `YYYY-MM-DD`
52
- - `amount` from `Amount` (signed as-is)
53
- - `name` from `Type` (or normalized label if fixture requires)
54
- - `account` from CLI account label
55
- - `notes` as traceable text including `XTB <ID>` and source comment
56
-
57
- ## Output B: Investments CSV
58
-
59
- Header:
60
- - `date,ticker,currency,qty,price,account,name`
61
-
62
- Includes operation types:
63
- - `Stock purchase`
64
- - `Stock sell`
65
-
66
- Mapping:
67
- - `date` from `Time` -> `YYYY-MM-DD`
68
- - `ticker` from XTB `Ticker` as-is (e.g. `IPOL.UK`, `SXRV.DE`)
69
- - `currency` from CLI currency argument
70
- - `qty` parsed from `Comment` (sell rows negative where reflected in fixture)
71
- - `price` parsed from `Comment` (`@ <price>`)
72
- - `account` from CLI account label
73
- - `name` fixture-compatible human-readable label including `XTB <ID>`
74
-
75
- ## CLI (v1)
76
-
77
- Command:
78
- - `xtb2sure convert <input.xlsx> --currency <CODE> --account "<ACCOUNT_NAME>"`
79
-
80
- Options:
81
- - `--out-dir <dir>`
82
- - `--transactions-out <file>`
83
- - `--investments-out <file>`
84
- - `--strict`
85
- - `--dry-run`
86
- - `--report <file>`
87
-
88
- Exit codes:
89
- - `0` success
90
- - `1` conversion/validation failure
91
- - `2` usage/config error
92
-
93
- ## TDD Plan
94
-
95
- ### Unit tests
96
- - Excel serial date conversion
97
- - operation classification (transaction/investment/ignored)
98
- - qty/price parser from comment text
99
- - per-row mapping by type
100
-
101
- ### Integration tests (fixture exact)
102
- - `sample_data/IKE/input.xlsx` -> exact match:
103
- - `sample_data/IKE/output_transactions.csv`
104
- - `sample_data/IKE/output_investments.csv`
105
- - `sample_data/PLN/input.xlsx` -> exact match:
106
- - `sample_data/PLN/output_transactions.csv`
107
- - `sample_data/PLN/output_investments.csv`
108
- - investments schema regression checks:
109
- - `ticker` equals source `Cash Operations.Ticker` value for the matching XTB operation ID
110
-
111
- ### CLI tests
112
- - required args validation (`--currency`, `--account`)
113
- - output file generation
114
- - `--dry-run` behavior
115
- - strict-mode failure behavior
116
- - summary counters and exit codes
117
-
118
- ## Open Source Baseline
119
-
120
- Include in repo from the beginning:
121
- - `README.md`
122
- - `LICENSE` (permissive and inclusive; MIT recommended unless you choose otherwise)
123
- - `CONTRIBUTING.md`
124
- - `CODE_OF_CONDUCT.md`
125
- - optional but recommended: `SECURITY.md`, CI workflow, issue templates
126
-
127
- ## Non-goals (v1)
128
-
129
- - `Closed Positions` / `Open Positions` parsing
130
- - Sure API upload automation
131
- - FX conversion logic
132
- - Data enrichment beyond deterministic mapping
133
-
134
- ## Fixture Status
135
-
136
- Current fixtures are aligned and usable for TDD:
137
- - `sample_data/IKE`
138
- - `sample_data/PLN`
139
-
140
- Investments outputs now use:
141
- - ticker from input as-is
package/knip.config.js DELETED
@@ -1,5 +0,0 @@
1
- const config = {
2
- project: ["src/**!", "test/**!"],
3
- };
4
-
5
- export default config;
package/renovate.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
- "extends": ["config:recommended"],
4
- "packageRules": [
5
- {
6
- "automerge": true,
7
- "matchUpdateTypes": ["minor", "patch"]
8
- }
9
- ]
10
- }
package/test/cli.test.ts DELETED
@@ -1,148 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { spawn } from "node:child_process";
3
- import { access, mkdtemp, readFile } from "node:fs/promises";
4
- import os from "node:os";
5
- import path from "node:path";
6
- import test from "node:test";
7
- import { fileURLToPath } from "node:url";
8
- import xlsx from "xlsx";
9
-
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
12
- const rootDir = path.resolve(__dirname, "..");
13
- const cliPath = path.join(rootDir, "src", "cli.ts");
14
-
15
- test("CLI validates required --currency and --account args", async () => {
16
- const result = await runCli([
17
- "convert",
18
- path.join(rootDir, "test", "fixtures", "IKE", "input.xlsx"),
19
- "--currency",
20
- "USD",
21
- ]);
22
-
23
- assert.equal(result.code, 2);
24
- assert.match(result.stderr, /required option/);
25
- });
26
-
27
- test("CLI generates output files", async () => {
28
- const tmpDir = await mkdtemp(path.join(os.tmpdir(), "xtb2sure-cli-"));
29
- const txPath = path.join(tmpDir, "tx.csv");
30
- const invPath = path.join(tmpDir, "inv.csv");
31
-
32
- const result = await runCli([
33
- "convert",
34
- path.join(rootDir, "test", "fixtures", "IKE", "input.xlsx"),
35
- "--currency",
36
- "USD",
37
- "--account",
38
- "XTB IKE",
39
- "--transactions-out",
40
- txPath,
41
- "--investments-out",
42
- invPath,
43
- ]);
44
-
45
- assert.equal(result.code, 0);
46
- await access(txPath);
47
- await access(invPath);
48
- });
49
-
50
- test("CLI dry-run does not create output files", async () => {
51
- const tmpDir = await mkdtemp(path.join(os.tmpdir(), "xtb2sure-dry-"));
52
- const txPath = path.join(tmpDir, "tx.csv");
53
- const invPath = path.join(tmpDir, "inv.csv");
54
-
55
- const result = await runCli([
56
- "convert",
57
- path.join(rootDir, "test", "fixtures", "IKE", "input.xlsx"),
58
- "--currency",
59
- "USD",
60
- "--account",
61
- "XTB IKE",
62
- "--transactions-out",
63
- txPath,
64
- "--investments-out",
65
- invPath,
66
- "--dry-run",
67
- ]);
68
-
69
- assert.equal(result.code, 0);
70
- await assert.rejects(() => access(txPath));
71
- await assert.rejects(() => access(invPath));
72
- });
73
-
74
- test("CLI strict mode fails when mapping issue is encountered", async () => {
75
- const tmpDir = await mkdtemp(path.join(os.tmpdir(), "xtb2sure-strict-"));
76
- const inputPath = path.join(tmpDir, "bad.xlsx");
77
- const reportPath = path.join(tmpDir, "report.json");
78
- await writeInvalidTradeWorkbook(inputPath);
79
-
80
- const strictResult = await runCli([
81
- "convert",
82
- inputPath,
83
- "--currency",
84
- "USD",
85
- "--account",
86
- "XTB TEST",
87
- "--strict",
88
- "--report",
89
- reportPath,
90
- ]);
91
-
92
- assert.equal(strictResult.code, 1);
93
-
94
- const relaxedResult = await runCli([
95
- "convert",
96
- inputPath,
97
- "--currency",
98
- "USD",
99
- "--account",
100
- "XTB TEST",
101
- "--report",
102
- reportPath,
103
- ]);
104
-
105
- assert.equal(relaxedResult.code, 0);
106
- const report = JSON.parse(await readFile(reportPath, "utf8"));
107
- assert.equal(report.totals.issues, 1);
108
- });
109
-
110
- function runCli(args: string[]): Promise<{ code: number | null; stdout: string; stderr: string }> {
111
- return new Promise((resolve, reject) => {
112
- const child = spawn(process.execPath, ["--experimental-transform-types", cliPath, ...args], {
113
- cwd: rootDir,
114
- stdio: ["ignore", "pipe", "pipe"],
115
- });
116
-
117
- let stdout = "";
118
- let stderr = "";
119
-
120
- child.stdout.on("data", (chunk) => {
121
- stdout += String(chunk);
122
- });
123
-
124
- child.stderr.on("data", (chunk) => {
125
- stderr += String(chunk);
126
- });
127
-
128
- child.on("error", reject);
129
- child.on("close", (code) => {
130
- resolve({ code, stdout, stderr });
131
- });
132
- });
133
- }
134
-
135
- async function writeInvalidTradeWorkbook(outPath: string): Promise<void> {
136
- const worksheet = xlsx.utils.aoa_to_sheet([
137
- ["Account number", "123"],
138
- ["Cash Operations"],
139
- ["Date from (UTC)", 46000],
140
- ["Date to (UTC)", 46001],
141
- ["Type", "Instrument", "Ticker", "Category", "Time", "Amount", "ID", "Comment", "Product", "Position ID"],
142
- ["Stock purchase", "Test instrument", "TEST.UK", "", 46000.5, 123.45, "1", "BROKEN COMMENT", "My Trades", ""],
143
- ]);
144
-
145
- const workbook = xlsx.utils.book_new();
146
- xlsx.utils.book_append_sheet(workbook, worksheet, "Cash Operations");
147
- xlsx.writeFile(workbook, outPath);
148
- }
package/test/date.test.ts DELETED
@@ -1,8 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { excelSerialToYYYYMMDD } from "../src/date.ts";
4
-
5
- test("excelSerialToYYYYMMDD converts known fixture serials", () => {
6
- assert.equal(excelSerialToYYYYMMDD(46266.43755445602), "2026-09-01");
7
- assert.equal(excelSerialToYYYYMMDD(46079.041666666664), "2026-02-26");
8
- });
Binary file
@@ -1,3 +0,0 @@
1
- date,ticker,currency,qty,price,account,name
2
- 2026-04-30,EIMI.UK,USD,24,51.81,XTB IKE,XTB 900000003: Stock purchase
3
- 2026-04-30,IPOL.UK,USD,0.6747,35.68993627,XTB IKE,XTB 900000004: Stock purchase
@@ -1,3 +0,0 @@
1
- date,amount,name,account,notes
2
- 2026-09-01,0.11,Free funds interest,XTB IKE,XTB 900000001: Mock interest period 2026-08
3
- 2026-04-30,14556.14,IKE deposit,XTB IKE,XTB 900000002: Mock transfer in from account 100001
Binary file
@@ -1,5 +0,0 @@
1
- date,ticker,currency,qty,price,account,name
2
- 2026-01-05,IUSQ.DE,PLN,1.0,93.88,XTB PLN,MSCI ACWI - Stock purchase (XTB 910000005)
3
- 2026-01-06,IPOL.UK,PLN,3.0,34.04666667,XTB PLN,MSCI Poland - Stock purchase (XTB 910000007)
4
- 2026-01-09,SXR8.DE,PLN,-0.1019,637.68400393,XTB PLN,Core S&P 500 - Stock sell (XTB 910000006)
5
- 2026-01-21,EIMI.UK,PLN,114.0,47.33,XTB PLN,Core MSCI EM IMI - Stock purchase (XTB 910000008)
@@ -1,6 +0,0 @@
1
- date,amount,name,account,notes
2
- 2026-09-04,5000.00,Deposit,XTB PLN,XTB 910000001: Mock bank deposit txn=MOCK-TXN-001 [subaccount: Investment Plans]
3
- 2026-03-05,0.01,Free funds interest,XTB PLN,XTB 910000002: Mock free-funds interest 2026-02 [subaccount: My Trades]
4
- 2026-03-05,-0.01,Free funds interest tax,XTB PLN,XTB 910000003: Mock free-funds interest tax 2026-02 [subaccount: My Trades]
5
- 2026-01-02,0.93,Withholding tax,XTB PLN,XTB 910000004: mock WHT correction [subaccount: Investment Plans]
6
- 2026-02-26,-11304.00,IKZE deposit,XTB PLN,XTB 910000009: Mock transfer out to IKZE 200002 [subaccount: My Trades]
@@ -1,171 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { readFile } from "node:fs/promises";
3
- import path from "node:path";
4
- import test from "node:test";
5
- import { fileURLToPath } from "node:url";
6
- import { readCashOperationsSheet } from "../src/excel.ts";
7
- import { convertFile } from "../src/index.ts";
8
- import { classifyOperation } from "../src/operations.ts";
9
-
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
12
- const rootDir = path.resolve(__dirname, "..");
13
-
14
- test("IKE fixture conversion matches expected outputs", async () => {
15
- const result = await convertFile({
16
- account: "XTB IKE",
17
- currency: "USD",
18
- dryRun: true,
19
- inputPath: path.join(rootDir, "test", "fixtures", "IKE", "input.xlsx"),
20
- });
21
-
22
- const expectedTransactions = await readExpected("test/fixtures/IKE/output_transactions.csv");
23
- const expectedInvestments = await readExpected("test/fixtures/IKE/output_investments.csv");
24
-
25
- assert.equal(normalizeEol(result.transactionsCsv), expectedTransactions);
26
- assert.equal(normalizeEol(result.investmentsCsv), expectedInvestments);
27
- });
28
-
29
- test("PLN fixture conversion matches expected outputs", async () => {
30
- const result = await convertFile({
31
- account: "XTB PLN",
32
- currency: "PLN",
33
- dryRun: true,
34
- inputPath: path.join(rootDir, "test", "fixtures", "PLN", "input.xlsx"),
35
- });
36
-
37
- const expectedTransactions = await readExpected("test/fixtures/PLN/output_transactions.csv");
38
- const expectedInvestments = await readExpected("test/fixtures/PLN/output_investments.csv");
39
-
40
- assert.equal(normalizeEol(result.transactionsCsv), expectedTransactions);
41
- assert.equal(normalizeEol(result.investmentsCsv), expectedInvestments);
42
- });
43
-
44
- test("investment ticker regression: output ticker equals source ticker by XTB operation ID", async () => {
45
- const inputPath = path.join(rootDir, "test", "fixtures", "PLN", "input.xlsx");
46
- const result = await convertFile({
47
- account: "XTB PLN",
48
- currency: "PLN",
49
- dryRun: true,
50
- inputPath,
51
- });
52
-
53
- const rows = readCashOperationsSheet(inputPath);
54
- const byId = new Map<string, string>();
55
-
56
- for (const row of rows) {
57
- if (classifyOperation(row.Type) === "investment") {
58
- byId.set(row.ID, row.Ticker);
59
- }
60
- }
61
-
62
- for (const line of normalizeEol(result.investmentsCsv).split("\n").slice(1)) {
63
- if (!line.trim()) {
64
- continue;
65
- }
66
-
67
- const cells = parseCsvLine(line);
68
- const ticker = cells[1];
69
- const name = cells[6];
70
- assert.ok(typeof ticker === "string" && ticker.length > 0);
71
- assert.ok(typeof name === "string" && name.length > 0);
72
- const idMatch = name.match(/XTB\s+(\d+)/);
73
- assert.ok(idMatch, `Missing XTB ID in row: ${line}`);
74
- const id = idMatch[1];
75
- assert.ok(id);
76
- assert.equal(ticker, byId.get(id));
77
- }
78
- });
79
-
80
- test("investment currency is always taken from CLI argument", async () => {
81
- const result = await convertFile({
82
- account: "XTB PLN",
83
- currency: "PLN",
84
- dryRun: true,
85
- inputPath: path.join(rootDir, "test", "fixtures", "PLN", "input.xlsx"),
86
- });
87
-
88
- for (const line of normalizeEol(result.investmentsCsv).split("\n").slice(1)) {
89
- if (!line.trim()) {
90
- continue;
91
- }
92
-
93
- const cells = parseCsvLine(line);
94
- assert.equal(cells[2], "PLN");
95
- }
96
- });
97
-
98
- test("investment price is derived from Amount and qty", async () => {
99
- const inputPath = path.join(rootDir, "test", "fixtures", "PLN", "input.xlsx");
100
- const result = await convertFile({
101
- account: "XTB PLN",
102
- currency: "PLN",
103
- dryRun: true,
104
- inputPath,
105
- });
106
-
107
- const rows = readCashOperationsSheet(inputPath);
108
- const amountById = new Map<string, number>();
109
-
110
- for (const row of rows) {
111
- if (classifyOperation(row.Type) === "investment") {
112
- amountById.set(row.ID, Math.abs(row.Amount));
113
- }
114
- }
115
-
116
- for (const line of normalizeEol(result.investmentsCsv).split("\n").slice(1)) {
117
- if (!line.trim()) {
118
- continue;
119
- }
120
-
121
- const cells = parseCsvLine(line);
122
- const qty = Math.abs(Number(cells[3]));
123
- const price = Number(cells[4]);
124
- const name = cells[6] ?? "";
125
- const idMatch = name.match(/XTB\s+(\d+)/);
126
- assert.ok(idMatch, `Missing XTB ID in row: ${line}`);
127
- const id = idMatch[1];
128
- assert.ok(id);
129
- const expectedAmount = amountById.get(id);
130
- assert.ok(typeof expectedAmount === "number", `No amount found for ID ${id}`);
131
-
132
- const actualAmount = qty * price;
133
- assert.ok(Math.abs(actualAmount - expectedAmount) < 0.02, `Amount mismatch for ${id}`);
134
- }
135
- });
136
-
137
- async function readExpected(relativePath: string): Promise<string> {
138
- const value = await readFile(path.join(rootDir, relativePath), "utf8");
139
- return normalizeEol(value);
140
- }
141
-
142
- function normalizeEol(value: string): string {
143
- return value.replace(/\r\n/g, "\n");
144
- }
145
-
146
- function parseCsvLine(line: string): string[] {
147
- const cells: string[] = [];
148
- let current = "";
149
- let inQuotes = false;
150
-
151
- for (let i = 0; i < line.length; i += 1) {
152
- const char = line[i];
153
- if (char === '"') {
154
- if (inQuotes && line[i + 1] === '"') {
155
- current += '"';
156
- i += 1;
157
- } else {
158
- inQuotes = !inQuotes;
159
- }
160
- } else if (char === "," && !inQuotes) {
161
- cells.push(current);
162
- current = "";
163
- } else {
164
- current += char;
165
- }
166
- }
167
-
168
- cells.push(current);
169
-
170
- return cells;
171
- }
@@ -1,23 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { classifyOperation, parseTradeValues } from "../src/operations.ts";
4
-
5
- test("classifyOperation maps supported operation groups", () => {
6
- assert.equal(classifyOperation("Deposit"), "transaction");
7
- assert.equal(classifyOperation("Stock purchase"), "investment");
8
- assert.equal(classifyOperation("Total"), "ignored");
9
- });
10
-
11
- test("parseTradeValues parses BUY format with split quantity", () => {
12
- const parsed = parseTradeValues("OPEN BUY 7/7.7575 @ 42.750");
13
- assert.equal(parsed.qty, "7");
14
- assert.equal(parsed.price, "42.75");
15
- assert.equal(parsed.qtyHadExplicitDecimal, false);
16
- });
17
-
18
- test("parseTradeValues parses SELL format with decimal-preserving integer price", () => {
19
- const parsed = parseTradeValues("CLOSE SELL 0.9179 @ 1233.0");
20
- assert.equal(parsed.qty, "0.9179");
21
- assert.equal(parsed.price, "1233.0");
22
- assert.equal(parsed.qtyHadExplicitDecimal, true);
23
- });
package/tsconfig.json DELETED
@@ -1,24 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "skipLibCheck": true,
4
- "target": "esnext",
5
- "resolveJsonModule": true,
6
- "verbatimModuleSyntax": true,
7
- "rootDir": ".",
8
- "allowImportingTsExtensions": true,
9
- "noEmit": true,
10
- "module": "NodeNext",
11
- "esModuleInterop": true,
12
- "strict": true,
13
- "noUncheckedIndexedAccess": true,
14
- "noImplicitOverride": true,
15
- "forceConsistentCasingInFileNames": true,
16
- "allowJs": false,
17
- "baseUrl": "./",
18
- "paths": {
19
- "#*": ["./src/*"]
20
- }
21
- },
22
- "include": ["**/*"],
23
- "exclude": ["node_modules", "dist"]
24
- }