Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
05507d6
Add Ledger core model
Morpheus1w3 Jun 26, 2026
809492d
Add Ledger definitions and validation
Morpheus1w3 Jun 26, 2026
3a04f1a
Add Ledger projection materialization
Morpheus1w3 Jun 26, 2026
3f58e32
Add Ledger XML and Protobuf persistence
Morpheus1w3 Jun 26, 2026
5894d3b
Add Ledger compatibility creators and editors
Morpheus1w3 Jun 26, 2026
cb05774
Wire Ledger into client and legacy transaction access
Morpheus1w3 Jun 26, 2026
4ea2a84
Add Ledger conversion and mutation support
Morpheus1w3 Jun 26, 2026
a836403
Wire Ledger into transaction dialogs and viewers
Morpheus1w3 Jun 26, 2026
8eab2b2
Add Ledger importer investment-plan and stock-split guardrails
Morpheus1w3 Jun 26, 2026
93f8839
Add Ledger reporting snapshot bridge
Morpheus1w3 Jun 26, 2026
47ce2a6
Add Ledger diagnostic codes and NLS entries
Morpheus1w3 Jun 26, 2026
844497b
Move Ledger spin-off XML fixture to tests
Morpheus1w3 Jun 26, 2026
d6058ca
Add Ledger semantic posting vocabulary
Morpheus1w3 Jul 1, 2026
f3b6751
Add derived Ledger projection descriptors
Morpheus1w3 Jul 1, 2026
da0dd6c
Emit semantic postings from Ledger creators
Morpheus1w3 Jul 1, 2026
3b9b4a5
Switch Ledger materialization to derived descriptors
Morpheus1w3 Jul 1, 2026
bd9d2be
Link InvestmentPlan executions by plan key
Morpheus1w3 Jul 1, 2026
e0f80dc
Persist Ledger XML without UUID truth
Morpheus1w3 Jul 1, 2026
a3d38ee
Persist Ledger protobuf without UUID truth
Morpheus1w3 Jul 1, 2026
720a837
Remove Ledger projection membership truth
Morpheus1w3 Jul 2, 2026
87ff076
Validate Ledger semantic posting shapes
Morpheus1w3 Jul 2, 2026
c2b79ba
Log Ledger migration diagnostics
Morpheus1w3 Jul 2, 2026
f2afaf4
Clean Ledger semantic facade APIs
Morpheus1w3 Jul 2, 2026
e6eee14
Finalize Ledger no-UUID release gate
Morpheus1w3 Jul 2, 2026
7e9dbd9
Tighten Ledger no-UUID release checks
Morpheus1w3 Jul 2, 2026
acdfe12
Normalize Ledger message bundles
Morpheus1w3 Jul 2, 2026
e6d7871
Restore ClientFactory direct save path
Morpheus1w3 Jul 2, 2026
19e5e92
Wrap Ledger XML persistence support
Morpheus1w3 Jul 2, 2026
e57f1c4
Wrap Ledger protobuf persistence support
Morpheus1w3 Jul 2, 2026
78f5d91
Wrap Ledger import insertion support
Morpheus1w3 Jul 2, 2026
36a7eda
Wrap Ledger dialog commit support
Morpheus1w3 Jul 2, 2026
c2bbbd4
Wrap Ledger transaction UI routing
Morpheus1w3 Jul 2, 2026
7604777
Wrap Ledger inline editing policy
Morpheus1w3 Jul 2, 2026
99e9365
Wrap Ledger conversion action support
Morpheus1w3 Jul 2, 2026
a28949d
Store Ledger entry type as protobuf code
Morpheus1w3 Jul 2, 2026
cd42e25
Decouple Ledger entry type code from enum name
Morpheus1w3 Jul 2, 2026
5f78a4d
Route ex-date backfill through Ledger editor
Morpheus1w3 Jul 2, 2026
6cfbba4
Write protobuf shadows for native Ledger projections
Morpheus1w3 Jul 2, 2026
644fc8c
Clean up Ledger Java hygiene
Morpheus1w3 Jul 3, 2026
f9f3dec
Prove Ledger multi-movement CA persistence
Morpheus1w3 Jul 3, 2026
f37d126
Add semantic keys for repeated Ledger descriptors
Morpheus1w3 Jul 3, 2026
d46738b
Validate repeated Ledger primary legs by semantic keys
Morpheus1w3 Jul 3, 2026
22a2984
Allow repeated Ledger spin-off movement legs
Morpheus1w3 Jul 3, 2026
1f61064
Add Ledger native repeated movement assembler API
Morpheus1w3 Jul 3, 2026
abebcbf
Materialize repeated Ledger spin-off projections
Morpheus1w3 Jul 3, 2026
faa10db
Clean up Ledger corporate action configuration
Morpheus1w3 Jul 3, 2026
f4b5819
Generalize Ledger corporate action entry type
Morpheus1w3 Jul 3, 2026
afd3c56
Register supported Ledger corporate action kinds
Morpheus1w3 Jul 3, 2026
8be56b1
Require portfolio for Ledger native security postings
Morpheus1w3 Jul 5, 2026
5cac0e2
Prepare Ledger corporate action security context
Morpheus1w3 Jul 5, 2026
2414ee8
Register Ledger corporate action security context rules
Morpheus1w3 Jul 5, 2026
7af5c56
Validate Ledger corporate action registry configuration
Morpheus1w3 Jul 5, 2026
5dd6e45
Add Ledger corporate action primary movement validation
Morpheus1w3 Jul 5, 2026
c3efc98
Register Ledger cash distribution corporate action
Morpheus1w3 Jul 5, 2026
ab8867a
Validate Ledger corporate action interest components
Morpheus1w3 Jul 5, 2026
57a389d
Add Ledger corporate action basis treatment
Morpheus1w3 Jul 5, 2026
f7a64fc
Add Ledger corporate action fluent API
Morpheus1w3 Jul 5, 2026
70f4ce6
Project Ledger cash corporate actions
Morpheus1w3 Jul 5, 2026
8872030
Project Ledger security-in corporate actions
Morpheus1w3 Jul 5, 2026
995ae8f
Project Ledger redemption corporate actions
Morpheus1w3 Jul 5, 2026
5b04ae1
Project Ledger conversion exchange corporate actions
Morpheus1w3 Jul 5, 2026
4de61e2
Project Ledger open-movement corporate actions
Morpheus1w3 Jul 5, 2026
f0d35e4
Add Ledger corporate action fluent end-to-end tests
Morpheus1w3 Jul 5, 2026
0da68f9
Add Ledger corporate action leg query handles
Morpheus1w3 Jul 5, 2026
bc42b94
Prune redundant Ledger corporate action tests
Morpheus1w3 Jul 5, 2026
37648ec
Further prune redundant Ledger corporate action tests
Morpheus1w3 Jul 5, 2026
e983e11
Reduce redundant Ledger test coverage
Morpheus1w3 Jul 5, 2026
399c548
Consolidate Ledger test suite
Morpheus1w3 Jul 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package name.abuchen.portfolio;

