Skip to content

feat: multi-broker architecture with Degiro CSV parser#12

Merged
GeiserX merged 5 commits into
mainfrom
feat/multi-broker-interface
Apr 15, 2026
Merged

feat: multi-broker architecture with Degiro CSV parser#12
GeiserX merged 5 commits into
mainfrom
feat/multi-broker-interface

Conversation

@GeiserX

@GeiserX GeiserX commented Apr 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • BrokerParser interface (src/types/broker.ts): common detect() + parse() contract for all broker parsers
  • Parser registry (src/parsers/index.ts): detectBroker() auto-detection, getBroker() lookup, extensible array
  • IBKR BrokerParser wrapper: existing parser now implements the interface
  • Degiro CSV parser (src/parsers/degiro.ts): full implementation supporting:
    • Transactions CSV → trades (quantity, price, costs, exchange)
    • Account CSV → dividends + withholding taxes
    • Multi-language headers (ES/EN/NL/DE)
    • Auto-detect delimiter (comma vs semicolon)
    • EU number format (1.234,56) and standard format
    • DD-MM-YYYY → YYYYMMDD date conversion
  • CLI updates: --broker <name> flag, auto-detection fallback, improved error messages
  • ROADMAP updated with broker tax integration research:
    • High priority (no AEAT integration): Degiro, Scalable Capital, eToro, Freedom24
    • Low priority (already report to AEAT): Trade Republic (since mid-2025), XTB
    • Key finding: no broker generates Modelo 100 XML — DeclaRenta fills that gap

Broker research highlights

Broker Reports to AEAT Retenciones 720 needed Priority
Degiro No No Yes High
Scalable Capital No No Yes High
Trade Republic Yes (mid-2025) Yes No (DGT ruling) Low
XTB Yes Yes No Low

Test plan

  • 99 tests passing (24 new)
  • 18 Degiro parser tests: detection (6), transactions CSV (6), account CSV (4), error handling (2)
  • 6 registry tests: auto-detection, lookup, full parse-through
  • ESLint clean
  • Build passes (tsup + vite)
  • Review Degiro CSV parsing against real Degiro exports

Summary by CodeRabbit

  • New Features

    • Multi-broker support: Degiro parser added; broker autodetection plus optional --broker override; CLI shows resolved parser names and clearer errors.
    • Library exports updated to expose broker selection helpers and additional parser utilities.
  • Documentation

    • ROADMAP bumped to v0.2.0 with expanded tests, milestones, and multi-broker planning.
  • Tests

    • Comprehensive suites for Degiro parser and broker registry (detection, parsing, real fixtures).
  • Chores

    • Release automation consolidated into a single auto-release workflow with manual dispatch and skip-ci guard; old auto-tag workflow removed.

GeiserX added 2 commits April 15, 2026 01:04
- Merge auto-tag + release into one workflow (like CashPilot/LynxPrompt)
- Tag, sync package.json, generate changelog, create release — all in one job
- Uses only GITHUB_TOKEN (no PAT secret required)
- CLI reads version from package.json (single source of truth)
- Update ROADMAP to reflect v0.2.0 state
- Add BrokerParser interface (detect + parse → Statement)
- Add parser registry with auto-detection (detectBroker, getBroker)
- Wrap IBKR parser with BrokerParser interface
- Implement Degiro parser: Transactions CSV (trades) + Account CSV
  (dividends/withholdings), multi-language (ES/EN/NL/DE), auto-detect
  delimiter (comma/semicolon), EU number format support
- Update CLI: add --broker flag, auto-detect broker from file content
- Update ROADMAP with broker tax integration research findings:
  Degiro/Scalable (high priority, no AEAT integration) vs
  Trade Republic/XTB (low priority, already report to AEAT)
- 99 tests passing (24 new: 18 Degiro parser, 6 registry)
@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 419c2497-e81c-4412-9767-539418add5ce

📥 Commits

Reviewing files that changed from the base of the PR and between fc66b93 and e8a6803.

📒 Files selected for processing (1)
  • src/parsers/degiro.ts
✅ Files skipped from review due to trivial changes (1)
  • src/parsers/degiro.ts

📝 Walkthrough

Walkthrough

Adds a broker-agnostic parsing layer with a BrokerParser interface, registry, and implementations for IBKR and Degiro; updates CLI to use detection/selection (optional --broker); adds comprehensive Degiro and registry tests; exposes broker parser utilities in the public API; and replaces the tag-triggered workflow with a main-branch auto-release workflow that bumps and syncs versions.

Changes

Cohort / File(s) Summary
CI Automation
\.github/workflows/auto-tag.yml, \.github/workflows/release.yml
Removed auto-tag.yml. Reworked release.yml to run on main (and manual dispatch), compute latest v* tag (defaulting if absent), bump patch, create annotated tag, push tag, and optionally commit package.json/lockfile with a [skip ci] guard; added job-level skip when commit message contains [skip ci].
Broker Types & Registry
src/types/broker.ts, src/parsers/index.ts, src/types/index.ts
Added BrokerParser interface and Statement alias; created brokerParsers registry and helpers detectBroker() and getBroker(); re-exported broker types.
Parser Implementations
src/parsers/ibkr.ts, src/parsers/degiro.ts
Added ibkrParser wrapper for IBKR Flex XML and new degiroParser that detects Transactions vs Account CSVs, auto-detects delimiter, normalizes EU/local numbers and dates, and emits normalized Statement (trades and cashTransactions).
Public API surface
src/index.ts
Re-exported parser utilities and implementations: ibkrParser, degiroParser, detectBroker, getBroker, and brokerParsers.
CLI Integration
src/cli/index.ts
Switched commands to broker-agnostic parsing: added optional --broker flag, use getBroker/detectBroker, improved detection-failure errors listing available parsers, and prefixed per-file logs with the resolved parser name.
Tests
tests/parsers/degiro.test.ts, tests/parsers/registry.test.ts
Added comprehensive Vitest suites covering Degiro detection/parsing (multi-locale decimals, delimiters, real fixtures, error cases) and registry detection/lookup (case-insensitive name matching and end-to-end parse).
Docs / Roadmap
ROADMAP.md
Updated release status to v0.2.0, expanded “what works” and multi-broker planning, documented BrokerParser and Degiro parser, and adjusted milestones/metrics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the main architectural change: introducing multi-broker support with a Degiro parser implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/multi-broker-interface

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/cli/index.ts (1)

