Add user-facing income/revenue submission endpoint

Mirrors the existing expense submission flow so non-admin users can log
income on behalf of the organization for super-user review. New endpoint
POST /api/v1/entries/income takes invoice-key auth, creates a Beancount
transaction with the pending '!' flag, and reuses the existing
/entries/{id}/approve and /reject endpoints (which match by libra-{id}
link regardless of entry type).

Adds PermissionType.SUBMIT_INCOME granted on revenue accounts (parallel
to SUBMIT_EXPENSE on expense accounts) rather than overloading
SUBMIT_EXPENSE — the two operations target distinct account types and
should be grantable independently. Enforces AccountType.REVENUE on the
income account and AccountType.ASSET on the payment-method account;
fiat currency is required (matches the expense flow's effective
requirement). Income entries get a 'income-entry' tag and an
^inc-{entry_id} link for tracking, and surface in the existing
/entries/pending list for super-user approval.

UI work lives in the standalone webapp, out of scope here.

Closes #9

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-16 19:01:47 +02:00
commit 93b5c2677c
3 changed files with 219 additions and 0 deletions

View file

@ -885,3 +885,67 @@ def format_revenue_entry(
links=links,
meta=entry_meta
)
def format_income_entry(
user_id: str,
payment_account: str,
revenue_account: str,
amount_sats: int,
description: str,
entry_date: date,
fiat_currency: str,
fiat_amount: Decimal,
reference: Optional[str] = None,
entry_id: Optional[str] = None,
) -> Dict[str, Any]:
"""
Format a user-submitted income/revenue entry for Fava (pending approval).
Mirrors format_expense_entry: pending flag (!) for super-user review,
fiat-first price notation (@@ SATS) for BQL queryability, unique link
(^inc-{entry_id}) for tracking through the approve/reject flow.
Postings: DR payment_account (asset receives funds), CR revenue_account.
"""
if not fiat_currency or not fiat_amount or fiat_amount <= 0:
raise ValueError("fiat_currency and a positive fiat_amount are required for income entries")
if not entry_id:
entry_id = generate_entry_id()
fiat_amount_abs = abs(fiat_amount)
sats_abs = abs(amount_sats)
narration = f"{description} ({fiat_amount_abs:.2f} {fiat_currency})"
postings = [
{
"account": payment_account,
"amount": f"{fiat_amount_abs:.2f} {fiat_currency} @@ {sats_abs} SATS",
},
{
"account": revenue_account,
"amount": f"-{fiat_amount_abs:.2f} {fiat_currency} @@ {sats_abs} SATS",
},
]
entry_meta = {
"user-id": user_id,
"source": "libra-api",
"entry-id": entry_id,
}
links = [f"inc-{entry_id}"]
if reference:
links.append(sanitize_link(reference))
return format_transaction(
date_val=entry_date,
flag="!", # Pending - requires admin approval
narration=narration,
postings=postings,
tags=["income-entry"],
links=links,
meta=entry_meta,
)