05/31 Phase 1: initial codes

This commit is contained in:
2026-05-31 10:03:28 -04:00
parent b0a5ce5399
commit b0c0bd363b
35 changed files with 2871 additions and 164 deletions
+12
View File
@@ -0,0 +1,12 @@
from app.models.user import User
from app.models.account import Account
from app.models.category import Category
from app.models.receipt import Receipt
from app.models.recurring_rule import RecurringRule
from app.models.transaction import Transaction
from app.models.budget import Budget
from app.models.goal import Goal, GoalContribution
from app.models.investment import Investment, InvestmentTransaction
from app.models.net_worth_snapshot import NetWorthSnapshot
from app.models.ai_insight import AiInsight
from app.models.fx_rate import FxRate
+32
View File
@@ -0,0 +1,32 @@
from app.extensions import db
from datetime import datetime
class Account(db.Model):
__tablename__ = 'accounts'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
account_type = db.Column(
db.Enum('checking', 'savings', 'cash', 'credit_card', 'crypto', 'investment', 'other'),
nullable=False,
default='checking'
)
balance = db.Column(db.Numeric(15, 2), default=0.00, nullable=False)
credit_limit = db.Column(db.Numeric(15, 2), nullable=True) # credit cards
due_date = db.Column(db.Integer, nullable=True) # day of month bill due
color = db.Column(db.String(7), default='#4F81C7') # hex color for UI
icon = db.Column(db.String(50), default='bi-bank') # bootstrap icon name
is_active = db.Column(db.Boolean, default=True)
notes = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
transactions = db.relationship('Transaction', back_populates='account',
lazy='dynamic', foreign_keys='Transaction.account_id')
transfer_transactions = db.relationship('Transaction', back_populates='to_account',
lazy='dynamic', foreign_keys='Transaction.to_account_id')
def __repr__(self):
return f'<Account {self.name}>'
+22
View File
@@ -0,0 +1,22 @@
from app.extensions import db
from datetime import datetime
class AiInsight(db.Model):
__tablename__ = 'ai_insights'
id = db.Column(db.Integer, primary_key=True)
insight_date = db.Column(db.Date, nullable=False, index=True)
insight_type = db.Column(
db.Enum('daily_summary', 'weekly_summary', 'chat_response', 'alert'),
nullable=False,
default='daily_summary'
)
prompt_summary = db.Column(db.Text, nullable=True) # what context was sent
content = db.Column(db.Text, nullable=False) # AI response
model_used = db.Column(db.String(64), nullable=True)
tokens_used = db.Column(db.Integer, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def __repr__(self):
return f'<AiInsight {self.insight_type} {self.insight_date}>'
+23
View File
@@ -0,0 +1,23 @@
from app.extensions import db
from datetime import datetime
class Budget(db.Model):
__tablename__ = 'budgets'
id = db.Column(db.Integer, primary_key=True)
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=False)
month = db.Column(db.String(7), nullable=False, index=True) # YYYY-MM
limit_amount = db.Column(db.Numeric(15, 2), nullable=False)
rollover_enabled = db.Column(db.Boolean, default=False)
rollover_amount = db.Column(db.Numeric(15, 2), default=0.00)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
category = db.relationship('Category')
__table_args__ = (
db.UniqueConstraint('category_id', 'month', name='uq_budget_category_month'),
)
def __repr__(self):
return f'<Budget {self.month} cat={self.category_id} limit={self.limit_amount}>'
+28
View File
@@ -0,0 +1,28 @@
from app.extensions import db
from datetime import datetime
class Category(db.Model):
__tablename__ = 'categories'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
category_type = db.Column(
db.Enum('income', 'expense', 'both'),
nullable=False,
default='expense'
)
parent_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
color = db.Column(db.String(7), default='#6B7280')
icon = db.Column(db.String(50), default='bi-tag')
is_system = db.Column(db.Boolean, default=False) # system categories can't be deleted
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Self-referential relationship for subcategories
subcategories = db.relationship('Category', backref=db.backref('parent', remote_side=[id]),
lazy='dynamic')
transactions = db.relationship('Transaction', back_populates='category', lazy='dynamic')
def __repr__(self):
return f'<Category {self.name}>'
+15
View File
@@ -0,0 +1,15 @@
from app.extensions import db
from datetime import datetime
class FxRate(db.Model):
__tablename__ = 'fx_rates'
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.Date, nullable=False, unique=True, index=True)
usd_to_vnd = db.Column(db.Numeric(12, 2), nullable=False)
source = db.Column(db.String(50), nullable=True) # 'exchangerate-api' | 'vcb' | 'manual'
fetched_at = db.Column(db.DateTime, default=datetime.utcnow)
def __repr__(self):
return f'<FxRate {self.date} 1USD={self.usd_to_vnd}VND>'
+50
View File
@@ -0,0 +1,50 @@
from app.extensions import db
from datetime import datetime
class Goal(db.Model):
__tablename__ = 'goals'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text, nullable=True)
target_amount = db.Column(db.Numeric(15, 2), nullable=False)
current_amount = db.Column(db.Numeric(15, 2), default=0.00)
target_date = db.Column(db.Date, nullable=True)
linked_account_id = db.Column(db.Integer, db.ForeignKey('accounts.id'), nullable=True)
color = db.Column(db.String(7), default='#10B981')
icon = db.Column(db.String(50), default='bi-piggy-bank')
is_completed = db.Column(db.Boolean, default=False)
completed_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
linked_account = db.relationship('Account')
contributions = db.relationship('GoalContribution', back_populates='goal',
lazy='dynamic', cascade='all, delete-orphan')
@property
def progress_percent(self):
if self.target_amount <= 0:
return 0
pct = (float(self.current_amount) / float(self.target_amount)) * 100
return min(round(pct, 1), 100.0)
def __repr__(self):
return f'<Goal {self.name}>'
class GoalContribution(db.Model):
__tablename__ = 'goal_contributions'
id = db.Column(db.Integer, primary_key=True)
goal_id = db.Column(db.Integer, db.ForeignKey('goals.id'), nullable=False)
amount = db.Column(db.Numeric(15, 2), nullable=False)
notes = db.Column(db.String(255), nullable=True)
date = db.Column(db.Date, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
goal = db.relationship('Goal', back_populates='contributions')
def __repr__(self):
return f'<GoalContribution goal={self.goal_id} amount={self.amount}>'
+69
View File
@@ -0,0 +1,69 @@
from app.extensions import db
from datetime import datetime
class Investment(db.Model):
__tablename__ = 'investments'
id = db.Column(db.Integer, primary_key=True)
asset_name = db.Column(db.String(100), nullable=False)
ticker = db.Column(db.String(20), nullable=True)
asset_type = db.Column(
db.Enum('stock', 'etf', 'crypto', 'real_estate', 'bond', 'cash', 'other'),
nullable=False,
default='stock'
)
shares = db.Column(db.Numeric(18, 8), nullable=False, default=0)
avg_cost_basis = db.Column(db.Numeric(15, 4), nullable=False, default=0)
current_price = db.Column(db.Numeric(15, 4), nullable=True)
last_price_update = db.Column(db.DateTime, nullable=True)
notes = db.Column(db.Text, nullable=True)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
inv_transactions = db.relationship('InvestmentTransaction', back_populates='investment',
lazy='dynamic', cascade='all, delete-orphan')
@property
def total_cost(self):
return float(self.shares) * float(self.avg_cost_basis)
@property
def current_value(self):
if self.current_price:
return float(self.shares) * float(self.current_price)
return self.total_cost
@property
def unrealized_gain(self):
return self.current_value - self.total_cost
@property
def unrealized_gain_pct(self):
if self.total_cost == 0:
return 0
return round((self.unrealized_gain / self.total_cost) * 100, 2)
def __repr__(self):
return f'<Investment {self.asset_name}>'
class InvestmentTransaction(db.Model):
__tablename__ = 'investment_transactions'
id = db.Column(db.Integer, primary_key=True)
investment_id = db.Column(db.Integer, db.ForeignKey('investments.id'), nullable=False)
transaction_type = db.Column(db.Enum('buy', 'sell', 'dividend', 'split'), nullable=False)
shares = db.Column(db.Numeric(18, 8), nullable=False)
price_per_share = db.Column(db.Numeric(15, 4), nullable=False)
total_amount = db.Column(db.Numeric(15, 2), nullable=False)
fees = db.Column(db.Numeric(10, 2), default=0.00)
date = db.Column(db.Date, nullable=False, index=True)
notes = db.Column(db.String(255), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
investment = db.relationship('Investment', back_populates='inv_transactions')
def __repr__(self):
return f'<InvTransaction {self.transaction_type} {self.shares}@{self.price_per_share}>'
+18
View File
@@ -0,0 +1,18 @@
from app.extensions import db
from datetime import datetime
class NetWorthSnapshot(db.Model):
__tablename__ = 'net_worth_snapshots'
id = db.Column(db.Integer, primary_key=True)
snapshot_date = db.Column(db.Date, nullable=False, unique=True, index=True)
total_assets = db.Column(db.Numeric(15, 2), nullable=False, default=0)
total_liabilities = db.Column(db.Numeric(15, 2), nullable=False, default=0)
net_worth = db.Column(db.Numeric(15, 2), nullable=False, default=0)
account_balances = db.Column(db.JSON, nullable=True) # snapshot of each account balance
investment_value = db.Column(db.Numeric(15, 2), default=0)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def __repr__(self):
return f'<NetWorthSnapshot {self.snapshot_date} nw={self.net_worth}>'
+18
View File
@@ -0,0 +1,18 @@
from app.extensions import db
from datetime import datetime
class Receipt(db.Model):
__tablename__ = 'receipts'
id = db.Column(db.Integer, primary_key=True)
filename = db.Column(db.String(255), nullable=False)
original_filename = db.Column(db.String(255), nullable=False)
file_size = db.Column(db.Integer, nullable=True)
mime_type = db.Column(db.String(50), nullable=True)
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
transaction = db.relationship('Transaction', back_populates='receipt', uselist=False)
def __repr__(self):
return f'<Receipt {self.filename}>'
+35
View File
@@ -0,0 +1,35 @@
from app.extensions import db
from datetime import datetime
class RecurringRule(db.Model):
__tablename__ = 'recurring_rules'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
account_id = db.Column(db.Integer, db.ForeignKey('accounts.id'), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
transaction_type = db.Column(
db.Enum('income', 'expense'),
nullable=False
)
amount = db.Column(db.Numeric(15, 2), nullable=False)
description = db.Column(db.String(255), nullable=False)
frequency = db.Column(
db.Enum('daily', 'weekly', 'biweekly', 'monthly', 'quarterly', 'yearly'),
nullable=False,
default='monthly'
)
start_date = db.Column(db.Date, nullable=False)
end_date = db.Column(db.Date, nullable=True)
last_run = db.Column(db.Date, nullable=True)
next_run = db.Column(db.Date, nullable=True, index=True)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
transactions = db.relationship('Transaction', back_populates='recurring_rule', lazy='dynamic')
account = db.relationship('Account')
category = db.relationship('Category')
def __repr__(self):
return f'<RecurringRule {self.name} {self.frequency}>'
+37
View File
@@ -0,0 +1,37 @@
from app.extensions import db
from datetime import datetime
class Transaction(db.Model):
__tablename__ = 'transactions'
id = db.Column(db.Integer, primary_key=True)
account_id = db.Column(db.Integer, db.ForeignKey('accounts.id'), nullable=False)
to_account_id = db.Column(db.Integer, db.ForeignKey('accounts.id'), nullable=True) # transfers
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
recurring_rule_id = db.Column(db.Integer, db.ForeignKey('recurring_rules.id'), nullable=True)
receipt_id = db.Column(db.Integer, db.ForeignKey('receipts.id'), nullable=True)
transaction_type = db.Column(
db.Enum('income', 'expense', 'transfer'),
nullable=False
)
amount = db.Column(db.Numeric(15, 2), nullable=False)
description = db.Column(db.String(255), nullable=False)
notes = db.Column(db.Text, nullable=True)
date = db.Column(db.Date, nullable=False, index=True)
is_recurring = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
account = db.relationship('Account', back_populates='transactions',
foreign_keys=[account_id])
to_account = db.relationship('Account', back_populates='transfer_transactions',
foreign_keys=[to_account_id])
category = db.relationship('Category', back_populates='transactions')
receipt = db.relationship('Receipt', back_populates='transaction')
recurring_rule = db.relationship('RecurringRule', back_populates='transactions')
def __repr__(self):
return f'<Transaction {self.transaction_type} {self.amount} on {self.date}>'
+34
View File
@@ -0,0 +1,34 @@
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app.extensions import db, login_manager
from datetime import datetime
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=False, index=True)
email = db.Column(db.String(120), unique=True, nullable=True)
password_hash = db.Column(db.String(256), nullable=False)
display_name = db.Column(db.String(100), nullable=True)
timezone = db.Column(db.String(64), default='Asia/Ho_Chi_Minh')
currency = db.Column(db.String(10), default='USD')
currency_symbol = db.Column(db.String(5), default='$')
groq_model = db.Column(db.String(64), default='llama-3.3-70b-versatile')
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_login = db.Column(db.DateTime, nullable=True)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.username}>'
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id))