Spaces:
Running
Running
Commit Β·
00a4c1d
1
Parent(s): 54839eb
Add the categories for the own users
Browse files- .DS_Store +0 -0
- app/constants/categories.py +15 -12
- app/db/categories_budget.py +14 -4
- app/routers/expense.py +40 -17
.DS_Store
CHANGED
|
Binary files a/.DS_Store and b/.DS_Store differ
|
|
|
app/constants/categories.py
CHANGED
|
@@ -1,14 +1,17 @@
|
|
| 1 |
CATEGORIES = [
|
| 2 |
-
|
| 3 |
-
{"name": "
|
| 4 |
-
{"name": "
|
| 5 |
-
{"name": "
|
| 6 |
-
{"name": "
|
| 7 |
-
{"name": "
|
| 8 |
-
{"name": "
|
| 9 |
-
{"name": "
|
| 10 |
-
{"name": "
|
| 11 |
-
{"name": "
|
| 12 |
-
|
| 13 |
-
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
CATEGORIES = [
|
| 2 |
+
# ---------- HEAD CATEGORIES ----------
|
| 3 |
+
{"name": "Groceries", "icon": "π", "scope": "head"},
|
| 4 |
+
{"name": "Food", "icon": "π½", "scope": "head"},
|
| 5 |
+
{"name": "Transport", "icon": "π", "scope": "head"},
|
| 6 |
+
{"name": "Health", "icon": "π", "scope": "head"},
|
| 7 |
+
{"name": "Gifts", "icon": "π", "scope": "head"},
|
| 8 |
+
{"name": "Rent", "icon": "π ", "scope": "head"},
|
| 9 |
+
{"name": "Utilities", "icon": "β‘", "scope": "head"},
|
| 10 |
+
{"name": "Entertainment", "icon": "π", "scope": "head"},
|
| 11 |
+
{"name": "Education", "icon": "π", "scope": "head"},
|
| 12 |
+
{"name": "Insurance", "icon": "π‘", "scope": "head"},
|
|
|
|
| 13 |
|
| 14 |
+
# ---------- MEMBER CATEGORIES ----------
|
| 15 |
+
{"name": "Student Groceries", "icon": "π", "scope": "member"},
|
| 16 |
+
{"name": "Food", "icon": "π½", "scope": "member"},
|
| 17 |
+
]
|
app/db/categories_budget.py
CHANGED
|
@@ -1,17 +1,27 @@
|
|
| 1 |
-
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
|
| 2 |
from sqlalchemy.orm import relationship
|
| 3 |
from sqlalchemy.sql import func
|
| 4 |
from app.db.database import Base
|
| 5 |
|
| 6 |
-
|
| 7 |
class CategoryBudget(Base):
|
| 8 |
__tablename__ = "category_budgets"
|
| 9 |
|
| 10 |
id = Column(Integer, primary_key=True, index=True)
|
| 11 |
-
family_code = Column(String(10), index=True, nullable=False)
|
| 12 |
|
|
|
|
| 13 |
category_name = Column(String(50), nullable=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
budget = Column(Float, default=0.0)
|
| 15 |
spent = Column(Float, default=0.0)
|
| 16 |
|
| 17 |
-
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, UniqueConstraint
|
| 2 |
from sqlalchemy.orm import relationship
|
| 3 |
from sqlalchemy.sql import func
|
| 4 |
from app.db.database import Base
|
| 5 |
|
|
|
|
| 6 |
class CategoryBudget(Base):
|
| 7 |
__tablename__ = "category_budgets"
|
| 8 |
|
| 9 |
id = Column(Integer, primary_key=True, index=True)
|
|
|
|
| 10 |
|
| 11 |
+
family_code = Column(String(10), index=True, nullable=False)
|
| 12 |
category_name = Column(String(50), nullable=False)
|
| 13 |
+
|
| 14 |
+
scope = Column(String(10), nullable=False) # "family" | "member"
|
| 15 |
+
owner_id = Column(Integer, nullable=True) # NULL for family, member_id for member
|
| 16 |
+
|
| 17 |
budget = Column(Float, default=0.0)
|
| 18 |
spent = Column(Float, default=0.0)
|
| 19 |
|
| 20 |
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 21 |
+
|
| 22 |
+
__table_args__ = (
|
| 23 |
+
UniqueConstraint(
|
| 24 |
+
"family_code", "scope", "owner_id", "category_name",
|
| 25 |
+
name="uq_category_budget_scope"
|
| 26 |
+
),
|
| 27 |
+
)
|
app/routers/expense.py
CHANGED
|
@@ -13,7 +13,7 @@ from app.db.categories_budget import CategoryBudget
|
|
| 13 |
|
| 14 |
router = APIRouter(prefix="/expense", tags=["expense"])
|
| 15 |
|
| 16 |
-
|
| 17 |
{"name": "Groceries", "icon": "π", "budget": 0, "spent": 0, "remaining": 0},
|
| 18 |
{"name": "Food", "icon": "π½", "budget": 0, "spent": 0, "remaining": 0},
|
| 19 |
{"name": "Transport", "icon": "π", "budget": 0, "spent": 0, "remaining": 0},
|
|
@@ -26,6 +26,14 @@ CATEGORIES = [
|
|
| 26 |
{"name": "Insurance", "icon": "π‘", "budget": 0, "spent": 0, "remaining": 0},
|
| 27 |
]
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
@router.get("/categories")
|
| 31 |
def get_categories(
|
|
@@ -34,45 +42,60 @@ def get_categories(
|
|
| 34 |
):
|
| 35 |
family_code = current_user.family_code
|
| 36 |
|
| 37 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
rows = db.query(CategoryBudget).filter(
|
| 39 |
-
CategoryBudget.family_code == family_code
|
|
|
|
|
|
|
| 40 |
).all()
|
| 41 |
|
| 42 |
-
#
|
| 43 |
db_map = {row.category_name: row for row in rows}
|
| 44 |
|
|
|
|
| 45 |
result = []
|
| 46 |
|
| 47 |
-
for cat in
|
| 48 |
name = cat["name"]
|
| 49 |
icon = cat["icon"]
|
| 50 |
|
| 51 |
-
|
| 52 |
-
if
|
| 53 |
-
|
| 54 |
-
budget = row.budget
|
| 55 |
-
spent = row.spent
|
| 56 |
-
remaining = budget - spent
|
| 57 |
-
else:
|
| 58 |
-
budget = 0
|
| 59 |
-
spent = 0
|
| 60 |
-
remaining = 0
|
| 61 |
|
| 62 |
result.append({
|
| 63 |
"name": name,
|
| 64 |
"icon": icon,
|
| 65 |
"budget": budget,
|
| 66 |
"spent": spent,
|
| 67 |
-
"remaining":
|
| 68 |
})
|
| 69 |
|
| 70 |
return {
|
| 71 |
"status": True,
|
|
|
|
| 72 |
"categories": result
|
| 73 |
}
|
| 74 |
|
| 75 |
-
|
| 76 |
@router.post("/add")
|
| 77 |
def add_expense(
|
| 78 |
payload: AddExpenseRequest,
|
|
|
|
| 13 |
|
| 14 |
router = APIRouter(prefix="/expense", tags=["expense"])
|
| 15 |
|
| 16 |
+
HEAD_CATEGORIES = [
|
| 17 |
{"name": "Groceries", "icon": "π", "budget": 0, "spent": 0, "remaining": 0},
|
| 18 |
{"name": "Food", "icon": "π½", "budget": 0, "spent": 0, "remaining": 0},
|
| 19 |
{"name": "Transport", "icon": "π", "budget": 0, "spent": 0, "remaining": 0},
|
|
|
|
| 26 |
{"name": "Insurance", "icon": "π‘", "budget": 0, "spent": 0, "remaining": 0},
|
| 27 |
]
|
| 28 |
|
| 29 |
+
MEMBER_CATEGORIES = [
|
| 30 |
+
{"name": "Food", "icon": "π½", "budget": 0, "spent": 0, "remaining": 0},
|
| 31 |
+
{"name": "Transport", "icon": "π", "budget": 0, "spent": 0, "remaining": 0},
|
| 32 |
+
{"name": "Entertainment", "icon": "π", "budget": 0, "spent": 0, "remaining": 0},
|
| 33 |
+
{"name": "Education", "icon": "π", "budget": 0, "spent": 0, "remaining": 0},
|
| 34 |
+
{"name": "Gifts", "icon": "π", "budget": 0, "spent": 0, "remaining": 0},
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
|
| 38 |
@router.get("/categories")
|
| 39 |
def get_categories(
|
|
|
|
| 42 |
):
|
| 43 |
family_code = current_user.family_code
|
| 44 |
|
| 45 |
+
# ---------------- ROLE CHECK ----------------
|
| 46 |
+
if current_user.role == "member":
|
| 47 |
+
fm = db.query(FamilyMember).filter(
|
| 48 |
+
FamilyMember.family_code == family_code,
|
| 49 |
+
FamilyMember.user_id == current_user.id
|
| 50 |
+
).first()
|
| 51 |
+
|
| 52 |
+
if not fm:
|
| 53 |
+
raise HTTPException(400, "Member record not found")
|
| 54 |
+
|
| 55 |
+
scope = "member"
|
| 56 |
+
owner_id = fm.id
|
| 57 |
+
base_categories = MEMBER_CATEGORIES
|
| 58 |
+
|
| 59 |
+
else: # head
|
| 60 |
+
scope = "family"
|
| 61 |
+
owner_id = None
|
| 62 |
+
base_categories = HEAD_CATEGORIES
|
| 63 |
+
|
| 64 |
+
# ---------------- FETCH BUDGETS ----------------
|
| 65 |
rows = db.query(CategoryBudget).filter(
|
| 66 |
+
CategoryBudget.family_code == family_code,
|
| 67 |
+
CategoryBudget.scope == scope,
|
| 68 |
+
CategoryBudget.owner_id == owner_id
|
| 69 |
).all()
|
| 70 |
|
| 71 |
+
# Safe lookup
|
| 72 |
db_map = {row.category_name: row for row in rows}
|
| 73 |
|
| 74 |
+
# ---------------- BUILD RESPONSE ----------------
|
| 75 |
result = []
|
| 76 |
|
| 77 |
+
for cat in base_categories:
|
| 78 |
name = cat["name"]
|
| 79 |
icon = cat["icon"]
|
| 80 |
|
| 81 |
+
row = db_map.get(name)
|
| 82 |
+
budget = row.budget if row else 0
|
| 83 |
+
spent = row.spent if row else 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
result.append({
|
| 86 |
"name": name,
|
| 87 |
"icon": icon,
|
| 88 |
"budget": budget,
|
| 89 |
"spent": spent,
|
| 90 |
+
"remaining": max(budget - spent, 0),
|
| 91 |
})
|
| 92 |
|
| 93 |
return {
|
| 94 |
"status": True,
|
| 95 |
+
"role": current_user.role,
|
| 96 |
"categories": result
|
| 97 |
}
|
| 98 |
|
|
|
|
| 99 |
@router.post("/add")
|
| 100 |
def add_expense(
|
| 101 |
payload: AddExpenseRequest,
|