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
36 changes: 36 additions & 0 deletions AUTODEV_REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# AUTODEV Report - Issue #133 Goal-based savings tracking & milestones

## Summary
Implemented backend savings-goal tracking with milestone progress:
- Added a new `savings_goals` data model/table.
- Added authenticated endpoints to list/create goals and add contributions.
- Added milestone and progress calculations (25/50/75/100%), next milestone, remaining amount, monthly target, and status.
- Added backend tests and updated API/docs references.

## Changed Files
- `packages/backend/tests/test_savings_goals.py`
- `packages/backend/app/models.py`
- `packages/backend/app/routes/savings.py`
- `packages/backend/app/routes/__init__.py`
- `packages/backend/app/db/schema.sql`
- `packages/backend/app/openapi.yaml`
- `README.md`

## Test Commands
1. `sh ./scripts/test-backend.sh tests/test_savings_goals.py` (RED phase before implementation)
2. `sh ./scripts/test-backend.sh tests/test_savings_goals.py` (GREEN after implementation)
3. `sh ./scripts/test-backend.sh` (full backend regression suite)
4. `docker compose run --rm backend sh -lc "flake8 app/routes/savings.py app/models.py tests/test_savings_goals.py"`
5. `cd packages/backend && ../../.venv/bin/python - <<'PY' ... yaml.safe_load('app/openapi.yaml') ... PY` (OpenAPI YAML parse check)

## Results
- RED phase: **FAIL as expected** (`404` on missing `/savings/goals` endpoints).
- New savings tests after implementation: **PASS** (`2 passed`).
- Full backend suite: **PASS** (`24 passed`).
- Flake8 on touched backend files: **PASS**.
- OpenAPI YAML parse check: **PASS**.

## Risks / Follow-ups
- Existing deployed Postgres databases may require applying updated `schema.sql` (or equivalent migration) before using new savings endpoints.
- Milestones are currently fixed at 25/50/75/100; custom milestone definitions are not yet supported.
- No frontend wiring was added in this issue; current budgets UI remains static and does not yet consume the new savings API.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ flowchart LR

## PostgreSQL Schema (DDL)
See `backend/app/db/schema.sql`. Key tables:
- users, categories, expenses, bills, reminders
- users, categories, expenses, bills, reminders, savings_goals
- ad_impressions, subscription_plans, user_subscriptions
- refresh_tokens (optional if rotating), audit_logs

Expand All @@ -66,6 +66,7 @@ OpenAPI: `backend/app/openapi.yaml`
- Bills: CRUD `/bills`, pay/mark `/bills/{id}/pay`
- Reminders: CRUD `/reminders`, trigger `/reminders/run`
- Insights: `/insights/monthly`, `/insights/budget-suggestion`
- Savings: goal tracking `/savings/goals`, contribute `/savings/goals/{id}/contributions`

## MVP UI/UX Plan
- Auth screens: register/login.
Expand Down
13 changes: 13 additions & 0 deletions packages/backend/app/db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,19 @@ CREATE INDEX IF NOT EXISTS idx_bills_user_due ON bills(user_id, next_due_date);
ALTER TABLE bills
ADD COLUMN IF NOT EXISTS autopay_enabled BOOLEAN NOT NULL DEFAULT FALSE;

CREATE TABLE IF NOT EXISTS savings_goals (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(200) NOT NULL,
target_amount NUMERIC(12,2) NOT NULL,
current_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
currency VARCHAR(10) NOT NULL DEFAULT 'INR',
target_date DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_savings_goals_user_target ON savings_goals(user_id, target_date);

CREATE TABLE IF NOT EXISTS reminders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
Expand Down
18 changes: 18 additions & 0 deletions packages/backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,24 @@ class Bill(db.Model):
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)
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)
target_date = db.Column(db.Date, 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,
)


class Reminder(db.Model):
__tablename__ = "reminders"
id = db.Column(db.Integer, primary_key=True)
Expand Down
122 changes: 122 additions & 0 deletions packages/backend/app/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ tags:
- name: Bills
- name: Reminders
- name: Insights
- name: Savings
paths:
/auth/register:
post:
Expand Down Expand Up @@ -481,6 +482,86 @@ paths:
application/json:
schema: { $ref: '#/components/schemas/Error' }

/savings/goals:
get:
summary: List savings goals
tags: [Savings]
security: [{ bearerAuth: [] }]
responses:
'200':
description: List of savings goals
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/SavingsGoal'
post:
summary: Create savings goal
tags: [Savings]
security: [{ bearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewSavingsGoal'
example:
name: Emergency Fund
target_amount: 10000
current_amount: 2500
currency: USD
target_date: 2026-12-31
responses:
'201': { description: Created }
'400':
description: Validation error
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }

/savings/goals/{goalId}/contributions:
post:
summary: Add contribution to savings goal
tags: [Savings]
security: [{ bearerAuth: [] }]
parameters:
- in: path
name: goalId
required: true
schema: { type: integer }
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SavingsContribution'
example:
amount: 500
responses:
'200':
description: Updated goal with milestone progress
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/SavingsGoal'
- type: object
properties:
newly_reached_milestones:
type: array
items: { type: integer }
'400':
description: Validation error
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
'404':
description: Goal not found
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }

components:
securitySchemes:
bearerAuth:
Expand Down Expand Up @@ -587,3 +668,44 @@ components:
message: { type: string }
send_at: { type: string, format: date-time }
channel: { type: string, enum: [email, whatsapp], default: email }
SavingsGoal:
type: object
properties:
id: { type: integer }
name: { type: string }
target_amount: { type: number, format: float }
current_amount: { type: number, format: float }
remaining_amount: { type: number, format: float }
progress_pct: { type: number, format: float }
status: { type: string, enum: [ahead, on-track, behind, completed] }
monthly_target: { type: number, format: float }
currency: { type: string }
target_date: { type: string, format: date, nullable: true }
milestones:
type: array
items:
type: object
properties:
percentage: { type: integer }
amount: { type: number, format: float }
reached: { type: boolean }
next_milestone:
type: object
nullable: true
properties:
percentage: { type: integer }
amount: { type: number, format: float }
NewSavingsGoal:
type: object
required: [name, target_amount]
properties:
name: { type: string }
target_amount: { type: number, format: float }
current_amount: { type: number, format: float, default: 0 }
currency: { type: string, default: INR }
target_date: { type: string, format: date, nullable: true }
SavingsContribution:
type: object
required: [amount]
properties:
amount: { type: number, format: float }
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 .savings import bp as savings_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(savings_bp, url_prefix="/savings")
Loading
Loading