Skip to content

Ledger Implementation - Corporate Actions#5816

Draft
Morpheus1w3 wants to merge 68 commits into
portfolio-performance:masterfrom
Morpheus1w3:PP-LedgerFeature
Draft

Ledger Implementation - Corporate Actions#5816
Morpheus1w3 wants to merge 68 commits into
portfolio-performance:masterfrom
Morpheus1w3:PP-LedgerFeature

Conversation

@Morpheus1w3

@Morpheus1w3 Morpheus1w3 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Feature blocks in the pull request

  • Adds a new Ledger subsystem as the client-owned transaction truth.
  • Adds Ledger configuration, entry/posting definitions, parameter domains, validation, and structural guardrails.
  • Materializes Ledger projections into the existing legacy transaction model.
  • Adds XML and Protobuf persistence for Ledger truth while keeping compatibility projections derived.
  • Adds compatibility creators and editor APIs for existing transaction families.
  • Adds mutation, converter, deleter, repair, and owner-patch infrastructure.
  • Integrates Ledger access into Client, AccountTransaction, PortfolioTransaction, and CrossEntry compatibility paths.
  • Wires Ledger-backed transactions into dialogs, viewers, tables, and inline editing paths.
  • Centralizes inline-edit routing and blocks unsupported structural edits on Ledger-backed projections.
  • Adds importer, InvestmentPlan, and stock-split guardrails so write paths do not silently bypass Ledger truth.
  • Adds a reporting/snapshot bridge so existing reporting consumers can continue to work through compatibility projections.
  • Adds diagnostic codes, debug output, and NLS entries for Ledger validation and troubleshooting.
  • Prepares native corporate-action modeling for SPIN_OFF, STOCK_DIVIDEND, BONUS_ISSUE, RIGHTS_DISTRIBUTION, and BOND_CONVERSION.

Summary of the Ledger

The Ledger introduces a transaction model where one business operation is stored as one LedgerEntry with one or more LedgerPosting records. A LedgerProjectionRef connects that Ledger truth back to the existing AccountTransaction and PortfolioTransaction views.

The reason for this change is that many Portfolio Performance operations are not naturally single legacy transactions. BUY/SELL operations, transfers, deliveries, fees, taxes, investment-plan executions, repairs, and corporate actions can span account rows, portfolio rows, units, and CrossEntry links. In the old model, this creates multiple places that can appear to own the same business fact. That makes validation, persistence, migration, debugging, and future extension harder.

The Ledger does not remove the existing public transaction API. AccountTransaction, PortfolioTransaction, BuySellEntry, AccountTransferEntry, PortfolioTransferEntry, and CrossEntry remain visible because existing UI, reporting, import/export, checks, tests, and external callers still use them. For Ledger-backed transactions, those objects are compatibility projections derived from Ledger entries, not a second persisted truth.

This gives the model a single persisted booking source while preserving existing callers. It also gives Save/Load, validation, debug output, and future corporate-action support a more explicit structure: facts belong to Ledger entries, postings, or typed parameters; compatibility projections can be rebuilt from that structure; and unsupported mutations can be rejected instead of partially updating only one side of a paired transaction.

Benefit

The main benefit is not a new transaction UI by itself. The main benefit is a safer transaction foundation.

The Ledger provides one central booking truth for operations that previously had to be represented by several related legacy objects. This reduces duplicated truth between account transactions, portfolio transactions, transaction units, and CrossEntry links.

It also makes transaction shape explicit and validateable. Ledger validation can check required fields, UUID uniqueness, owner compatibility, projection references, posting groups, and parameter consistency before data is saved, projected, or mutated.

Mutation becomes more controlled. Standard creation goes through semantic creator APIs. Same-shape edits go through Ledger editors. Structural changes go through converters, helpers, and LedgerMutationContext. This avoids setter replay, preserves surviving UUIDs, and reduces the risk of updating only one side of a paired operation.

Debugging and support also become easier. Stable LEDGER-* diagnostic codes and richer diagnostic output can report the relevant date, type, account, portfolio, security, amount, shares, source, and note where those facts can be reconstructed.

Finally, the Ledger prepares Portfolio Performance for more complex transaction families. Corporate actions such as spin-offs, stock dividends, bonus issues, rights distributions, and bond conversions can be modeled as Ledger-native targeted entries instead of being forced into dividend, buy, delivery, or transfer workarounds. Existing UI and API compatibility remain intact while the persisted model becomes more expressive.

Why the pull request is large

This pull request is large because the Ledger is not just a new data object. It changes the transaction ownership model and therefore has to touch the model layer, validation, projection materialization, XML persistence, Protobuf persistence, mutation paths, compatibility creators, dialog integration, importer guardrails, investment-plan references, reporting/snapshot access, diagnostics, and NLS entries.

A smaller pull request would be easier to read by line count, but harder to validate architecturally. The central questions only make sense together: Ledger truth must persist correctly, compatibility projections must remain usable, old transaction APIs must not become a second persisted truth, and write paths must not bypass the Ledger after creation.

The branch is therefore cut for review by functional topic, not by arbitrary hunks. It uses 11 review-oriented commits. Each commit has one main review theme. The split is file-based rather than hunk-based, so reviewers can inspect coherent areas of the implementation. Individual commits are not expected to compile in isolation; the final branch is the tested unit.

The final branch was verified with:

mvn -f portfolio-app/pom.xml clean verify -Plocal-dev -o

Documentation and feature analysis are handled separately from the draft pull request to keep the code review focused.


For review, the branch is split into 11 functional commits. The table below shows the review goal and local Git statistics per commit. Generated files, NLS/properties, protobuf changes, documentation, test files, and production files are separated so the size is easier to evaluate.

The size comes from the number of integration boundaries, not from one isolated implementation area. The Ledger has to keep persistence, projections, compatibility APIs, mutation guardrails, import paths, investment-plan references, reporting access, diagnostics, and NLS consistent at the same time.

ä# Ledger PR Commit Statistics

Summary Totals

Across the 11 review commits: 71856 added lines, 1436 deleted lines, 209 new files, 118 modified files, 4 deleted files, 0 renamed files, 1 copied files, 0 binary files.
Classified file touches: 200 production, 70 test, 42 resource, 40 NLS/properties, 1 protobuf, 0 documentation, 22 generated.
Unique affected parent directories: 41. Unique Java packages: 23. Top-level areas: name.abuchen.portfolio; name.abuchen.portfolio.tests; name.abuchen.portfolio.ui; name.abuchen.portfolio.ui.tests; nls/properties; proto.

Sanity Check

Per-commit LOC sum for the 11 review commits: +71856 / -1436, binary rows 0.
Direct git diff --numstat fde90b55ef31..HEAD: +72562 / -1429, binary rows 0.
Direct git diff --numstat fde90b55ef31..47ce2a618: +71891 / -1428, binary rows 0.
HEAD contains commit(s) after the 11 listed review commits: 844497b Move Ledger spin-off XML fixture to tests.
Differences are expected because per-commit totals measure churn in each commit, while direct range totals measure the net tree difference. The HEAD comparison also includes any commits after the listed review sequence.

Compact Per-Commit Table

Summary Totals

Across the 11 review commits: 71856 added lines, 1436 deleted lines, 209 new files, 118 modified files, 4 deleted files, 0 renamed files, 1 copied files, 0 binary files.
Classified file touches: 200 production, 70 test, 42 resource, 40 NLS/properties, 1 protobuf, 0 documentation, 22 generated.
Unique affected parent directories: 41. Unique Java packages: 23. Top-level areas: name.abuchen.portfolio; name.abuchen.portfolio.tests; name.abuchen.portfolio.ui; name.abuchen.portfolio.ui.tests; nls/properties; proto.

Sanity Check

Per-commit LOC sum for the 11 review commits: +71856 / -1436, binary rows 0.
Direct git diff --numstat fde90b55ef31..HEAD: +72562 / -1429, binary rows 0.
Direct git diff --numstat fde90b55ef31..47ce2a618: +71891 / -1428, binary rows 0.
HEAD contains commit(s) after the 11 listed review commits: 844497b Move Ledger spin-off XML fixture to tests.
Differences are expected because per-commit totals measure churn in each commit, while direct range totals measure the net tree difference. The HEAD comparison also includes any commits after the listed review sequence.

Review-oriented commit structure

