Skip to content
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
29 changes: 25 additions & 4 deletions packages/backend/app/db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ CREATE INDEX IF NOT EXISTS idx_expenses_user_spent_at ON expenses(user_id, spent
ALTER TABLE expenses
ADD COLUMN IF NOT EXISTS expense_type VARCHAR(20) NOT NULL DEFAULT 'EXPENSE';

DO $$ BEGIN
DO ` BEGIN
CREATE TYPE recurring_cadence AS ENUM ('DAILY','WEEKLY','MONTHLY','YEARLY');
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
END `;

CREATE TABLE IF NOT EXISTS recurring_expenses (
id SERIAL PRIMARY KEY,
Expand All @@ -59,11 +59,11 @@ CREATE INDEX IF NOT EXISTS idx_recurring_expenses_user_start ON recurring_expens
ALTER TABLE expenses
ADD COLUMN IF NOT EXISTS source_recurring_id INT REFERENCES recurring_expenses(id) ON DELETE SET NULL;

DO $$ BEGIN
DO ` BEGIN
CREATE TYPE bill_cadence AS ENUM ('MONTHLY','WEEKLY','YEARLY','ONCE');
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
END `;

CREATE TABLE IF NOT EXISTS bills (
id SERIAL PRIMARY KEY,
Expand Down Expand Up @@ -123,3 +123,24 @@ CREATE TABLE IF NOT EXISTS audit_logs (
action VARCHAR(100) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

-- Detected subscriptions table
CREATE TABLE IF NOT EXISTS detected_subscriptions (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(200) NOT NULL,
amount NUMERIC(12,2) NOT NULL,
currency VARCHAR(10) NOT NULL DEFAULT 'INR',
detected_cadence VARCHAR(20) NOT NULL,
next_expected_date DATE NOT NULL,
confidence NUMERIC(3,2) NOT NULL,
last_occurrence_date DATE NOT NULL,
occurrence_count INT NOT NULL DEFAULT 1,
pattern_description VARCHAR(500),
confirmed BOOLEAN NOT NULL DEFAULT FALSE,
dismissed BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_detected_subscriptions_user ON detected_subscriptions(user_id, dismissed, confirmed);
CREATE INDEX IF NOT EXISTS idx_detected_subscriptions_pattern ON detected_subscriptions(user_id, pattern_description);
99 changes: 99 additions & 0 deletions packages/backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,102 @@ 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)


class SavingsGoal(db.Model):
__tablename__ = 'savings_goals'
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)
description = db.Column(db.String(500), nullable=True)
target_amount = db.Column(db.Numeric(12, 2), nullable=False)
current_amount = db.Column(db.Numeric(12, 2), default=0, nullable=False)
currency = db.Column(db.String(10), default='INR', nullable=False)
deadline = db.Column(db.Date, nullable=True)
completed = db.Column(db.Boolean, default=False, nullable=False)
completed_at = db.Column(db.DateTime, 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)
milestones = db.relationship('SavingsMilestone', backref='goal', lazy=True, cascade='all, delete-orphan')

class SavingsMilestone(db.Model):
__tablename__ = 'savings_milestones'
id = db.Column(db.Integer, primary_key=True)
goal_id = db.Column(db.Integer, db.ForeignKey('savings_goals.id'), nullable=False)
name = db.Column(db.String(200), nullable=False)
target_amount = db.Column(db.Numeric(12, 2), nullable=False)
reached = db.Column(db.Boolean, default=False, nullable=False)
reached_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)


class RuleField(str, Enum):
PAYEE = 'payee'
AMOUNT = 'amount'
DESCRIPTION = 'description'
NOTES = 'notes'

class RuleOperator(str, Enum):
CONTAINS = 'contains'
EQUALS = 'equals'
REGEX = 'regex'
GT = 'gt'
LT = 'lt'
GTE = 'gte'
LTE = 'lte'
STARTSWITH = 'startswith'
ENDSWITH = 'endswith'

class ConditionType(str, Enum):
AND = 'AND'
OR = 'OR'

class CategorizationRule(db.Model):
__tablename__ = 'categorization_rules'
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(100), nullable=False)
field = db.Column(SAEnum(RuleField), nullable=False)
operator = db.Column(SAEnum(RuleOperator), nullable=False)
value = db.Column(db.String(500), nullable=False)
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
tag = db.Column(db.String(100), nullable=True)
priority = db.Column(db.Integer, default=0, nullable=False)
condition_type = db.Column(SAEnum(ConditionType), default=ConditionType.AND, nullable=False)
active = db.Column(db.Boolean, default=True, nullable=False)
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)

def to_dict(self):
return {
'id': self.id,
'name': self.name,
'field': self.field.value if self.field else None,
'operator': self.operator.value if self.operator else None,
'value': self.value,
'category_id': self.category_id,
'tag': self.tag,
'priority': self.priority,
'condition_type': self.condition_type.value if self.condition_type else None,
'active': self.active,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
}

class RuleCondition(db.Model):
__tablename__ = 'rule_conditions'
id = db.Column(db.Integer, primary_key=True)
rule_id = db.Column(db.Integer, db.ForeignKey('categorization_rules.id', ondelete='CASCADE'), nullable=False)
field = db.Column(SAEnum(RuleField), nullable=False)
operator = db.Column(SAEnum(RuleOperator), nullable=False)
value = db.Column(db.String(500), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)

def to_dict(self):
return {
'id': self.id,
'rule_id': self.rule_id,
'field': self.field.value if self.field else None,
'operator': self.operator.value if self.operator else None,
'value': self.value,
}
20 changes: 12 additions & 8 deletions packages/backend/app/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@
from .categories import bp as categories_bp
from .docs import bp as docs_bp
from .dashboard import bp as dashboard_bp
from .savings_goals import bp as savings_goals_bp
from .analytics import bp as analytics_bp


def register_routes(app: Flask):
app.register_blueprint(auth_bp, url_prefix="/auth")
app.register_blueprint(expenses_bp, url_prefix="/expenses")
app.register_blueprint(bills_bp, url_prefix="/bills")
app.register_blueprint(reminders_bp, url_prefix="/reminders")
app.register_blueprint(insights_bp, url_prefix="/insights")
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(auth_bp, url_prefix='/auth')
app.register_blueprint(expenses_bp, url_prefix='/expenses')
app.register_blueprint(bills_bp, url_prefix='/bills')
app.register_blueprint(reminders_bp, url_prefix='/reminders')
app.register_blueprint(insights_bp, url_prefix='/insights')
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(savings_goals_bp, url_prefix='/savings-goals')
app.register_blueprint(analytics_bp, url_prefix='/analytics')
Loading