# Project Hourly Budget

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 139  
**Tier:** All tiers  
**Depends on:** `projects-module`, `time-management`, `real-time-infrastructure`, `foundation-auth-rbac`  
**Referenced by:** `projects-module`, `time-management`

---

## Overview

Spec 7 (`projects-module`) defines `billing_config` for hourly projects as `{ "rate_per_hour": 150, "overtime_enabled": false, ... }` with no budget cap. Without a budget, hourly projects have no way to surface scope creep or client budget overruns. This spec adds `budget_hours` to `billing_config` and defines the UI for setting, tracking, and alerting on hourly project budgets.

---

## Data Model

`billing_config` shape update for hourly projects:

```json
{
  "rate_per_hour": 150,
  "overtime_enabled": false,
  "overtime_threshold_hours": 8,
  "overtime_multiplier": 1.5,
  "budget_hours": 80,
  "budget_alert_pct": 80
}
```

`budget_hours`: total hours budgeted for the project lifetime (not per-month). `null` = no budget.
`budget_alert_pct`: send alert when this percentage of budget is consumed. Default `80`.

No schema migration needed — `billing_config` is already JSONB.

---

## Budget Configuration (Project Settings)

In `/projects/:id/settings` (or project edit sheet), for `billing_type = 'hourly'` only:

```
┌──────────────────────────────────────────────────────────────┐
│  Budget                                                      │
│                                                              │
│  Hours budget   ● Enabled   ○ None                           │
│                                                              │
│  Budget hours   [80____] h                                   │
│                                                              │
│  Alert me when  [80__] % consumed (64h of 80h)               │
│                 ☑ Email notification                         │
│                 ☑ In-app notification                        │
│                                                              │
│  [Save]                                                      │
└──────────────────────────────────────────────────────────────┘
```

---

## Budget Progress on Project Overview

In the project Overview tab, for hourly projects with `budget_hours` set:

```
Budget
██████████████░░░░░░ 67% · 53.5h of 80h used
₪8,025 of ₪12,000 billed    [16h · ₪2,400 remaining]
```

Color coding:
- 0–79%: green bar (`--success`)
- 80–99%: amber bar (`--warning`)
- 100%+: red bar (`--danger`) with "Over budget" badge

---

## Budget Exceeded Banner on Project Detail

When `logged_hours >= budget_hours`:

```
┌──────────────────────────────────────────────────────────────┐
│  ⚠ This project is over budget                               │
│  80h budgeted · 83.5h logged · 3.5h over (₪525)             │
│  [Update budget]  [Notify client]                            │
└──────────────────────────────────────────────────────────────┘
```

**[Notify client]** → opens invoice-compose pre-filled with "Additional hours notification" subject. Not an invoice; just a custom email to the project's customer.

---

## Budget Alert Notifications

When `logged_hours / budget_hours >= budget_alert_pct / 100` for the first time:

1. `POST /api/notifications` → creates in-app notification for project OWNER and ADMIN
2. Email: subject "Budget alert: {project_name} has reached {pct}%"
3. Notification body: current hours, budget hours, hours remaining, link to project

Alert fires only once per threshold crossing. If budget updated above current usage, alert resets.

---

## Time Entry — Budget Indicator

When logging time on an hourly project with a budget, the time entry form shows a mini budget indicator:

```
│  Project  [Website Redesign ▾]                               │
│           Budget: 53.5h / 80h ██████████░░░░ 67%            │
│           After this entry: 56.0h / 80h (70%)                │
```

Updates live as the user changes duration field.

---

## Budget Tracking in Time Reports

In `/reports/time` (spec 64), projects with budgets get an additional column:

| Project | Hours | Budget | Used % |
|---------|-------|--------|--------|
| Website Redesign | 53.5h | 80h | 67% |
| Mobile App | 120h | 100h | ⚠ 120% |

---

## API

No new endpoints. Budget is read/written via existing project endpoints:

```
PATCH /api/projects/:id
      → update billing_config (includes budget_hours, budget_alert_pct)
        body: { billing_config: { ...existing, budget_hours: 80, budget_alert_pct: 80 } }
        Requires: projects:write

GET /api/projects/:id/budget
    → budget summary: { budget_hours, logged_hours, billable_hours, alert_pct, over_budget }
      Requires: projects:read
```

Budget summary endpoint computes `logged_hours` from `SUM(time_entries.duration_seconds) / 3600` where `project_id` matches (all statuses, all team members + contractors).

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| `budget_hours` in JSONB | Not new column | `billing_config` already covers all billing-type-specific fields; adding a separate column for one billing type creates schema asymmetry |
| Lifetime budget | Not monthly | Hourly projects are fixed-scope engagements; monthly budget makes more sense for retainers (which already have `monthly_hours_included`) |
| Alert at configurable % | Not always 80% | Different projects have different tolerance; agency projects often alert at 90%, dev shops at 75% |
| Over-budget banner | Not block time entries | Time may need to be logged after budget exceeded (completed work); blocking creates data integrity problems |
| `budget_alert_pct` alert fires once | Not every new entry | Repeated alerts become noise; single alert at threshold with visible banner ongoing |