# Commit Review area Files A/M/D/R/C LOC + / - Notes
1 05507d6b0 Ledger core model 7/0/0/0/0 +1,528 / -0 Core entry/posting/projection model
2 809492d16 Definitions and validation 57/0/0/0/0 +11,412 / -0 Static definitions, parameter domains, validator rules
3 3a04f1af7 Projection materialization 16/0/0/0/0 +3,361 / -0 Compatibility projection creation and restoration
4 3f58e32d9 XML and Protobuf persistence 22/9/0/0/0 +17,549 / -186 Includes protobuf schema/generated output and save/load tests
5 5894d3be9 Compatibility creators and editors 38/0/0/0/0 +8,464 / -0 Standard creation and same-shape edit layer
6 cb0577439 Client and legacy transaction access 3/9/0/0/0 +3,778 / -39 Client ownership and compatibility access paths
7 4ea2a84cc Conversion and mutation support 40/0/0/0/0 +9,346 / -0 Mutation context, converters, deleter, structural helpers
8 a836403ad Dialogs, viewers, tables, inline editing 16/31/0/0/1 +7,192 / -97 Broad UI integration and centralized inline-edit routing
9 8eab2b275 Importer, InvestmentPlan, stock-split guardrails 6/23/4/0/0 +3,330 / -1,107 Guardrails and removal of unsafe bypass paths
10 93f883970 Reporting and snapshot bridge 0/4/0/0/0 +266 / -7 Compatibility bridge for reporting/snapshot consumers
11 47ce2a618 Diagnostics, debug output, NLS 4/42/0/0/0 +5,630 / -0 Stable diagnostic codes, debug context, NLS/properties
12 844497ba3 Test fixture placement TBD TBD Moves the Ledger spin-off XML fixture to the test area

Detail Table

Commit Affected paths Java packages
05507d6b0 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model/ledger; name.abuchen.portfolio/src/name/abuchen/portfolio/model/ledger name.abuchen.portfolio.model.ledger
809492d16 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model/ledger; name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model/ledger/nativeentry; name.abuchen.portfolio/src/name/abuchen/portfolio/model/ledger; name.abuchen.portfolio/src/name/abuchen/portfolio/model/ledger/configuration; name.abuchen.portfolio/src/name/abuchen/portfolio/model/ledger/configuration/rule; name.abuchen.portfolio/src/name/abuchen/portfolio/model/ledger/nativeentry name.abuchen.portfolio.model.ledger; name.abuchen.portfolio.model.ledger.configuration; name.abuchen.portfolio.model.ledger.configuration.rule; name.abuchen.portfolio.model.ledger.nativeentry
3a04f1af7 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model/ledger; name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model/ledger/projection; name.abuchen.portfolio/src/name/abuchen/portfolio/model/ledger; name.abuchen.portfolio/src/name/abuchen/portfolio/model/ledger/projection name.abuchen.portfolio.model.ledger; name.abuchen.portfolio.model.ledger.projection
3f58e32d9 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model; name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model/ledger; name.abuchen.portfolio/META-INF; name.abuchen.portfolio/protos/name/abuchen/portfolio/model/proto/v1; name.abuchen.portfolio/src/name/abuchen/portfolio/model name.abuchen.portfolio.model; name.abuchen.portfolio.model.ledger; name.abuchen.portfolio.model.proto.v1
5894d3be9 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model/ledger; name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model/ledger/compatibility; name.abuchen.portfolio/src/name/abuchen/portfolio/model/ledger/compatibility name.abuchen.portfolio.model.ledger; name.abuchen.portfolio.model.ledger.compatibility
cb0577439 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/checks/impl; name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model; name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model/ledger/legacy; name.abuchen.portfolio/src/name/abuchen/portfolio/model; name.abuchen.portfolio/src/name/abuchen/portfolio/model/ledger/legacy name.abuchen.portfolio.checks.impl; name.abuchen.portfolio.model; name.abuchen.portfolio.model.ledger.legacy
4ea2a84cc name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model; name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model/ledger; name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model/ledger/compatibility; name.abuchen.portfolio/src/name/abuchen/portfolio/model; name.abuchen.portfolio/src/name/abuchen/portfolio/model/ledger; name.abuchen.portfolio/src/name/abuchen/portfolio/model/ledger/compatibility name.abuchen.portfolio.model; name.abuchen.portfolio.model.ledger; name.abuchen.portfolio.model.ledger.compatibility
a836403ad name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model/ledger; name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model/ledger/compatibility; name.abuchen.portfolio.ui.tests/src/name/abuchen/portfolio/ui/dialogs/transactions; name.abuchen.portfolio.ui.tests/src/name/abuchen/portfolio/ui/util/viewers; name.abuchen.portfolio.ui.tests/src/name/abuchen/portfolio/ui/views; name.abuchen.portfolio.ui.tests/src/name/abuchen/portfolio/ui/views/actions; name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/dialogs/transactions; name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/preferences; name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/util/viewers; name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views; name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/actions; name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/panes; name.abuchen.portfolio/src/name/abuchen/portfolio/model/ledger/compatibility name.abuchen.portfolio.model.ledger; name.abuchen.portfolio.model.ledger.compatibility; name.abuchen.portfolio.ui.dialogs.transactions; name.abuchen.portfolio.ui.preferences; name.abuchen.portfolio.ui.util.viewers; name.abuchen.portfolio.ui.views; name.abuchen.portfolio.ui.views.actions; name.abuchen.portfolio.ui.views.panes
8eab2b275 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/checks/impl; name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/actions; name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model; name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model/ledger/compatibility; name.abuchen.portfolio.ui.tests/src/name/abuchen/portfolio/ui/dialogs/transactions; name.abuchen.portfolio.ui.tests/src/name/abuchen/portfolio/ui/wizards/splits; name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/jobs; name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/wizards/splits; name.abuchen.portfolio/META-INF/services; name.abuchen.portfolio/src/name/abuchen/portfolio/checks/impl; name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/actions; name.abuchen.portfolio/src/name/abuchen/portfolio/model name.abuchen.portfolio.checks.impl; name.abuchen.portfolio.datatransfer.actions; name.abuchen.portfolio.model; name.abuchen.portfolio.model.ledger.compatibility; name.abuchen.portfolio.ui.dialogs.transactions; name.abuchen.portfolio.ui.jobs; name.abuchen.portfolio.ui.wizards.splits
93f883970 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/snapshot; name.abuchen.portfolio.tests/src/scenarios; name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot name.abuchen.portfolio.snapshot; scenarios
47ce2a618 name.abuchen.portfolio.tests/src/name/abuchen/portfolio; name.abuchen.portfolio.tests/src/name/abuchen/portfolio/model/ledger; name.abuchen.portfolio.ui.tests/src/name/abuchen/portfolio/ui; name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui; name.abuchen.portfolio/src/name/abuchen/portfolio; name.abuchen.portfolio/src/name/abuchen/portfolio/model; name.abuchen.portfolio/src/name/abuchen/portfolio/model/ledger name.abuchen.portfolio; name.abuchen.portfolio.model; name.abuchen.portfolio.model.ledger; name.abuchen.portfolio.ui

Notes

  • Commit-local stats use git show --name-status --find-renames --find-copies --format= <commit>, git show --numstat --find-renames --find-copies --format= <commit>, and git show --stat --find-renames --find-copies --format= <commit>.
  • LOC totals exclude binary numstat rows where added/deleted are -; those rows are counted in binary_files.
  • Renames and copies count as one classified file at the destination path. Affected parent directories include both source and destination paths.
  • Production counts include non-test .java and .proto source files that are not generated. Test/resource/NLS/protobuf/documentation/generated counts are rule-based and can overlap when a file satisfies multiple rules.
  • Generated files are detected by /generated/, Java files under /protos/, or Java files ending in Protos.java; this identifies the repository protobuf output convention used by name.abuchen.portfolio/protos/.../ClientProtos.java.

#5466 #5813

Add the core Ledger domain objects and focused model tests.

This establishes the subsystem data model before integration points are introduced.

Persistence, UI wiring, diagnostics, and documentation are intentionally left to later commits.
Add Ledger configuration, native definition, and structural validation files with their tests.

This makes the Ledger rules reviewable separately from persistence and UI integration.

Runtime projection wiring, import paths, and documentation are intentionally left unchanged in this commit.
Add Ledger projection references, membership support, runtime projection classes, materialization, and projection tests.

This isolates how Ledger truth becomes compatibility transaction rows.

Persistence, UI editing, and import guardrails are intentionally left to later commits.
Add Ledger XML and Protobuf persistence support, schema/generated output, and save/load tests.

This makes persisted Ledger truth reviewable as one file-level slice.

UI wiring, import guardrails, diagnostics/NLS, and documentation are intentionally left out.
Add fixed-shape Ledger compatibility creators, editors, value objects, and tests.

This groups transaction creation and update behavior separately from conversion guardrails.

UI routing, persistence schema, and documentation are intentionally left unchanged in this commit.
Wire Ledger into client ownership, legacy transaction access, and migration compatibility paths.

This connects Ledger truth to existing model APIs without mixing in UI review concerns.

Persistence schema, UI routing, diagnostics/NLS, and documentation are intentionally left unchanged here.
Add Ledger converters, mutation helpers, owner/unit/share patching, deletion support, and tests.

This keeps mutation and conversion review separate from basic creators and UI wiring.

Dialog/viewer availability, NLS text, and documentation are intentionally left to later commits.
Wire Ledger-aware behavior into transaction dialogs, viewers, inline editing, actions, and UI tests.