156-186: ⚠️ Potential issue | 🟠 Major

Don't treat broker files without holdings data as “no Modelo 720”.

This path now accepts any detected broker file, but the new Degiro support in this PR only parses transactions/account movements. Those inputs can reach the openPositions = [] branch and print “No es necesario presentar Modelo 720”, which is a false negative for users who still hold positions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/index.ts` around lines 156 - 186, The code assumes an empty
statement.openPositions means "no Modelo 720" which is wrong for parsers that
only produce transactions (e.g., the new Degiro parser); after parser.parse(...)
check for parsers/outputs that indicate transactions-only (e.g.,
statement.transactions?.length > 0 or parser.name === "degiro" or a
parser.capabilities flag) and treat an empty openPositions as "unknown" rather
than "below threshold": instead of directly printing "Posiciones... No es
necesario...", surface a clear warning/error asking the user to provide holdings
data or run a balance-reconstruction path (or exit non-zero), so update the
logic around statement.openPositions and the generateModelo720 call to skip the
false-negative path when transactions exist but openPositions is empty.
.github/workflows/release.yml (1)

91-91: ⚠️ Potential issue | 🟡 Minor

Git log filtering logic won't exclude both feat and fix commits.

Multiple --invert-grep --grep pairs don't stack. The current command won't properly filter out both feat and fix commits from "Other Changes".

🔧 Fix using extended regex
-            OTHERS=$(git log $PREV_TAG..$VERSION --pretty=format:"- %s" --invert-grep --grep="^feat" --invert-grep --grep="^fix" 2>/dev/null | head -10 || true)
+            OTHERS=$(git log $PREV_TAG..$VERSION --pretty=format:"- %s" --invert-grep --extended-regexp --grep="^(feat|fix)" 2>/dev/null | head -10 || true)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml at line 91, The git log command assigned to
OTHERS won't exclude both feat and fix because multiple --invert-grep/--grep
pairs don't combine; update the OTHERS assignment to use a single --invert-grep
with an alternation regex (e.g., --invert-grep --grep="^(feat|fix)" or the POSIX
equivalent) so both commit types are filtered out from PREV_TAG..VERSION; modify
the line that defines OTHERS (the git log invocation) to use that single
combined grep pattern.
🧹 Nitpick comments (3)
tests/parsers/degiro.test.ts (1)

46-71: Detection tests look solid.

Consider adding a detection test for semicolon-delimited CSV (TRANSACTIONS_CSV_SEMICOLON) to ensure delimiter doesn't affect header recognition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/parsers/degiro.test.ts` around lines 46 - 71, Add a test case to the
degiroParser.detect suite that verifies semicolon-delimited CSV is recognized:
call degiroParser.detect with the TRANSACTIONS_CSV_SEMICOLON fixture and expect
true, similar to the existing TRANSACTIONS_CSV_ES/EN tests; place it alongside
the other detect tests to ensure delimiter variations don't break header
recognition.
.github/workflows/release.yml (1)

27-43: Patch version extraction assumes well-formed semver.

If an existing tag lacks a patch component (e.g., v1.0), cut -d. -f3 returns empty and $((PATCH + 1)) fails. This is unlikely with the v0.0.0 default, but worth noting.

🛡️ Optional: Add fallback for empty PATCH
           PATCH=$(echo "$VERSION" | cut -d. -f3)
+          PATCH=${PATCH:-0}
           NEW_PATCH=$((PATCH + 1))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 27 - 43, The current tag parsing
(variables VERSION, PATCH, NEW_PATCH) can fail if the tag lacks a third
component; modify the extraction so PATCH defaults to 0 when empty/non-numeric
(e.g., use parameter expansion or a numeric fallback for PATCH after VERSION is
split), then compute NEW_PATCH as PATCH+1 and form NEW_TAG as before; update the
logic around VERSION, MAJOR, MINOR, PATCH, NEW_PATCH and NEW_TAG to ensure PATCH
is set to 0 if cut -f3 yields empty or non-numeric input (refer to variables
VERSION, PATCH, NEW_PATCH, NEW_TAG, LATEST).
src/parsers/degiro.ts (1)

237-239: parseFloat used for sign determination.

This is only for comparing signs to determine buy/sell direction, not for financial calculations. The monetary values remain as strings. Acceptable, but if extreme precision is needed, consider using Decimal.js here too. As per coding guidelines: "Check that all string-to-number conversions go through Decimal.js."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/parsers/degiro.ts` around lines 237 - 239, The code uses parseFloat on
value and quantity to set isSell (valueNum and qtyNum); per guidelines, all
string-to-number conversions must go through Decimal.js. Replace parseFloat
usage with Decimal (e.g., new Decimal(value).gt(0) / new Decimal(quantity).lt(0)
or Decimal(value).isPositive()/isNegative()) to compute isSell, and add the
necessary Decimal import/require if missing; leave the original money strings
untouched for arithmetic elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Around line 45-62: The workflow currently runs the "Create tag" step (git tag
-a "${{ steps.version.outputs.new_tag }}") before the "Sync package.json
version" step which can leave an annotated tag pointing to a commit that doesn't
include the updated package.json; move the package.json sync logic so the "Sync
package.json version" step runs before the "Create tag" step, ensure npm version
"$VERSION" --no-git-tag-version, git add/commit/push happen first, and then
create and push the annotated tag using the same ${{
steps.version.outputs.new_tag }} so the tag and committed package.json are in
sync (update any step names or ordering referencing "Create tag" and "Sync
package.json version" accordingly).

In `@src/cli/index.ts`:
- Around line 190-191: The current write to opts.output uses writeFileSync(...,
{ encoding: "latin1" }) which yields ISO-8859-1 bytes; instead install and
import iconv-lite and transcode the output720 buffer/string to ISO-8859-15 (aka
"ISO-8859-15" or "latin9") before writing. Locate the write in src/cli/index.ts
where output720 is written and replace the encoding option with writing the
result of iconv.encode(output720, "ISO-8859-15") (or equivalent API), ensuring
the output is written as a Buffer so the resulting file conforms to Modelo 720
encoding requirements.

In `@src/parsers/degiro.ts`:
- Around line 11-12: Update the top docstring to remove the outdated claim that
"Account CSV support is planned for a future version" and instead reflect that
account CSV parsing is implemented; specifically edit the parser description
that currently references Transactions-only to mention support for both
Transactions CSV and Account CSV (see parseAccountCsv function) or remove the
future-planned sentence entirely so the docstring accurately matches the
implemented parseAccountCsv behavior.

In `@src/parsers/index.ts`:
- Around line 27-29: The getBroker function currently does substring matching on
brokerParsers even when name is empty; update getBroker to first reject empty or
whitespace-only names (e.g., if (!name || name.trim().length === 0) return
undefined) before computing lower and performing brokerParsers.find((p) =>
p.name.toLowerCase().includes(lower)), so that getBroker("") returns undefined
rather than the first parser.

---

Outside diff comments:
In @.github/workflows/release.yml:
- Line 91: The git log command assigned to OTHERS won't exclude both feat and
fix because multiple --invert-grep/--grep pairs don't combine; update the OTHERS
assignment to use a single --invert-grep with an alternation regex (e.g.,
--invert-grep --grep="^(feat|fix)" or the POSIX equivalent) so both commit types
are filtered out from PREV_TAG..VERSION; modify the line that defines OTHERS
(the git log invocation) to use that single combined grep pattern.

In `@src/cli/index.ts`:
- Around line 156-186: The code assumes an empty statement.openPositions means
"no Modelo 720" which is wrong for parsers that only produce transactions (e.g.,
the new Degiro parser); after parser.parse(...) check for parsers/outputs that
indicate transactions-only (e.g., statement.transactions?.length > 0 or
parser.name === "degiro" or a parser.capabilities flag) and treat an empty
openPositions as "unknown" rather than "below threshold": instead of directly
printing "Posiciones... No es necesario...", surface a clear warning/error
asking the user to provide holdings data or run a balance-reconstruction path
(or exit non-zero), so update the logic around statement.openPositions and the
generateModelo720 call to skip the false-negative path when transactions exist
but openPositions is empty.

---

Nitpick comments:
In @.github/workflows/release.yml:
- Around line 27-43: The current tag parsing (variables VERSION, PATCH,
NEW_PATCH) can fail if the tag lacks a third component; modify the extraction so
PATCH defaults to 0 when empty/non-numeric (e.g., use parameter expansion or a
numeric fallback for PATCH after VERSION is split), then compute NEW_PATCH as
PATCH+1 and form NEW_TAG as before; update the logic around VERSION, MAJOR,
MINOR, PATCH, NEW_PATCH and NEW_TAG to ensure PATCH is set to 0 if cut -f3
yields empty or non-numeric input (refer to variables VERSION, PATCH, NEW_PATCH,
NEW_TAG, LATEST).

In `@src/parsers/degiro.ts`:
- Around line 237-239: The code uses parseFloat on value and quantity to set
isSell (valueNum and qtyNum); per guidelines, all string-to-number conversions
must go through Decimal.js. Replace parseFloat usage with Decimal (e.g., new
Decimal(value).gt(0) / new Decimal(quantity).lt(0) or
Decimal(value).isPositive()/isNegative()) to compute isSell, and add the
necessary Decimal import/require if missing; leave the original money strings
untouched for arithmetic elsewhere.

In `@tests/parsers/degiro.test.ts`:
- Around line 46-71: Add a test case to the degiroParser.detect suite that
verifies semicolon-delimited CSV is recognized: call degiroParser.detect with
the TRANSACTIONS_CSV_SEMICOLON fixture and expect true, similar to the existing
TRANSACTIONS_CSV_ES/EN tests; place it alongside the other detect tests to
ensure delimiter variations don't break header recognition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2f4811c0-f992-4ce5-a8c7-8f73329eaa03

📥 Commits

Reviewing files that changed from the base of the PR and between 9c52f0d and 4f090f9.

📒 Files selected for processing (12)
  • .github/workflows/auto-tag.yml
  • .github/workflows/release.yml
  • ROADMAP.md
  • src/cli/index.ts
  • src/index.ts
  • src/parsers/degiro.ts
  • src/parsers/ibkr.ts
  • src/parsers/index.ts
  • src/types/broker.ts
  • src/types/index.ts
  • tests/parsers/degiro.test.ts
  • tests/parsers/registry.test.ts
💤 Files with no reviewable changes (1)
  • .github/workflows/auto-tag.yml

Comment on lines +45 to +62
- name: Create tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "${{ steps.version.outputs.new_tag }}" -m "Release ${{ steps.version.outputs.new_tag }}"
git push origin "${{ steps.version.outputs.new_tag }}"

- name: Sync package.json version
run: |
VERSION="${{ steps.version.outputs.new_tag }}"
VERSION="${VERSION#v}"
CURRENT=$(node -p "require('./package.json').version")
if [ "$CURRENT" != "$VERSION" ]; then
npm version "$VERSION" --no-git-tag-version
git add package.json package-lock.json
git commit -m "chore: bump version to ${{ steps.version.outputs.new_tag }} [skip ci]"
git push origin main
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tag is created before package.json is synced—consider reordering.

If the workflow fails after creating the tag (line 49-50) but before syncing package.json (line 52-62), you'll have a published tag with a stale package.json version. Users running declarenta --version would see the old version.

Consider: create the tag after package.json is updated and committed, so both are in sync or neither is published.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 45 - 62, The workflow currently
runs the "Create tag" step (git tag -a "${{ steps.version.outputs.new_tag }}")
before the "Sync package.json version" step which can leave an annotated tag
pointing to a commit that doesn't include the updated package.json; move the
package.json sync logic so the "Sync package.json version" step runs before the
"Create tag" step, ensure npm version "$VERSION" --no-git-tag-version, git
add/commit/push happen first, and then create and push the annotated tag using
the same ${{ steps.version.outputs.new_tag }} so the tag and committed
package.json are in sync (update any step names or ordering referencing "Create
tag" and "Sync package.json version" accordingly).

Comment thread src/cli/index.ts
Comment on lines 190 to +191
if (opts.output) {
writeFileSync(opts.output, content, { encoding: "latin1" });
writeFileSync(opts.output, output720, { encoding: "latin1" });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does Node.js fs.writeFileSync support ISO-8859-15 directly, and is "latin1" equivalent to ISO-8859-1 rather than ISO-8859-15?

💡 Result:

No, Node.js fs.writeFileSync does not support ISO-8859-15 directly as a built-in encoding option. The fs module uses the same character encodings as the Buffer class, which explicitly supports 'latin1' as ISO-8859-1 (a single-byte mapping of Unicode code points U+0000 to U+00FF, truncating higher characters). Yes, "latin1" is equivalent to ISO-8859-1, not ISO-8859-15 (which differs in positions like the euro symbol and differs from ISO-8859-1 in byte mappings for some characters). Node.js official docs list supported encodings as 'utf8', 'utf16le', 'latin1' (ISO-8859-1), 'ascii', 'binary' (alias for latin1), 'base64', 'base64url', 'hex'. No mention of ISO-8859-15 in any official source. For ISO-8859-15, third-party libraries like iconv-lite are required.

Citations:


🏁 Script executed:

# Read the file at the specified lines
head -n 195 src/cli/index.ts | tail -n 10

Repository: GeiserX/DeclaRenta

Length of output: 419


🏁 Script executed:

# Check package.json for encoding-related dependencies
cat package.json | grep -A 20 '"dependencies"' | head -n 30

Repository: GeiserX/DeclaRenta

Length of output: 451


🏁 Script executed:

# Search for ISO-8859-15 references in codebase
rg "8859-15|iso-8859-15|ISO-8859-15" -i

Repository: GeiserX/DeclaRenta

Length of output: 696


🏁 Script executed:

# Search for iconv or encoding handling
rg "iconv|encode|transcode" -i --type ts | head -n 20

Repository: GeiserX/DeclaRenta

Length of output: 44


🏁 Script executed:

# Look for Modelo 720 related code
rg "modelo.*720|720.*modelo" -i --type ts | head -n 20

Repository: GeiserX/DeclaRenta

Length of output: 1296


Use ISO-8859-15 encoding for Modelo 720 output, not latin1.

Node.js's writeFileSync(..., { encoding: "latin1" }) uses ISO-8859-1 semantics, which produces incorrect byte sequences for Modelo 720 files that must comply with ISO-8859-15 encoding (as documented in src/generators/modelo720.ts comments and ROADMAP). Since Node.js doesn't natively support ISO-8859-15, add a library like iconv-lite to the dependencies and transcode the output before writing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/index.ts` around lines 190 - 191, The current write to opts.output
uses writeFileSync(..., { encoding: "latin1" }) which yields ISO-8859-1 bytes;
instead install and import iconv-lite and transcode the output720 buffer/string
to ISO-8859-15 (aka "ISO-8859-15" or "latin9") before writing. Locate the write
in src/cli/index.ts where output720 is written and replace the encoding option
with writing the result of iconv.encode(output720, "ISO-8859-15") (or equivalent
API), ensuring the output is written as a Buffer so the resulting file conforms
to Modelo 720 encoding requirements.

