Add user-facing income/revenue submission endpoint #13
3 changed files with 21 additions and 23 deletions
Record income receipts as a user receivable, not an entity asset
When a user submits income, the money is physically in *their* pocket,
not the entity's cash drawer. The original income endpoint posted DR
on a configurable payment-method asset account (Cash/Bank/Lightning),
which implicitly assumed the entity already had the funds.
Mirror the expense flow instead: DR Assets:Receivable:User-{id[:8]}
(via get_or_create_user_account), CR the revenue account. The user
now owes the entity until they hand the cash over via the existing
/settle-receivable workflow. With this, the per-user Outstanding
Balances card correctly nets expenses (entity owes user, -liability)
against income receipts (user owes entity, +receivable).
Drops payment_method_account from IncomeEntry — no longer needed.
commit
0f2a38ee7f
|
|
@ -889,7 +889,7 @@ def format_revenue_entry(
|
||||||
|
|
||||||
def format_income_entry(
|
def format_income_entry(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
payment_account: str,
|
user_account: str,
|
||||||
revenue_account: str,
|
revenue_account: str,
|
||||||
amount_sats: int,
|
amount_sats: int,
|
||||||
description: str,
|
description: str,
|
||||||
|
|
@ -906,7 +906,8 @@ def format_income_entry(
|
||||||
fiat-first price notation (@@ SATS) for BQL queryability, unique link
|
fiat-first price notation (@@ SATS) for BQL queryability, unique link
|
||||||
(^inc-{entry_id}) for tracking through the approve/reject flow.
|
(^inc-{entry_id}) for tracking through the approve/reject flow.
|
||||||
|
|
||||||
Postings: DR payment_account (asset receives funds), CR revenue_account.
|
Postings: DR user_account (Assets:Receivable:User-{id} — user owes
|
||||||
|
the entity until they hand the cash over), CR revenue_account.
|
||||||
"""
|
"""
|
||||||
if not fiat_currency or not fiat_amount or fiat_amount <= 0:
|
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")
|
raise ValueError("fiat_currency and a positive fiat_amount are required for income entries")
|
||||||
|
|
@ -921,7 +922,7 @@ def format_income_entry(
|
||||||
|
|
||||||
postings = [
|
postings = [
|
||||||
{
|
{
|
||||||
"account": payment_account,
|
"account": user_account,
|
||||||
"amount": f"{fiat_amount_abs:.2f} {fiat_currency} @@ {sats_abs} SATS",
|
"amount": f"{fiat_amount_abs:.2f} {fiat_currency} @@ {sats_abs} SATS",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
10
models.py
10
models.py
|
|
@ -128,12 +128,18 @@ class RevenueEntry(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class IncomeEntry(BaseModel):
|
class IncomeEntry(BaseModel):
|
||||||
"""Helper model for user-facing income/revenue submission (pending approval)"""
|
"""Helper model for user-facing income/revenue submission (pending approval).
|
||||||
|
|
||||||
|
The user records that they personally received money on the entity's
|
||||||
|
behalf — so the postings are DR Assets:Receivable:User-{id} / CR
|
||||||
|
revenue_account. The user now owes the entity until they settle via
|
||||||
|
the existing /settle-receivable flow. Symmetric with ExpenseEntry,
|
||||||
|
which credits Liabilities:Payable:User-{id} (entity owes user).
|
||||||
|
"""
|
||||||
|
|
||||||
description: str
|
description: str
|
||||||
amount: Decimal # Fiat amount in the specified currency
|
amount: Decimal # Fiat amount in the specified currency
|
||||||
revenue_account: str # Income/Revenue account name or ID
|
revenue_account: str # Income/Revenue account name or ID
|
||||||
payment_method_account: str # Asset account receiving the funds (Cash, Bank, Lightning)
|
|
||||||
currency: str # Required: fiat currency code (EUR, USD, etc.)
|
currency: str # Required: fiat currency code (EUR, USD, etc.)
|
||||||
reference: Optional[str] = None
|
reference: Optional[str] = None
|
||||||
entry_date: Optional[datetime] = None
|
entry_date: Optional[datetime] = None
|
||||||
|
|
|
||||||
27
views_api.py
27
views_api.py
|
|
@ -1235,21 +1235,6 @@ async def api_create_income_entry(
|
||||||
detail=f"Account '{revenue_account.name}' is not a revenue account (type: {revenue_account.account_type.value})",
|
detail=f"Account '{revenue_account.name}' is not a revenue account (type: {revenue_account.account_type.value})",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Resolve payment method account by name or ID
|
|
||||||
payment_account = await get_account_by_name(data.payment_method_account)
|
|
||||||
if not payment_account:
|
|
||||||
payment_account = await get_account(data.payment_method_account)
|
|
||||||
if not payment_account:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND,
|
|
||||||
detail=f"Payment account '{data.payment_method_account}' not found",
|
|
||||||
)
|
|
||||||
if payment_account.account_type != AccountType.ASSET:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail=f"Account '{payment_account.name}' is not an asset account (type: {payment_account.account_type.value})",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Permission check on the revenue account
|
# Permission check on the revenue account
|
||||||
from .crud import get_user_permissions_with_inheritance
|
from .crud import get_user_permissions_with_inheritance
|
||||||
|
|
||||||
|
|
@ -1262,6 +1247,12 @@ async def api_create_income_entry(
|
||||||
detail=f"You do not have permission to submit income to account '{revenue_account.name}'. Please contact an administrator to request access.",
|
detail=f"You do not have permission to submit income to account '{revenue_account.name}'. Please contact an administrator to request access.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Income lands on the user as a receivable — they're holding cash on
|
||||||
|
# behalf of the entity until they hand it over via /settle-receivable.
|
||||||
|
user_account = await get_or_create_user_account(
|
||||||
|
wallet.wallet.user, AccountType.ASSET, "Accounts Receivable"
|
||||||
|
)
|
||||||
|
|
||||||
# Convert fiat to sats
|
# Convert fiat to sats
|
||||||
fiat_currency = data.currency.upper()
|
fiat_currency = data.currency.upper()
|
||||||
amount_sats = await fiat_amount_as_satoshis(float(data.amount), data.currency)
|
amount_sats = await fiat_amount_as_satoshis(float(data.amount), data.currency)
|
||||||
|
|
@ -1288,7 +1279,7 @@ async def api_create_income_entry(
|
||||||
|
|
||||||
entry = format_income_entry(
|
entry = format_income_entry(
|
||||||
user_id=wallet.wallet.user,
|
user_id=wallet.wallet.user,
|
||||||
payment_account=payment_account.name,
|
user_account=user_account.name,
|
||||||
revenue_account=revenue_account.name,
|
revenue_account=revenue_account.name,
|
||||||
amount_sats=amount_sats,
|
amount_sats=amount_sats,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
|
|
@ -1323,9 +1314,9 @@ async def api_create_income_entry(
|
||||||
EntryLine(
|
EntryLine(
|
||||||
id=f"line-1-{entry_id}",
|
id=f"line-1-{entry_id}",
|
||||||
journal_entry_id=entry_id,
|
journal_entry_id=entry_id,
|
||||||
account_id=payment_account.id,
|
account_id=user_account.id,
|
||||||
amount=amount_sats,
|
amount=amount_sats,
|
||||||
description=f"Income received into {payment_account.name}",
|
description=f"User holds cash receivable to entity ({user_account.name})",
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
),
|
),
|
||||||
EntryLine(
|
EntryLine(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue