Skip to content
This repository was archived by the owner on Jun 19, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,15 @@ OpenAPI: `backend/app/openapi.yaml`
- Auth: `/auth/register`, `/auth/login`, `/auth/refresh`
- Expenses: CRUD `/expenses`
- Bills: CRUD `/bills`, pay/mark `/bills/{id}/pay`
- Accounts: CRUD `/accounts`, consolidated net-worth overview `/accounts/overview`
- Reminders: CRUD `/reminders`, trigger `/reminders/run`
- Insights: `/insights/monthly`, `/insights/budget-suggestion`

## MVP UI/UX Plan
- Auth screens: register/login.
- Dashboard:
- Monthly spend chart, category breakdown donut.
- Multi-account net worth overview with assets, liabilities, and balances by account type.
- Upcoming bills list with due dates and pay status.
- AI budget suggestion card.
- Expenses page: add expense (amount, category, notes, date), list & filter.
Expand Down
33 changes: 33 additions & 0 deletions packages/backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,36 @@ class AuditLog(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
action = db.Column(db.String(100), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)


# ---------------------------------------------------------------------------
# Financial Accounts (multi-account support)
# ---------------------------------------------------------------------------

class AccountType(str, Enum):
CHECKING = "CHECKING"
SAVINGS = "SAVINGS"
CREDIT = "CREDIT"
INVESTMENT = "INVESTMENT"
CASH = "CASH"
OTHER = "OTHER"


class FinancialAccount(db.Model):
"""A named financial account belonging to a user (bank, wallet, credit card, …)."""
__tablename__ = "financial_accounts"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
name = db.Column(db.String(200), nullable=False)
account_type = db.Column(
db.String(20), default=AccountType.CHECKING.value, nullable=False
)
balance = db.Column(db.Numeric(14, 2), default=0, nullable=False)
currency = db.Column(db.String(10), default="INR", nullable=False)
institution = db.Column(db.String(200), nullable=True) # e.g. "HDFC Bank"
is_default = db.Column(db.Boolean, default=False, nullable=False)
notes = db.Column(db.String(500), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
192 changes: 192 additions & 0 deletions packages/backend/app/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ tags:
- name: Categories
- name: Expenses
- name: Bills
- name: Accounts
- name: Reminders
- name: Insights
paths:
Expand Down Expand Up @@ -329,6 +330,157 @@ paths:
application/json:
schema: { $ref: '#/components/schemas/Error' }

/accounts:
get:
summary: List financial accounts
tags: [Accounts]
security: [{ bearerAuth: [] }]
responses:
'200':
description: User-owned financial accounts
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/FinancialAccount'
example:
- { id: 1, name: Everyday Checking, account_type: CHECKING, balance: 1250.75, currency: USD, institution: FinBank, is_default: true, notes: Primary account }
'401':
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
post:
summary: Create financial account
tags: [Accounts]
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewFinancialAccount'
example:
name: Brokerage
account_type: INVESTMENT
balance: 5000
currency: USD
institution: FinInvest
is_default: false
notes: Long-term investments
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/FinancialAccount'
'400':
description: Validation error
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }

/accounts/overview:
get:
summary: Get consolidated financial overview
tags: [Accounts]
security: [{ bearerAuth: [] }]
responses:
'200':
description: Net worth and account totals for the authenticated user
content:
application/json:
schema:
$ref: '#/components/schemas/FinancialAccountsOverview'
example:
account_count: 3
total_assets: 8250.75
total_liabilities: 900.0
net_worth: 7350.75
by_type:
CHECKING: 1250.75
INVESTMENT: 5000
CREDIT: 900
accounts: []
'401':
description: Unauthorized
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }

/accounts/{accountId}:
get:
summary: Get financial account
tags: [Accounts]
security: [{ bearerAuth: [] }]
parameters:
- in: path
name: accountId
required: true
schema: { type: integer }
responses:
'200':
description: Financial account
content:
application/json:
schema:
$ref: '#/components/schemas/FinancialAccount'
'404':
description: Not found
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
patch:
summary: Update financial account
tags: [Accounts]
security: [{ bearerAuth: [] }]
parameters:
- in: path
name: accountId
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewFinancialAccount'
responses:
'200':
description: Updated financial account
content:
application/json:
schema:
$ref: '#/components/schemas/FinancialAccount'
'400':
description: Validation error
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'404':
description: Not found
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
delete:
summary: Delete financial account
tags: [Accounts]
security: [{ bearerAuth: [] }]
parameters:
- in: path
name: accountId
required: true
schema: { type: integer }
responses:
'204': { description: Deleted }
'404':
description: Not found
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }

/reminders:
get:
summary: List reminders
Expand Down Expand Up @@ -572,6 +724,46 @@ components:
autopay_enabled: { type: boolean, default: false }
channel_email: { type: boolean, default: true }
channel_whatsapp: { type: boolean, default: false }
FinancialAccount:
type: object
properties:
id: { type: integer }
name: { type: string }
account_type: { type: string, enum: [CHECKING, SAVINGS, CREDIT, INVESTMENT, CASH, OTHER] }
balance: { type: number, format: float }
currency: { type: string }
institution: { type: string, nullable: true }
is_default: { type: boolean }
notes: { type: string, nullable: true }
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
NewFinancialAccount:
type: object
required: [name]
properties:
name: { type: string }
account_type: { type: string, enum: [CHECKING, SAVINGS, CREDIT, INVESTMENT, CASH, OTHER], default: CHECKING }
balance: { type: number, format: float, default: 0 }
currency: { type: string }
institution: { type: string, nullable: true }
is_default: { type: boolean, default: false }
notes: { type: string, nullable: true }
FinancialAccountsOverview:
type: object
properties:
account_count: { type: integer }
total_assets: { type: number, format: float }
total_liabilities: { type: number, format: float }
net_worth: { type: number, format: float }
by_type:
type: object
additionalProperties:
type: number
format: float
accounts:
type: array
items:
$ref: '#/components/schemas/FinancialAccount'
Reminder:
type: object
properties:
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/app/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .categories import bp as categories_bp
from .docs import bp as docs_bp
from .dashboard import bp as dashboard_bp
from .accounts import bp as accounts_bp


def register_routes(app: Flask):
Expand All @@ -18,3 +19,4 @@ def register_routes(app: Flask):
app.register_blueprint(categories_bp, url_prefix="/categories")
app.register_blueprint(docs_bp, url_prefix="/docs")
app.register_blueprint(dashboard_bp, url_prefix="/dashboard")
app.register_blueprint(accounts_bp, url_prefix="/accounts")
80 changes: 80 additions & 0 deletions packages/backend/app/routes/accounts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Financial accounts routes (issue #132).

GET /accounts list all accounts
POST /accounts create account
GET /accounts/overview net-worth summary across all accounts
GET /accounts/<id> get one account
PATCH /accounts/<id> update account
DELETE /accounts/<id> delete account (204)
"""
from flask import Blueprint, jsonify, request
from flask_jwt_extended import jwt_required, get_jwt_identity
import logging

from ..services.accounts import (
account_to_dict,
create_account,
delete_account,
get_account,
get_accounts,
get_overview,
update_account,
)

bp = Blueprint("accounts", __name__)
logger = logging.getLogger("finmind.accounts.routes")


@bp.get("")
@jwt_required()
def list_accounts():
uid = int(get_jwt_identity())
return jsonify([account_to_dict(a) for a in get_accounts(uid)])


@bp.post("")
@jwt_required()
def create_account_endpoint():
uid = int(get_jwt_identity())
acc, err = create_account(uid, request.get_json() or {})
if err:
return jsonify(error=err), 400
return jsonify(account_to_dict(acc)), 201


@bp.get("/overview")
@jwt_required()
def overview():
uid = int(get_jwt_identity())
return jsonify(get_overview(uid))


@bp.get("/<int:account_id>")
@jwt_required()
def get_account_endpoint(account_id: int):
uid = int(get_jwt_identity())
acc = get_account(uid, account_id)
if not acc:
return jsonify(error="not found"), 404
return jsonify(account_to_dict(acc))


@bp.patch("/<int:account_id>")
@jwt_required()
def update_account_endpoint(account_id: int):
uid = int(get_jwt_identity())
acc, err = update_account(uid, account_id, request.get_json() or {})
if err == "not found":
return jsonify(error=err), 404
if err:
return jsonify(error=err), 400
return jsonify(account_to_dict(acc))


@bp.delete("/<int:account_id>")
@jwt_required()
def delete_account_endpoint(account_id: int):
uid = int(get_jwt_identity())
if not delete_account(uid, account_id):
return jsonify(error="not found"), 404
return "", 204
Loading