mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-13 02:56:39 +07:00
add "debit_card" as a new AccountType (#16)
This commit is contained in:
2
DOCS.md
2
DOCS.md
@@ -126,7 +126,7 @@ An account is a place money lives — a bank account, a card, a cash wallet. Eve
|
||||
to exactly one account (transfers touch two). Balances are **derived** from transactions, not
|
||||
stored — there's no "set balance" endpoint.
|
||||
|
||||
**`AccountType`:** `checking` · `savings` · `credit_card` · `cash` · `investment` · `other`
|
||||
**`AccountType`:** `checking` · `debit_card` · `savings` · `credit_card` · `cash` · `investment` · `other`
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
|
||||
@@ -4,6 +4,7 @@ import enum
|
||||
|
||||
class AccountType(str, enum.Enum):
|
||||
CHECKING = "checking"
|
||||
DEBIT_CARD = "debit_card"
|
||||
SAVINGS = "savings"
|
||||
CREDIT_CARD = "credit_card"
|
||||
CASH = "cash"
|
||||
|
||||
@@ -33,7 +33,7 @@ MAX_LOOKBACK_MONTHS = 24
|
||||
# Accounts where a big, untouched balance means money isn't working — a
|
||||
# credit card balance isn't "your cash", an investment account is already
|
||||
# invested, and OTHER is too ambiguous to guess at.
|
||||
_IDLE_CASH_ACCOUNT_TYPES = (AccountType.CHECKING, AccountType.SAVINGS, AccountType.CASH)
|
||||
_IDLE_CASH_ACCOUNT_TYPES = (AccountType.CHECKING, AccountType.DEBIT_CARD, AccountType.SAVINGS, AccountType.CASH)
|
||||
|
||||
|
||||
def _previous_month(year: int, month: int) -> tuple[int, int]:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
asset class.
|
||||
|
||||
Design note: "Cash" is not stored anywhere — it's derived live from
|
||||
Account/Transaction data (checking/savings/cash/investment accounts) so the
|
||||
Account/Transaction data (checking/debit-card/savings/cash/investment accounts) so the
|
||||
Transactions feature stays the single source of truth for liquid money.
|
||||
Investment accounts count here too: their balance is uninvested/unallocated
|
||||
money sitting on the account, not the market value of what's actually
|
||||
@@ -36,7 +36,13 @@ from app.schemas.net_worth import (
|
||||
RiskLevelSummary,
|
||||
)
|
||||
|
||||
CASH_ACCOUNT_TYPES = {AccountType.CHECKING, AccountType.SAVINGS, AccountType.CASH, AccountType.INVESTMENT}
|
||||
CASH_ACCOUNT_TYPES = {
|
||||
AccountType.CHECKING,
|
||||
AccountType.DEBIT_CARD,
|
||||
AccountType.SAVINGS,
|
||||
AccountType.CASH,
|
||||
AccountType.INVESTMENT,
|
||||
}
|
||||
|
||||
RANGE_DAYS = {"30d": 30, "90d": 90, "1y": 365, "5y": 365 * 5}
|
||||
|
||||
|
||||
34
backend/tests/test_accounts.py
Normal file
34
backend/tests/test_accounts.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Account-type behavior shared by the accounts API and financial summaries."""
|
||||
from datetime import date
|
||||
|
||||
from httpx import AsyncClient
|
||||
|
||||
from tests.helpers import money, txn_payload
|
||||
|
||||
|
||||
async def test_debit_card_is_a_liquid_account_type(client: AsyncClient, categories):
|
||||
response = await client.post(
|
||||
"/accounts",
|
||||
json={"name": "Debit card", "type": "debit_card", "currency": "RUB"},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
debit_card = response.json()
|
||||
assert debit_card["type"] == "debit_card"
|
||||
|
||||
transaction = await client.post(
|
||||
"/transactions",
|
||||
json=txn_payload(
|
||||
debit_card["id"],
|
||||
type="income",
|
||||
amount="1000.00",
|
||||
category_id=categories["Salary"]["id"],
|
||||
date=date.today().isoformat(),
|
||||
),
|
||||
)
|
||||
assert transaction.status_code == 201
|
||||
|
||||
summary = await client.get("/net-worth/summary", params={"range": "all"})
|
||||
|
||||
assert summary.status_code == 200
|
||||
assert money(summary.json()["current"]) == money("1000.00")
|
||||
@@ -51,6 +51,25 @@ async def test_idle_cash_ignores_investment_accounts(client: AsyncClient, catego
|
||||
assert "idle_cash" not in await _alert_keys(client)
|
||||
|
||||
|
||||
async def test_idle_cash_includes_debit_cards(client: AsyncClient, categories):
|
||||
debit_card = await client.post(
|
||||
"/accounts", json={"name": "Debit card", "type": "debit_card", "currency": "USD"}
|
||||
)
|
||||
debit_card_id = debit_card.json()["id"]
|
||||
await client.post(
|
||||
"/transactions",
|
||||
json=_txn(
|
||||
debit_card_id,
|
||||
type="income",
|
||||
amount="5000.00",
|
||||
category_id=categories["Salary"]["id"],
|
||||
date=STALE_DATE,
|
||||
),
|
||||
)
|
||||
|
||||
assert "idle_cash" in await _alert_keys(client)
|
||||
|
||||
|
||||
async def test_idle_cash_respects_custom_threshold(client: AsyncClient, account_id, categories):
|
||||
await client.post(
|
||||
"/transactions",
|
||||
|
||||
@@ -12,7 +12,7 @@ interface AccountFormModalProps {
|
||||
account?: Account | null;
|
||||
}
|
||||
|
||||
const ACCOUNT_TYPES: AccountType[] = ["checking", "savings", "credit_card", "cash", "investment", "other"];
|
||||
const ACCOUNT_TYPES: AccountType[] = ["checking", "debit_card", "savings", "credit_card", "cash", "investment", "other"];
|
||||
|
||||
const EMPTY_FORM = { name: "", type: "checking" as AccountType };
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ interface AccountListProps {
|
||||
|
||||
const TYPE_ICONS: Record<AccountType, LucideIcon> = {
|
||||
checking: Wallet,
|
||||
debit_card: CreditCard,
|
||||
savings: PiggyBank,
|
||||
credit_card: CreditCard,
|
||||
cash: Banknote,
|
||||
|
||||
@@ -424,6 +424,7 @@ const ru = {
|
||||
"account.confirmDelete":
|
||||
"Удалить счёт «{{name}}»?\n\nЭто безвозвратно удалит и ВСЕ транзакции по этому счёту. Если хотите сохранить историю, заархивируйте счёт вместо удаления.",
|
||||
"account.type.checking": "Текущий счёт",
|
||||
"account.type.debit_card": "Дебетовая карта",
|
||||
"account.type.savings": "Накопительный счёт",
|
||||
"account.type.credit_card": "Кредитная карта",
|
||||
"account.type.cash": "Наличные",
|
||||
@@ -918,6 +919,7 @@ const en: Record<keyof typeof ru, string> = {
|
||||
"account.confirmDelete":
|
||||
"Delete the account “{{name}}”?\n\nThis will permanently delete ALL transactions on this account too. If you want to keep the history, archive the account instead of deleting it.",
|
||||
"account.type.checking": "Checking",
|
||||
"account.type.debit_card": "Debit card",
|
||||
"account.type.savings": "Savings",
|
||||
"account.type.credit_card": "Credit card",
|
||||
"account.type.cash": "Cash",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type AccountType = "checking" | "savings" | "credit_card" | "cash" | "investment" | "other";
|
||||
export type AccountType = "checking" | "debit_card" | "savings" | "credit_card" | "cash" | "investment" | "other";
|
||||
export type CategoryKind = "income" | "expense";
|
||||
export type TransactionType = "income" | "expense" | "transfer";
|
||||
export type RecurringFrequency = "weekly" | "monthly" | "yearly";
|
||||
|
||||
Reference in New Issue
Block a user