Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package name.abuchen.portfolio.model.ledger;

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

import java.util.Arrays;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;

import org.junit.Test;

import name.abuchen.portfolio.model.ledger.configuration.CashCompensationKind;
import name.abuchen.portfolio.model.ledger.configuration.CorporateActionKind;
import name.abuchen.portfolio.model.ledger.configuration.CorporateActionLeg;
import name.abuchen.portfolio.model.ledger.configuration.CorporateActionSubtype;
import name.abuchen.portfolio.model.ledger.configuration.CostAllocationMethod;
import name.abuchen.portfolio.model.ledger.configuration.EventStage;
import name.abuchen.portfolio.model.ledger.configuration.FeeReason;
import name.abuchen.portfolio.model.ledger.configuration.FractionTreatment;
import name.abuchen.portfolio.model.ledger.configuration.LedgerCode;
import name.abuchen.portfolio.model.ledger.configuration.LedgerEntryType;
import name.abuchen.portfolio.model.ledger.configuration.LedgerParameterCodeDomain;
import name.abuchen.portfolio.model.ledger.configuration.QuotationStyle;
import name.abuchen.portfolio.model.ledger.configuration.RoundingModeCode;
import name.abuchen.portfolio.model.ledger.configuration.TaxReason;

/**
* Tests ledger configuration and validation metadata.
* These tests make sure entry definitions, posting rules, and parameter domains stay stable for Ledger-V6 transactions.
*/
@SuppressWarnings("nls")
public class LedgerCodeTest
{
/**
* Checks the ledger rule scenario: every code belongs to one domain and is uppercase.
* Invalid entry shapes must be rejected before they can be stored.
* This keeps higher-level Ledger-V6 transaction flows predictable.
*/
@Test
public void testEveryCodeBelongsToOneDomainAndIsUppercase()
{
for (var code : allCodes())
{
assertTrue(EnumSet.allOf(LedgerParameterCodeDomain.class).contains(code.getDomain()));
assertFalse(code.getCode().isBlank());
assertThat(code.getCode(), is(code.getCode().toUpperCase(Locale.ROOT)));
}
}

/**
* Checks the ledger rule scenario: no duplicate codes within domain.
* Invalid entry shapes must be rejected before they can be stored.
* This keeps higher-level Ledger-V6 transaction flows predictable.
*/
@Test
public void testNoDuplicateCodesWithinDomain()
{
var seen = new EnumMap<LedgerParameterCodeDomain, HashSet<String>>(LedgerParameterCodeDomain.class);

for (var domain : LedgerParameterCodeDomain.values())
seen.put(domain, new HashSet<>());

for (var code : allCodes())
assertTrue(code.getDomain() + ":" + code.getCode(), seen.get(code.getDomain()).add(code.getCode()));
}

/**
* Checks the ledger rule scenario: domain allowed codes match domain enums.
* Invalid entry shapes must be rejected before they can be stored.
* This keeps higher-level Ledger-V6 transaction flows predictable.
*/
@Test
public void testDomainAllowedCodesMatchDomainEnums()
{
for (var domain : LedgerParameterCodeDomain.values())
{
var codes = allCodes().stream().filter(code -> code.getDomain() == domain).toList();

assertFalse(codes.isEmpty());
assertThat(codes.stream().map(LedgerCode::getCode).toList(), is(domain.getAllowedCodes()));
}
}

/**
* Checks the ledger rule scenario: corporate action kinds classify the generic
* native corporate action entry family.
*/
@Test
public void testCorporateActionKindsClassifyGenericNativeEntryType()
{
assertTrue(LedgerEntryType.CORPORATE_ACTION.isLedgerNativeTargeted());
assertThat(List.of(CorporateActionKind.values()), is(List.of(
CorporateActionKind.STOCK_DIVIDEND,
CorporateActionKind.SPIN_OFF,
CorporateActionKind.BONUS_ISSUE,
CorporateActionKind.RIGHTS_DISTRIBUTION,
CorporateActionKind.COUPON_PAYMENT,
CorporateActionKind.PIK_INTEREST,
CorporateActionKind.DEFAULTED_INTEREST,
CorporateActionKind.MATURITY,
CorporateActionKind.PARTIAL_REDEMPTION,
CorporateActionKind.CALL,
CorporateActionKind.PUT,
CorporateActionKind.CONVERSION,
CorporateActionKind.EXCHANGE,
CorporateActionKind.RESTRUCTURING,
CorporateActionKind.DEFAULT)));
assertThat(CorporateActionKind.fromCode("SPIN_OFF").orElseThrow(), is(CorporateActionKind.SPIN_OFF));
assertThat(CorporateActionKind.fromCode("MATURITY").orElseThrow(), is(CorporateActionKind.MATURITY));
assertTrue(CorporateActionKind.fromCode("OTHER").isEmpty());
}

private List<LedgerCode> allCodes()
{
return Stream.<LedgerCode[]>of(
CorporateActionLeg.values(),
CorporateActionKind.values(),
CorporateActionSubtype.values(),
EventStage.values(),
CashCompensationKind.values(),
FractionTreatment.values(),
RoundingModeCode.values(),
CostAllocationMethod.values(),
QuotationStyle.values(),
FeeReason.values(),
TaxReason.values()).flatMap(Arrays::stream).toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package name.abuchen.portfolio.model.ledger;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

import org.junit.Test;

import name.abuchen.portfolio.model.LedgerDiagnosticCode;

@SuppressWarnings("nls")
public class LedgerDiagnosticCodeTest
{
@Test
public void testCodeTextAndPrefix()
{
assertThat(LedgerDiagnosticCode.LEDGER_CONVERT_001.getGroup(), is("CONVERT"));
assertThat(LedgerDiagnosticCode.LEDGER_CONVERT_001.getCode(), is("LEDGER-CONVERT-001"));
assertThat(LedgerDiagnosticCode.LEDGER_CONVERT_002.getCode(), is("LEDGER-CONVERT-002"));
assertThat(LedgerDiagnosticCode.LEDGER_CONVERT_071.getCode(), is("LEDGER-CONVERT-071"));
assertThat(LedgerDiagnosticCode.LEDGER_CONVERT_001.prefix(), is("[LEDGER-CONVERT-001]"));
assertThat(LedgerDiagnosticCode.LEDGER_CONVERT_001.toString(), is("LEDGER-CONVERT-001"));
assertThat(LedgerDiagnosticCode.LEDGER_CORE_001.getGroup(), is("CORE"));
assertThat(LedgerDiagnosticCode.LEDGER_CORE_001.getCode(), is("LEDGER-CORE-001"));
assertThat(LedgerDiagnosticCode.LEDGER_CORE_026.getCode(), is("LEDGER-CORE-026"));
assertThat(LedgerDiagnosticCode.LEDGER_IMPORT_001.getGroup(), is("IMPORT"));
assertThat(LedgerDiagnosticCode.LEDGER_IMPORT_001.getCode(), is("LEDGER-IMPORT-001"));
assertThat(LedgerDiagnosticCode.LEDGER_IMPORT_021.getCode(), is("LEDGER-IMPORT-021"));
assertThat(LedgerDiagnosticCode.LEDGER_STRUCT_001.getCode(), is("LEDGER-STRUCT-001"));
assertThat(LedgerDiagnosticCode.LEDGER_STRUCT_055.getCode(), is("LEDGER-STRUCT-055"));
assertThat(LedgerDiagnosticCode.LEDGER_PROJ_001.getCode(), is("LEDGER-PROJ-001"));
assertThat(LedgerDiagnosticCode.LEDGER_PROJ_077.getCode(), is("LEDGER-PROJ-077"));
assertThat(LedgerDiagnosticCode.LEDGER_PERSIST_001.getCode(), is("LEDGER-PERSIST-001"));
assertThat(LedgerDiagnosticCode.LEDGER_PERSIST_002.getCode(), is("LEDGER-PERSIST-002"));
assertThat(LedgerDiagnosticCode.LEDGER_PERSIST_010.getCode(), is("LEDGER-PERSIST-010"));
assertThat(LedgerDiagnosticCode.LEDGER_FOREX_001.getCode(), is("LEDGER-FOREX-001"));
assertThat(LedgerDiagnosticCode.LEDGER_FOREX_005.getCode(), is("LEDGER-FOREX-005"));
assertThat(LedgerDiagnosticCode.LEDGER_UI_001.getCode(), is("LEDGER-UI-001"));
assertThat(LedgerDiagnosticCode.LEDGER_UI_002.getCode(), is("LEDGER-UI-002"));
assertThat(LedgerDiagnosticCode.LEDGER_UI_020.getCode(), is("LEDGER-UI-020"));
}

@Test
public void testMessageFormattingKeepsTextSeparate()
{
assertThat(LedgerDiagnosticCode.LEDGER_UI_001.message("Meldung"),
is("[LEDGER-UI-001] Meldung"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package name.abuchen.portfolio.model.ledger;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertFalse;

import java.time.LocalDateTime;

import org.junit.Test;

import name.abuchen.portfolio.Messages;
import name.abuchen.portfolio.model.Account;
import name.abuchen.portfolio.model.Client;
import name.abuchen.portfolio.model.Security;
import name.abuchen.portfolio.model.ledger.configuration.LedgerEntryType;
import name.abuchen.portfolio.model.ledger.configuration.LedgerPostingType;
import name.abuchen.portfolio.money.CurrencyUnit;
import name.abuchen.portfolio.money.Values;

@SuppressWarnings("nls")
public class LedgerDiagnosticMessageFormatterTest
{
@Test
public void testDiagnosticNlsTextDoesNotContainLedgerCodes()
{
assertFalse(Messages.LedgerDiagnosticMessageFormatterTransactionContext.isBlank());
assertThat(Messages.LedgerStructuralValidatorPostingCurrencyRequired, containsString("{0}"));
assertFalse(Messages.LedgerDiagnosticMessageFormatterTransactionContext.contains("LEDGER-"));
assertFalse(Messages.LedgerStructuralValidatorPostingCurrencyRequired.contains("LEDGER-"));
}

@Test
public void testValidationFormattingAddsFullEntryContext()
{
var ledger = new Ledger();
var account = new Account();
var security = new Security("Siemens AG", CurrencyUnit.EUR);
var entry = new LedgerEntry("entry-1");
var posting = new LedgerPosting("posting-1");

account.setName("Cash Account");
security.setIsin("DE0007236101");
entry.setType(LedgerEntryType.DIVIDENDS);
entry.setDateTime(LocalDateTime.of(2026, 1, 2, 0, 0));
entry.setSource("import");
entry.setNote("note");
posting.setType(LedgerPostingType.CASH);
posting.setAccount(account);
posting.setSecurity(security);
posting.setAmount(Values.Amount.factorize(12));
posting.setCurrency(null);
entry.addPosting(posting);
ledger.addEntry(entry);

var result = LedgerStructuralValidator.validate(ledger);
var message = LedgerDiagnosticMessageFormatter.formatValidationResult(ledger, result);

assertFalse(result.isOK());
assertThat(message, containsString("[POSTING_CURRENCY_REQUIRED] "));
assertThat(message, containsString(Messages.LedgerDiagnosticMessageFormatterTransactionContext + ":"));
assertThat(message, containsString(Messages.LedgerDiagnosticMessageFormatterDate + ": 2026-01-02T00:00"));
assertThat(message, containsString(Messages.LedgerDiagnosticMessageFormatterType + ": DIVIDENDS"));
assertThat(message, containsString(Messages.LedgerDiagnosticMessageFormatterAccount + ": Cash Account"));
assertThat(message, containsString(Messages.LedgerDiagnosticMessageFormatterSecurity
+ ": Siemens AG (" + Messages.LedgerDiagnosticMessageFormatterIsin + "=DE0007236101)"));
assertThat(message, containsString(Messages.LedgerDiagnosticMessageFormatterSource + ": import"));
assertThat(message, containsString(Messages.LedgerDiagnosticMessageFormatterNote + ": note"));
assertThat(message, containsString("UUID: posting-1"));
}

@Test
public void testValidationFormattingReportsUnavailableContext()
{
var result = LedgerStructuralValidator.validate(null);
var message = LedgerDiagnosticMessageFormatter.formatValidationResult(null, result);

assertFalse(result.isOK());
assertThat(message, containsString("[LEDGER_REQUIRED] "));
assertThat(message, containsString(Messages.LedgerDiagnosticMessageFormatterTransactionContext + ":\n "
+ Messages.LedgerDiagnosticMessageFormatterContextUnavailable));
}

@Test
public void testMigrationFormattingAddsPartialLegacyContext()
{
var client = new Client();
var account = new Account();
var transaction = new name.abuchen.portfolio.model.AccountTransaction(
name.abuchen.portfolio.model.AccountTransaction.Type.DEPOSIT);

account.setName("Cash Account");
account.setCurrencyCode(CurrencyUnit.EUR);
client.addAccount(account);
transaction.setDateTime(LocalDateTime.of(2026, 1, 3, 0, 0));
transaction.setCurrencyCode(CurrencyUnit.EUR);
transaction.setAmount(Values.Amount.factorize(25));
transaction.setSource("bank import");
account.addTransaction(transaction);

var message = LedgerDiagnosticMessageFormatter.formatMigrationDiagnostic(client,
"[LEDGER-IMPORT-001] family=ACCOUNT reason=TEST uuids=[" + transaction.getUUID() + "]",
transaction);

assertThat(message, containsString("[LEDGER-IMPORT-001] family=ACCOUNT reason=TEST"));
assertThat(message, containsString(Messages.LedgerDiagnosticMessageFormatterTransactionContext + ":"));
assertThat(message, containsString(Messages.LedgerDiagnosticMessageFormatterDate + ": 2026-01-03T00:00"));
assertThat(message, containsString(Messages.LedgerDiagnosticMessageFormatterType + ":"));
assertThat(message, containsString(Messages.LedgerDiagnosticMessageFormatterAccount + ": Cash Account"));
assertThat(message, containsString(Messages.LedgerDiagnosticMessageFormatterSource + ": bank import"));
}
}
Loading
Loading