Skip to content

refactor: extract _SpendingTotals base class from spending summary models#138

Merged
Aureliolo merged 3 commits intomainfrom
refactor/spending-totals-base
Mar 6, 2026
Merged

refactor: extract _SpendingTotals base class from spending summary models#138
Aureliolo merged 3 commits intomainfrom
refactor/spending-totals-base

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

  • Extract _SpendingTotals base class with shared aggregation fields (total_cost_usd, total_input_tokens, total_output_tokens, record_count)
  • AgentSpending, DepartmentSpending, and PeriodSpending now extend _SpendingTotals instead of independently defining identical fields
  • Update DESIGN_SPEC.md §10.2 and §15.5 to reflect "Shared field groups" convention as Adopted (M2.5)
  • Net reduction of 41 lines with no behavioral changes

Closes #111

Details

  • _SpendingTotals is private (underscore-prefixed, excluded from __init__.py exports)
  • model_config = ConfigDict(frozen=True) defined once on base, inherited by all subclasses
  • PeriodSpending._validate_period_ordering validator preserved unchanged
  • Field constraints (ge=0, defaults) remain identical

Test plan

  • All 1830 existing tests pass without modification
  • spending_summary.py maintains 100% coverage
  • Frozen model inheritance verified by existing test_frozen tests on each subclass
  • JSON roundtrip test confirms serialization unaffected
  • mypy strict mode passes
  • ruff lint + format clean

Review coverage

Pre-reviewed by 8 agents (code-reviewer, python-reviewer, pr-test-analyzer, comment-analyzer, type-design-analyzer, logging-audit, resilience-audit, docs-consistency). 6 findings total, 2 implemented, 4 skipped as non-applicable or out-of-scope.

Aureliolo and others added 2 commits March 6, 2026 15:36
…dels (#111)

Extract shared aggregation fields (total_cost_usd, total_input_tokens,
total_output_tokens, record_count) into a private _SpendingTotals base
class. AgentSpending, DepartmentSpending, and PeriodSpending now inherit
from it, eliminating ~48 lines of duplicated field definitions.

Update DESIGN_SPEC.md §10.2 and §15.5 to reflect the extraction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add instantiation guidance note and align attribute descriptions
with Field(description=...) values for consistency.

Pre-reviewed by 8 agents, 2 findings addressed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 6, 2026 14:43
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 6, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the spending summary models by introducing a common base class. This change centralizes the definition of shared aggregation fields, reducing duplication and making the codebase more modular and easier to maintain. The refactoring ensures that all related models consistently handle their core spending metrics while allowing for dimension-specific extensions.

Highlights

  • Code Refactoring: Extracted a new private base class, _SpendingTotals, to encapsulate shared aggregation fields (total_cost_usd, total_input_tokens, total_output_tokens, record_count) common to spending summary models.
  • Model Inheritance: AgentSpending, DepartmentSpending, and PeriodSpending models now inherit from _SpendingTotals, eliminating redundant field definitions and promoting code reuse.
  • Documentation Update: Updated DESIGN_SPEC.md to reflect the adoption of the 'Shared field groups' convention (M2.5) and the implementation of the _SpendingTotals base class.
  • Code Reduction: Achieved a net reduction of 41 lines of code without introducing any behavioral changes, improving maintainability.
  • Pydantic Configuration: The model_config = ConfigDict(frozen=True) is now defined once in the base class and inherited by all subclasses, ensuring consistent immutability.
Changelog
  • DESIGN_SPEC.md
    • Updated implementation note in section 10.2 to reference the new _SpendingTotals base class.
    • Changed the status of the 'Shared field groups' convention from 'Planned' to 'Adopted (M2.5)' in section 15.5, reflecting the implementation of _SpendingTotals.
  • src/ai_company/budget/spending_summary.py
    • Created a new _SpendingTotals base class to hold common spending aggregation fields and Pydantic configuration.
    • Refactored PeriodSpending to inherit from _SpendingTotals, moving shared fields to the base class.
    • Refactored AgentSpending to inherit from _SpendingTotals, removing duplicated aggregation fields.
    • Refactored DepartmentSpending to inherit from _SpendingTotals, removing duplicated aggregation fields.
Activity
  • All 1830 existing tests passed without modification, confirming no behavioral changes.
  • spending_summary.py maintained 100% test coverage.
  • Frozen model inheritance was verified by existing test_frozen tests on each subclass.
  • JSON roundtrip tests confirmed serialization was unaffected.
  • mypy strict mode passed, ensuring type consistency.
  • ruff lint and format checks passed cleanly.
  • The pull request was pre-reviewed by 8 agents (code-reviewer, python-reviewer, pr-test-analyzer, comment-analyzer, type-design-analyzer, logging-audit, resilience-audit, docs-consistency), resulting in 6 findings, 2 of which were implemented and 4 skipped as non-applicable or out-of-scope.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 6, 2026

Warning

Rate limit exceeded

@Aureliolo has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 22 minutes and 8 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: abf1496a-34ba-4b22-bbac-babc7a4fe5d8

📥 Commits

Reviewing files that changed from the base of the PR and between 538fced and 3fa29a7.

📒 Files selected for processing (2)
  • DESIGN_SPEC.md
  • src/ai_company/budget/spending_summary.py
📝 Walkthrough

Walkthrough

This PR extracts a shared _SpendingTotals base class containing four common aggregation fields (total_cost_usd, total_input_tokens, total_output_tokens, record_count) and refactors PeriodSpending, AgentSpending, and DepartmentSpending to inherit from it, eliminating field duplication. Documentation is updated to reflect this structural change.

Changes

Cohort / File(s) Summary
Design Documentation
DESIGN_SPEC.md
Updated §10.2 and §15.5 to reflect adoption of _SpendingTotals base class pattern, marking "Shared field groups" as Adopted (M2.5) with explicit inheritance chain documentation.
Spending Summary Models
src/ai_company/budget/spending_summary.py
Introduced new _SpendingTotals base class with 4 shared fields (total_cost_usd, total_input_tokens, total_output_tokens, record_count). Refactored PeriodSpending, AgentSpending, and DepartmentSpending to inherit from base instead of duplicating fields. Updated SpendingSummary to reference new PeriodSpending type with validation.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly summarizes the main refactoring change: extracting a _SpendingTotals base class from spending summary models.
Description check ✅ Passed The description is well-detailed and directly related to the changeset, explaining the extraction of _SpendingTotals, model inheritance, line reduction, test results, and DESIGN_SPEC.md updates.
Linked Issues check ✅ Passed The PR substantially meets all acceptance criteria from #111: _SpendingTotals base created with 4 shared fields, all three models extended it, net 41-line reduction achieved, existing tests pass, and DESIGN_SPEC.md updated to reflect Adopted status.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #111 requirements: base class extraction, model inheritance, field consolidation, DESIGN_SPEC.md documentation updates, and validation preservation. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 100.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/spending-totals-base

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a great refactoring that extracts a _SpendingTotals base class from the spending summary models, effectively reducing code duplication and improving maintainability. The changes are well-executed, and the documentation in DESIGN_SPEC.md has been updated accordingly. I have one suggestion to further improve the robustness of the new base class by programmatically preventing its direct instantiation, which aligns with the intent described in its docstring.

Comment on lines +19 to 30
class _SpendingTotals(BaseModel):
"""Shared aggregation fields for spending summary models.

Not intended for direct instantiation — subclass with a
dimension-specific identifier (agent, department, or period).

Attributes:
start: Period start (inclusive).
end: Period end (exclusive).
total_cost_usd: Total cost for the period.
total_cost_usd: Total cost for the aggregation group.
total_input_tokens: Total input tokens consumed.
total_output_tokens: Total output tokens consumed.
record_count: Number of cost records aggregated.
"""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The docstring correctly states that _SpendingTotals is not intended for direct instantiation. To enforce this and prevent potential misuse, you can add a check to prevent it from being instantiated directly. This improves the robustness of the design.

Here's how you could do it using model_post_init (you'll also need to from typing import Any):

    def model_post_init(self, __context: Any) -> None:
        """Prevent direct instantiation of this base class."""
        if type(self) is _SpendingTotals:
            raise TypeError(
                "_SpendingTotals is a base class and cannot be instantiated directly. "
                "Subclass it and add a dimension-specific identifier."
            )

Repository owner deleted a comment from coderabbitai bot Mar 6, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors the budget spending summary Pydantic models to reduce duplication by introducing a shared private base model for common aggregation totals, and updates the design spec to document the new “shared field groups” convention.

Changes:

  • Extract _SpendingTotals base model containing shared aggregation fields and frozen config.
  • Update PeriodSpending, AgentSpending, and DepartmentSpending to inherit from _SpendingTotals.
  • Update DESIGN_SPEC sections (§10.2, §15.5) to reflect the convention as adopted.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/ai_company/budget/spending_summary.py Introduces _SpendingTotals and refactors spending summary models to inherit shared aggregation fields.
DESIGN_SPEC.md Updates documentation to reflect the new shared-field-group base model convention as adopted.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 34 to 38
total_cost_usd: float = Field(
default=0.0,
ge=0.0,
description="Total cost for the period",
description="Total cost for the aggregation group",
)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

total_cost_usd Field description has been changed from the per-dimension wording (period/agent/department) to the generic "aggregation group" wording. This alters generated JSON schema / API docs and contradicts the PR claim of "no behavioral changes" if schema/metadata is considered part of the public surface. Either restore the previous per-model descriptions (e.g., by overriding field metadata in subclasses) or update the PR summary/changelog to explicitly call out the schema/doc metadata change.

Copilot uses AI. Check for mistakes.
Repository owner deleted a comment from coderabbitai bot Mar 6, 2026
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR is a clean structural refactor that extracts a private _SpendingTotals base class to eliminate the four duplicated aggregation fields (total_cost_usd, total_input_tokens, total_output_tokens, record_count) previously copy-pasted across AgentSpending, DepartmentSpending, and PeriodSpending. The accompanying DESIGN_SPEC.md updates are accurate and consistent.

  • _SpendingTotals centralises model_config = ConfigDict(frozen=True) and the four shared fields with their constraints; all three subclasses now only define their dimension-specific identifier field(s).
  • PeriodSpending._validate_period_ordering is preserved without modification.
  • __init__.py exports remain unchanged; _SpendingTotals is correctly excluded.
  • Note: As flagged in an earlier review thread, Pydantic v2 serialises parent fields before subclass fields, so model_dump() / model_dump_json() now emit the totals fields before the identifier fields (agent_id, department_name, start/end). Deserialization is unaffected, but any snapshot tests doing string-level JSON comparison would need updating.
  • One minor design gap: _SpendingTotals is directly instantiable (all fields have defaults) despite the docstring prohibiting it — consider enforcing this constraint at runtime.

Confidence Score: 4/5

  • Safe to merge — pure structural refactor with no behavioral changes to validation logic, field constraints, or public API.
  • The change is well-scoped: it only reorganises inheritance without touching any business logic, validators, or public exports. The existing test suite covers frozen behaviour, defaults, validators, and JSON roundtrip. The one point deducted is for the silent field-ordering change in serialized output (noted in a prior thread) and the unenforced "not for direct instantiation" contract on _SpendingTotals, both of which are low-risk but worth addressing before the class is reused further.
  • src/ai_company/budget/spending_summary.py — specifically the directly-instantiable _SpendingTotals base class and the serialization field-order change.

Important Files Changed

Filename Overview
src/ai_company/budget/spending_summary.py Clean extraction of _SpendingTotals base class; all field constraints and validators are preserved. One minor concern: the base class is directly instantiable despite the docstring's prohibition, since all fields have defaults.
DESIGN_SPEC.md Documentation accurately updated in §10.2 and §15.5 to reflect the _SpendingTotals extraction and promote "Shared field groups" from Planned to Adopted (M2.5). No issues found.

Class Diagram

%%{init: {'theme': 'neutral'}}%%
classDiagram
    class _SpendingTotals {
        +float total_cost_usd
        +int total_input_tokens
        +int total_output_tokens
        +int record_count
        model_config: frozen=True
    }

    class PeriodSpending {
        +datetime start
        +datetime end
        _validate_period_ordering()
    }

    class AgentSpending {
        +NotBlankStr agent_id
    }

    class DepartmentSpending {
        +NotBlankStr department_name
    }

    class SpendingSummary {
        +PeriodSpending period
        +tuple~AgentSpending~ by_agent
        +tuple~DepartmentSpending~ by_department
        +float budget_total_monthly
        +float budget_used_percent
        +BudgetAlertLevel alert_level
        model_config: frozen=True
        _validate_unique_agent_ids()
        _validate_unique_department_names()
    }

    _SpendingTotals <|-- PeriodSpending
    _SpendingTotals <|-- AgentSpending
    _SpendingTotals <|-- DepartmentSpending
    SpendingSummary o-- PeriodSpending
    SpendingSummary o-- AgentSpending
    SpendingSummary o-- DepartmentSpending
Loading

Last reviewed commit: 3fa29a7

Comment on lines 56 to 98
@@ -65,78 +76,26 @@ def _validate_period_ordering(self) -> Self:
return self


class AgentSpending(BaseModel):
class AgentSpending(_SpendingTotals):
"""Spending aggregation for a single agent.

Attributes:
agent_id: Agent identifier.
total_cost_usd: Total cost for this agent.
total_input_tokens: Total input tokens consumed.
total_output_tokens: Total output tokens consumed.
record_count: Number of cost records.
"""

model_config = ConfigDict(frozen=True)

agent_id: NotBlankStr = Field(description="Agent identifier")
total_cost_usd: float = Field(
default=0.0,
ge=0.0,
description="Total cost for this agent",
)
total_input_tokens: int = Field(
default=0,
ge=0,
description="Total input tokens consumed",
)
total_output_tokens: int = Field(
default=0,
ge=0,
description="Total output tokens consumed",
)
record_count: int = Field(
default=0,
ge=0,
description="Number of cost records",
)


class DepartmentSpending(BaseModel):
class DepartmentSpending(_SpendingTotals):
"""Spending aggregation for a department.

Attributes:
department_name: Department name.
total_cost_usd: Total cost for this department.
total_input_tokens: Total input tokens consumed.
total_output_tokens: Total output tokens consumed.
record_count: Number of cost records.
"""

model_config = ConfigDict(frozen=True)

department_name: NotBlankStr = Field(
description="Department name",
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

JSON serialization field order has changed

The refactor silently changes the field ordering in model_dump() / model_dump_json() output for all three subclasses. Pydantic v2 emits fields in definition order — parent fields first, then subclass fields — so the identifier fields (agent_id, department_name, start/end) are now serialized last instead of first.

Before (e.g. AgentSpending):

{"agent_id": "alice", "total_cost_usd": 10.0, "total_input_tokens": 0, "total_output_tokens": 0, "record_count": 0}

After:

{"total_cost_usd": 10.0, "total_input_tokens": 0, "total_output_tokens": 0, "record_count": 0, "agent_id": "alice"}

Semantically this is harmless for roundtrip deserialization (Pydantic and most JSON consumers are order-agnostic), but the PR description claims "no behavioral changes." The change is real, and it could surface in:

  • snapshot / golden-file tests that do string-level JSON comparisons
  • downstream consumers that parse the JSON positionally (e.g. some CSV-style streaming parsers)
  • OpenAPI schema generation, where field order affects documentation readability

If preserving the original order matters, one lightweight fix is to redeclare the inherited dimension-specific field at the top of each subclass (with no body change) — Pydantic respects the MRO order and moves a redeclared field to where it's first seen in the subclass. Otherwise, consider adding an explicit note to the PR description and/or DESIGN_SPEC acknowledging the order change.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ai_company/budget/spending_summary.py
Line: 56-98

Comment:
**JSON serialization field order has changed**

The refactor silently changes the field ordering in `model_dump()` / `model_dump_json()` output for all three subclasses. Pydantic v2 emits fields in definition order — parent fields first, then subclass fields — so the identifier fields (`agent_id`, `department_name`, `start`/`end`) are now serialized *last* instead of first.

Before (e.g. `AgentSpending`):
```json
{"agent_id": "alice", "total_cost_usd": 10.0, "total_input_tokens": 0, "total_output_tokens": 0, "record_count": 0}
```

After:
```json
{"total_cost_usd": 10.0, "total_input_tokens": 0, "total_output_tokens": 0, "record_count": 0, "agent_id": "alice"}
```

Semantically this is harmless for roundtrip deserialization (Pydantic and most JSON consumers are order-agnostic), but the PR description claims "no behavioral changes." The change is real, and it could surface in:
- snapshot / golden-file tests that do string-level JSON comparisons
- downstream consumers that parse the JSON positionally (e.g. some CSV-style streaming parsers)
- OpenAPI schema generation, where field order affects documentation readability

If preserving the original order matters, one lightweight fix is to redeclare the inherited dimension-specific field at the top of each subclass (with no body change) — Pydantic respects the MRO order and moves a redeclared field to where it's first seen in the subclass. Otherwise, consider adding an explicit note to the PR description and/or DESIGN_SPEC acknowledging the order change.

How can I resolve this? If you propose a fix, please make it concise.

Repository owner deleted a comment from coderabbitai bot Mar 6, 2026
- Soften module docstring CFO agent reference (M5+ feature, not yet built)
- Update DESIGN_SPEC §15.3 spending_summary.py comment to mention _SpendingTotals

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Aureliolo Aureliolo merged commit 2f39c1b into main Mar 6, 2026
8 checks passed
@Aureliolo Aureliolo deleted the refactor/spending-totals-base branch March 6, 2026 14:54
Comment on lines 20 to 54
@@ -53,6 +53,18 @@ class PeriodSpending(BaseModel):
description="Number of cost records aggregated",
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

_SpendingTotals is directly instantiable

The docstring states "Not intended for direct instantiation", but because all four fields carry defaults (0.0 / 0), the class can be silently instantiated without error:

t = _SpendingTotals()          # works — produces a dimensionless totals object
t = _SpendingTotals(total_cost_usd=9.99)  # also works

The underscore prefix communicates the intent by convention, but nothing prevents accidental use. Consider enforcing the constraint by raising in __init_subclass__ or, more idiomatically with Pydantic v2, by overriding model_post_init:

def model_post_init(self, __context: object) -> None:
    if type(self) is _SpendingTotals:
        raise TypeError(
            "_SpendingTotals is not intended for direct instantiation; "
            "use AgentSpending, DepartmentSpending, or PeriodSpending instead."
        )

This turns a documentation-only contract into a runtime-enforced one, which is particularly valuable given the class lives in a public package with three concrete subclasses already defined.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ai_company/budget/spending_summary.py
Line: 20-54

Comment:
**`_SpendingTotals` is directly instantiable**

The docstring states *"Not intended for direct instantiation"*, but because all four fields carry defaults (`0.0` / `0`), the class can be silently instantiated without error:

```python
t = _SpendingTotals()          # works — produces a dimensionless totals object
t = _SpendingTotals(total_cost_usd=9.99)  # also works
```

The underscore prefix communicates the intent by convention, but nothing prevents accidental use. Consider enforcing the constraint by raising in `__init_subclass__` or, more idiomatically with Pydantic v2, by overriding `model_post_init`:

```python
def model_post_init(self, __context: object) -> None:
    if type(self) is _SpendingTotals:
        raise TypeError(
            "_SpendingTotals is not intended for direct instantiation; "
            "use AgentSpending, DepartmentSpending, or PeriodSpending instead."
        )
```

This turns a documentation-only contract into a runtime-enforced one, which is particularly valuable given the class lives in a public package with three concrete subclasses already defined.

How can I resolve this? If you propose a fix, please make it concise.

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.

Extract shared _SpendingTotals base for spending summary models

2 participants