Skip to content

feat(revolut): support transaction-log format#154

Merged
GeiserX merged 3 commits intomainfrom
feat/revolut-transaction-log
May 3, 2026
Merged

feat(revolut): support transaction-log format#154
GeiserX merged 3 commits intomainfrom
feat/revolut-transaction-log

Conversation

@GeiserX
Copy link
Copy Markdown
Owner

@GeiserX GeiserX commented May 3, 2026

Summary

  • Adds support for Revolut's transaction-log XLSX format — the format users actually get from the Revolut app (Date | Ticker | Type | Quantity | Price per share | Total Amount | Currency | FX Rate)
  • Fixes bug report: auto-detection and manual selection both failed for this format, leaving users unable to process their Revolut exports
  • Both Revolut formats now work: closed-positions summary (existing) and transaction log (new)

What's new in the parser

  • Transaction-log detection in detectRevolutXlsx() (both formats detected)
  • Individual BUY/SELL trade parsing (BUY-MARKET, BUY-LIMIT, SELL-MARKET, SELL-LIMIT)
  • "CCY amount" string parser for Revolut's "EUR 15.61" / "USD -217.14" format
  • ISO 8601 timestamp support (millisecond and microsecond precision)
  • Cash transaction extraction (CASH TOP-UP → deposit, CASH WITHDRAWAL → withdrawal)
  • Open position inference from unmatched buys (enables Modelo 720/D-6)
  • Text-based detect() updated for transaction-log headers (no false positives with Trading 212)

Tests

  • 35 new tests covering transaction-log format (78 total for Revolut parser)
  • Anonymized XLSX fixture based on real user file
  • All 1000 tests pass, typecheck clean, lint clean

Test plan

  • Upload a Revolut transaction-log XLSX — should auto-detect as "Revolut"
  • Upload a Revolut closed-positions XLSX — should still work as before
  • Verify trades, cash transactions, and open positions are correctly extracted
  • Verify Modelo 720 shows open positions from transaction-log format
  • Run full test suite: npm test

Summary by CodeRabbit

  • New Features

    • Added support for Revolut transaction-log XLSX file format alongside the existing closed-positions summary format.
    • Enhanced wash-sale detection to handle trades missing ISIN data by falling back to ticker symbol matching.
  • Tests

    • Expanded test coverage for transaction-log parsing including various trade types and cash flow scenarios.
    • Added wash-sale detection tests for ISIN-missing and cryptocurrency scenarios.

…ades)

Revolut's Trading Account Statement has two XLSX formats: closed-positions
summary (existing) and transaction log (Date/Ticker/Type/Quantity/Price per
share/Total Amount/Currency/FX Rate). The transaction log is what users
actually get from the Revolut app, and the parser was rejecting it entirely.

- Add transaction-log detection in detectRevolutXlsx (both formats now detected)
- Parse BUY-MARKET, BUY-LIMIT, SELL-MARKET, SELL-LIMIT as individual trades
- Parse "CCY amount" strings (e.g. "EUR 15.61", "USD -217.14")
- Parse ISO 8601 timestamps with ms/μs precision
- Extract CASH TOP-UP / CASH WITHDRAWAL as cashTransactions
- Infer open positions from unmatched buys (enables Modelo 720/D-6)
- Update text-based detect() for transaction-log headers
- 35 new tests (78 total), anonymized fixture from real user file
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

Warning

Rate limit exceeded

@GeiserX has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 53 minutes and 17 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a36efc78-a4f1-4f1c-a1be-f273e51b3b1f

📥 Commits

Reviewing files that changed from the base of the PR and between 1e147d1 and 61a3c13.

📒 Files selected for processing (1)
  • src/parsers/revolut.ts
📝 Walkthrough

Walkthrough

Added Revolut transaction-log XLSX format support to the parser with new format detection and parsing logic, integrated it into the CLI broker-file flow, and implemented wash-sale detection for trades without ISIN by falling back to symbol-based matching. Includes comprehensive test coverage.

Changes

Revolut Transaction-Log XLSX Support

Layer / File(s) Summary
Format Detection & Utilities
src/parsers/revolut.ts (lines 47–110, 252–295, 478–486)
Transaction-log headers, row-type classifiers, and parseRevolutDate extended for ISO-8601 timestamps; TxnLogColumns and findTxnLogColumns helpers locate columns.
Transaction-Log Parsing
src/parsers/revolut.ts (lines 296–462)
parseTransactionLog converts BUY/SELL rows into trades, cash-flow rows into cashTransactions, and infers openPositions from unmatched buys with quantity/cost tracking.
Workbook Format Routing
src/parsers/revolut.ts (lines 491–549)
parseRevolutXlsx detects workbook format (transaction-log vs. closed-positions) and routes to appropriate parser; detectRevolutXlsx accepts either format.
Text-Based Detection
src/parsers/revolut.ts (lines 565–573)
revolutParser.detect expanded to recognize transaction-log text headers while avoiding false positives (e.g., Trading 212).
CLI Integration
src/cli/index.ts (lines 16–20, 75–83)
Imported parseRevolutXlsx and detectRevolutXlsx; added early detection branch in parseAndMerge to parse Revolut XLSX, merge statement, and record broker name before other parsers.
Parser Tests
tests/parsers/revolut.test.ts (lines 1–2, 397–840)
Detection tests for transaction-log format and false positives; extended parseRevolutDate for ISO-8601; comprehensive parsing assertions for trades, cash transactions, open positions, crypto symbols, FX defaults, and format coexistence with closed-positions.

Wash-Sale Symbol Fallback for Empty ISIN

Layer / File(s) Summary
Core Logic
src/engine/wash-sale.ts (lines 91–96)
homogeneousKey now derives a key from assetCategory + uppercase symbol when isin is empty (after existing CRYPTO/ISIN handling), enabling wash-sale matching on symbol alone.
Logic Tests
tests/engine/wash-sale.test.ts (lines 97–144)
Added coverage for loss blocking when disposal/repurchase have matching empty ISIN via symbol; non-matching symbols do not block; crypto assets use 1-year window.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

  • PR #60: Records per-file broker names in CLI merge and surfaces them in the web review UI; shares the brokerNames propagation path.
  • PR #111: Implements Revolut XLSX parser/detection across parsing, UI, docs, and tests with overlapping changes to src/parsers/revolut.ts and parser registry.
  • PR #6: Modifies wash-sale detection logic in src/engine/wash-sale.ts for date handling improvements.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding support for Revolut's transaction-log XLSX format, which is the primary focus across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/revolut-transaction-log

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 53 minutes and 17 seconds.

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

@codecov
Copy link
Copy Markdown

codecov Bot commented May 3, 2026

Codecov Report

❌ Patch coverage is 97.29730% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.62%. Comparing base (d805714) to head (61a3c13).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/parsers/revolut.ts 97.28% 6 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #154      +/-   ##
==========================================
+ Coverage   97.61%   97.62%   +0.01%     
==========================================
  Files          37       37              
  Lines        7380     7585     +205     
  Branches     1499     1551      +52     
==========================================
+ Hits         7204     7405     +201     
- Misses        175      179       +4     
  Partials        1        1              
Files with missing lines Coverage Δ
src/engine/wash-sale.ts 94.00% <100.00%> (+0.12%) ⬆️
src/parsers/revolut.ts 98.48% <97.28%> (-0.48%) ⬇️
🚀 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.

- Add Math.abs + isFinite guard on transaction-log quantity to prevent
  negative and Infinity propagation
- Cap sell quantity at net position to prevent negative position state
- Tighten hasTxnLogHeaders to require "price per share" column
- Add symbol-based fallback in wash-sale homogeneousKey() for trades
  without ISINs (Revolut, Lightyear, etc.)
- Add Revolut XLSX detection in CLI before eToro
- Add 9 edge case tests (BUY-LIMIT, negative qty, sell overflow,
  Infinity, corrupted XLSX, empty sheet, wash-sale symbol fallback)
@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented May 3, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@GeiserX
Copy link
Copy Markdown
Owner Author

GeiserX commented May 3, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/parsers/revolut.ts (1)

310-314: ⚡ Quick win

Position tracking uses Number arithmetic instead of Decimal.

