Add team budget system with spending limits #10

Open
opened 2026-04-25 13:46:07 +00:00 by padreug · 1 comment
Owner

Summary

Castle currently has no budget tracking or enforcement. We need a budget system where teams (modeled as RBAC roles) can have spending limits per account per period. When a user submits an expense that would exceed their team's budget, the submission should be blocked or flagged.

Use Case

Organizations with multiple teams/departments need to:

  • Set monthly/quarterly/annual budgets per expense category
  • Track spending against those budgets in real-time
  • Block or warn users when they're about to exceed a budget
  • Allow admins to increase budgets when needed

Proposed Data Model

class Budget(BaseModel):
    id: str
    account_id: str            # Which expense account (e.g., "Expenses:Supplies:Food")
    role_id: Optional[str]     # Which role/team (None = org-wide)
    period: str                # "monthly", "quarterly", "annual"
    limit_sats: int            # Budget limit in satoshis
    limit_fiat: Optional[Decimal]  # Optional fiat limit
    limit_currency: Optional[str]  # Fiat currency code
    start_date: Optional[date] # Budget period start (for custom periods)
    is_active: bool = True
    created_by: str
    created_at: datetime

Proposed Endpoints

POST   /api/v1/budgets              - Create budget (super user)
GET    /api/v1/budgets              - List all budgets
GET    /api/v1/budgets/{id}         - Get budget with current utilization
PUT    /api/v1/budgets/{id}         - Update budget limit (super user)
DELETE /api/v1/budgets/{id}         - Delete budget (super user)
GET    /api/v1/budgets/{id}/utilization - Current spend vs limit
GET    /api/v1/users/me/budgets     - User's applicable budgets with utilization

Budget Utilization Query

Current spending can be calculated via BQL:

SELECT sum(weight)
WHERE account ~ '{expense_account}'
  AND date >= '{period_start}'
  AND date <= '{period_end}'
  AND flag = '*'

Enforcement

When a user submits an expense via POST /entries/expense:

  1. Check if any budget applies (via user's roles → budget assignments)
  2. Calculate current period spend for that account
  3. If spend + new amount > limit: return 403 with budget details
  4. Response should include: budget_remaining, budget_limit, budget_utilization_pct

Frontend Integration

The Castle standalone app will display:

  • Budget utilization bars per category on the Balance page
  • Warning indicators when approaching limits (>80%)
  • Clear error messages when blocked by budget limits
## Summary Castle currently has no budget tracking or enforcement. We need a budget system where teams (modeled as RBAC roles) can have spending limits per account per period. When a user submits an expense that would exceed their team's budget, the submission should be blocked or flagged. ## Use Case Organizations with multiple teams/departments need to: - Set monthly/quarterly/annual budgets per expense category - Track spending against those budgets in real-time - Block or warn users when they're about to exceed a budget - Allow admins to increase budgets when needed ## Proposed Data Model ```python class Budget(BaseModel): id: str account_id: str # Which expense account (e.g., "Expenses:Supplies:Food") role_id: Optional[str] # Which role/team (None = org-wide) period: str # "monthly", "quarterly", "annual" limit_sats: int # Budget limit in satoshis limit_fiat: Optional[Decimal] # Optional fiat limit limit_currency: Optional[str] # Fiat currency code start_date: Optional[date] # Budget period start (for custom periods) is_active: bool = True created_by: str created_at: datetime ``` ## Proposed Endpoints ``` POST /api/v1/budgets - Create budget (super user) GET /api/v1/budgets - List all budgets GET /api/v1/budgets/{id} - Get budget with current utilization PUT /api/v1/budgets/{id} - Update budget limit (super user) DELETE /api/v1/budgets/{id} - Delete budget (super user) GET /api/v1/budgets/{id}/utilization - Current spend vs limit GET /api/v1/users/me/budgets - User's applicable budgets with utilization ``` ## Budget Utilization Query Current spending can be calculated via BQL: ```sql SELECT sum(weight) WHERE account ~ '{expense_account}' AND date >= '{period_start}' AND date <= '{period_end}' AND flag = '*' ``` ## Enforcement When a user submits an expense via `POST /entries/expense`: 1. Check if any budget applies (via user's roles → budget assignments) 2. Calculate current period spend for that account 3. If spend + new amount > limit: return 403 with budget details 4. Response should include: `budget_remaining`, `budget_limit`, `budget_utilization_pct` ## Frontend Integration The Castle standalone app will display: - Budget utilization bars per category on the Balance page - Warning indicators when approaching limits (>80%) - Clear error messages when blocked by budget limits
Author
Owner

Cross-linking aiolabs/webapp#55 — there's a cross-module "teams" concept brewing that also wants to land in tasks (aiolabs/tasks#1) and possibly events. Worth a quick alignment before this issue's RBAC-role model lands, because the two designs collide in an interesting way:

libra#10 today models a team as an internal RBAC role inside castle. Memberships live in libra's own DB and aren't visible to other modules.

webapp#55 is leaning toward teams as a Nostr-shared identifier (either a t tag convention or a NIP-51 follow set), so a user's "I'm on the animal-chores team" subscription is the same primitive whether they're seeing chores in tasks or checking the budget envelope in libra.

These aren't incompatible, but they imply different sources of truth:

  • If teams live in libra (RBAC), then tasks would need to query libra to know "who's on the farmers team" — heavy coupling.
  • If teams live in Nostr (NIP-51 list owned by a team admin), then libra's Budget.role_id could optionally store a Nostr team address (30000:admin-pubkey:team:farmers) instead of a local role id, and the libra UI resolves it the same way tasks does.

Worth resolving the umbrella's avenue A vs B before locking the schema here, so we don't end up with libra's role_id meaning something different from everything else's "team."

(Filing this as a comment rather than a separate issue since the work itself stays here — this is just a heads-up that the team primitive may want to come from outside libra.)

Cross-linking [aiolabs/webapp#55](https://git.atitlan.io/aiolabs/webapp/issues/55) — there's a cross-module "teams" concept brewing that also wants to land in tasks ([aiolabs/tasks#1](https://git.atitlan.io/aiolabs/tasks/issues/1)) and possibly events. Worth a quick alignment before this issue's RBAC-role model lands, because the two designs collide in an interesting way: **libra#10 today** models a team as an internal RBAC role inside castle. Memberships live in libra's own DB and aren't visible to other modules. **webapp#55** is leaning toward teams as a Nostr-shared identifier (either a `t` tag convention or a NIP-51 follow set), so a user's "I'm on the animal-chores team" subscription is the same primitive whether they're seeing chores in tasks or checking the budget envelope in libra. These aren't incompatible, but they imply different sources of truth: - If teams live in libra (RBAC), then tasks would need to query libra to know "who's on the farmers team" — heavy coupling. - If teams live in Nostr (NIP-51 list owned by a team admin), then libra's `Budget.role_id` could optionally store a Nostr team address (`30000:admin-pubkey:team:farmers`) instead of a local role id, and the libra UI resolves it the same way tasks does. Worth resolving the umbrella's avenue A vs B before locking the schema here, so we don't end up with libra's `role_id` meaning something different from everything else's "team." (Filing this as a comment rather than a separate issue since the work itself stays here — this is just a heads-up that the team primitive may want to come from outside libra.)
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/libra#10
No description provided.