feat: life-hours framing in verdict nudge + real hourly rate calculator#28
Conversation
- New hourly_rate_service.py: compute real hourly rate after commute/overtime, format as Malaysian "jam kerja" / "hari kerja", convert amount to life hours - budget_service.py: fetch commute_hours_per_day and overtime_hours_per_week from profiles for the reasoning graph - reasoning_graph.py / nudge_agent/service.py: pass life_hours_str and real_hourly_rate through the pipeline so Claude weaves in the framing - CheckResponse schema + service: expose real_hourly_rate and life_hours_cost - VerdictOverlay: animated pill showing "X hours of your working life" below nudge - income/page.tsx: commute (0–4h/day) and overtime (0–20h/week) sliders with live apparent vs real hourly rate comparison and RM100 = X hours motivator - Supabase migration: add commute_hours_per_day and overtime_hours_per_week columns to profiles https://claude.ai/code/session_017cLXjZu6kkjXRf2BemMPpp
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
bajet-buddy | 4275334 | Jun 10 2026, 09:02 AM |
There was a problem hiding this comment.
Code Review
This pull request introduces a "Life-hours framing" feature that calculates a user's real hourly rate (accounting for commute and overtime) and displays purchase costs in terms of working life hours. Key feedback includes using the real monthly income from the budget summary instead of mock profile data, defensively handling potential null values in the hourly rate calculation, synchronizing all updated fields in the frontend state upon saving, and adding database check constraints to ensure data integrity for the new columns.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
|
||
| # Life-hours framing | ||
| commute, overtime = await fetch_work_hours_profile(state.user_id) | ||
| monthly_income = profile.get("monthly_income", 3200) |
There was a problem hiding this comment.
The profile dictionary (which is state.user_profile) contains hardcoded mock data initialized in _observe_transaction_intent (with monthly_income set to 3200.0). Using profile.get("monthly_income", 3200) here ignores the user's actual monthly income saved in the database.\n\nInstead, you should use the real monthly income fetched from the database, which is already available in state.budget_summary["total_income"].
| monthly_income = profile.get("monthly_income", 3200) | |
| monthly_income = state.budget_summary.get("total_income") or profile.get("monthly_income", 3200) |
| def compute_real_hourly_rate( | ||
| monthly_salary: float, | ||
| commute_hours_per_day: float = 0.0, | ||
| overtime_hours_per_week: float = 0.0, | ||
| ) -> float: | ||
| """ | ||
| Real hourly rate = salary / (standard + commute + overtime hours). | ||
|
|
||
| Standard: 22 working days × 8h = 176h/month | ||
| Commute: commute_hours_per_day × 22 days | ||
| Overtime: overtime_hours_per_week × 4.33 weeks | ||
| """ | ||
| commute_hours = commute_hours_per_day * 22 | ||
| overtime_hours = overtime_hours_per_week * 4.33 | ||
| total_hours = STANDARD_WORK_HOURS_PER_MONTH + commute_hours + overtime_hours | ||
| return round(monthly_salary / max(total_hours, 1), 2) |
There was a problem hiding this comment.
If monthly_salary is None (e.g., if the user's profile does not have a monthly income set yet), this function will raise a TypeError when performing division.\n\nAdditionally, we should defensively handle potential None values for commute_hours_per_day and overtime_hours_per_week to prevent runtime exceptions.
def compute_real_hourly_rate(\n monthly_salary: float | None,\n commute_hours_per_day: float | None = 0.0,\n overtime_hours_per_week: float | None = 0.0,\n) -> float:\n \"\"\"\n Real hourly rate = salary / (standard + commute + overtime hours).\n\n Standard: 22 working days × 8h = 176h/month\n Commute: commute_hours_per_day × 22 days\n Overtime: overtime_hours_per_week × 4.33 weeks\n \"\"\"\n salary = monthly_salary or 0.0\n commute_hours = (commute_hours_per_day or 0.0) * 22\n overtime_hours = (overtime_hours_per_week or 0.0) * 4.33\n total_hours = STANDARD_WORK_HOURS_PER_MONTH + commute_hours + overtime_hours\n return round(salary / max(total_hours, 1.0), 2)| .update({ monthly_income: gross, commute_hours_per_day: commute, overtime_hours_per_week: overtime }) | ||
| .eq("id", user.id); | ||
| if (dbErr) throw dbErr; | ||
| setIncome((prev) => ({ ...prev, monthly_income: gross })); |
There was a problem hiding this comment.
When saving the income data, only monthly_income is updated in the local income state. The commute_hours_per_day and overtime_hours_per_week fields should also be synchronized in the local income state to ensure data consistency across the application.
setIncome((prev) => ({\n ...prev,\n monthly_income: gross,\n commute_hours_per_day: commute,\n overtime_hours_per_week: overtime,\n }));
| ALTER TABLE public.profiles | ||
| ADD COLUMN IF NOT EXISTS commute_hours_per_day NUMERIC(4,1) DEFAULT 0, | ||
| ADD COLUMN IF NOT EXISTS overtime_hours_per_week NUMERIC(4,1) DEFAULT 0; |
There was a problem hiding this comment.
To ensure data integrity and prevent invalid values (such as negative hours or unrealistically high values), it is highly recommended to add CHECK constraints to the new columns.
ALTER TABLE public.profiles\n ADD COLUMN IF NOT EXISTS commute_hours_per_day NUMERIC(4,1) DEFAULT 0 CHECK (commute_hours_per_day >= 0 AND commute_hours_per_day <= 24),\n ADD COLUMN IF NOT EXISTS overtime_hours_per_week NUMERIC(4,1) DEFAULT 0 CHECK (overtime_hours_per_week >= 0 AND overtime_hours_per_week <= 168);
❌ Deploy Preview for bajet-buddy failed.
|
- hourly_rate_service: accept None for all three params and coerce with `or 0.0` so missing profile data never raises TypeError - reasoning_graph: use real DB income (budget_summary.total_income) for hourly rate; fall back to mock profile value only when absent - income/page.tsx: sync commute and overtime into local income state on save so displayed values stay consistent - migration: add CHECK constraints (commute 0-24h, overtime 0-168h) to guard data integrity https://claude.ai/code/session_017cLXjZu6kkjXRf2BemMPpp
Summary
commute_hours_per_dayandovertime_hours_per_weekcolumns toprofiles.Test plan
/income, set a gross salary, drag the commute and overtime sliders — verify the real rate card updates live and "Every RM100 = X hours" recalculates/check— verify the verdict overlay shows the life-hours pill for amounts > 0ruff check apppasses (already verified)npx tsc --noEmitpasses (already verified)20260610000001_life_hours_columns.sqlto the linked projecthttps://claude.ai/code/session_017cLXjZu6kkjXRf2BemMPpp
Generated by Claude Code
Summary by cubic
Adds life-hours framing to purchase verdicts and a real hourly rate calculator so spending feels concrete. Uses saved income and work hours to compute a real hourly rate and show “X hours of your working life.”
New Features
life_hours_strandreal_hourly_rateand includes the framing when it adds impact./income: commute (0–4h/day) and overtime (0–20h/week) sliders with live apparent vs. real hourly rate and “Every RM100 = X hours”.CheckResponseaddsreal_hourly_rateandlife_hours_cost.Migration
supabase/migrations/20260610000001_life_hours_columns.sqlto addcommute_hours_per_dayandovertime_hours_per_weektoprofiles, with CHECK constraints (commute 0–24h/day, overtime 0–168h/week).Written for commit 4275334. Summary will update on new commits.