The netQty, totalCost, and costPerUnit calculations use JavaScript Number, which can introduce floating-point precision errors on large quantities or costs. While the output strings go to the FIFO engine (which uses Decimal), the open position inference here could accumulate rounding drift.

Per coding guidelines: "Always use Decimal from decimal.js for any monetary calculation."

This is lower risk since open positions are informational (Modelo 720/D-6) rather than tax-critical P&L, but worth noting.

♻️ Suggested fix using Decimal for position tracking
+import Decimal from "decimal.js";

 // Track net positions per symbol for open-position inference
 const positions = new Map<string, {
   symbol: string; currency: string; assetCategory: AssetCategory;
-  netQty: number; totalCost: number;
+  netQty: Decimal; totalCost: Decimal;
 }>();

 // In BUY block:
 const pos = positions.get(ticker) ?? {
-  symbol: ticker, currency: currencyRaw, assetCategory, netQty: 0, totalCost: 0,
+  symbol: ticker, currency: currencyRaw, assetCategory, netQty: new Decimal(0), totalCost: new Decimal(0),
 };
-pos.netQty += qty;
-pos.totalCost += absTotalAmount;
+pos.netQty = pos.netQty.plus(qty);
+pos.totalCost = pos.totalCost.plus(absTotalAmount);

 // In SELL block:
-if (pos && pos.netQty > 0) {
-  const sellQty = Math.min(qty, pos.netQty);
-  const costPerUnit = pos.totalCost / pos.netQty;
-  pos.totalCost -= costPerUnit * sellQty;
-  pos.netQty -= sellQty;
+if (pos && pos.netQty.greaterThan(0)) {
+  const sellQty = Decimal.min(qty, pos.netQty);
+  const costPerUnit = pos.totalCost.div(pos.netQty);
+  pos.totalCost = pos.totalCost.minus(costPerUnit.times(sellQty));
+  pos.netQty = pos.netQty.minus(sellQty);
 }

Also applies to: 428-434, 440-458

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

In `@src/parsers/revolut.ts` around lines 310 - 314, The positions Map currently
stores netQty, totalCost and computes costPerUnit using JavaScript Number which
can cause floating-point drift; change the positions value shape to hold Decimal
instances (import Decimal from 'decimal.js'), initialize netQty and totalCost as
new Decimal(0), and replace all arithmetic updates to use Decimal methods (plus,
minus, times, div) when adjusting positions and computing costPerUnit; ensure
any outputs passed to the FIFO engine are converted to string (or toNumber only
where safe), and apply the same Decimal changes to the other occurrences noted
(the update blocks around the regions referenced as 428-434 and 440-458) while
keeping symbol/assetCategory fields as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/parsers/revolut.ts`:
- Around line 310-314: The positions Map currently stores netQty, totalCost and
computes costPerUnit using JavaScript Number which can cause floating-point
drift; change the positions value shape to hold Decimal instances (import
Decimal from 'decimal.js'), initialize netQty and totalCost as new Decimal(0),
and replace all arithmetic updates to use Decimal methods (plus, minus, times,
div) when adjusting positions and computing costPerUnit; ensure any outputs
passed to the FIFO engine are converted to string (or toNumber only where safe),
and apply the same Decimal changes to the other occurrences noted (the update
blocks around the regions referenced as 428-434 and 440-458) while keeping
symbol/assetCategory fields as-is.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: daf575d4-75e0-4d77-84be-a21d4f1de380

📥 Commits

Reviewing files that changed from the base of the PR and between 1ac4b53 and 1e147d1.

⛔ Files ignored due to path filters (1)
  • tests/fixtures/revolut-txnlog-sample.xlsx is excluded by !**/*.xlsx
📒 Files selected for processing (5)
  • src/cli/index.ts
  • src/engine/wash-sale.ts
  • src/parsers/revolut.ts
  • tests/engine/wash-sale.test.ts
  • tests/parsers/revolut.test.ts

Address CodeRabbit nitpick: replace Number with Decimal.js for
netQty, totalCost, and costPerUnit in open-position inference to
prevent floating-point drift per project financial precision rules.
@GeiserX GeiserX merged commit 36ddc9f into main May 3, 2026
5 checks passed
@GeiserX GeiserX deleted the feat/revolut-transaction-log branch May 3, 2026 10:42
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