Comment thread src/parsers/degiro.ts Outdated
Comment thread src/parsers/index.ts
Comment on lines +27 to +29
export function getBroker(name: string): BrokerParser | undefined {
const lower = name.toLowerCase();
return brokerParsers.find((p) => p.name.toLowerCase().includes(lower));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reject empty broker names before substring matching.

getBroker("") currently returns the first registered parser because every string includes "". That makes --broker "" resolve to a real parser instead of failing, and the partial-name lookup will get more ambiguous as the registry grows.

♻️ Minimal fix
 export function getBroker(name: string): BrokerParser | undefined {
-  const lower = name.toLowerCase();
-  return brokerParsers.find((p) => p.name.toLowerCase().includes(lower));
+  const query = name.trim().toLowerCase();
+  if (!query) return undefined;
+  return (
+    brokerParsers.find((p) => p.name.toLowerCase() === query) ??
+    brokerParsers.find((p) => p.name.toLowerCase().includes(query))
+  );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/parsers/index.ts` around lines 27 - 29, The getBroker function currently
does substring matching on brokerParsers even when name is empty; update
getBroker to first reject empty or whitespace-only names (e.g., if (!name ||
name.trim().length === 0) return undefined) before computing lower and
performing brokerParsers.find((p) => p.name.toLowerCase().includes(lower)), so
that getBroker("") returns undefined rather than the first parser.

@codecov

codecov Bot commented Apr 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.91892% with 41 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.78%. Comparing base (9c52f0d) to head (e8a6803).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/cli/index.ts 0.00% 28 Missing ⚠️
src/parsers/degiro.ts 96.85% 10 Missing ⚠️
src/index.ts 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #12      +/-   ##
==========================================
+ Coverage   56.97%   65.78%   +8.81%     
==========================================
  Files          17       20       +3     
  Lines        1083     1438     +355     
  Branches      174      284     +110     
==========================================
+ Hits          617      946     +329     
- Misses        465      491      +26     
  Partials        1        1              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

GeiserX added 2 commits April 15, 2026 11:53
- Handle real 19-column Transactions CSV format (Número, Bolsa de,
  Valor local + Valor + Tipo de cambio columns)
- Add "número"/"number" to quantity header detection
- Parse FX rate from CSV, default to "1" for EUR trades
- Skip zero-price rows (rights assignments, non-tradeable conversions)
- Add real Degiro CSV fixture from mxmhos/degiro (GitHub)
- 7 new integration tests against real export data
- 106 tests passing total
Handle dual Account CSV layouts: old format with "Importe" column
vs real export with "Variación" header mapping to currency position.
Add "Retención del dividendo" withholding pattern. Validate against
real Degiro Account export fixture (4 dividends + 4 withholdings).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/parsers/degiro.ts`:
- Around line 335-379: The parser currently drops the "value date" / "Fecha
valor" column which causes parseAccountCsv() to set both booking and settlement
dates from the same "Fecha/Date" column; update the AccountColumns type to
include a separate settleDate (or valueDate) field, update
resolveAccountColumns() to locate the "Fecha valor"/"Value date" header (e.g.,
findColumn(headers, ["Fecha valor","Value date"]) and assign it to that new
field), and then modify parseAccountCsv() to use the new settleDate/valueDate
column when populating settleDate (leaving dateTime populated from the original
date column); ensure isDegiroAccount() still checks the new settleDate/valueDate
column as required.
- Around line 200-206: The parser currently extracts Degiro's broker-supplied FX
column into the fxRate variable via findColumn and assigns it directly to
trade.fxRateToBase and cashTransaction.fxRateToBase; instead stop populating
fxRateToBase from parsed FX data so downstream logic can rely on getEcbRate() to
supply the canonical rate. Remove or skip setting fxRateToBase where it's
assigned (the assignments around trade.fxRateToBase and
cashTransaction.fxRateToBase), keep the fxRate detection (findColumn) if needed
for diagnostics but do not persist its value into parsed trade or cash
transaction objects so getEcbRate() remains the authoritative source.
- Around line 89-110: The current parseNumber normalizes numeric strings but the
code later (checks that compare parsed strings to "0" or "0.0000" and subsequent
arithmetic) must use Decimal instead of string comparisons; import Decimal from
"decimal.js", convert parseNumber(...) results into new
Decimal(parseNumber(value)) before performing zero checks or math, replace
string equality checks like parsed === "0" or parsed === "0.0000" with
Decimal.equals(0) / decimal.isZero(), and ensure any arithmetic on values
previously done with string-to-number casting now uses Decimal methods so all
monetary calculations (those using parseNumber outputs and the logic around
them) follow the Decimal.js guidelines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 231af07d-1b45-4850-81da-bd9f70ec1b98

📥 Commits

Reviewing files that changed from the base of the PR and between 4f090f9 and fc66b93.

⛔ Files ignored due to path filters (2)
  • tests/fixtures/degiro-account-real.csv is excluded by !**/*.csv
  • tests/fixtures/degiro-transactions-real.csv is excluded by !**/*.csv
📒 Files selected for processing (2)
  • src/parsers/degiro.ts
  • tests/parsers/degiro.test.ts

Comment thread src/parsers/degiro.ts
Comment on lines +89 to +110
function parseNumber(val: string): string {
const trimmed = val.trim();
if (!trimmed) return "0";

// If it has both dot and comma, the last one is the decimal separator
const lastDot = trimmed.lastIndexOf(".");
const lastComma = trimmed.lastIndexOf(",");

if (lastComma > lastDot) {
// EU format: dots are thousands, comma is decimal
return trimmed.replace(/\./g, "").replace(",", ".");
}
if (lastDot > lastComma && lastComma >= 0) {
// US/UK format: commas are thousands, dot is decimal
return trimmed.replace(/,/g, "");
}
// Only one separator or none — comma might be decimal (no thousands)
if (lastComma >= 0 && lastDot < 0) {
return trimmed.replace(",", ".");
}
return trimmed;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/parsers/degiro.ts | head -150

Repository: GeiserX/DeclaRenta

Length of output: 6053


🏁 Script executed:

cat -n src/parsers/degiro.ts | sed -n '280,295p'

Repository: GeiserX/DeclaRenta

Length of output: 689


🏁 Script executed:

grep -n "import.*Decimal" src/parsers/degiro.ts

Repository: GeiserX/DeclaRenta

Length of output: 44


🏁 Script executed:

rg "import.*Decimal" src/parsers/

Repository: GeiserX/DeclaRenta

Length of output: 44


🏁 Script executed:

rg "Decimal" src/parsers/degiro.ts

Repository: GeiserX/DeclaRenta

Length of output: 44


🏁 Script executed:

cat -n src/parsers/degiro.ts | sed -n '1,30p'

Repository: GeiserX/DeclaRenta

Length of output: 1540


🏁 Script executed:

rg "decimal\.js" src/ --type ts

Repository: GeiserX/DeclaRenta

Length of output: 566


🏁 Script executed:

rg "Decimal" src/ --type ts -l

Repository: GeiserX/DeclaRenta

Length of output: 255


🏁 Script executed:

grep -n "parseFloat\|parseInt" src/parsers/*.ts | head -20

Repository: GeiserX/DeclaRenta

Length of output: 194


🏁 Script executed:

cat -n src/parsers/degiro.ts | sed -n '280,310p'

Repository: GeiserX/DeclaRenta

Length of output: 1350


Replace string comparisons with Decimal for zero-value filtering.

parseNumber() normalizes "0,00" and "0.00" to "0.00", but line 281 checks only for exact "0" and line 284 checks for "0" or "0.0000". This allows normalized zero values to leak through. Additionally, lines 287–289 violate the guideline requiring all string-to-number conversions use Decimal.js for monetary calculations.

Import Decimal and replace the zero/sign checks:

Diff
+import Decimal from "decimal.js";
import type { BrokerParser, Statement } from "../types/broker.js";

...
-    if (!isin || quantity === "0") continue;
+    const quantityDec = new Decimal(quantity || "0");
+    const priceDec = new Decimal(price || "0");
+    const valueDec = new Decimal(value || "0");
+
+    if (!isin || quantityDec.isZero()) continue;

     // Skip zero-price rows (rights assignments, non-tradeable conversions)
-    if (price === "0" || price === "0.0000") continue;
+    if (priceDec.isZero()) continue;

     // Determine buy/sell: negative local value = you paid = buy; or negative quantity = sell
-    const valueNum = parseFloat(value);
-    const qtyNum = parseFloat(quantity);
-    const isSell = valueNum > 0 || qtyNum < 0;
+    const isSell = valueDec.gt(0) || quantityDec.lt(0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function parseNumber(val: string): string {
const trimmed = val.trim();
if (!trimmed) return "0";
// If it has both dot and comma, the last one is the decimal separator
const lastDot = trimmed.lastIndexOf(".");
const lastComma = trimmed.lastIndexOf(",");
if (lastComma > lastDot) {
// EU format: dots are thousands, comma is decimal
return trimmed.replace(/\./g, "").replace(",", ".");
}
if (lastDot > lastComma && lastComma >= 0) {
// US/UK format: commas are thousands, dot is decimal
return trimmed.replace(/,/g, "");
}
// Only one separator or none — comma might be decimal (no thousands)
if (lastComma >= 0 && lastDot < 0) {
return trimmed.replace(",", ".");
}
return trimmed;
}
import Decimal from "decimal.js";
import type { BrokerParser, Statement } from "../types/broker.js";
// ... existing code ...
const quantityDec = new Decimal(quantity || "0");
const priceDec = new Decimal(price || "0");
const valueDec = new Decimal(value || "0");
if (!isin || quantityDec.isZero()) continue;
// Skip zero-price rows (rights assignments, non-tradeable conversions)
if (priceDec.isZero()) continue;
// Determine buy/sell: negative local value = you paid = buy; or negative quantity = sell
const isSell = valueDec.gt(0) || quantityDec.lt(0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/parsers/degiro.ts` around lines 89 - 110, The current parseNumber
normalizes numeric strings but the code later (checks that compare parsed
strings to "0" or "0.0000" and subsequent arithmetic) must use Decimal instead
of string comparisons; import Decimal from "decimal.js", convert
parseNumber(...) results into new Decimal(parseNumber(value)) before performing
zero checks or math, replace string equality checks like parsed === "0" or
parsed === "0.0000" with Decimal.equals(0) / decimal.isZero(), and ensure any
arithmetic on values previously done with string-to-number casting now uses
Decimal methods so all monetary calculations (those using parseNumber outputs
and the logic around them) follow the Decimal.js guidelines.

Comment thread src/parsers/degiro.ts
Comment on lines +200 to +206
// FX rate column (only in 19-col format)
const fxRate = findColumn(headers, [
"Tipo de cambio",
"Exchange rate",
"Wechselkurs",
"Wisselkoers",
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the file structure and check the specific lines mentioned
wc -l src/parsers/degiro.ts

Repository: GeiserX/DeclaRenta

Length of output: 87


🏁 Script executed:

# Examine the code around the specified line ranges
sed -n '195,210p' src/parsers/degiro.ts

Repository: GeiserX/DeclaRenta

Length of output: 493


🏁 Script executed:

# Check lines 277-280
sed -n '275,285p' src/parsers/degiro.ts

Repository: GeiserX/DeclaRenta

Length of output: 507


🏁 Script executed:

# Check lines 351-352
sed -n '348,355p' src/parsers/degiro.ts

Repository: GeiserX/DeclaRenta

Length of output: 511


🏁 Script executed:

# Check lines 409-435
sed -n '405,440p' src/parsers/degiro.ts

Repository: GeiserX/DeclaRenta

Length of output: 1123


🏁 Script executed:

# Search for fxRateToBase in degiro.ts to see where FX rates are assigned
rg "fxRateToBase" src/parsers/degiro.ts -A 2 -B 2

Repository: GeiserX/DeclaRenta

Length of output: 357


🏁 Script executed:

# Check if getEcbRate function is available and used elsewhere
rg "getEcbRate" src/ -l

Repository: GeiserX/DeclaRenta

Length of output: 188


🏁 Script executed:

# Look for the function signature of getEcbRate
rg "getEcbRate" src/ -B 2 -A 5

Repository: GeiserX/DeclaRenta

Length of output: 6257


Remove Degiro's FX rates from the normalized statement.

Both trade and cash transaction objects persist broker-supplied rates into fxRateToBase, embedding execution rates into downstream tax calculations. The codebase already uses getEcbRate() to override these; don't bake the wrong rates in at parse time.

Affected code at lines 309-311 (trade) and 418 (cash transaction) where fxRateToBase is set from parsed FX data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/parsers/degiro.ts` around lines 200 - 206, The parser currently extracts
Degiro's broker-supplied FX column into the fxRate variable via findColumn and
assigns it directly to trade.fxRateToBase and cashTransaction.fxRateToBase;
instead stop populating fxRateToBase from parsed FX data so downstream logic can
rely on getEcbRate() to supply the canonical rate. Remove or skip setting
fxRateToBase where it's assigned (the assignments around trade.fxRateToBase and
cashTransaction.fxRateToBase), keep the fxRate detection (findColumn) if needed
for diagnostics but do not persist its value into parsed trade or cash
transaction objects so getEcbRate() remains the authoritative source.

Comment thread src/parsers/degiro.ts
Comment on lines +335 to +379
interface AccountColumns {
date: number;
product: number;
isin: number;
description: number;
fx: number;
currency: number;
amount: number;
orderId: number;
}

function resolveAccountColumns(headers: string[]): AccountColumns {
const date = findColumn(headers, ["Fecha", "Date", "Datum"]);
const product = findColumn(headers, ["Producto", "Product", "Produkt"]);
const isin = findColumn(headers, ["ISIN"]);
const description = findColumn(headers, DESCRIPTION_HEADERS);
const fx = findColumn(headers, ["Tipo de cambio", "Tipo", "FX", "Wisselkoers", "Wechselkurs"]);

// Degiro Account CSV has two known layouts:
// Old: ..., Tipo de cambio, [empty], Importe, [empty], Saldo, ...
// → "Importe" = amount col, currency at fx+1
// New (real export): ..., Tipo, Variación, [empty], Saldo, ...
// → "Variación" col holds CURRENCY in data, amount is at Variación+1

let currency: number;
let amount = findColumn(headers, ["Importe", "Amount", "Change", "Mutatie", "Änderung"]);

if (amount >= 0) {
// Old format: amount found directly, currency is before it
currency = findColumn(headers, ["Divisa tipo de cambio", "Currency", "Währung", "Valuta"]);
if (currency < 0 && fx >= 0) currency = fx + 1;
} else {
// New format: "Variación" header = currency position, amount = next column
const varCol = findColumn(headers, ["Variación", "Variation"]);
if (varCol >= 0) {
currency = varCol;
amount = varCol + 1;
} else {
currency = fx >= 0 ? fx + 1 : -1;
}
}

const orderId = findColumn(headers, ["ID Orden", "Order ID", "Auftrags-ID"]);

return { date, product, isin, description, fx, currency, amount, orderId };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep the account value date instead of dropping it.

isDegiroAccount() requires Fecha valor / Value date, but AccountColumns never stores that column and parseAccountCsv() sets both dateTime and settleDate from Fecha/Date. If booking date and value date differ, dividends and withholdings will be normalized onto the wrong settlement day.

Minimal fix
 interface AccountColumns {
   date: number;
+  valueDate: number;
   product: number;
   isin: number;
   description: number;
   fx: number;
   currency: number;
   amount: number;
   orderId: number;
 }

 function resolveAccountColumns(headers: string[]): AccountColumns {
   const date = findColumn(headers, ["Fecha", "Date", "Datum"]);
+  const valueDate = findColumn(headers, VALUE_DATE_HEADERS);
   const product = findColumn(headers, ["Producto", "Product", "Produkt"]);
   const isin = findColumn(headers, ["ISIN"]);
   const description = findColumn(headers, DESCRIPTION_HEADERS);
   const fx = findColumn(headers, ["Tipo de cambio", "Tipo", "FX", "Wisselkoers", "Wechselkurs"]);
   ...
-  return { date, product, isin, description, fx, currency, amount, orderId };
+  return { date, valueDate, product, isin, description, fx, currency, amount, orderId };
 }
 ...
-    const tradeDate = convertDate(dateStr);
+    const bookingDate = convertDate(dateStr);
+    const settleDate =
+      cols.valueDate >= 0 ? convertDate(fields[cols.valueDate] ?? "") : bookingDate;
 ...
-      dateTime: tradeDate,
-      settleDate: tradeDate,
+      dateTime: bookingDate,
+      settleDate,

Also applies to: 402-435

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/parsers/degiro.ts` around lines 335 - 379, The parser currently drops the
"value date" / "Fecha valor" column which causes parseAccountCsv() to set both
booking and settlement dates from the same "Fecha/Date" column; update the
AccountColumns type to include a separate settleDate (or valueDate) field,
update resolveAccountColumns() to locate the "Fecha valor"/"Value date" header
(e.g., findColumn(headers, ["Fecha valor","Value date"]) and assign it to that
new field), and then modify parseAccountCsv() to use the new
settleDate/valueDate column when populating settleDate (leaving dateTime
populated from the original date column); ensure isDegiroAccount() still checks
the new settleDate/valueDate column as required.

@GeiserX
GeiserX merged commit 5f3a2ca into main Apr 15, 2026
6 checks passed
@GeiserX
GeiserX deleted the feat/multi-broker-interface branch April 15, 2026 10:42
GeiserX added a commit that referenced this pull request Apr 15, 2026
* ci: single-workflow release pipeline (no PAT needed)

- Merge auto-tag + release into one workflow (like CashPilot/LynxPrompt)
- Tag, sync package.json, generate changelog, create release — all in one job
- Uses only GITHUB_TOKEN (no PAT secret required)
- CLI reads version from package.json (single source of truth)
- Update ROADMAP to reflect v0.2.0 state

* feat: multi-broker architecture with Degiro CSV parser

- Add BrokerParser interface (detect + parse → Statement)
- Add parser registry with auto-detection (detectBroker, getBroker)
- Wrap IBKR parser with BrokerParser interface
- Implement Degiro parser: Transactions CSV (trades) + Account CSV
  (dividends/withholdings), multi-language (ES/EN/NL/DE), auto-detect
  delimiter (comma/semicolon), EU number format support
- Update CLI: add --broker flag, auto-detect broker from file content
- Update ROADMAP with broker tax integration research findings:
  Degiro/Scalable (high priority, no AEAT integration) vs
  Trade Republic/XTB (low priority, already report to AEAT)
- 99 tests passing (24 new: 18 Degiro parser, 6 registry)

* fix: validate Degiro parser against real CSV export

- Handle real 19-column Transactions CSV format (Número, Bolsa de,
  Valor local + Valor + Tipo de cambio columns)
- Add "número"/"number" to quantity header detection
- Parse FX rate from CSV, default to "1" for EUR trades
- Skip zero-price rows (rights assignments, non-tradeable conversions)
- Add real Degiro CSV fixture from mxmhos/degiro (GitHub)
- 7 new integration tests against real export data
- 106 tests passing total

* fix: Degiro Account CSV parser for real export format

Handle dual Account CSV layouts: old format with "Importe" column
vs real export with "Variación" header mapping to currency position.
Add "Retención del dividendo" withholding pattern. Validate against
real Degiro Account export fixture (4 dividends + 4 withholdings).

* docs: update Degiro parser docstring to reflect Account CSV support
GeiserX added a commit that referenced this pull request Apr 28, 2026
* ci: single-workflow release pipeline (no PAT needed)

- Merge auto-tag + release into one workflow (like CashPilot/LynxPrompt)
- Tag, sync package.json, generate changelog, create release — all in one job
- Uses only GITHUB_TOKEN (no PAT secret required)
- CLI reads version from package.json (single source of truth)
- Update ROADMAP to reflect v0.2.0 state

* feat: multi-broker architecture with Degiro CSV parser

- Add BrokerParser interface (detect + parse → Statement)
- Add parser registry with auto-detection (detectBroker, getBroker)
- Wrap IBKR parser with BrokerParser interface
- Implement Degiro parser: Transactions CSV (trades) + Account CSV
  (dividends/withholdings), multi-language (ES/EN/NL/DE), auto-detect
  delimiter (comma/semicolon), EU number format support
- Update CLI: add --broker flag, auto-detect broker from file content
- Update ROADMAP with broker tax integration research findings:
  Degiro/Scalable (high priority, no AEAT integration) vs
  Trade Republic/XTB (low priority, already report to AEAT)
- 99 tests passing (24 new: 18 Degiro parser, 6 registry)

* fix: validate Degiro parser against real CSV export

- Handle real 19-column Transactions CSV format (Número, Bolsa de,
  Valor local + Valor + Tipo de cambio columns)
- Add "número"/"number" to quantity header detection
- Parse FX rate from CSV, default to "1" for EUR trades
- Skip zero-price rows (rights assignments, non-tradeable conversions)
- Add real Degiro CSV fixture from mxmhos/degiro (GitHub)
- 7 new integration tests against real export data
- 106 tests passing total

* fix: Degiro Account CSV parser for real export format

Handle dual Account CSV layouts: old format with "Importe" column
vs real export with "Variación" header mapping to currency position.
Add "Retención del dividendo" withholding pattern. Validate against
real Degiro Account export fixture (4 dividends + 4 withholdings).

* docs: update Degiro parser docstring to reflect Account CSV support
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant