Skip to content

Add fiscal calendar, closing cadence, and close demo for RoboLedger#613

Merged
jfrench9 merged 7 commits into
mainfrom
feature/roboledger-closing-cadence
Apr 13, 2026
Merged

Add fiscal calendar, closing cadence, and close demo for RoboLedger#613
jfrench9 merged 7 commits into
mainfrom
feature/roboledger-closing-cadence

Conversation

@jfrench9
Copy link
Copy Markdown
Member

Summary

Introduces a complete fiscal calendar and closing cadence system for RoboLedger, along with an end-to-end demo showcasing the month-end close workflow for a sample entity (Cascade Advisory Group LLC). This feature enables organizations to define fiscal calendars, manage closing schedules with task dependencies, and track period close progress through new APIs, MCP tools, and reporting enhancements.

Key Accomplishments

Fiscal Calendar System (New)

  • Fiscal calendar models and API schemas: New domain models (fiscal_calendar.py) and API request/response schemas for defining fiscal years, quarters, and periods with configurable start dates and period boundaries.
  • Fiscal calendar service layer: Full CRUD operations for fiscal calendars including automatic period generation, period lookups, and calendar-to-entity association.
  • Period management: Dedicated period operations module supporting period status transitions (open, closing, closed, locked), period queries by date range, and bulk operations.
  • REST API routers: New /fiscal_calendar and /periods endpoint groups under the ledger router with comprehensive route coverage for calendar and period lifecycle management.
  • MCP tools: New fiscal_calendar_tools.py exposing fiscal calendar operations to the AI middleware layer.

Closing Cadence / Schedule Enhancements

  • Expanded schedule service: Significantly extended the schedules service (~540 lines added) to support closing cadence workflows — task dependencies, status tracking, recurrence tied to fiscal periods, and schedule-to-period binding.
  • Schedule MCP tools: Major expansion of schedule_tools.py (~433 lines added) to surface closing cadence management through the MCP tool interface.
  • Schedule API models: Extended schedule request/response models to accommodate cadence-specific fields and period associations.

Financial Reporting Enhancements

  • Fact grid improvements: Enhanced the fact grid report operations (~185 lines added) to support period-aware financial statement generation.
  • Financial statement tool: Expanded MCP financial statement tool (~367 lines added) with richer reporting capabilities tied to fiscal periods.
  • Reports router: Added new report endpoints for period-based financial statements.

Close Demo

  • End-to-end example: A fully self-contained demo (examples/close_demo/) that provisions a sample entity graph, chart of accounts, trial balance data, fiscal calendar, and closing schedule, then walks through the close process.
  • Sample data and policies: Realistic demo data for Cascade Advisory Group LLC including account mappings, journal entries, and configurable close policies.
  • Documentation: Comprehensive README and prompt file explaining demo setup and usage.

Schema & Migration Changes

  • Consolidated initial migration: The 0001_initial_schema.py migration has been expanded (~139 lines added) to include fiscal calendar and related tables. The separate 0002_add_entry_provenance.py migration has been removed and folded into the initial schema.
  • Graph creation service: Updated to support fiscal calendar association during entity graph provisioning.
  • Core graph models: Minor additions to support calendar metadata on graph entities.

Breaking Changes

  • Migration 0002_add_entry_provenance deleted: The entry provenance schema has been consolidated into 0001_initial_schema. Any environment that has already run 0002 will need a migration reconciliation (e.g., stamp or fresh migrate). This should be coordinated before deploying to shared environments.
  • Schema router changes: Minor modifications to the database schema router and its tests — existing clients should be unaffected, but verify integration tests pass.

Testing

  • New test suites (5 new test files, ~2,589 lines of tests):
    • tests/operations/fiscal_calendar/test_service.py — 804 lines covering calendar CRUD, period generation, edge cases
    • tests/operations/fiscal_calendar/test_periods.py — 145 lines covering period operations
    • tests/routers/ledger/test_fiscal_calendar.py — 395 lines covering calendar API endpoints
    • tests/routers/ledger/test_periods.py — 609 lines covering period API endpoints
    • tests/operations/schedules/test_service.py — 636 lines of additions covering closing cadence schedule logic
  • Updated existing tests: Schedule tools, financial statement, schema, and schedule router tests updated for new interfaces.

Infrastructure Considerations

  • A new environment variable has been added (see .env.local.example) — ensure this is configured in all deployment environments.
  • The consolidated migration requires careful rollout on any database that has already applied the now-deleted 0002 migration.
  • The fiscal calendar service introduces new database tables; ensure migration execution is included in deployment pipelines.
  • The demo is intended for local/dev use and does not affect production infrastructure.

🤖 Generated with Claude Code

Branch Info:

  • Source: feature/roboledger-closing-cadence
  • Target: main
  • Type: feature

Co-Authored-By: Claude noreply@anthropic.com

- Introduced a comprehensive AI month-end close demo, including synthetic data generation for a consulting firm.
- Added new files for demo setup, including data generation, accounting policies, and OLTP data writing.
- Implemented a close workflow with a rolling 16-month window, ensuring evergreen data for testing.
- Updated the .env.local.example to enable extensions and added relevant commands in the justfile for running the demo.
- Created a README to guide users through the demo setup and execution process.
@jfrench9
Copy link
Copy Markdown
Member Author

@claude please review this PR

@claude
Copy link
Copy Markdown

claude Bot commented Apr 12, 2026

Claude finished @jfrench9's task in 3m 43s —— View job


PR Review: Add fiscal calendar, closing cadence, and close demo for RoboLedger

  • Gather context and understand PR scope
  • Review migration changes (breaking change: 0002 deleted)
  • Review fiscal calendar models and operations
  • Review API routers and schemas
  • Review MCP tools
  • Review schedule service expansions
  • Review close demo
  • Review test coverage

This is a substantial, well-structured PR. The core domain design — rolling closed_through/close_target pointers, sequential close enforcement, audit events, and the closeable gate — is clean and correct. The test suite is solid at 2,589 lines. The issues below range from a significant migration concern to several code quality items.


Critical: Migration breaking change needs a reconciliation path

The PR deletes 0002_add_entry_provenance.py and folds it into 0001. The PR description correctly flags this as a breaking change and says "stamp or fresh migrate" is needed, but doesn't provide a script. Before this merges, staging environments that have already applied 0002 need a concrete runbook, e.g.:

# On any env that has already run 0002:
alembic stamp 0001  # mark the DB as being at 0001

Without this, deploying to staging will fail with Alembic seeing an unknown head. Consider adding the runbook to the PR description or a MIGRATION_NOTES.md.


Issues

1. Close-period logic is fully duplicated between REST and MCP (routers/ledger/periods.py vs middleware/mcp/tools/fiscal_calendar_tools.py)

The draft-to-posted transition, BS equation check, re-close routing, and advance_closed_through call all appear twice. A bug fix in the REST path won't automatically fix the MCP path. The service layer already handles the calendar state — the entry-posting logic should also live in ScheduleService or a new PeriodCloseService, called from both. Fix this →

2. session.rollback() followed by normal context-manager exit (fiscal_calendar_tools.py ~line 342)

session.rollback()
return {"error": "unbalanced", ...}   # ← returns normally, no exception raised

After session.rollback(), returning without raising means extensions_session.__exit__ runs normally and may attempt a commit() on the rolled-back session. The safe pattern is to raise an exception so the context manager's error path runs, or to restructure the check so it raises before any mutations. Fix this →

3. Private method _earliest_open_period called from routers and MCP tools

_svc._earliest_open_period(session, graph_id) is accessed in periods.py:241 and fiscal_calendar_tools.py:386. Making it public (or adding a is_latest_sequential_close(session, graph_id, period) helper) removes the coupling to service internals. Fix this →

4. Closeable gate blocks closing on the last day of the month (service.py:595)

if period_end >= today:         # ← strict >=
    blockers.append(PERIOD_INCOMPLETE)

This means January cannot be closed on January 31 — you must wait until February 1. Most accounting systems allow closing on the last day. If intentional (ensuring all intraday transactions are in), add a comment explaining the policy. If not, > would be more intuitive. Fix this →

5. Manual period parsing in MCP ClosePeriodTool (fiscal_calendar_tools.py ~line 284)

year, month = int(period[:4]), int(period[5:7])
period_start = date(year, month, 1)
period_end = date(year, month, monthrange(year, month)[1])

period_date_range(period) is already imported in this module via robosystems.operations.fiscal_calendar. Use it consistently. Fix this →

6. fp.closed_by identity in MCP tool (fiscal_calendar_tools.py ~line 367)

fp.closed_by = f"mcp:{graph_id}"   # ← graph_id is not a user identity

The REST endpoint correctly uses current_user.id. The MCP tool should record user identity from self.client context if available, or fall back to a meaningful sentinel like "mcp_agent" rather than repeating the graph ID which is stored in other columns anyway.

7. allow_stale_sync override is not audited (periods.py and fiscal_calendar_tools.py)

When allow_stale_sync=True is used, the close audit event (period_closed) gives no indication this override was in play. For compliance contexts, this override is significant — it means a human asserted "the data is complete." It should appear in note or as a separate event field. Fix this →

8. _ledger_404() is duplicated (fiscal_calendar.py:51 and periods.py:73)

Two identical private functions. Extract to a shared helper module or robosystems/routers/ledger/__init__.py.


Observations (no change required)

  • String comparison for period ordering — Using >, <, <= on YYYY-MM strings is correct here. Lexicographic order matches chronological order given the validated format. Worth a comment in periods.py at the top since it's a non-obvious property.

  • Re-close routing logic — The "latest reopen" detection (next_period(closed_through) == period) is subtle but correct. The tests cover this path well.

  • add_months implementation — Python's floor-division semantics mean negative months work correctly across year boundaries (e.g., add_months("2026-01", -1)"2025-12"). The implementation is correct.

  • closeable_gate is read-only — Correctly documented and structured. The fact that it returns all blockers rather than short-circuiting is good UX for the MCP layer.

  • Model CheckConstraint matches migration DDLck_fiscal_calendar_target_ge_through, ck_fiscal_calendar_events_event_type, etc. all match between model and migration. No drift.

  • Test quality — The custom _InMemorySession stub in test_service.py is well-built and much better than full mocking for service unit tests. Integration tests noted as Phase 2 work.

  • Demo script — Clean evergreen window (16 months ending now), correctly structured, no hardcoded dates. Good companion to the implementation.


Summary

The fiscal calendar design and implementation are solid — sequential enforcement, the audit trail, and the closeable gate logic are well-thought-out. The main concern before merging is items 1 (migration runbook for staging) and 2 (the session.rollback() + normal return pattern in the MCP tool). Items 3–7 are quality improvements that would reduce future maintenance burden, especially as the MCP close path diverges from the REST path over time.

Comment thread examples/close_demo/main.py Fixed
Comment thread robosystems/routers/ledger/periods.py Fixed
- Introduced PeriodCloseService to encapsulate the month-end close workflow, improving maintainability and reducing duplication across REST and MCP paths.
- Updated ClosePeriodTool to leverage the new service, ensuring consistent error handling and user identity management.
- Enhanced error handling for close operations, including specific exceptions for close gate failures, unbalanced ledgers, and missing fiscal periods.
- Improved API responses for closing operations, providing clearer feedback on errors and outcomes.
- Added unit tests for the new service and tool to ensure robust functionality and error handling.
…edger namespace