This groups user-facing transaction table and dialog integration for review.

Importer guardrails, reporting bridge, diagnostic NLS, and documentation are intentionally left to later commits.
Add Ledger guardrails for importer insertion, investment plans, checks, and stock split paths with tests.

This isolates write-path protections outside the main transaction viewers.

Reporting bridge, diagnostic NLS, and documentation are intentionally left unchanged in this commit.
Add the Ledger bridge for reporting snapshots and related scenario coverage.

This keeps reporting effects reviewable separately from transaction write paths.

Diagnostics, NLS, persistence, and documentation are intentionally left unchanged in this commit.
Add Ledger diagnostic codes, diagnostic formatting, NLS entries, translations, and locale-safe tests.

This groups the support and user-facing diagnostic text work after functional wiring.

XML/protobuf schema, UI routing, import behavior, and documentation are intentionally left unchanged in this commit.
@Morpheus1w3 Morpheus1w3 marked this pull request as draft June 26, 2026 16:18
@Morpheus1w3

Morpheus1w3 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Example SPIN-OFF

This is an example how the ledger is dealing with one SPIN-OFF posting towards to AccountTransaction and PortfolioTransaction.

image
<ledger>
	<entries>
		<ledger-entry uuid="cd4163da-7ffe-47fd-a797-25daa797f832" type="DEPOSIT" dateTime="2019-12-30T00:00" updatedAt="2026-06-27T08:39:37.865074300Z">
			<postings>
				<ledger-posting uuid="ba8b8b54-2f08-4b23-b529-6048d8b41559" type="CASH" amount="1000000" currency="EUR" shares="0">
					<account reference="../../../../../../accounts/account"/>
				</ledger-posting>
			</postings>
			<projectionRefs>
				<ledger-projection-ref uuid="ead9e5ae-e39e-4c80-a4a6-7548465fb867" role="ACCOUNT">
					<account reference="../../../../../../accounts/account"/>
					<memberships>
						<membership postingUUID="ba8b8b54-2f08-4b23-b529-6048d8b41559" role="PRIMARY"/>
					</memberships>
				</ledger-projection-ref>
			</projectionRefs>
		</ledger-entry>
		<ledger-entry uuid="4f807878-9ce4-47af-a4b2-c2a738988794" type="BUY" dateTime="2020-01-02T00:00" updatedAt="2026-06-27T08:39:37.980488Z">
			<postings>
				<ledger-posting uuid="44b82aae-b37d-4bb7-9b14-74a451799b97" type="CASH" amount="118640" currency="EUR" shares="0">
					<account reference="../../../../../../accounts/account"/>
				</ledger-posting>
				<ledger-posting uuid="2dfc81b6-9549-4032-b8e2-438354a6239e" type="SECURITY" amount="118640" currency="EUR" shares="1000000000">
					<security reference="../../../../../../securities/security"/>
					<portfolio reference="../../../../../../portfolios/portfolio"/>
				</ledger-posting>
			</postings>
			<projectionRefs>
				<ledger-projection-ref uuid="9cf2e760-1be4-40f9-b4f5-f9d94f8d2894" role="ACCOUNT">
					<account reference="../../../../../../accounts/account"/>
					<memberships>
						<membership postingUUID="44b82aae-b37d-4bb7-9b14-74a451799b97" role="PRIMARY"/>
					</memberships>
				</ledger-projection-ref>
				<ledger-projection-ref uuid="215c99f1-5f1b-4a0a-b1ea-1d8e3fc6e560" role="PORTFOLIO">
					<portfolio reference="../../../../../../portfolios/portfolio"/>
					<memberships>
						<membership postingUUID="2dfc81b6-9549-4032-b8e2-438354a6239e" role="PRIMARY"/>
					</memberships>
				</ledger-projection-ref>
			</projectionRefs>
		</ledger-entry>
		<ledger-entry uuid="67839a41-e45b-4121-b21f-21d5b4ae2de9" type="SPIN_OFF" dateTime="2020-09-28T00:00" updatedAt="2026-06-27T08:39:38.062345200Z">
			<note>Siemens Energy spin-off</note>
			<source>Ledger-V6 native example</source>
			<parameters>
				<ledger-parameter type="CORPORATE_ACTION_KIND" valueKind="STRING" value="SPIN_OFF"/>
				<ledger-parameter type="CORPORATE_ACTION_SUBTYPE" valueKind="STRING" value="STANDARD"/>
				<ledger-parameter type="EVENT_REFERENCE" valueKind="STRING" value="SIEMENS-ENERGY-2020"/>
				<ledger-parameter type="EVENT_STAGE" valueKind="STRING" value="SETTLED"/>
				<ledger-parameter type="EFFECTIVE_DATE" valueKind="LOCAL_DATE" value="2020-09-28"/>
			</parameters>
			<postings>
				<ledger-posting uuid="c719c2a2-d114-4f1e-8faa-05608bc4b42c" type="SECURITY" amount="109960" currency="EUR" shares="1000000000">
					<security reference="../../../../../../securities/security"/>
					<portfolio reference="../../../../../../portfolios/portfolio"/>
					<parameters>
						<ledger-parameter type="CORPORATE_ACTION_LEG" valueKind="STRING" value="SOURCE_SECURITY"/>
						<ledger-parameter type="SOURCE_SECURITY" valueKind="SECURITY">
							<value reference="../../../../../../../../securities/security"/>
						</ledger-parameter>
						<ledger-parameter type="TARGET_SECURITY" valueKind="SECURITY">
							<value reference="../../../../../../../../securities/security[2]"/>
						</ledger-parameter>
						<ledger-parameter type="RATIO_NUMERATOR" valueKind="DECIMAL" value="1"/>
						<ledger-parameter type="RATIO_DENOMINATOR" valueKind="DECIMAL" value="2"/>
					</parameters>
				</ledger-posting>
				<ledger-posting uuid="7c5ad8c3-e750-40e6-a57f-2e0cb37dae5c" type="SECURITY" amount="109960" currency="EUR" shares="1000000000">
					<security reference="../../../../../../securities/security"/>
					<portfolio reference="../../../../../../portfolios/portfolio"/>
					<parameters>
						<ledger-parameter type="CORPORATE_ACTION_LEG" valueKind="STRING" value="TARGET_SECURITY"/>
						<ledger-parameter type="SOURCE_SECURITY" valueKind="SECURITY">
							<value reference="../../../../../../../../securities/security"/>
						</ledger-parameter>
						<ledger-parameter type="TARGET_SECURITY" valueKind="SECURITY">
							<value reference="../../../../../../../../securities/security"/>
						</ledger-parameter>
						<ledger-parameter type="RATIO_NUMERATOR" valueKind="DECIMAL" value="1"/>
						<ledger-parameter type="RATIO_DENOMINATOR" valueKind="DECIMAL" value="2"/>
					</parameters>
				</ledger-posting>
				<ledger-posting uuid="b10142b7-8004-4fa8-9545-12f45e00e9d1" type="SECURITY" amount="10605" currency="EUR" shares="500000000">
					<security reference="../../../../../../securities/security[2]"/>
					<portfolio reference="../../../../../../portfolios/portfolio"/>
					<parameters>
						<ledger-parameter type="CORPORATE_ACTION_LEG" valueKind="STRING" value="TARGET_SECURITY"/>
						<ledger-parameter type="SOURCE_SECURITY" valueKind="SECURITY">
							<value reference="../../../../../../../../securities/security"/>
						</ledger-parameter>
						<ledger-parameter type="TARGET_SECURITY" valueKind="SECURITY">
							<value reference="../../../../../../../../securities/security[2]"/>
						</ledger-parameter>
						<ledger-parameter type="RATIO_NUMERATOR" valueKind="DECIMAL" value="1"/>
						<ledger-parameter type="RATIO_DENOMINATOR" valueKind="DECIMAL" value="2"/>
					</parameters>
				</ledger-posting>
				<ledger-posting uuid="41c4961c-d17d-4847-a7f4-e73fefee253b" type="CASH_COMPENSATION" amount="500" currency="EUR" shares="0">
					<account reference="../../../../../../accounts/account"/>
					<parameters>
						<ledger-parameter type="CASH_ACCOUNT" valueKind="ACCOUNT">
							<value reference="../../../../../../../../accounts/account"/>
						</ledger-parameter>
						<ledger-parameter type="CASH_IN_LIEU_AMOUNT" valueKind="MONEY" amount="500" currency="EUR"/>
						<ledger-parameter type="CORPORATE_ACTION_LEG" valueKind="STRING" value="CASH_COMPENSATION"/>
						<ledger-parameter type="CASH_COMPENSATION_KIND" valueKind="STRING" value="CASH_IN_LIEU"/>
					</parameters>
				</ledger-posting>
				<ledger-posting uuid="0a803e35-9d46-46f2-ae9f-d68b758bf5d3" type="FEE" amount="200" currency="EUR" shares="0">
					<account reference="../../../../../../accounts/account"/>
					<parameters>
						<ledger-parameter type="CORPORATE_ACTION_LEG" valueKind="STRING" value="FEE"/>
						<ledger-parameter type="FEE_REASON" valueKind="STRING" value="CORPORATE_ACTION_FEE"/>
					</parameters>
				</ledger-posting>
				<ledger-posting uuid="9bd3ca4d-c7ad-4015-83ea-acb32573615c" type="TAX" amount="100" currency="EUR" shares="0">
					<account reference="../../../../../../accounts/account"/>
					<parameters>
						<ledger-parameter type="CORPORATE_ACTION_LEG" valueKind="STRING" value="TAX"/>
						<ledger-parameter type="TAX_REASON" valueKind="STRING" value="WITHHOLDING_TAX"/>
						<ledger-parameter type="WITHHOLDING_TAX" valueKind="BOOLEAN" value="true"/>
					</parameters>
				</ledger-posting>
			</postings>
			<projectionRefs>
				<ledger-projection-ref uuid="49db76c8-0d62-457b-8c89-4c034b991985" role="OLD_SECURITY_LEG">
					<portfolio reference="../../../../../../portfolios/portfolio"/>
					<memberships>
						<membership postingUUID="c719c2a2-d114-4f1e-8faa-05608bc4b42c" role="PRIMARY"/>
					</memberships>
				</ledger-projection-ref>
				<ledger-projection-ref uuid="1d615945-6715-410d-9e71-7b41cdd9b40f" role="DELIVERY_INBOUND">
					<portfolio reference="../../../../../../portfolios/portfolio"/>
					<memberships>
						<membership postingUUID="7c5ad8c3-e750-40e6-a57f-2e0cb37dae5c" role="PRIMARY"/>
					</memberships>
				</ledger-projection-ref>
				<ledger-projection-ref uuid="54b85d17-56fa-4a0a-818f-88947e94691b" role="NEW_SECURITY_LEG">
					<portfolio reference="../../../../../../portfolios/portfolio"/>
					<memberships>
						<membership postingUUID="b10142b7-8004-4fa8-9545-12f45e00e9d1" role="PRIMARY"/>
					</memberships>
				</ledger-projection-ref>
				<ledger-projection-ref uuid="567e4614-3767-4cd3-a255-d345bd9635ff" role="CASH_COMPENSATION">
					<account reference="../../../../../../accounts/account"/>
					<memberships>
						<membership postingUUID="41c4961c-d17d-4847-a7f4-e73fefee253b" role="PRIMARY"/>
						<membership postingUUID="41c4961c-d17d-4847-a7f4-e73fefee253b" role="GROUP_ANCHOR"/>
					</memberships>
				</ledger-projection-ref>
			</projectionRefs>
		</ledger-entry>
	</entries>
</ledger>
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.LocalDateTime;
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.Portfolio;
import name.abuchen.portfolio.model.PortfolioTransaction;
import name.abuchen.portfolio.model.Security;
import name.abuchen.portfolio.model.Transaction.Unit;
import name.abuchen.portfolio.model.ledger.LedgerEntry;
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.LedgerTransactionMetadata;
import name.abuchen.portfolio.model.ledger.ProjectionMembership;
import name.abuchen.portfolio.model.ledger.ProjectionMembershipRole;
import name.abuchen.portfolio.model.ledger.compatibility.LedgerAccountCashLeg;
import name.abuchen.portfolio.model.ledger.compatibility.LedgerCreationUnits;
import name.abuchen.portfolio.model.ledger.compatibility.LedgerPortfolioSecurityLeg;
import name.abuchen.portfolio.model.ledger.compatibility.LedgerSecurityQuantity;
import name.abuchen.portfolio.model.ledger.compatibility.LedgerTransactionCreator;
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.EventStage;
import name.abuchen.portfolio.model.ledger.configuration.FeeReason;
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.nativeentry.LedgerNativeEntryAssembler;
import name.abuchen.portfolio.model.ledger.nativeentry.NativeCashCompensation;
import name.abuchen.portfolio.model.ledger.nativeentry.NativeCorporateActionEvent;
import name.abuchen.portfolio.model.ledger.nativeentry.NativeEntryMetadata;
import name.abuchen.portfolio.model.ledger.nativeentry.NativeFee;
import name.abuchen.portfolio.model.ledger.nativeentry.NativeSecurityLeg;
import name.abuchen.portfolio.model.ledger.nativeentry.NativeTax;
import name.abuchen.portfolio.model.ledger.nativeentry.Ratio;
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.Money;
import name.abuchen.portfolio.money.Values;


public class LedgerV6SpinOffSiemensEnergyExample
{
private static final LocalDateTime DEPOSIT_DATE = LocalDateTime.of(2019, 12, 30, 0, 0);
private static final LocalDateTime BUY_DATE = LocalDateTime.of(2020, 1, 2, 0, 0);
private static final LocalDateTime SPIN_OFF_DATE = LocalDateTime.of(2020, 9, 28, 0, 0);


public static void main(String[] args) throws Exception
{
    var output = args.length == 0
                    ? Path.of("docs", "ledger-v6", "examples",
                                    "ledger-v6-spin-off-siemens-energy-example.xml")
                    : Path.of(args[0]);

    var client = createClient();

    saveXml(client, output);
    validateLoadedXml(output);

    System.out.println("Wrote " + output.toAbsolutePath());
}

public static Client createClient()
{
    var client = new Client();
    var account = account("Spin-off cash account");
    var portfolio = portfolio("Corporate action proof portfolio", account);
    var siemens = security("Siemens AG", "DE0007236101");
    var siemensEnergy = security("Siemens Energy AG", "DE000ENER6Y0");

    client.addAccount(account);
    client.addPortfolio(portfolio);
    client.addSecurity(siemens);
    client.addSecurity(siemensEnergy);

    createDeposit(client, account);
    createBuy(client, account, portfolio, siemens);
    createSpinOff(client, account, portfolio, siemens, siemensEnergy);

    LedgerProjectionService.materialize(client);
    assertScenario(client);

    return client;
}

public static void saveXml(Client client, Path output) throws Exception
{
    Files.createDirectories(output.toAbsolutePath().getParent());
    ClientFactory.save(client, output.toFile());
}

public static Client validateLoadedXml(Path input) throws Exception
{
    var xml = Files.readString(input, StandardCharsets.UTF_8);

    assertNoPersistedLegacyRows(xml);

    var loaded = ClientFactory.load(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));

    assertScenario(loaded);

    var roundtrip = Files.createTempFile("ledger-v6-spin-off-roundtrip", ".xml");

    try
    {
        ClientFactory.save(loaded, roundtrip.toFile());
        assertNoPersistedLegacyRows(Files.readString(roundtrip, StandardCharsets.UTF_8));
    }
    finally
    {
        Files.deleteIfExists(roundtrip);
    }

    return loaded;
}

private static void createDeposit(Client client, Account account)
{
    new LedgerTransactionCreator(client).createDeposit(
                    LedgerTransactionMetadata.of(DEPOSIT_DATE),
                    LedgerAccountCashLeg.of(account, eur(10000_00L)));
}

private static void createBuy(Client client, Account account, Portfolio portfolio, Security siemens)
{
    new LedgerTransactionCreator(client).createBuy(
                    LedgerTransactionMetadata.of(BUY_DATE),
                    LedgerAccountCashLeg.of(account, eur(1186_40L)),
                    LedgerPortfolioSecurityLeg.of(portfolio,
                                    LedgerSecurityQuantity.of(siemens, Values.Share.factorize(10)),
                                    eur(1186_40L)),
                    LedgerCreationUnits.none());
}

private static void createSpinOff(Client client, Account account, Portfolio portfolio, Security siemens,
                Security siemensEnergy)
{
    LedgerNativeEntryAssembler.forClient(client).spinOff()
                    .metadata(NativeEntryMetadata.of(SPIN_OFF_DATE)
                                    .note("Siemens Energy spin-off")
                                    .source("Ledger-V6 native example"))
                    .event(NativeCorporateActionEvent.builder()
                                    .kind(CorporateActionKind.SPIN_OFF)
                                    .subtype(CorporateActionSubtype.STANDARD)
                                    .reference("SIEMENS-ENERGY-2020")
                                    .stage(EventStage.SETTLED)
                                    .effectiveDate(LocalDate.of(2020, 9, 28))
                                    .build())
                    .securityLeg(NativeSecurityLeg.source()
                                    .portfolio(portfolio)
                                    .security(siemens)
                                    .shares(Values.Share.factorize(10))
                                    .amount(eur(1099_60L))
                                    .sourceSecurity(siemens)
                                    .targetSecurity(siemensEnergy)
                                    .ratio(Ratio.of(java.math.BigDecimal.ONE, java.math.BigDecimal.valueOf(2)))
                                    .build())
                    // The current native API has source/target security-leg builders.
                    // The retained Siemens leg is a target posting projected as an inbound delivery.
                    .securityLeg(NativeSecurityLeg.target()
                                    .portfolio(portfolio)
                                    .security(siemens)
                                    .shares(Values.Share.factorize(10))
                                    .amount(eur(1099_60L))
                                    .sourceSecurity(siemens)
                                    .targetSecurity(siemens)
                                    .ratio(Ratio.of(java.math.BigDecimal.ONE, java.math.BigDecimal.valueOf(2)))
                                    .projectAs(LedgerProjectionRole.DELIVERY_INBOUND)
                                    .build())
                    .securityLeg(NativeSecurityLeg.target()
                                    .portfolio(portfolio)
                                    .security(siemensEnergy)
                                    .shares(Values.Share.factorize(5))
                                    .amount(eur(106_05L))
                                    .sourceSecurity(siemens)
                                    .targetSecurity(siemensEnergy)
                                    .ratio(Ratio.of(java.math.BigDecimal.ONE, java.math.BigDecimal.valueOf(2)))
                                    .build())
                    .cashCompensation(NativeCashCompensation.builder()
                                    .account(account)
                                    .amount(eur(5_00L))
                                    .kind(CashCompensationKind.CASH_IN_LIEU)
                                    .build())
                    .fee(NativeFee.of(account, eur(2_00L), FeeReason.CORPORATE_ACTION_FEE))
                    .tax(NativeTax.withholding(account, eur(1_00L)))
                    .buildAndAdd();
}

private static void assertScenario(Client client)
{
    LedgerProjectionService.materialize(client);

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

    if (!validation.isOK())
        throw new IllegalStateException(validation.format());

    assertEquals(3, client.getLedger().getEntries().size(), "ledger entry count");
    assertEquals(1, client.getAccounts().size(), "account count");
    assertEquals(1, client.getPortfolios().size(), "portfolio count");
    assertEquals(2, client.getSecurities().size(), "security count");

    var account = client.getAccounts().get(0);
    var portfolio = client.getPortfolios().get(0);
    var siemens = securityByIsin(client, "DE0007236101");
    var siemensEnergy = securityByIsin(client, "DE000ENER6Y0");
    var spinOff = spinOffEntry(client);

    assertEquals(6, spinOff.getPostings().size(), "spin-off posting count");
    assertEquals(4, spinOff.getProjectionRefs().size(), "spin-off projection count");
    assertProjection(spinOff, LedgerProjectionRole.OLD_SECURITY_LEG,
                    securityPosting(spinOff, siemens, CorporateActionLeg.SOURCE_SECURITY, siemensEnergy), null);
    assertProjection(spinOff, LedgerProjectionRole.DELIVERY_INBOUND,
                    securityPosting(spinOff, siemens, CorporateActionLeg.TARGET_SECURITY, siemens), null);
    assertProjection(spinOff, LedgerProjectionRole.NEW_SECURITY_LEG,
                    securityPosting(spinOff, siemensEnergy, CorporateActionLeg.TARGET_SECURITY, siemensEnergy),
                    null);

    var cashPosting = cashPosting(spinOff, account, eur(5_00L).getAmount());

    assertProjection(spinOff, LedgerProjectionRole.CASH_COMPENSATION, cashPosting, cashPosting);
    assertEquals(3, account.getTransactions().size(), "account transaction count");
    assertEquals(4, portfolio.getTransactions().size(), "portfolio transaction count");

    var buy = portfolioTransaction(portfolio, PortfolioTransaction.Type.BUY, siemens);
    assertEquals(Values.Share.factorize(10), buy.getShares(), "buy shares");
    assertEquals(eur(1186_40L).getAmount(), buy.getAmount(), "buy amount");

    var oldLeg = portfolioProjection(portfolio, LedgerProjectionRole.OLD_SECURITY_LEG, siemens);
    var retainedLeg = portfolioProjection(portfolio, LedgerProjectionRole.DELIVERY_INBOUND, siemens);
    var newLeg = portfolioProjection(portfolio, LedgerProjectionRole.NEW_SECURITY_LEG, siemensEnergy);

    assertEquals(PortfolioTransaction.Type.DELIVERY_OUTBOUND, oldLeg.getType(), "old Siemens leg type");
    assertEquals(Values.Share.factorize(10), oldLeg.getShares(), "old Siemens shares");
    assertEquals(eur(1099_60L).getAmount(), oldLeg.getAmount(), "old Siemens amount");
    assertEquals(PortfolioTransaction.Type.DELIVERY_INBOUND, retainedLeg.getType(), "retained Siemens leg type");
    assertEquals(oldLeg.getShares(), retainedLeg.getShares(), "retained Siemens shares");
    assertEquals(oldLeg.getAmount(), retainedLeg.getAmount(), "retained Siemens amount");
    assertEquals(0L, retainedLeg.getShares() - oldLeg.getShares(), "Siemens position neutralization");
    assertEquals(PortfolioTransaction.Type.DELIVERY_INBOUND, newLeg.getType(), "Siemens Energy leg type");
    assertEquals(Values.Share.factorize(5), newLeg.getShares(), "Siemens Energy shares");
    assertEquals(eur(106_05L).getAmount(), newLeg.getAmount(), "Siemens Energy amount");

    var compensation = accountProjection(account, LedgerProjectionRole.CASH_COMPENSATION);
    var fee = compensation.getUnit(Unit.Type.FEE).orElseThrow().getAmount().getAmount();
    var tax = compensation.getUnit(Unit.Type.TAX).orElseThrow().getAmount().getAmount();

    assertEquals(AccountTransaction.Type.DEPOSIT, compensation.getType(), "cash compensation type");
    assertEquals(eur(5_00L).getAmount(), compensation.getAmount(), "cash compensation net amount");
    assertEquals(eur(2_00L).getAmount(), fee, "cash compensation fee");
    assertEquals(eur(1_00L).getAmount(), tax, "cash compensation tax");
    assertEquals(eur(8_00L).getAmount(), compensation.getAmount() + fee + tax,
                    "cash compensation gross amount");
}

private static void assertNoPersistedLegacyRows(String xml)
{
    if (xml.contains("<account-transaction") || xml.contains("<portfolio-transaction"))
        throw new IllegalStateException("XML contains persisted legacy transaction rows");
    if (xml.contains("LedgerBacked"))
        throw new IllegalStateException("XML contains runtime projection classes");
}

private static void assertProjection(LedgerEntry entry, LedgerProjectionRole role, LedgerPosting primaryPosting,
                LedgerPosting postingGroup)
{
    var projection = projection(entry, role);

    assertEquals(primaryPosting.getUUID(), primaryPostingUUID(projection), role + " primary posting");
    assertEquals(postingGroup == null ? null : postingGroup.getUUID(), postingGroupUUID(projection),
                    role + " posting group");
}

private static String primaryPostingUUID(LedgerProjectionRef projection)
{
    return projection.getPrimaryMembership().map(ProjectionMembership::getPostingUUID)
                    .orElse(projection.getPrimaryPostingUUID());
}

private static String postingGroupUUID(LedgerProjectionRef projection)
{
    return projection.getMembershipsByRole(ProjectionMembershipRole.GROUP_ANCHOR).stream().findFirst()
                    .map(ProjectionMembership::getPostingUUID).orElse(projection.getPostingGroupUUID());
}

private static LedgerEntry spinOffEntry(Client client)
{
    return client.getLedger().getEntries().stream()
                    .filter(entry -> entry.getType() == LedgerEntryType.SPIN_OFF)
                    .filter(entry -> SPIN_OFF_DATE.equals(entry.getDateTime()))
                    .findFirst().orElseThrow();
}

private static LedgerProjectionRef projection(LedgerEntry entry, LedgerProjectionRole role)
{
    return entry.getProjectionRefs().stream()
                    .filter(ref -> ref.getRole() == role)
                    .findFirst().orElseThrow();
}

private static LedgerPosting securityPosting(LedgerEntry entry, Security security, CorporateActionLeg leg,
                Security targetSecurity)
{
    return entry.getPostings().stream()
                    .filter(posting -> posting.getType() == LedgerPostingType.SECURITY)
                    .filter(posting -> posting.getSecurity() == security)
                    .filter(posting -> hasCorporateActionLeg(posting, leg))
                    .filter(posting -> hasTargetSecurity(posting, targetSecurity))
                    .findFirst().orElseThrow();
}

private static LedgerPosting cashPosting(LedgerEntry entry, Account account, long amount)
{
    return entry.getPostings().stream()
                    .filter(posting -> posting.getType() == LedgerPostingType.CASH_COMPENSATION)
                    .filter(posting -> posting.getAccount() == account)
                    .filter(posting -> posting.getAmount() == amount)
                    .findFirst().orElseThrow();
}

private static PortfolioTransaction portfolioTransaction(Portfolio portfolio, PortfolioTransaction.Type type,
                Security security)
{
    return portfolio.getTransactions().stream()
                    .filter(transaction -> transaction.getType() == type)
                    .filter(transaction -> transaction.getSecurity() == security)
                    .findFirst().orElseThrow();
}

private static PortfolioTransaction portfolioProjection(Portfolio portfolio, LedgerProjectionRole role,
                Security security)
{
    return portfolio.getTransactions().stream()
                    .filter(LedgerBackedTransaction.class::isInstance)
                    .filter(transaction -> ((LedgerBackedTransaction) transaction).getLedgerProjectionRef()
                                    .getRole() == role)
                    .filter(transaction -> transaction.getSecurity() == security)
                    .findFirst().orElseThrow();
}

private static AccountTransaction accountProjection(Account account, LedgerProjectionRole role)
{
    return account.getTransactions().stream()
                    .filter(LedgerBackedTransaction.class::isInstance)
                    .filter(transaction -> ((LedgerBackedTransaction) transaction).getLedgerProjectionRef()
                                    .getRole() == role)
                    .findFirst().orElseThrow();
}

private static boolean hasCorporateActionLeg(LedgerPosting posting, CorporateActionLeg leg)
{
    return posting.getParameters().stream()
                    .filter(parameter -> parameter.getType() == LedgerParameterType.CORPORATE_ACTION_LEG)
                    .anyMatch(parameter -> leg.getCode().equals(parameter.getValue()));
}

private static boolean hasTargetSecurity(LedgerPosting posting, Security security)
{
    return posting.getParameters().stream()
                    .filter(parameter -> parameter.getType() == LedgerParameterType.TARGET_SECURITY)
                    .anyMatch(parameter -> parameter.getValue() == security);
}

private static Security securityByIsin(Client client, String isin)
{
    return client.getSecurities().stream()
                    .filter(security -> isin.equals(security.getIsin()))
                    .findFirst().orElseThrow();
}

private static Account account(String name)
{
    var account = new Account();

    account.setName(name);
    account.setCurrencyCode(CurrencyUnit.EUR);

    return account;
}

private static Portfolio portfolio(String name, Account referenceAccount)
{
    var portfolio = new Portfolio();

    portfolio.setName(name);
    portfolio.setReferenceAccount(referenceAccount);

    return portfolio;
}

private static Security security(String name, String isin)
{
    var security = new Security(name, CurrencyUnit.EUR);

    security.setIsin(isin);

    return security;
}

private static Money eur(long amount)
{
    return Money.of(CurrencyUnit.EUR, amount);
}

private static void assertEquals(Object expected, Object actual, String label)
{
    if (!java.util.Objects.equals(expected, actual))
        throw new IllegalStateException(label + ": expected <" + expected + "> but was <" + actual + ">");
}

@Morpheus1w3 Morpheus1w3 marked this pull request as ready for review June 27, 2026 03:50
@Morpheus1w3 Morpheus1w3 changed the title Portfolio Performance Ledger Feature Portfolio Performance - Ledger Jun 27, 2026
@Morpheus1w3 Morpheus1w3 changed the title Portfolio Performance - Ledger Portfolio Performance - Ledger (proposal towards Corporate Actions) Jun 27, 2026
@Morpheus1w3 Morpheus1w3 changed the title Portfolio Performance - Ledger (proposal towards Corporate Actions) Ledger (proposal towards Corporate Actions) Jun 27, 2026
@Morpheus1w3 Morpheus1w3 changed the title Ledger (proposal towards Corporate Actions) Ledger Implementation - Corporate Actions framework Jun 27, 2026
@Morpheus1w3 Morpheus1w3 changed the title Ledger Implementation - Corporate Actions framework Ledger Implementation - Corporate Actions Jun 27, 2026
@buchen buchen self-assigned this Jun 28, 2026
@buchen

buchen commented Jun 28, 2026

Copy link
Copy Markdown
Member

Hi @Morpheus1w3,

thanks for the contribution. I like the idea of a ledger with each entry including the posting to the various accounts. I think that is a prerequisite to support more (complex) corporate actions. I also like that it keeps the existing transactions available for existing code to allow for a more incremental migration (at least this is my understanding so far).

I'll need some time to digest this. As you write, this pull request "touches the model layer, validation, projection materialization, XML persistence, Protobuf persistence, mutation paths, compatibility creators, dialog integration, importer guardrails, investment-plan references, reporting/snapshot access, diagnostics, and NLS entries." To put it into the perspective: the whole of PP is roughly 205K Line of Code (if I leave out the very verbose PDF importer tests for a moment). This pull request has "71856 added lines" (I assume it is lines not lines of code, but still). And until now, I used to look at every single line of every pull request... as I will have to maintain the code as I did it for the last 14 years.

I would love to have a conversation about this with you. Please drop me an email at portfolio dot performance dot help at gmail dot com and we can have a zoom session. Some apparent complexities of the approach are better understood if I understand the thinking behind it.

Andreas.

@Morpheus1w3

Copy link
Copy Markdown
Contributor Author

Hi @buchen,

the eMail was send yesterday as requested.

Regarding the whole size of the PR, based on the master as of 12th June, the existing model remain unchanged.

Key Value
Java Lies of Source (as of 12th June) 246.026
Existing Classes - Added 4.665
Existing Classes - Deleted 1.355
New Classes - Added Lines 31.029
New Classes - Added Files 154

The java class changes were made in the following packages, distinguished between existig classes and new created classes:

Core Resources

Projekt Folder File Type Lines of Source
as of 12th June
Addition (existing files) Addition (new files) Deletion (existig files)
name.abuchen.portfolio META-INF MF 85 1 0
name.abuchen.portfolio META-INF/services Check 17 1 5
name.abuchen.portfolio protos/name/abuchen/portfolio/
model/proto/v1
java 41.487 870 11.409 125
name.abuchen.portfolio java 483 71 0
name.abuchen.portfolio properties 6.783 2.698 0
name.abuchen.portfolio checks.impl java 1.939 172 39 1.013
name.abuchen.portfolio datatransfer.actions java 698 190 69
name.abuchen.portfolio model java 10.726 1.995 995 53
name.abuchen.portfolio model properties 419 0 0
name.abuchen.portfolio model proto 292 144 25
name.abuchen.portfolio model.ledger java 2.842
name.abuchen.portfolio model.ledger.compatibility java 7.119
name.abuchen.portfolio model.ledger.configuration java 3.498
name.abuchen.portfolio model.ledger.configuration.rule java 510
name.abuchen.portfolio model.ledger.legacy java 1.129
name.abuchen.portfolio model.ledger.nativeentry java 1.383
name.abuchen.portfolio model.ledger.projection java 1.729
name.abuchen.portfolio snapshot java 3.638 33 1
name.abuchen.portfolio snapshot properties 229 0 0
name.abuchen.portfolio.ui java 2.100 51 0
name.abuchen.portfolio.ui properties 27.949 1.938 0
name.abuchen.portfolio.ui dialogs.transactions java 4.458 251 223 19
name.abuchen.portfolio.ui jobs java 576 1 1
name.abuchen.portfolio.ui preferences java 3.385 2 2
name.abuchen.portfolio.ui util.viewers java 3.874 531 18
name.abuchen.portfolio.ui views java 14.493 289 37
name.abuchen.portfolio.ui views properties 96 0 0
name.abuchen.portfolio.ui views.actions java 487 113 153 0
name.abuchen.portfolio.ui views.panes java 2.403 84 13
name.abuchen.portfolio.ui wizards.splits java 562 12 4
127.179 9.447 31.029 1.385

Test Cases

Projekt Folder File Type Lines of Source
as of 12th June
Addition (existing files) Addition (new files) Deletion (existig files)
name.abuchen.portfolio.tests java 98 12 0
name.abuchen.portfolio.tests checks.impl java 218 545 194 23
name.abuchen.portfolio.tests datatransfer.actions java 828 839 423 15
name.abuchen.portfolio.tests model java 2.459 577 3.035 0
name.abuchen.portfolio.tests model.ledger java 10.775
name.abuchen.portfolio.tests model.ledger xml 676
name.abuchen.portfolio.tests model.ledger.compatibility java 6.585
name.abuchen.portfolio.tests model.ledger.legacy java 1.689
name.abuchen.portfolio.tests model.ledger.nativeentry java 744
name.abuchen.portfolio.tests model.ledger.projection java 638
name.abuchen.portfolio.tests snapshot java 1.957 205 0
name.abuchen.portfolio.tests src/scenarios java 1.730 28 6
name.abuchen.portfolio.tests src/scenarios xml 9.001 0 0
name.abuchen.portfolio.ui.tests java 48 12 0
name.abuchen.portfolio.ui.tests dialogs.transactions java 165 185 1.329 0
name.abuchen.portfolio.ui.tests util.viewers java 49 12 0
name.abuchen.portfolio.ui.tests views java 720 0 2.297 0
name.abuchen.portfolio.ui.tests views xml 196 0 0
name.abuchen.portfolio.ui.tests views.actions java 990
name.abuchen.portfolio.ui.tests wizards.splits java 74 64 232 0
17.543 2.479 29.607 44

@hporten

hporten commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Just curious: I was trying to follow the changes. Looks sophisticated. But since I as a human was overwhelmed: were those changes generated with an LLM by chance? :)

@Morpheus1w3

Copy link
Copy Markdown
Contributor Author

@hporten
Yes, it is LLM-assisted. Actually, heavily.

I used ChatGPT to structure the problem, challenge the architecture, and prepare review/documentation notes. I used Codex heavily for implementation, tests, refactoring, and repeated iterations on the branch.

I do not want to hide that. At the same time, it was not a random “let the AI generate something” exercise. The underlying problem and constraint were clear to me: Portfolio Performance needs a way to support more complex corporate actions without forcing them into dividend/buy/delivery workarounds, but also without throwing away "AccountTransaction", "PortfolioTransaction", "CrossEntry", and all existing UI/reporting infrastructure that depends on them.

That is the reason for the Ledger approach in this PR: keep the existing transaction classes available as compatibility views, but introduce a persisted Ledger underneath them as the source of truth.

Whether this is the right direction for PP is exactly the open question. I see this PR more as a proof of capability and architecture proposal than as a small ready-to-merge feature patch. If the direction makes sense, it would probably need to be reviewed, reduced, reshaped, or split into smaller steps before it could realistically be maintained long term.

@hporten

hporten commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Thanks for preparing this proof-of-concept btw. I was following your changes as they were committed. Especially the fact that the "views" apparently continued to work is an applaudable achievement. My comment is really just subjective as a mortal human being: just consuming the volume of the documentation that goes along with the change was challenging. Without a doubt this is due to the complexity of the solved problem. And I am not that deep into the PP code base, yet. So those with a deeper knowledge of the code will have to judge the impact on future maintainability. LLMs are not necessarily optimizing for that. But one may have to bite the bullet for the targetted features...

@Morpheus1w3

Copy link
Copy Markdown
Contributor Author

Hi @hporten,

thanks, that is a fair point.

I also see maintainability as the key question here. The PR is not meant to bypass normal review or present the current shape as final; it is mainly a proof of capability for a ledger-based foundation while keeping the existing views usable. If the direction is considered useful, I fully expect that the approach may need to be reduced, reshaped, or split into smaller steps so that it can be reviewed and maintained by humans over time.

@buchen buchen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hey @Morpheus1w3,

I started digging into the code. It looks really good (and, yes, we all use AI these days but it is also about how to prompt those tools).

I started in familiar territory—the persistence layer—and will work my way up. Please take my comments as conversation starters rather than final judgments. There are a few areas where I’d like to better understand the prerequisites, assumptions, and design boundaries before jumping to conclusions.

If you have a chance, please send me an email so we can continue the discussion outside of GitHub. I think a short conversation would be much more productive than long comment threads.

For the time being, please don't force-push into the pull request. I’d like to avoid the review becoming a moving target. Feel free to add follow-up commits instead. I also tried to anchor my comments to specific lines so we can use each thread to discuss a particular topic.

Andreas.

Comment thread name.abuchen.portfolio/src/name/abuchen/portfolio/model/ProtobufWriter.java Outdated
Comment thread name.abuchen.portfolio/src/name/abuchen/portfolio/model/ProtobufWriter.java Outdated
Comment on lines +88 to +91
return entry.getPostings().stream() //
.filter(posting -> posting != primaryPosting) //
.filter(LedgerProjectionSupport::isUnitPosting) //
.map(LedgerProjectionSupport::unit);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A behavioral change worth pointing out: for a legacy-fixed BUY/SELL, units() returns every non-primary unit-posting to whichever projection asks, so the account side of a buy/sell now exposes FEE/TAX units. The legacy BuySellEntry.accountTransaction never carried units — consumers historically read buy/sell fees and taxes off the portfolio side only. So this is currently benign (the duplicate units on the account side just aren't read by the snapshots we checked), but it's an unmitigated latent trap: any consumer that sums units across account transactions — or a future one — would double-count buy/sell fees/taxes. I'd prefer the account projection of a buy/sell expose no units, matching legacy behavior, rather than relying on every reader to know to ignore them. Can we scope unit resolution by projection role instead of returning the whole entry's units to both sides?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are right about the current behavior. The account-side BUY/SELL projection does expose fee/tax/gross-value units through getUnits(). Some existing consumers are protected because they branch by transaction type and use the portfolio side for BUY/SELL, so this is not proven current snapshot double counting. But it is still a mismatch with the historical BuySellEntry.accountTransaction shape and a latent API risk for generic account-transaction consumers. I agree the safer direction is projection-role-scoped unit resolution: keep BUY/SELL units on the portfolio projection and suppress them on the account projection unless there is a specific legacy-compatible reason.

@buchen

buchen commented Jul 4, 2026

Copy link
Copy Markdown
Member

Semantic enhancemnt to cover ISO 20025 seev.036 Registry.md

The "Registry.md" is attached here - could you be so kind and post the link?

Sorry, I did not have time to look at the additional 30+ commits and will not find time this weekend because I am on the road. At the end of the day, if I decide to move to a ledger (this is a significant re-write - therefore my prototype to support corporate actions w/o ledger in #5830), then I see this pull request as a quarry not a merge source. Any delivery has to be done in manageable phases.

I am still thinking about this, but from a high-level I could see:

Stage On disk Truth Version Purpose
0 — Observe unchanged legacy stays 69 Build ledger in-memory on load, run parity, discard. No writes. Learn projection fidelity.
1 — Persist legacy + ledger (both formats) legacy bump 70 Dual-write. Legacy still truth → still non-destructive. Learn persistence round-trip safety.
2 — Flip legacy shadow + ledger ledger, per file 70 A file flips only when it individually passes parity. Build-level eligibility dial layered on top. This is the finish line.
3 — Contract ledger only ledger Drop legacy shadows.

With a delivery along the lines of:

  1. Triaged independent fixes first — every non-ledger hunk sorted by "if the ledger work
    were cancelled tomorrow, would this still be correct and worth keeping?"
    Yes → merge now,
    standalone. No → it's a coupled prerequisite, stays welded to its stage.
  2. 0a model + enums (pure data, no callers)
  3. 0b migrator (legacy → ledger, deterministic — verified)
  4. 0c projection (ledger → legacy)
  5. 0d parity oracle + on-demand check + CI gate (first user-visible surface)
  6. Stage 1 dual-write persistence (both formats) + version bump, behind flag
  7. Stage 2 per-file flip + eligibility dial

I tried to experiment with phase 0a - still coming in at 7000 lines with 63 files. Very hard to digest. I haven't pruned the corporate actions yet though. I assume a first step would be to achieve parity of the existing transactions types before adding new ledger entry types for corporate actions.

@Morpheus1w3

Copy link
Copy Markdown
Contributor Author

@buchen

I am going to withdraw the open Ledger-related pull requests for now.

The current branch has grown beyond a size that is realistically reviewable as a merge source, and I do not want to keep adding more work without a clear first slice that would actually be reviewed. I understand the concern about manageable phases, but at this point the work is better treated as prototype/quarry material rather than an active PR.

I will keep the branches and commits available as reference material, but I am stopping active development on these PRs.

@buchen

buchen commented Jul 4, 2026

Copy link
Copy Markdown
Member

@Morpheus1w3 writes:

As you've asked for the registry.md content

Sorry. What I meant is the source of this information - a link, a PDF document. If that was generated, don't worry. That is also okay.

The current branch has grown beyond a size that is realistically reviewable as a merge source, and I do not want to keep adding more work without a clear first slice that would actually be reviewed. I understand the concern about manageable phases, but at this point the work is better treated as prototype/quarry material rather than an active PR.

I agree. That is also my understanding. In an earlier comment you wrote: "I see this PR more as a proof of capability and architecture proposal than as a small ready-to-merge feature patch." (emphasis mine). The "quarry" approach makes the most sense.

For example, I believe it could make sense to first migrate the existing transactions to the ledger before adding new ones. Adding spin-off support to PP is more than adding the ledger entry type. With the PR we know how the shape of a spin-off would look like, but the merged code does not contain the spin-off yet.

Please leave the pull request open. It is super valuable for the quarry work.

@Morpheus1w3 What are your thoughts on the first draft of possible phases I jotted down in an earlier comment? Would you shape them differently?

@buchen

buchen commented Jul 4, 2026

Copy link
Copy Markdown
Member

Add drafted #5836 to have a basis for the discussion around phases.

@Morpheus1w3

Copy link
Copy Markdown
Contributor Author

As you've asked for the registry.md content

Sorry. What I meant is the source of this information - a link, a PDF document. If that was generated, don't worry. That is also okay.

@buchen
The sourcing n..n carnallite for security events are downloaded at https://www.iso20022.org/iso-20022-message-definitions?search=seev, section seev.036

archive_corporate_actions_17_f7a208bf82.zip

Add a SECURITY_CONTEXT corporate action leg and a non-projecting native leg role so corporate action entries can carry depot-specific security context without reusing source or target movement semantics.

This keeps the existing Security => Portfolio invariant meaningful for context rows and adds validation, descriptor, and XML/protobuf roundtrip coverage for non-economic context postings.

This intentionally leaves full SEC_CTX profile rules, primary-movement one-of validation, BASIS, interest-detail semantics, UI, importers, schema changes, and legacy transaction mappings unchanged.
Register SECURITY_CONTEXT legs on the supported corporate action definitions using the existing native Ledger leg-cardinality model.

This applies required context to must-profile kinds and optional context to should or optional profiles while keeping context non-projecting and depot-specific through Portfolio plus Security.

This intentionally leaves PRIMARY_MOVEMENT one-of rules, BASIS, correction and reversal semantics, mandatory interest detail rules, Cash Dividend registration, schemas, and legacy PTransaction mappings unchanged.
Align the supported Ledger corporate action subset with Registry.md by requiring a Spin-off target/security-in leg and by documenting overview-only cash distribution names as intentionally absent from the current code domain.

This keeps the Java registry honest about what is currently representable while preserving the kind-only registrations for profiles blocked by primary-movement, basis, correction, reversal, and interest-detail semantics.

No XML or protobuf schema, legacy transaction type mapping, descriptor/materializer behavior, assembler API, UUID selector, projection membership, or Markdown/Registry.md inputs were changed.
Added configurable primary movement requirement predicates for native Ledger corporate action definitions and wired them into native definition validation.

This lets DEFAULTED_INTEREST, RESTRUCTURING, and DEFAULT require at least one real booked movement while excluding context, forex, valuation metadata, correction, reversal, and basis facts.

This intentionally leaves UI, importer, report, tax, performance, BASIS, correction/reversal, interest-detail, legacy transaction mapping, and persistence schema behavior unchanged.
Added CASH_DISTRIBUTION as the canonical corporate action kind and registered its security-context plus required cash movement definition.

This models the Registry.md cash distribution row without adding a CASH_DIVIDEND synonym or changing projection behavior.

This intentionally leaves BASIS, correction/reversal, mandatory interest-detail semantics, UI, importer, reporting, tax/performance logic, legacy transaction mapping, and persistence schema unchanged.
Add a reusable Ledger component requirement that lets native corporate action definitions require a detail leg on the same groupKey as a primary movement. Wire the rule into coupon payment cash legs and PIK interest target security legs, and validate missing or mismatched accrued-interest details.

This is needed so mandatory interest-detail semantics are checked by definition metadata instead of being faked as detached required legs or kind-specific validator branches.

Correction and reversal semantics, BASIS support, UI/report/importer work, projection behavior, protobuf schema, and legacy PTransaction mappings are intentionally left unchanged.
Add entry-level corporate action basis treatment metadata with status and method code domains plus semantic allocation rows persisted through existing Ledger parameters.

Validate provided percentage allocations by semantic leg role, local key, and optional group key so allocations do not depend on posting UUIDs, list order, projections, or legacy transaction types.

Leave automatic cost-basis calculation, amount-allocation validation, provider/source categories, BASIS hard requirements, UI, importers, reports, tax logic, FIFO, correction, and reversal semantics unchanged.
Adds a contributor-facing corporate action builder on top of the native Ledger assembler, including helpers for security context, security in/out, cash, fee, tax, principal, accrued interest, event dates, and basis allocations.

This gives tests and service code a semantic way to create and edit native corporate actions without constructing low-level postings or selecting by UUID.

Projection generalization, UI, importers, reporting, automatic basis calculation, correction/reversal, and legacy transaction type changes are intentionally left unchanged.
Adds cash-oriented corporate action descriptors and materialization for cash distributions and coupon payments, including repeated cash leg semantic keys and existing account transaction type mapping.

This makes supported cash corporate actions visible through Ledger-backed account projections while preserving no-UUID selection by role, localKey, and groupKey.

UI, importers, reports, tax/FIFO/performance, automatic basis calculation, correction/reversal, protobuf/XML schema, and new legacy transaction types are intentionally unchanged.
Adds corporate action descriptor derivation and materialization coverage for stock dividends, bonus issues, rights distributions, and PIK interest, including repeated inbound security semantic keys and optional cash compensation projections.

This exposes supported security-in corporate actions through Ledger-backed portfolio and account projections while preserving no-UUID selection by role, localKey, and groupKey.

UI, importers, reports, tax/FIFO/performance, automatic basis calculation, correction/reversal, XML/protobuf schema, and new legacy transaction types are intentionally unchanged.
Adds descriptor derivation and materialization coverage for maturity, partial redemption, call, and put corporate actions, including outbound portfolio projections and redemption cash account projections.

This makes fixed-income redemption Ledger entries visible through existing legacy projection types while preserving semantic selection by role, localKey, and groupKey.

Conversion and exchange projections, UI, importers, reports, tax/FIFO/performance, automatic basis calculation, correction/reversal, XML/protobuf schema, and new legacy transaction types are intentionally unchanged.
Adds descriptor derivation and materialization coverage for conversion and exchange corporate actions, including source security-out, target security-in, and optional cash compensation projections.

This makes conversion and exchange Ledger entries visible through existing Ledger-backed portfolio and account projection types while preserving semantic selection by role, localKey, and groupKey.

Defaulted interest, restructuring, default projections, UI, importers, reports, tax/FIFO/performance, automatic basis calculation, correction/reversal, XML/protobuf schema, and new legacy transaction types are intentionally unchanged.
Adds generic projection descriptor support for open-movement corporate actions so defaulted interest, restructuring, and default entries project their actual cash, security-in, security-out, and standalone fee/tax movements.

This closes the remaining Ledger projection gap for broad corporate-action profiles while preserving semantic selection by role, localKey, and groupKey and reusing existing legacy projection types.

UI, importers, reports, tax/FIFO/performance, automatic basis calculation, correction/reversal, XML/protobuf schema, and new legacy transaction types are intentionally unchanged.
Adds an isolated fluent end-to-end test suite covering every registered Ledger corporate-action kind through create, validate, materialize, semantic edit, XML/protobuf roundtrip, and delete.

This gives contributors readable usage examples and verifies the final service/API/projection readiness path without depending on UI/import/reporting layers.

Existing tests, UI/importers/reports/tax/FIFO/performance, correction/reversal, legacy transaction types, and XML/protobuf schema are intentionally unchanged.
Adds a contributor-facing Corporate Action view, query, and leg handle API over semantic role, localKey, and groupKey addresses.

This makes native corporate-action entries easier to query and edit by business attributes while preserving the existing no-UUID semantic truth model.

Persistence, projections, legacy transaction types, UI, importers, reports, tax, FIFO, performance, automatic basis calculation, and correction/reversal behavior are intentionally unchanged.
Removes earlier single-slice Ledger corporate action tests whose happy-path creation, edit, roundtrip, projection, and delete coverage is now covered by the fluent end-to-end and family projection suites.

This keeps validator, registry, projection, persistence, diagnostic, and no-UUID coverage focused on unique behaviors while reducing duplicated maintenance load.

Production code, registry definitions, persistence schemas, projections, legacy transaction types, UI/import/report/tax/FIFO/performance, and correction/reversal behavior are intentionally unchanged.
Reduces the corporate-action projection-family tests to representative edge cases for repeated semantic keys, grouping, non-projecting details, standalone fee movement, and mixed open movement projection.

The all-kind fluent end-to-end suite now carries the per-action happy-path workflow, so the projection tests no longer repeat each historical implementation slice.

Production code, registry definitions, persistence schemas, projection behavior, legacy transaction types, UI/import/report/tax/FIFO/performance, and correction/reversal behavior are intentionally unchanged.
Removes historical Ledger slice tests whose happy-path coverage is now exercised by the fluent corporate-action end-to-end suite, compact projection tests, and persistence tests.

This reduces maintenance noise while keeping validator, persistence, projection edge, UI guardrail, diagnostic, and service write-path coverage focused on unique contracts.

Production code, registry definitions, schemas, projection behavior, legacy transaction types, UI/import/report/tax/FIFO/performance, and correction/reversal behavior are intentionally unchanged.
Removes historical low-level Ledger compatibility, native assembler, transaction creator, spin-off scenario, and save/load parity slice tests while keeping the durable migration, persistence, projection, editor, validator, and UI guardrail coverage as the source of truth.

This reduces the broad Ledger test inventory substantially and keeps the real Ledger-specific suite below the requested 30k-line target when non-Ledger importer false positives are excluded.

Production code, registry definitions, schemas, projection behavior, legacy transaction types, UI/import/report/tax/FIFO/performance, and correction/reversal behavior are intentionally unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants