# Adjustments (project-level)

**Status:** in production
**Lives at:** Tied to /projects · `app/(app)/projects/projects-table.tsx`,
`app/(app)/projects/actions.ts` · uses `projects.billout_adjustment_pct`,
`projects.billout_adjustment_amount`, and the `project_rate_overrides` table.

## Summary
Two related knobs sit on a project, both owner-only:

1. **Project billout adjustment** — a flat post-stamp tweak applied
   when displaying a project's total Billout. Stored as a percentage
   and/or a fixed dollar amount on `projects.billout_adjustment_pct`
   / `billout_adjustment_amount`. **Display-only — never alters
   `time_entries.billout_amount_usd`**, preserving ADR #014's
   locked-at-write invariant. See decisions.md #040.
2. **Per-project rate overrides** — replace the effective billout
   rate for entries on this project. One row per (project,
   team_member_id?) in `project_rate_overrides`. Two flavors per
   row: absolute (`override_rate_usd`) or percentage
   (`override_pct`). Precedence by specificity, not order added.

Together these let Rian say things like "Prompt Victoria gets all
work at -20%" without touching individual entries.

## Why
2026-05-24 — "Project billout adjustment: positive or negative fixed
amount or percentage applied to total billable amount. Rate
Adjustment: rate override amount. Rate Adjustment per user: by
default the rate override applies to any billed hour, but I'd like
to be able to expand that to set rate rules to a specific person."
2026-05-24 — "we need to add the ability to adjust the rate by a
percentage, so instead of $20 rate for all users, it will be -20%."

## Data model
- `projects.billout_adjustment_pct numeric` — e.g. `-10` for 10% off
  total. NULL ≡ 0.
- `projects.billout_adjustment_amount numeric` — e.g. `-50.00`. NULL
  ≡ 0.
- `project_rate_overrides(id, org_id, project_id, team_member_id?,
  override_rate_usd?, override_pct?, created_at)`.
  CHECK constraint: `(override_rate_usd IS NOT NULL) <>
  (override_pct IS NOT NULL)` — exactly one must be set.
- Unique index on `(project_id, COALESCE(team_member_id,
  'uuid-zero'))` so there's at most one project-wide and one
  per-user override per project.

## Precedence (effective_billout_rate SQL function)
Given a (`team_member_id`, `project_id`):

1. **User-specific override on this project** — row with matching
   `team_member_id`. Wins if present.
2. **Project-wide override** — row with `team_member_id IS NULL` on
   this project. Wins next.
3. **Team member default** — `team_members.billout_rate_usd`.

For each step, if it's a pct override, multiply the *next-fallback*
rate by `(1 + pct/100)`. So a -20% project-wide override applied to
a team_member with $25 billout = $20/hr effective rate. Absolute
overrides set the rate verbatim.

NULL at any level falls through to the next. Null at all levels →
NULL effective rate → NULL billout_amount on the entry.

## Behavior
- Adjustments + overrides are owner-only (`canTransfer`-equivalent).
- **Project billout adjustment** is edited via the ✏️ pencil next
  to the income cell on /projects. Both pct and fixed-amount inputs;
  saving re-stamps nothing (display-time only).
- **Rate overrides** are edited via the ⚙️ gear next to the
  pencil. Opens an inline panel showing the current override list
  with edit/delete actions and an "Add override" row (kind picker:
  Rate / Pct, user picker, value input).
- **Re-stamp on save**: editing an override re-stamps all entries
  on that project (and team_member if user-specific) using
  `restamp_billout_for_project` (single-query SQL UPDATE). The
  SubmitButton component shows "Re-stamping entries…" while the
  action runs. A success info banner reports how many rows were
  updated.
- The /projects summary table shows the **adjusted** total billout
  by default. Mouseover the cell to see the raw unadjusted sum and
  the applied adjustment string ("+10% +$50").

## Constraints & edge cases
- Adjustments are display-only — they never alter
  `billout_amount_usd` on individual entries. This keeps the ADR
  #014 "locked at write time" invariant intact.
- Rate overrides DO alter `billout_amount_usd` at re-stamp time.
  Entries written before the override existed get re-stamped on the
  next save of the override (or on explicit Recalculate / Apply).
- Two overrides matching the same entry (user-specific + project-
  wide) never both apply — precedence picks one. The UI doesn't
  surface this; if you want to "stack" overrides, model it as a
  single user-specific override at the desired final rate.
- Pct overrides are bounded only loosely (the value is just a
  number). -100% gives $0/hr; 200% triples. Garbage in, garbage out.
- An override on a project with no `team_members.billout_rate_usd`
  default still works for the pct variant (it's a pct of NULL =
  NULL, so the final rate stays NULL). Use absolute for those cases.

## Permissions
- **Owners + super admins (not in view-as)** can create / edit /
  delete adjustments and overrides. Server-side `canTransfer(eu)`.
- **Managers**: the columns + buttons are hidden in the UI. RLS
  doesn't filter the override rows (org-wide read), but the action
  enforces canTransfer.

## Tests
- 🟡 ADJ-001 effective_billout_rate returns user-specific override when present.
- 🟡 ADJ-002 effective_billout_rate falls through to project-wide override.
- 🟡 ADJ-003 effective_billout_rate falls through to team_member default.
- 🟡 ADJ-004 pct override multiplies the next-fallback rate (not the override row itself).
- 🟡 ADJ-005 Editing an override re-stamps existing entries on that project.
- 🟡 ADJ-006 The CHECK constraint rejects rows with both override_rate_usd and override_pct set.

## Open considerations
- No effective-date / valid-from-to on overrides — they apply to all
  past + future entries. If overrides change mid-engagement we'd
  need date scoping.
- No history of past override values. Could be added as an audit
  table if needed.
- pct overrides multiply the NEXT fallback. If a future "compound"
  semantic is wanted (user-specific pct stacks on top of
  project-wide pct), the SQL function would need refactoring.
- The /projects margin column uses the **adjusted** total billout
  minus cost — so adjustments do flow into displayed margin too.

## Changelog
- **2026-05-24** — Phase 1: project billout adjustment (pct + fixed
  amount). Display-only; no re-stamping.
- **2026-05-24** — Phase 2: per-project rate overrides
  (`project_rate_overrides` table, `effective_billout_rate` SQL
  function). User-specific takes precedence over project-wide takes
  precedence over team_member default.
- **2026-05-24** — Single-query `restamp_billout_for_project` SQL
  function replaced the previous N HTTP PATCHes. SubmitButton
  component added for visible saving state. See decisions.md ADRs
  for the eliminate-proportion change (#034) that the override
  system was built on top of.
- **2026-05-25** — Pct variant of rate overrides
  (`override_pct` column + CHECK constraint). The
  `effective_billout_rate` function applies pct to the
  next-fallback rate.