- Updated imports across multiple files to transition from the `fiscal_calendar` and `schedules` modules to the new `roboledger` namespace, enhancing organization and clarity.
- Introduced new modules for fiscal calendar and schedule operations, encapsulating related functionalities for better maintainability.
- Ensured consistent error handling and improved code structure by centralizing related services and utilities under the `roboledger` directory.
- Added documentation for new modules to clarify their purpose and usage within the system.
@jfrench9
Copy link
Copy Markdown
Member Author

@claude can you review the latest 2 commits

@claude
Copy link
Copy Markdown

claude Bot commented Apr 12, 2026

Claude finished @jfrench9's task in 3m 59s —— View job


Review: Latest 2 Commits

  • Gather last 2 commits
  • Review changed files
  • Post feedback

These two commits are a focused, high-quality response to the prior review. Here's what they accomplish and what's left.

Commit 61ba102Refactor fiscal calendar close logic and enhance error handling
Introduces PeriodCloseService and _common.py. The REST router and MCP tool both shed ~100 lines of duplicated close logic and delegate to the service instead.

Commit 59d3cbfRefactor fiscal calendar and schedule operations to use the new RoboLedger namespace
Moves operations/{fiscal_calendar,schedules,reports}/operations/roboledger/*/ to match the existing models/extensions/roboledger/ split. All import paths updated throughout.


Issues Addressed from Prior Review

# Issue Status
1 Close logic duplicated between REST and MCP PeriodCloseService is now the single path
2 session.rollback() + normal return in MCP ✅ Pre-flight _preflight_bs_check raises before any mutation — no rollback needed
3 Private _earliest_open_period called from routers/MCP is_latest_sequential_close() public helper added; no more _svc._earliest_open_period() in callers
5 Manual period parsing in MCP ClosePeriodTool period_date_range() used inside the service
6 fp.closed_by = f"mcp:{graph_id}" getattr(self.client, "user_id", None) or f"mcp:{graph_id}"
7 allow_stale_sync override not audited _audit_note() static method appends [sync gate overridden...] to the audit trail
8 _ledger_404() duplicated across two routers ✅ Extracted to _common.py
Security scanner API key prefix logged in demo ✅ Replaced with (saved to {CREDENTIALS_FILE})

Remaining Open Issues

Issue #4 — closeable gate still blocks on last day of month (service.py:595)
Not addressed in these commits — still period_end >= today. This was flagged as an intentional-or-not question. If it's intentional, the comment explaining why is still missing.

Critical migration runbook — still just described in the PR description, no concrete script.


New Issues in These Commits

1. FiscalCalendarError missing from package __init__.py

close_service.py imports it from .service and lists it in __all__ with the comment # Re-export for caller convenience. But __init__.py doesn't include it in its imports:

# __init__.py imports from close_service but NOT FiscalCalendarError:
from .close_service import (
    CloseGateFailed, PeriodCloseError, PeriodCloseResult,
    PeriodCloseService, PeriodNotFoundError, UnbalancedLedgerError,
)

All callers currently work around this by importing from .service directly, which is fine — but the "caller convenience" comment in close_service.__all__ is misleading and a future contributor may be confused. Either add FiscalCalendarError to __init__.py's imports, or remove the re-export comment from close_service. Fix this →

2. ReopenPeriodTool didn't get the actor_id improvement (fiscal_calendar_tools.py:453)

ClosePeriodTool was correctly updated to:

actor_id = getattr(self.client, "user_id", None) or f"mcp:{graph_id}"

But ReopenPeriodTool.execute still uses the old pattern:

actor_id=f"mcp:{graph_id}",   # line 453 — graph_id is not a user identity

These two tools are now inconsistent. Fix this →


Observations

  • Pre-flight balance check is semantically correct: The old _validate_bs_equation checked status = 'posted' after the draft→posted flush. The new _preflight_bs_check checks status IN ('draft', 'posted') before. These are equivalent because the transition doesn't change debit/credit amounts. The new approach is strictly better: an imbalance raises cleanly without any state to unwind.

  • Test refactoring is intentional and correct: The router TestClosePeriod dropped per-gate-failure tests in favor of a service-delegation test. The gate/BS/reclose mechanics are now covered in test_close_service.py at the right abstraction level. The class docstring explains the split explicitly.

  • Namespace move is clean: No lingering references to the old robosystems.operations.fiscal_calendar path anywhere in the codebase (confirmed by grep).

  • _close_svc is a module-level singleton in periods.py: _close_svc = PeriodCloseService(_svc) — this shares the same FiscalCalendarService instance with the existing _svc singleton. Fine for now given FiscalCalendarService is stateless.


Summary

The two commits fully resolve the structural concerns from the prior review. The PeriodCloseService extraction is clean, the pre-flight balance check eliminates the rollback hazard, and the namespace reorganization sets up a clean future extraction boundary. Two small follow-ups remain: add FiscalCalendarError to the package __init__ (or remove the misleading re-export comment), and apply the actor_id improvement to ReopenPeriodTool.

- Added detailed API endpoint documentation for local testing, clarifying usage and common mistakes.
- Updated the cash flow handling in various modules to reflect that the cash flow statement is not yet supported, ensuring consistent messaging across the codebase.
- Removed references to cash flow elements in taxonomy and tests, aligning with the current implementation status.
- Improved error handling in financial statement tools to provide clear feedback when unsupported statement types are requested.
@jfrench9
Copy link
Copy Markdown
Member Author

@claude can you review the latest changes

@claude
Copy link
Copy Markdown

claude Bot commented Apr 13, 2026

Claude finished @jfrench9's task in 3m 55s —— View job


Review: Latest Commit (f7955fe)

  • Gather context and identify new commits
  • Review changed files
  • Post feedback

This commit has one focus: finishing the clean-up items from the prior review, plus a meaningful bug fix in _build_rows. Here's the full picture.


Issues Addressed from Prior Review

# Issue Status
2 (new) ReopenPeriodTool.actor_id inconsistent with ClosePeriodTool ✅ Fixed — getattr(self.client, "user_id", None) or f"mcp:{graph_id}" applied consistently
1 (new) FiscalCalendarError missing from package __init__.py ❌ Still unresolved

Notable Changes

_build_rows bug fix (fact_grid.py:881) — the old code emitted values=[0.0] * n_periods for subtotal/abstract rows. Since Pass 1 already computes rolled-up sums for parent nodes in computed_per_period, subtotal rows now read from there just like leaf nodes. This was a real rendering bug — users would see section headers with no value. The fix is correct and the unified emit path is cleaner.

Aspirational calendar detection (fact_grid.py:138) — _ledger_has_re_postings detects the demo case where a fiscal calendar has a closed_through marker but no actual RE postings backing it. When detected, closed_through is cleared so the synthetic close runs from inception and the balance sheet balances. The concept is sound and the comment explains it well.

Cash flow deferral — Removing the partially-implemented CF generator is pragmatic: seed data removed from taxonomy/seed.py, guard rail removed from guard_rails.py, source="system" correctly excludes taxonomy-root elements from COA queries, and CreateStructureRequest drops cash_flow_statement from its Literal. Comments throughout explain the deferral consistently.

Closing book order — "Period Close" moved to first position. Good UX.


Issues

1. Redundant query in generate_report_facts (fact_grid.py:127-139)

if closed_through is None:
    closed_through = _derive_closed_through_from_ledger(session, mapping_id)  # reads MAX(posting_date)

if closed_through is not None and not _ledger_has_re_postings(session, mapping_id):  # runs LIMIT 1 query
    closed_through = None

When closed_through starts as None and _derive_closed_through_from_ledger returns a date, the second if fires and calls _ledger_has_re_postings — but _derive_closed_through_from_ledger already proved postings exist (it found a MAX date). _ledger_has_re_postings will always return True here; the extra query is wasted. The intent is to only nullify a calendar-provided closed_through that has no RE backing. The fix is elif:

if closed_through is None:
    closed_through = _derive_closed_through_from_ledger(session, mapping_id)
elif not _ledger_has_re_postings(session, mapping_id):
    closed_through = None

This matches the documented intent exactly: the derived path is always backed; only the externally-provided path needs the liveness check. Fix this →

2. _ = _validate_cash_flow is the wrong dead-code pattern (guard_rails.py:130)

_ = _validate_cash_flow   # Keep alive for the linter

Assigning to _ is the Python convention for "I'm intentionally discarding a value" (e.g., loop counters), not for pinning a function reference. It also pollutes the module namespace with an unexpected name. If the linter (basedpyright) flags it, the right suppression is inline on the function declaration:

def _validate_cash_flow(rows: list[FactRow]) -> ValidationResult:  # pyright: ignore[reportUnusedFunction]

Or, since this function is being kept specifically as a future placeholder, make that explicit by naming it for what it is. Fix this →

3. CreateStructureRequest removes cash_flow_statement from Literal (models/api/extensions/taxonomies.py:61-72)

Removing a value from a Literal type is a silent backwards-incompatible API change. If any tenant graph has an existing structure with structure_type = 'cash_flow_statement', round-tripping through this model (e.g., any endpoint that validates against it) will now fail with a 422. The DB column has no constraint enforcing this, so stale data can exist. Worth checking in staging before merge. If it's confirmed no records exist, a brief comment to that effect in the code would close the loop. Fix this →


Carried Over (still unresolved)

FiscalCalendarError not exported from package __init__.pyclose_service.__all__ lists it with a "Re-export for caller convenience" comment, but __init__.py doesn't import it. Either add it to __init__.py imports, or remove the misleading comment from close_service. (From prior review — still open.)

Closeable gate period_end >= today (service.py:632) — Still >=, still no comment explaining why closing on the last day of the period is blocked. (From original review — still open.)


Observations

  • source="system" is clean and correctCOA_SOURCES in __init__.py still doesn't include "system", so the FK-anchor root elements are invisible to Chart of Accounts queries. The model and migration check constraint both updated consistently.

  • cash_flow_statement still in VALID_STATEMENT_TYPES in financial_statement_tool.py — This is correct. SEC graphs can still generate cash flow statements; only the OLTP/roboledger path is rejected. The guard at line 192 handles the split cleanly. The tool description and SEC path are unaffected.

  • CLAUDE.md additions are useful — The API endpoints table and curl examples are good developer ergonomics for anyone building against the local stack. The X-API-Key vs Authorization: Bearer distinction in particular saves debugging time.


Summary

The _build_rows subtotal fix is a real bug fix that improves report correctness. The aspirational-calendar detection is a thoughtful solution for the demo scenario. The CF deferral is clean. Items to address before merge: the elif fix (correctness/perf), the _ = pattern, and the backwards-compat note on the Literal change. The two carried-over items (FiscalCalendarError export, closeable gate comment) should also be closed out.

…r error handling

- Bumped the `robosystems-client` version from 0.2.47 to 0.2.48 in `pyproject.toml` and `uv.lock`.
- Enhanced error handling in fiscal calendar tools by refining imports and ensuring consistent usage of `FiscalCalendarError`.
- Improved documentation comments regarding cash flow structure handling in the API, clarifying the current limitations and future plans for implementation.
@jfrench9
Copy link
Copy Markdown
Member Author

@claude ready to merge?

@claude
Copy link
Copy Markdown

claude Bot commented Apr 13, 2026

Claude finished @jfrench9's task in 4m 22s —— View job


Merge Readiness Assessment

  • Gather context from prior reviews
  • Check latest commit (206c602) against remaining open issues
  • Read relevant files for current state
  • Post final assessment

Short answer: Yes, with one pre-merge action and a Test CI run to confirm.

What the Latest Commit Fixed

206c602 cleanly closed out every open item from the three prior review rounds:

Issue Status
FiscalCalendarError re-export comment in close_service.__all__ ✅ Removed the re-export; __init__.py now imports it directly from .service
Redundant double-query (if/ifif/elif) in generate_report_facts ✅ Fixed; elif with explanatory comment added
_ = _validate_cash_flow wrong dead-code pattern ✅ Replaced with # pyright: ignore[reportUnusedFunction] on the function signature
CreateStructureRequest Literal backwards-compat concern ✅ Detailed comment added explaining the omission, the non-impact on read paths, and the staging spot-check requirement
FiscalCalendarError import in MCP tools using private path ✅ Both ClosePeriodTool and ReopenPeriodTool now import via the public package

And from earlier commits (confirmed still in place):

Issue Status
Closeable gate period_end >= today — intent unexplained ✅ Clear comment added: intraday transactions (bank feeds, QB polling) can post on the last day; this is intentional
Unused variable latest_reopen in periods.py ✅ Removed
CodeQL: sensitive data (API key) logged in demo ✅ Replaced with (saved to {CREDENTIALS_FILE})
Close logic duplicated REST ↔ MCP PeriodCloseService is the single path
session.rollback() + normal return ✅ Pre-flight check raises before any mutation
ReopenPeriodTool.actor_id inconsistent with ClosePeriodTool ✅ Both use getattr(self.client, "user_id", None) or f"mcp:{graph_id}"

One Pre-Merge Action Required

Migration coordination for staging environments that have already applied 0002_add_entry_provenance. The PR description describes this correctly but there's no runbook script. Before merging, confirm one of the following:

  • Staging has not had 0002 applied (clean state — just merge and migrate normally), or
  • Run alembic stamp 0001 on any environment that has applied 0002 before deploying this branch

This is a deployment step, not a code change — no further commits needed. Just needs to be consciously handled at rollout time.


CI Status

Check Result
Code Quality ✅ Passed
PR ✅ Passed
Test CI ⏳ In progress (view)

Recommend confirming Test CI passes before merging. All prior test runs on this branch have passed.


Overall

This is a well-reviewed PR. Three rounds of feedback produced measurable improvements each time — the extraction of PeriodCloseService, the pre-flight balance check, the elif fix, the guard rail pattern fix, and the export cleanup are all meaningful quality gains. The domain design (sequential close enforcement, audit trail, closeable gate, re-close routing) is sound and the 2,589-line test suite covers the logic at the right level.

Merge when Test CI is green and migration coordination is confirmed.

- Removed the `_resolve_private_closed_through` method and related logic to simplify the handling of retained earnings adjustments, ensuring that synthetic adjustments now always run from inception.
- Updated the `generate_report_facts` function to eliminate the `closed_through` parameter, reflecting the new approach to retained earnings calculations.
- Adjusted the report generation process to enhance clarity and maintainability, ensuring that real closing entries are correctly accounted for without double-counting prior earnings.
- Removed associated tests for the deleted `_derive_closed_through_from_ledger` function, streamlining the test suite.
@jfrench9
Copy link
Copy Markdown
Member Author

@claude can you review the last commit

@claude
Copy link
Copy Markdown

claude Bot commented Apr 13, 2026

Claude finished @jfrench9's task in 3m 55s —— View job


Review: Last Commit (510eb4a) — Refactor financial statement tool and report generation logic

  • Gather context and identify the last commit
  • Review changed files
  • Post feedback

This commit takes a clean approach: instead of tracking closed_through as a lower bound for the cumulative rev/exp query, it removes the bound entirely and argues "from inception is always correct." The argument is mathematically sound — let me trace through it and flag one genuine concern.


The Core Argument — Is It Right?

The claim: real closing entries zero out rev/exp accounts, so cumulative rev - exp from inception naturally returns only the still-unclosed portion.

Tracing through:

  • With real closing entries (QB year-end or roboledger close_period): the closing entry posts DR Revenue / CR Expense / CR RE. After it, cumulative revenue - cumulative expense = 0 for closed periods. _close_prior_periods_to_retained_earnings adds 0. RE balance from _read_mapped_balances already includes the real RE posting. ✓
  • Without closing entries (fresh ledger, no closes yet): cumulative captures all history; prior-period NI is correctly accumulated into RE. ✓

The argument holds. The elif fix in 206c602 addressed a specific complexity that this commit eliminates entirely — a good outcome.


Issues

1. Performance regression for mature ledgers (fact_grid.py:448–471)

The cumulative rev/exp query previously had a lower bound when closed_through was set:

# Old:
AND (:start_date IS NULL OR e.posting_date >= :start_date)

That bound (closed_through + 1 day) was set whenever the FiscalCalendar had a real closed_through_period. For a business that's been using close_period for 2 years and is now running a report for the current month, the query now scans all 24 months of P&L history instead of just the current open window.

The simplification is correct, but the old optimization wasn't wrong — it was just more complex. As ledger history grows this will become noticeable. Worth tracking. Fix this →

2. Undocumented precondition on closing entry structure (fact_grid.py:430–443)

The docstring correctly explains why "from inception" is safe for well-formed closing entries. But it doesn't state the precondition explicitly:

This assumes closing entries zero out all rev/exp line items they close (standard double-entry). If a closing entry only credits RE without debiting the corresponding revenue accounts, those revenue amounts will appear in both the ledger RE balance (via _read_mapped_balances) and the cumulative query — producing double-counted earnings.

This is an "import quirk" scenario (unusual but possible if data comes from an external system with non-standard close mechanics). Worth a one-liner in the docstring to protect future maintainers. Fix this →


Observations (no change required)

  • All callers updated consistentlyreports.py (create_report and regenerate_report) and financial_statement_tool.py all drop the closed_through argument. No stragglers.

  • Dead code fully excised_resolve_private_closed_through (MCP tool), _resolve_closed_through (REST router), _ledger_has_re_postings, and _derive_closed_through_from_ledger are all gone. The fact that 206c602's elif fix is now superseded is a better outcome than the fix itself: fewer code paths, fewer DB queries per report generation.

  • Test deletion is properly documented — The TestDeriveClosedThroughFromLedger class is replaced with a comment that explains why it was deleted and what behavior now makes it unnecessary. Much better than silent removal.

  • No new test for the "from inception with real closes" path — The argument is well-reasoned in the docstring, but there's no test exercising the scenario (real closing entry in period N, confirm no double-count in period N+1 balance sheet). This is a minor gap given the correctness of the argument; a future integration test would be the right place for it.


Summary

The simplification is mathematically correct and removes a genuine complexity hazard. The old _derive_closed_through_from_ledger path introduced the aspirational-calendar edge case that needed the if/elif fix — removing it is a better fix than patching it. Two items before shipping: (1) decide whether to preserve the performance optimization for mature ledgers, and (2) add the precondition note to the docstring. Neither blocks merge.

- Introduced a new test class `TestClosePriorPeriodsToRetainedEarnings` to validate the retained earnings calculations across various scenarios, including no closes, real closes, and partial closes.
- Implemented mock session methods to simulate database interactions and ensure accurate testing of retained earnings logic.
- Enhanced existing tests to cover edge cases and ensure the correctness of retained earnings adjustments after closing periods.
@jfrench9 jfrench9 merged commit 66756fa into main Apr 13, 2026
7 checks passed
@jfrench9 jfrench9 deleted the feature/roboledger-closing-cadence branch April 13, 2026 02:57
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