Skip to content

Complete accounting brainstorm remainders: NAV math, period-close intake, dividend withholding - #2149

Merged
rodoHasArrived merged 10 commits into
mainfrom
claude/data-provider-accounting-brainstorm-o73lsw
Jul 6, 2026
Merged

Complete accounting brainstorm remainders: NAV math, period-close intake, dividend withholding#2149
rodoHasArrived merged 10 commits into
mainfrom
claude/data-provider-accounting-brainstorm-o73lsw

Conversation

@rodoHasArrived

Copy link
Copy Markdown
Owner

Summary

Implements the three remaining accounting lanes from docs/product/data-provider-accounting-brainstorm-2026-07.md (merged in #2100):

  • NAV aggregation fix (idea 6 remainder): NavAttributionService computed totalNav by summing every account's normal balance — double-counting the fund, since Assets − Liabilities = Equity + Net Income. It now computes assets − liabilities, with ByAssetClass restricted to signed asset/liability contributions so the breakdown decomposes the corrected total. Components still report every account. Covered by new NavAttributionServiceTests (4 tests).
  • Period-close closing entries wired into close management (idea 8): the previously caller-less PeriodCloseProjector/PeriodCloseDraftBuilder are now reachable end-to-end: AutomatedJournalIntakeRunner.RunPeriodCloseIntakeAsync fetches a closed period's trial balance from ILedgerBookService, projects the revenue/expense → retained-earnings roll, and lands the governed draft in the manual journal workbench queue through the new /api/ledger/journal-automation/period-close-intake route (mirroring the existing dividend/fee intake routes). Open or missing periods are rejected loudly; a period with no temporary balances returns an empty intake; re-runs dedupe on the period-close|{periodId} idempotency key. AutomatedJournalDraftIntakeService gains a prepared-draft path (IntakeDraftsAsync) sharing dedup, chart mapping, and the human submit/approve lifecycle with event-projected drafts. 5 new runner tests.
  • Dividend withholding-tax accrual (idea 7 remainder): the corporate-action dividend intake lane accepts an optional WithholdingTaxRate (0 ≤ rate < 1) and produces a paired WithholdingTaxAccrued event per declared dividend — same corporate-action evidence, distinct corp-act-dividend-wht|… idempotency key. Plumbed through RunDividendDraftIntakeRequest and the existing endpoint. 3 new producer tests. (Management/performance-fee producers already existed and are wired; this closes the last missing accrual flow.)

Also included:

Reason

Follow-up request to complete the NAV aggregation fix (#6 remainder), wire PeriodCloseProjector into close management (#8), and finish the fee-accrual lane (#7 remainder) identified by the brainstorm's 2026-07-06 status review.

Testing performed

  • bash scripts/ci.sh completed successfully
  • Relevant unit tests were added or updated
  • Relevant integration tests were added or updated
  • GitHub Actions quality-gate passed

Local validation: dotnet build Meridian.sln -c Release /p:EnableWindowsTargeting=true → 0 errors. Targeted suites all green: NavAttributionServiceTests (4/4), AutomatedJournalEventProducerTests + AutomatedJournalDraftIntakeServiceTests (21/21 including 8 new), WorkstationContractSnapshotTests + WorkstationEndpointContractCompatibilityTests (15/15), WorkstationEndpointsTests (265/265). Docs automation core profile regenerated cleanly. Full quality-gate runs on this PR.

Safety review

  • This pull request targets main
  • No direct push to main was performed
  • No tests were disabled or bypassed
  • No secrets or credentials were committed
  • No unrelated changes were included

Governance changes

Check every governance file modified:

  • No governance files changed
  • .github/workflows/**
  • .github/CODEOWNERS
  • .github/pull_request_template.md
  • AGENTS.md
  • scripts/ci.sh

🤖 Generated with Claude Code

https://claude.ai/code/session_01PdMuw9yNjY451tw5VEordn


Generated by Claude Code

…ake, dividend withholding

Implements the three remaining accounting lanes from the data-provider/
accounting brainstorm:

- NavAttributionService now computes NAV as assets minus liabilities
  instead of summing every account's normal balance, with ByAssetClass
  decomposing the corrected total; covered by NavAttributionServiceTests.
- Period-close closing entries are wired into close management:
  AutomatedJournalIntakeRunner.RunPeriodCloseIntakeAsync projects
  PeriodCloseProjector output from a closed period's trial balance and
  lands the governed draft in the manual journal workbench queue via the
  new /api/ledger/journal-automation/period-close-intake route. The
  intake service gains a prepared-draft path (IntakeDraftsAsync) sharing
  dedup, chart mapping, and the human approve lifecycle.
- The corporate-action dividend intake lane gains an optional
  WithholdingTaxRate that accrues paired WithholdingTaxAccrued drafts
  per declared dividend with shared evidence and distinct idempotency.

Also fixes the governance-readiness dashboard's false gap by scanning
UiApiRoutes.cs for constant-backed routes, refreshes the brainstorm
document's status table, restores its product README link, and
regenerates the route mirror and status dashboards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdMuw9yNjY451tw5VEordn
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

claude added 5 commits July 6, 2026 05:32
The tracker's source fingerprint covers the test-suite text scan, so
the new test files shifted it; regenerates via
scripts/generate-diagrams.mjs --tracker-only to satisfy the
regenerate-docs check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdMuw9yNjY451tw5VEordn
…ze cap

Adding the period-close intake endpoint pushed LedgerEndpoints.cs past
its file-size ratchet baseline (3754 > 3712). Rather than grow the
baseline, move the three journal-automation intake endpoints (dividend,
fee-accrual, period-close) into a LedgerEndpoints.JournalAutomation.cs
partial, matching the WorkstationEndpoints.*.cs pattern. Behavior is
unchanged; the class is now partial and MapLedgerEndpoints calls
MapJournalAutomationEndpoints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdMuw9yNjY451tw5VEordn
Merges latest main and refreshes the repository structure, doc-health
dashboard, and API-contract dashboards so the generated outputs include
the new LedgerEndpoints.JournalAutomation.cs partial, satisfying the
regenerate-docs check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdMuw9yNjY451tw5VEordn
The consolidated portfolio cut's shadow NAV is nav.TotalNav from the
NAV attribution service. With the NAV aggregation fix, that is now
assets minus liabilities (1000) rather than the sum of every account's
normal balance (2000). Updates the structured-export row expectation to
match; the -150 shadow-NAV variance already reflects the corrected NAV
against run/account equity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdMuw9yNjY451tw5VEordn
@rodoHasArrived
rodoHasArrived marked this pull request as ready for review July 6, 2026 07:58

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f53cb3ef7d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

request.Currency,
[draft],
request.Actor,
request.LedgerBookId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bind closing drafts to the period's ledger book

When the period-close endpoint is called without LedgerBookId (the new request makes it optional) or with an unrelated book id, this path saves the generated closing-entry draft under request.LedgerBookId instead of the summary.LedgerBookId returned for the closed period. The manual journal workbench filters drafts by ledger book, so a valid closed period can create an unscoped or wrong-book draft that the actual book's close/reporting workflow will not see or control; use summary.LedgerBookId and reject mismatches.

Useful? React with 👍 / 👎.


var projection = PeriodCloseProjector.Project(new PeriodCloseInput(
request.PeriodId.ToString("D"),
summary.CompletedAt,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use a persisted close date for soft closes

For soft-closed periods, PostgresLedgerBookService.GetPeriodSummaryAsync currently returns CompletedAt as DateTimeOffset.UtcNow because soft close does not persist ClosedAt; passing that value into PeriodCloseInput makes the draft accounting date depend on when this intake endpoint is run. If a June period is soft-closed and the intake runs days later, the closing entries are dated to the run day rather than the close/period date, skewing date-based NAV and reporting until corrected.

Useful? React with 👍 / 👎.

… end

Addresses two review findings on the period-close intake path:

- Bind the closing-entry draft to the closed period's authoritative
  ledger book (summary.LedgerBookId) instead of the optional request
  book id. The workbench filters drafts by book, so a null or mismatched
  request book id would orphan the draft; a supplied book id that does
  not match the period is now rejected loudly.
- Date closing entries to the period's end date rather than the summary
  CompletedAt. Soft close does not persist a close timestamp (the
  summary reports the current time), which would make the accounting
  date drift with the run time and skew date-based NAV/reporting.

Adds tests for the period-end dating, book binding when the request
omits the book id, and mismatched-book rejection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdMuw9yNjY451tw5VEordn

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a8e0865e11

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +212 to +213
var account = new LedgerAccount(line.AccountName, accountType, line.Symbol, line.FinancialAccountId);
balances[account] = balances.GetValueOrDefault(account) + line.Balance;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve trial-balance dimensions in close drafts

When a closed period has trial-balance rows split by LedgerDimensionSetDto (for example the same revenue account across two entities or sleeves), this conversion keys only the ledger account identity and then sums the balances, so LedgerPeriodTrialBalanceLineDto.Dimensions is dropped before the closing draft is built. The manual posting path already supports per-line Dimensions, so the generated close posts one unscoped aggregate instead of zeroing the dimension-specific revenue/expense balances, leaving entity/sleeve P&L and retained earnings wrong after the close; keep the dimension key and line dimensions through the period-close projection.

Useful? React with 👍 / 👎.

Addresses the review finding that a dimension-split trial balance (the
same revenue/expense account across two entities or sleeves) was
collapsed to one unscoped aggregate closing entry, leaving entity/sleeve
P&L and retained earnings wrong after the close.

Threads LedgerLineDimensionSet through the whole close path:
- PeriodCloseInput now carries a list of dimensioned trial-balance rows
  (PeriodCloseAccountBalance); a dictionary overload keeps the
  dimension-flat in-memory ledger path working.
- PeriodCloseProjector zeroes each account+dimension slice independently
  and rolls retained earnings per (financial-account, dimension) scope,
  with each closing and retained-earnings line carrying its source
  dimensions. NetIncomeByScope stays a per-financial-account summary.
- PeriodCloseLine and PeriodCloseProjection.JournalLines carry
  dimensions; the shared AutomatedJournalDraft line and the approval to
  JournalEntry conversion do too, so posting is dimension-aware.
  Non-dimensional event drafts (dividends, fees, marks) pass null.
- AutomatedJournalDraftIntakeService emits per-line Dimensions on the
  workbench draft; the runner preserves each trial-balance row's
  dimensional scope instead of summing by account. Adds a shared
  LedgerDimensionMapper for the core/contract conversion.

Adds core and intake tests for the dimension-split close.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdMuw9yNjY451tw5VEordn

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b9554760c5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

[draft],
request.Actor,
summary.LedgerBookId,
request.PeriodId.ToString("D"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use an approvable period for close drafts

When the real workbench is backed by an ILedgerJournalStore, this PeriodId is the same soft/hard-closed period required earlier in the runner. ManualJournalEntryWorkbenchService.ValidateLedgerBookPeriodScopeAsync adds critical manual-je.period-closed for any non-Open period, and SubmitApprovalAsync refuses drafts with critical validation issues, so the generated closing-entry draft is saved as NeedsFix and cannot be approved/posted as generated in the normal Postgres path. Route close entries through an open adjustment/closing-entry period or add a dedicated close-entry override instead of stamping the closed period id.

Useful? React with 👍 / 👎.

…d periods

Addresses the review finding that closing-entry drafts stamped with the
closed period id could never be approved/posted against a real
ILedgerJournalStore: the workbench adds a critical manual-je.period-closed
issue for non-Open periods, and the posting guard bars postings to
soft-closed (non-adjustment) and hard-closed periods. Closing entries
must post into the period being closed to finalize it.

Introduces a dedicated, governed exception:
- New LedgerPostingKindDto.ClosingEntry and ManualJournalEntryTypeDto.
  ClosingEntry (with matching TS mirrors and a DB check-constraint
  migration allowing the value).
- LedgerPeriodPostingGuard permits ClosingEntry postings into soft- and
  hard-closed periods (still constrained to the period's date range).
- The workbench period-scope validation skips the closed-period critical
  for closing entries, and BuildManualPostingKind maps the ClosingEntry
  entry type to the ClosingEntry posting kind.
- The period-close intake stamps closing drafts with the ClosingEntry
  entry type.

Adds store-guard tests proving ClosingEntry posts into soft- and
hard-closed periods, and asserts the intake stamps the ClosingEntry type
- the store-enforced coverage that was missing when the closed-period
posting gap slipped through.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdMuw9yNjY451tw5VEordn

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 07cbb9760d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +5032 to +5033
if (draft.EntryType != ManualJournalEntryTypeDto.ClosingEntry &&
!string.Equals(period.Status, "Open", StringComparison.OrdinalIgnoreCase))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restrict ClosingEntry to generated close drafts

Because ManualJournalEntryDraftDto.EntryType is client-supplied through the manual journal draft endpoint, this exemption lets any ledger-mutation user save a manual draft with EntryType = ClosingEntry against a soft- or hard-closed period and avoid the manual-je.period-closed critical validation; posting then maps that same type to LedgerPostingKindDto.ClosingEntry, which the storage guard now permits into closed periods. Limit this bypass to drafts produced by the period-close automation, or require close-specific provenance before skipping the closed-period check.

Useful? React with 👍 / 👎.

Comment on lines 221 to +225
SecurityId: line.account.Symbol is not null ? draft.Event.SecurityId : null,
SecurityDisplayName: line.account.Symbol,
Description: line.account.ToString(),
EvidenceLink: firstEvidenceLink);
EvidenceLink: firstEvidenceLink,
Dimensions: LedgerDimensionMapper.ToDto(line.dimensions));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve account scope when posting close drafts

When a closed period has temporary balances scoped by Symbol or FinancialAccountId (for example broker-specific dividend income), the close projection carries that scope into line.account, but this DTO only persists display/security fields and dimensions; the later posting path rebuilds ledger lines as new LedgerAccount(account.AccountName, accountType), so the approved closing entry posts to unscoped accounts and does not zero the scoped trial-balance rows. Carry the ledger account's symbol/financial-account identity through the manual draft/posting path before using it for period-close entries.

Useful? React with 👍 / 👎.

@rodoHasArrived
rodoHasArrived merged commit f65f2c9 into main Jul 6, 2026
19 of 22 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

new AutomatedJournalPreparedDraftIntakeRequest(

P2 Badge Carry the period's accounting basis into close drafts

When the closed period belongs to a non-Primary book (GAAP/Tax books are supported and GetPeriodSummaryAsync returns the basis), this new prepared-intake call only passes the book id; BuildDraftDto still creates the ManualJournalEntryDraftDto with AccountingBasisKindDto.Primary. The draft is saved under the right book, but posting later rejects it in PostApprovedManualJournalEntryAsync because the draft basis does not match the ledger book, so period-close automation cannot post closing entries for non-primary books. Pass summary.AccountingBasis through the prepared request/draft.


if (entry.PostingKind == LedgerPostingKindDto.ClosingEntry)
{
return;

P2 Badge Fail closed for unsupported ClosingEntry period statuses

If a period record has any non-Open status other than the intended SoftClosed/HardClosed values (for example bad data or a future status), this early return permits a ClosingEntry before the existing unsupported-status branch can throw. Other posting kinds still fail closed at the bottom of this method, so the ClosingEntry exception should only apply after explicitly validating the status is one of the closed-period states it is meant to support.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

rodoHasArrived pushed a commit that referenced this pull request Jul 6, 2026
…count scope

Follow-up to the merged period-close work (#2149), addressing two P1
review findings that arrived after merge.

Security: the ClosingEntry closed-period exemption keyed off the
client-supplied ManualJournalEntryDraftDto.EntryType, letting any
ledger-mutation user save a ClosingEntry draft against a closed period
and bypass the manual-je.period-closed bar. Only the in-process
period-close automation should produce closing entries, so the manual
journal save and validate HTTP endpoints now reject client-submitted
ClosingEntry drafts.

Correctness: scoped closing entries (a temporary account split by symbol
or financial account, e.g. broker-specific dividend income) posted to
unscoped accounts because the manual-journal posting rebuild dropped the
LedgerAccount symbol/financial-account identity, so they never zeroed
the scoped trial-balance rows. ManualJournalEntryLineDto now carries the
ledger account symbol/financial-account id (with TS mirror), the intake
sets them from the projected account, and the posting rebuild restores
the scoped LedgerAccount. The store already persists both columns, so
this round-trips; client-entered lines leave them null (unchanged).

Adds an endpoint test for the client rejection and an intake test for
scoped-account preservation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdMuw9yNjY451tw5VEordn
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants