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
32 changes: 32 additions & 0 deletions AUTODEV_REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# AUTODEV Report - Issue #134 Shared household budgeting support

## Changed Files
- `README.md`
- `packages/backend/app/__init__.py`
- `packages/backend/app/db/schema.sql`
- `packages/backend/app/models.py`
- `packages/backend/app/openapi.yaml`
- `packages/backend/app/routes/__init__.py`
- `packages/backend/app/routes/bills.py`
- `packages/backend/app/routes/categories.py`
- `packages/backend/app/routes/dashboard.py`
- `packages/backend/app/routes/expenses.py`
- `packages/backend/app/routes/households.py`
- `packages/backend/app/services/ai.py`
- `packages/backend/app/services/households.py`
- `packages/backend/tests/test_households.py`

## Test Commands
1. `sh ./scripts/test-backend.sh tests/test_households.py`
2. `sh ./scripts/test-backend.sh tests/test_households.py tests/test_categories.py tests/test_expenses.py tests/test_bills.py tests/test_dashboard.py tests/test_insights.py tests/test_auth.py`
3. `docker compose run --rm backend sh -lc "flake8 app tests"`

## Results
- Command 1: passed (`2 passed`)
- Command 2: passed (`20 passed`)
- Command 3: passed (no lint errors)

## Risks / Follow-ups
- This implementation enforces one household membership per user (`household_members.user_id` is unique). Multi-household membership is not supported.
- Frontend household management UI is not added in this change; collaboration is available through backend APIs and optional `household_id` payload fields.
- Some non-core flows (for example statement import and recurring-expense generation) still create personal expenses only unless explicitly extended in future work.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ flowchart LR

## PostgreSQL Schema (DDL)
See `backend/app/db/schema.sql`. Key tables:
- users, categories, expenses, bills, reminders
- users, households, household_members
- categories, expenses, bills, reminders (each supports optional `household_id` for shared records)
- ad_impressions, subscription_plans, user_subscriptions
- refresh_tokens (optional if rotating), audit_logs

Expand All @@ -62,8 +63,10 @@ See `backend/app/db/schema.sql`. Key tables:
## API Endpoints
OpenAPI: `backend/app/openapi.yaml`
- Auth: `/auth/register`, `/auth/login`, `/auth/refresh`
- Expenses: CRUD `/expenses`
- Bills: CRUD `/bills`, pay/mark `/bills/{id}/pay`
- Households: `/households`, `/households/current`, `/households/join`, `/households/leave`, `/households/members/{memberUserId}`
- Categories: CRUD `/categories` (supports optional `household_id`)
- Expenses: CRUD `/expenses` (supports optional `household_id`)
- Bills: CRUD `/bills`, pay/mark `/bills/{id}/pay` (supports optional `household_id`)
- Reminders: CRUD `/reminders`, trigger `/reminders/run`
- Insights: `/insights/monthly`, `/insights/budget-suggestion`

Expand Down
61 changes: 60 additions & 1 deletion packages/backend/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,69 @@ def _ensure_schema_compatibility(app: Flask) -> None:
NOT NULL DEFAULT 'INR'
"""
)
cur.execute(
"""
DO $$ BEGIN
CREATE TYPE household_role AS ENUM ('ADMIN','MEMBER');
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
"""
)
cur.execute(
"""
CREATE TABLE IF NOT EXISTS households (
id SERIAL PRIMARY KEY,
name VARCHAR(150) NOT NULL,
invite_code VARCHAR(32) UNIQUE NOT NULL,
created_by INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
)
"""
)
cur.execute(
"""
CREATE TABLE IF NOT EXISTS household_members (
id SERIAL PRIMARY KEY,
household_id INT NOT NULL REFERENCES households(id) ON DELETE CASCADE,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role household_role NOT NULL DEFAULT 'MEMBER',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT uq_household_member UNIQUE (household_id, user_id)
)
"""
)
cur.execute(
"""
CREATE UNIQUE INDEX IF NOT EXISTS idx_household_members_user_unique
ON household_members(user_id)
"""
)
cur.execute(
"""
ALTER TABLE categories
ADD COLUMN IF NOT EXISTS household_id INT
REFERENCES households(id) ON DELETE SET NULL
"""
)
cur.execute(
"""
ALTER TABLE expenses
ADD COLUMN IF NOT EXISTS household_id INT
REFERENCES households(id) ON DELETE SET NULL
"""
)
cur.execute(
"""
ALTER TABLE bills
ADD COLUMN IF NOT EXISTS household_id INT
REFERENCES households(id) ON DELETE SET NULL
"""
)
conn.commit()
except Exception:
app.logger.exception(
"Schema compatibility patch failed for users.preferred_currency"
"Schema compatibility patch failed for household compatibility changes"
)
conn.rollback()
finally:
Expand Down
40 changes: 40 additions & 0 deletions packages/backend/app/db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,47 @@ CREATE TABLE IF NOT EXISTS users (
ALTER TABLE users
ADD COLUMN IF NOT EXISTS preferred_currency VARCHAR(10) NOT NULL DEFAULT 'INR';

DO $$ BEGIN
CREATE TYPE household_role AS ENUM ('ADMIN','MEMBER');
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;

CREATE TABLE IF NOT EXISTS households (
id SERIAL PRIMARY KEY,
name VARCHAR(150) NOT NULL,
invite_code VARCHAR(32) UNIQUE NOT NULL,
created_by INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS household_members (
id SERIAL PRIMARY KEY,
household_id INT NOT NULL REFERENCES households(id) ON DELETE CASCADE,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role household_role NOT NULL DEFAULT 'MEMBER',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT uq_household_member UNIQUE (household_id, user_id)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_household_members_user_unique ON household_members(user_id);
CREATE INDEX IF NOT EXISTS idx_household_members_household ON household_members(household_id);

CREATE TABLE IF NOT EXISTS categories (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
household_id INT REFERENCES households(id) ON DELETE SET NULL,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_categories_household ON categories(household_id);

ALTER TABLE categories
ADD COLUMN IF NOT EXISTS household_id INT REFERENCES households(id) ON DELETE SET NULL;

CREATE TABLE IF NOT EXISTS expenses (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
household_id INT REFERENCES households(id) ON DELETE SET NULL,
category_id INT REFERENCES categories(id) ON DELETE SET NULL,
amount NUMERIC(12,2) NOT NULL,
currency VARCHAR(10) NOT NULL DEFAULT 'INR',
Expand All @@ -30,10 +61,14 @@ CREATE TABLE IF NOT EXISTS expenses (
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_expenses_user_spent_at ON expenses(user_id, spent_at DESC);
CREATE INDEX IF NOT EXISTS idx_expenses_household_spent_at ON expenses(household_id, spent_at DESC);

ALTER TABLE expenses
ADD COLUMN IF NOT EXISTS expense_type VARCHAR(20) NOT NULL DEFAULT 'EXPENSE';

ALTER TABLE expenses
ADD COLUMN IF NOT EXISTS household_id INT REFERENCES households(id) ON DELETE SET NULL;

DO $$ BEGIN
CREATE TYPE recurring_cadence AS ENUM ('DAILY','WEEKLY','MONTHLY','YEARLY');
EXCEPTION
Expand Down Expand Up @@ -68,6 +103,7 @@ END $$;
CREATE TABLE IF NOT EXISTS bills (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
household_id INT REFERENCES households(id) ON DELETE SET NULL,
name VARCHAR(200) NOT NULL,
amount NUMERIC(12,2) NOT NULL,
currency VARCHAR(10) NOT NULL DEFAULT 'INR',
Expand All @@ -80,10 +116,14 @@ CREATE TABLE IF NOT EXISTS bills (
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_bills_user_due ON bills(user_id, next_due_date);
CREATE INDEX IF NOT EXISTS idx_bills_household_due ON bills(household_id, next_due_date);

ALTER TABLE bills
ADD COLUMN IF NOT EXISTS autopay_enabled BOOLEAN NOT NULL DEFAULT FALSE;

ALTER TABLE bills
ADD COLUMN IF NOT EXISTS household_id INT REFERENCES households(id) ON DELETE SET NULL;

CREATE TABLE IF NOT EXISTS reminders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
Expand Down
41 changes: 41 additions & 0 deletions packages/backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,45 @@ class User(db.Model):
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)


class HouseholdRole(str, Enum):
ADMIN = "ADMIN"
MEMBER = "MEMBER"


class Household(db.Model):
__tablename__ = "households"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(150), nullable=False)
invite_code = db.Column(db.String(32), unique=True, nullable=False)
created_by = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)


class HouseholdMember(db.Model):
__tablename__ = "household_members"
id = db.Column(db.Integer, primary_key=True)
household_id = db.Column(
db.Integer, db.ForeignKey("households.id"), nullable=False, index=True
)
user_id = db.Column(
db.Integer, db.ForeignKey("users.id"), nullable=False, unique=True
)
role = db.Column(
SAEnum(HouseholdRole), default=HouseholdRole.MEMBER, nullable=False
)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
__table_args__ = (
db.UniqueConstraint("household_id", "user_id", name="uq_household_member"),
)


class Category(db.Model):
__tablename__ = "categories"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
household_id = db.Column(
db.Integer, db.ForeignKey("households.id"), nullable=True, index=True
)
name = db.Column(db.String(100), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)

Expand All @@ -31,6 +66,9 @@ class Expense(db.Model):
__tablename__ = "expenses"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
household_id = db.Column(
db.Integer, db.ForeignKey("households.id"), nullable=True, index=True
)
category_id = db.Column(db.Integer, db.ForeignKey("categories.id"), nullable=True)
amount = db.Column(db.Numeric(12, 2), nullable=False)
currency = db.Column(db.String(10), default="INR", nullable=False)
Expand Down Expand Up @@ -77,6 +115,9 @@ class Bill(db.Model):
__tablename__ = "bills"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
household_id = db.Column(
db.Integer, db.ForeignKey("households.id"), nullable=True, index=True
)
name = db.Column(db.String(200), nullable=False)
amount = db.Column(db.Numeric(12, 2), nullable=False)
currency = db.Column(db.String(10), default="INR", nullable=False)
Expand Down
Loading