import static org.junit.Assert.assertFalse;

import java.util.Collection;
import java.util.Locale;
import java.util.ResourceBundle;
Expand Down Expand Up @@ -39,6 +41,16 @@ public void testMessages()
test("name.abuchen.portfolio.messages"); //$NON-NLS-1$
}

@Test
public void testLedgerMessagesDoNotContainDiagnosticCodes()
{
ResourceBundle resources = ResourceBundle.getBundle("name.abuchen.portfolio.messages", //$NON-NLS-1$
Locale.forLanguageTag(language));

resources.keySet().stream().filter(key -> key.startsWith("Ledger")) //$NON-NLS-1$
.forEach(key -> assertFalse(key, resources.getString(key).contains("LEDGER-"))); //$NON-NLS-1$
}

@Test
public void testHolidayNameLabels()
{
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package name.abuchen.portfolio.checks.impl;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.LocalDateTime;

import org.junit.Test;

import name.abuchen.portfolio.model.Account;
import name.abuchen.portfolio.model.AccountTransaction;
import name.abuchen.portfolio.model.Client;
import name.abuchen.portfolio.model.ClientFactory;
import name.abuchen.portfolio.model.ledger.LedgerEntry;
import name.abuchen.portfolio.model.ledger.LedgerParameter;
import name.abuchen.portfolio.model.ledger.LedgerPosting;
import name.abuchen.portfolio.model.ledger.LedgerProjectionRef;
import name.abuchen.portfolio.model.ledger.LedgerProjectionRole;
import name.abuchen.portfolio.model.ledger.LedgerStructuralValidator;
import name.abuchen.portfolio.model.ledger.LedgerStructuralValidator.IssueCode;
import name.abuchen.portfolio.model.ledger.configuration.LedgerEntryType;
import name.abuchen.portfolio.model.ledger.configuration.LedgerParameterType;
import name.abuchen.portfolio.model.ledger.configuration.LedgerPostingType;
import name.abuchen.portfolio.model.ledger.projection.LedgerBackedTransaction;
import name.abuchen.portfolio.model.ledger.projection.LedgerProjectionService;
import name.abuchen.portfolio.money.CurrencyUnit;
import name.abuchen.portfolio.money.Values;

/**
* Tests repair behavior for account transactions that carry an ex-date without a security.
* These tests make sure legacy rows clear the field and ledger-backed rows remove only the ex-date fact.
*/
@SuppressWarnings("nls")
public class OnlyAccountTransactionsWithSecurityCanHaveExDateCheckTest
{
private static final LocalDateTime DATE_TIME = LocalDateTime.of(2026, 1, 2, 0, 0);
private static final LocalDateTime EX_DATE = LocalDateTime.of(2025, 12, 31, 0, 0);

/**
* Verifies that the legacy ex-date repair keeps its existing behavior.
* An account transaction without security must simply lose the invalid ex-date.
*/
@Test
public void testLegacyAccountTransactionExDateWithoutSecurityIsCleared()
{
var client = new Client();
var account = new Account();
var transaction = new AccountTransaction(DATE_TIME, CurrencyUnit.EUR, Values.Amount.factorize(10), null,
AccountTransaction.Type.DIVIDENDS);

client.addAccount(account);
transaction.setExDate(EX_DATE);
account.addTransaction(transaction);

new OnlyAccountTransactionsWithSecurityCanHaveExDateCheck().execute(client);

assertNull(transaction.getExDate());
}

/**
* Verifies that an invalid ex-date on a ledger-backed booking is removed as a single fact.
* The booking itself stays alive; no security or dividend structure is inferred.
*/
@Test
public void testLedgerBackedExDateWithoutSecurityClearsOnlyExDateFact()
{
var client = new Client();
var account = new Account();
var entry = new LedgerEntry("entry");
var posting = new LedgerPosting("posting");
var projection = new LedgerProjectionRef("projection");

client.addAccount(account);

entry.setType(LedgerEntryType.DIVIDENDS);
entry.setDateTime(DATE_TIME);

posting.setType(LedgerPostingType.CASH);
posting.setAccount(account);
posting.setAmount(Values.Amount.factorize(10));
posting.setCurrency(CurrencyUnit.EUR);
posting.addParameter(LedgerParameter.ofLocalDateTime(LedgerParameterType.EX_DATE, EX_DATE));
entry.addPosting(posting);

projection.setRole(LedgerProjectionRole.ACCOUNT);
projection.setAccount(account);
entry.addProjectionRef(projection);

client.getLedger().addEntry(entry);
LedgerProjectionService.materialize(client);

var transaction = account.getTransactions().get(0);

assertTrue(transaction instanceof LedgerBackedTransaction);
assertThat(transaction.getExDate(), is(EX_DATE));

var entryUUID = entry.getUUID();
var projectionUUID = projection.getUUID();
var projectionRole = projection.getRole();

new OnlyAccountTransactionsWithSecurityCanHaveExDateCheck().execute(client);

assertThat(client.getLedger().getEntries().size(), is(1));
assertThat(client.getLedger().getEntries().get(0).getUUID(), is(entryUUID));
assertThat(entry.getProjectionRefs().get(0).getUUID(), is(projectionUUID));
assertThat(entry.getProjectionRefs().get(0).getRole(), is(projectionRole));
assertFalse(posting.getParameters().stream()
.anyMatch(parameter -> parameter.getType() == LedgerParameterType.EX_DATE));
assertThat(account.getTransactions().size(), is(1));

var refreshed = account.getTransactions().get(0);

assertTrue(refreshed instanceof LedgerBackedTransaction);
assertThat(((LedgerBackedTransaction) refreshed).getLedgerProjectionRef().getUUID(), is(projectionUUID));
assertNull(refreshed.getExDate());

var validation = LedgerStructuralValidator.validate(client.getLedger());

assertFalse(validation.hasIssue(IssueCode.EX_DATE_SECURITY_REQUIRED));
assertTrue(validation.hasIssue(IssueCode.DIVIDEND_SECURITY_REQUIRED));
}

/**
* Verifies that clearing an invalid ledger-backed ex-date is stable after XML save/load.
* The ledger entry and projection remain present, but the ex-date fact does not come back.
*/
@Test
public void testLedgerBackedExDateClearSurvivesXmlReload() throws Exception
{
var client = new Client();
var account = new Account();
var entry = new LedgerEntry("entry");
var posting = new LedgerPosting("posting");
var projection = new LedgerProjectionRef("projection");

client.addAccount(account);

entry.setType(LedgerEntryType.FEES);
entry.setDateTime(DATE_TIME);

posting.setType(LedgerPostingType.CASH);
posting.setAccount(account);
posting.setAmount(Values.Amount.factorize(10));
posting.setCurrency(CurrencyUnit.EUR);
posting.addParameter(LedgerParameter.ofLocalDateTime(LedgerParameterType.EX_DATE, EX_DATE));
entry.addPosting(posting);

projection.setRole(LedgerProjectionRole.ACCOUNT);
projection.setAccount(account);
entry.addProjectionRef(projection);

client.getLedger().addEntry(entry);
LedgerProjectionService.materialize(client);

new OnlyAccountTransactionsWithSecurityCanHaveExDateCheck().execute(client);

assertFalse(LedgerStructuralValidator.validate(client.getLedger()).hasIssue(
IssueCode.EX_DATE_SECURITY_REQUIRED));

var loaded = reloadXml(client);

assertThat(loaded.getLedger().getEntries().size(), is(1));
assertThat(loaded.getLedger().getEntries().get(0).getUUID(), is(entry.getUUID()));
assertThat(loaded.getAccounts().get(0).getTransactions().size(), is(1));
assertNull(loaded.getAccounts().get(0).getTransactions().get(0).getExDate());
}

private Client reloadXml(Client client) throws Exception
{
return ClientFactory.load(new ByteArrayInputStream(saveXml(client).getBytes(StandardCharsets.UTF_8)));
}

private String saveXml(Client client) throws Exception
{
var file = File.createTempFile("ledger-ex-date-repair-policy", ".xml"); //$NON-NLS-1$ //$NON-NLS-2$

try
{
ClientFactory.save(client, file);
return Files.readString(file.toPath(), StandardCharsets.UTF_8);
}
finally
{
Files.deleteIfExists(file.toPath());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import name.abuchen.portfolio.model.Client;
import name.abuchen.portfolio.model.Portfolio;
import name.abuchen.portfolio.model.PortfolioTransaction;
import name.abuchen.portfolio.model.PortfolioTransferEntry;
import name.abuchen.portfolio.model.Security;
import name.abuchen.portfolio.model.Transaction;

Expand Down Expand Up @@ -129,6 +130,28 @@ public void testDuplicateDetectionWithTransferOutAndWithdrawal()
assertThat(status.getCode(), is(Code.WARNING));
}

@Test
public void testPortfolioTransferDuplicateDetectionChecksSourceAndTargetPortfolios()
{
var action = new DetectDuplicatesAction(new Client());

var existingSource = getPortfolioTransferEntry();
var sourceCandidate = getPortfolioTransferEntry();

var sourceStatus = action.process(sourceCandidate, portfolio(existingSource.getSourceTransaction()),
new Portfolio());

assertThat(sourceStatus.getCode(), is(Code.WARNING));

var existingTarget = getPortfolioTransferEntry();
var targetCandidate = getPortfolioTransferEntry();

var targetStatus = action.process(targetCandidate, new Portfolio(),
portfolio(existingTarget.getTargetTransaction()));

assertThat(targetStatus.getCode(), is(Code.WARNING));
}

private AccountTransaction getTestEntry(AccountTransaction.Type type)
{
var transaction = new AccountTransaction();
Expand All @@ -140,6 +163,17 @@ private AccountTransaction getTestEntry(AccountTransaction.Type type)
return transaction;
}

private PortfolioTransferEntry getPortfolioTransferEntry()
{
var entry = new PortfolioTransferEntry();
entry.setDate(LocalDateTime.of(2025, 12, 15, 0, 0));
entry.setAmount(1000);
entry.setCurrencyCode("EUR"); //$NON-NLS-1$
entry.setShares(100);

return entry;
}

private Account account(AccountTransaction t)
{
Account a = new Account();
Expand Down
Loading
Loading