Complete accounting brainstorm remainders: NAV math, period-close intake, dividend withholding - #2149
Conversation
…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
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
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
…accounting-brainstorm-o73lsw
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
There was a problem hiding this comment.
💡 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, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
💡 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".
| var account = new LedgerAccount(line.AccountName, accountType, line.Symbol, line.FinancialAccountId); | ||
| balances[account] = balances.GetValueOrDefault(account) + line.Balance; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
💡 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"), |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
💡 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".
| if (draft.EntryType != ManualJournalEntryTypeDto.ClosingEntry && | ||
| !string.Equals(period.Status, "Open", StringComparison.OrdinalIgnoreCase)) |
There was a problem hiding this comment.
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 👍 / 👎.
| 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)); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
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.
Meridian-main/src/Meridian.Storage/Ledger/LedgerPeriodPostingGuard.cs
Lines 35 to 37 in 1372fea
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".
…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
Summary
Implements the three remaining accounting lanes from
docs/product/data-provider-accounting-brainstorm-2026-07.md(merged in #2100):NavAttributionServicecomputedtotalNavby summing every account's normal balance — double-counting the fund, since Assets − Liabilities = Equity + Net Income. It now computes assets − liabilities, withByAssetClassrestricted to signed asset/liability contributions so the breakdown decomposes the corrected total. Components still report every account. Covered by newNavAttributionServiceTests(4 tests).PeriodCloseProjector/PeriodCloseDraftBuilderare now reachable end-to-end:AutomatedJournalIntakeRunner.RunPeriodCloseIntakeAsyncfetches a closed period's trial balance fromILedgerBookService, 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-intakeroute (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 theperiod-close|{periodId}idempotency key.AutomatedJournalDraftIntakeServicegains a prepared-draft path (IntakeDraftsAsync) sharing dedup, chart mapping, and the human submit/approve lifecycle with event-projected drafts. 5 new runner tests.WithholdingTaxRate(0 ≤ rate < 1) and produces a pairedWithholdingTaxAccruedevent per declared dividend — same corporate-action evidence, distinctcorp-act-dividend-wht|…idempotency key. Plumbed throughRunDividendDraftIntakeRequestand 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:
UiApiRoutesconstants: thegovernance-endpointscheck now scansUiApiRoutes.csalongsideWorkstationEndpoints.cs, restoring the score from 26.7% to 46.7% (addresses the Codex review comment on Add code-grounded brainstorm for data-provider and accounting improvements #2100 that arrived after merge).docs/product/README.mdlink that was lost in a concurrent merge.ui-api-routes.generated.ts) and the status dashboards/doc inventories.Reason
Follow-up request to complete the NAV aggregation fix (#6 remainder), wire
PeriodCloseProjectorinto 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.shcompleted successfullyquality-gatepassedLocal 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. Fullquality-gateruns on this PR.Safety review
mainmainwas performedGovernance changes
Check every governance file modified:
.github/workflows/**.github/CODEOWNERS.github/pull_request_template.mdAGENTS.mdscripts/ci.sh🤖 Generated with Claude Code
https://claude.ai/code/session_01PdMuw9yNjY451tw5VEordn
Generated by Claude Code