diff --git a/CLAUDE.md b/CLAUDE.md index 3b48ece..6914778 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,6 +72,50 @@ sessions; grep `core/models.py` before using any field you haven't used before: - `PayrollRecord.amount_paid` (DecimalField) + `PayrollRecord.work_logs` (M2M reverse) — NOT `total_amount` / `days_worked` (easy to guess wrong when writing test fixtures) - `Loan.principal_amount` — NOT `principal`. `Loan.save()` auto-sets `remaining_balance = principal_amount` on create, so tests rarely need to pass both. +## UI-vs-DB naming drift (Apr 2026) — READ BEFORE WRITING FORMULAS + +`PayrollAdjustment.type` is DISPLAYED to users with short labels, +but the raw string stored in the database is always the long +legacy value: + +| What the user SEES | What the DATABASE stores | +|---|---| +| Bonus | `'Bonus'` | +| Overtime | `'Overtime'` | +| Deduction | `'Deduction'` | +| Loan Repayment | `'Loan Repayment'` | +| Loan | `'New Loan'` ← mismatch | +| Advance | `'Advance Payment'` ← mismatch | +| Advance Repaid | `'Advance Repayment'` ← mismatch | + +When writing ANY formula, filter, comparison, ORM query, test +fixture, CSS class name, or `data-type=` attribute: use the +DATABASE value (left column of the model). + +- `ADDITIVE_TYPES = ['Bonus', 'Overtime', 'New Loan', 'Advance Payment']` + in `views.py` uses DB values. +- `if adj.type == 'New Loan':` checks the DB value. +- `` produces + `.badge-type-new-loan` from the DB value. +- `` emits the DB value. +- Tests use `PayrollAdjustment.objects.create(type='New Loan', ...)`. + +Only user-facing template TEXT uses the short label — via +`{{ adj.get_type_display }}`, Django's built-in choices lookup. +The label mapping lives in `PayrollAdjustment.TYPE_CHOICES` +(`core/models.py`). + +**How this happened:** originally the adjustment-creation dropdown +said "New Loan" because that's what the action meant (_"log a new +loan"_). That label then propagated into every other view — tables, +badges, reports. On 24 Apr 2026 we renamed the user-visible labels +to be shorter and cleaner BUT deliberately kept the database values +untouched — to avoid breaking historic rows, tests, and hardcoded +string comparisons across ~30 source locations. + +**Symptom of getting this wrong:** code that filters for +`type='Loan'` returns zero rows. Fix: use `type='New Loan'`. + ## Key Business Rules - All business logic lives in the `core/` app — do not create additional Django apps - Workers have a `daily_rate` property: `monthly_salary / Decimal('20.00')`