# ADR-071: Indian Number Formatting for All Monetary Amount Inputs

## Status

Accepted, 2026-06-27.

## Status History

```yaml
status_history:
  - date: 2026-06-27
    status: Proposed
    changed_by: hkl
    reason: Formalising canonical Indian number format implementation across all monetary input fields
    changed_via: adr-kit (360lm)
  - date: 2026-06-27
    status: Accepted
    changed_by: hkl
    reason: Standard live in custodian v11 (2026-06-17); ready to mandate across platform
    changed_via: adr-kit (360lm)
```

## Context

The 360lm platform spans multiple PWAs handling monetary transactions: expense, finance (multi-mini-PWA family), custodian, sales, HR (salary input), activity (client portal). Each requires users to enter amounts in Indian currency format.

The Indian number system groups digits as: last 3 digits form the first group from the right, then pairs of 2 moving left. For example:
- `1,23,45,678.50` (not `12,345,678.50`)
- `12,34,567.89`
- `1,23,456.00`

This is distinctly different from Western formatting (`12,345,678.50`) and provides immediate visual feedback to users familiar with Indian accounting conventions.

**Current state (as of 2026-06-17):**
- Custodian PWA implements the full standard via three canonical utility functions: `fmtAmountInput()`, `_indFmt()`, `_parseAmt()`.
- These functions are defined in `/var/www/360lm/docs/input_and_image_standards.md`, Section 1.
- The implementation handles input event binding, paste handling, cursor position preservation, and DB storage as plain numeric (formatting is presentation-only).
- Other PWAs (expense, finance, sales, HR, activity) have NOT yet adopted this standard.

**Design constraints:**
- Per ADR-013 (Single HTML File, No Framework), each PWA is self-contained with no shared imports from `/shared/`. The three utility functions must be copy-verbatim into each PWA's `index.html` `<script>` block.
- Per ADR-009 (Each PWA Owns Its DB Schema), each PWA owns schema closure; monetary columns are stored as plain numeric type (no string formatting in DB).
- Line-item rows that use `querySelectorAll('input[type="number"]')` for auto-calculation must remain as `type="number"` inputs—no formatting applied to those.

**Rationale for copying, not sharing:**
ADR-013 mandates single-file PWAs to avoid build pipelines and external dependencies. Importing from `/shared/` would violate this constraint. The three functions are stateless utility methods with no external dependencies, so copying is safe and maintains PWA isolation.

## Decision

1. **All monetary amount input fields across 360lm PWAs use Indian number formatting at the input layer.**
   - Applies to: transfer amount, advance amount, receipt amount, salary amount, expense amount, invoice value, client payment, etc.
   - Format: `??,??,???.??` (last 3 digits, then pairs of 2 moving left; up to 2 decimal places).

2. **The three canonical utility functions are copy-verbatim into each PWA's `index.html`.**
   - No shared dependency, no import from `/shared/`.
   - Each PWA includes the functions inline in its `<script>` block.
   - Functions: `fmtAmountInput(el)`, `_indFmt(n)`, `_parseAmt(elOrValue)`.

3. **HTML input element uses `type="text"` with `inputmode="decimal"` (not `type="number"`).**
   - `type="number"` rejects commas; formatting is impossible inside the field.
   - `type="text"` allows comma characters while still triggering numeric soft keyboards on mobile.

4. **Database storage remains plain numeric; formatting is presentation-only.**
   - Column definition: `DECIMAL(15, 2)` or `NUMERIC(15, 2)` (no string type).
   - No formatted string is ever stored in the DB.
   - Client retrieves plain numeric value, calls `fmtAmountInput(el)` to display it formatted.

5. **Exception: line-item rows using `querySelectorAll('input[type="number"]')` remain plain number inputs.**
   - These are typically auto-calculated totals or sub-rows within a transaction.
   - Do not apply formatting to these rows.

6. **When adding a new PWA with monetary fields, this standard is mandatory—no alternatives.**
   - Consult `input_and_image_standards.md` Section 1 for the canonical implementation.
   - Copy the three functions into the new PWA's index.html.
   - Update all `parseFloat(el.value)` or `Number(el.value)` calls on formatted fields to use `_parseAmt(el)`.

**Decision Maker:** hkl

## Implementation Notes

### Canonical Utility Functions

These three functions are defined in `/var/www/360lm/docs/input_and_image_standards.md` and must be copied verbatim (no modifications) into each PWA's index.html:

```javascript
function fmtAmountInput(el) {
  const raw = el.value, pos = el.selectionStart;
  const beforeCursor = raw.slice(0, pos).replace(/,/g, '');
  let clean = raw.replace(/[^0-9.]/g, '');
  const dot = clean.indexOf('.');
  if (dot !== -1)
    clean = clean.slice(0, dot + 1) + clean.slice(dot + 1).replace(/\./g, '').slice(0, 2);
  const [intPart, decPart] = clean.split('.');
  const formatted = _indFmt(intPart || '') + (decPart !== undefined ? '.' + decPart : '');
  el.value = formatted;
  if (el.setSelectionRange) {
    let digits = 0, newPos = formatted.length;
    for (let i = 0; i < formatted.length; i++) {
      if (formatted[i] !== ',') {
        digits++;
        if (digits === beforeCursor.length) { newPos = i + 1; break; }
      }
    }
    if (beforeCursor.length === 0) newPos = 0;
    el.setSelectionRange(newPos, newPos);
  }
}

function _indFmt(n) {
  if (!n || n.length <= 3) return n;
  const last3 = n.slice(-3);
  let rest = n.slice(0, -3);
  const groups = [];
  while (rest.length > 0) { groups.unshift(rest.slice(-2)); rest = rest.slice(0, -2); }
  return groups.join(',') + ',' + last3;
}

function _parseAmt(elOrValue) {
  const v = typeof elOrValue === 'string' ? elOrValue : elOrValue.value;
  return parseFloat(v.replace(/,/g, '')) || 0;
}
```

### HTML Template

```html
<input type="text" inputmode="decimal" id="tf-amount"
       placeholder="0" autocomplete="off"
       oninput="fmtAmountInput(this); onTfAmountChange()">
```

- `type="text"`: Allows commas to be displayed; essential for formatting.
- `inputmode="decimal"`: Triggers numeric soft keyboard on mobile while still accepting text input.
- `oninput="fmtAmountInput(this); onTfAmountChange()"`: Call `fmtAmountInput()` to format every keystroke, then invoke any PWA-specific change handler.

### Parsing from Formatted Fields

Replace every `parseFloat(el.value)` or `Number(el.value)` on a formatted field with `_parseAmt(el)`:

**Before:**
```javascript
const amount = parseFloat(document.getElementById('tf-amount').value);
```

**After:**
```javascript
const amount = _parseAmt(document.getElementById('tf-amount').value);
```

Or with an element reference:
```javascript
const amount = _parseAmt(amtEl);
```

### Auto-Fill / Pre-Fill from DB

After setting the `.value` property, immediately call `fmtAmountInput(el)` to apply formatting:

```javascript
amtEl.value = row.amount;  // e.g., 12345678.50
fmtAmountInput(amtEl);     // Displays as "1,23,45,678.50"
```

### Cursor Position Preservation

The `fmtAmountInput()` function preserves cursor position correctly during mid-number editing by:
1. Capturing the position of the cursor before formatting.
2. Counting non-comma characters (actual digits) before the cursor.
3. Restoring the cursor to the same digit-count position in the formatted string.

This allows users to edit amounts mid-field without the cursor jumping to the end.

### Line-Item Row Exception

Do NOT apply formatting to line-item rows that use `querySelectorAll('input[type="number"]')` for auto-calculation:

```javascript
// These rows remain type="number" — no formatting applied
const rows = document.querySelectorAll('table tbody tr');
rows.forEach(row => {
  const qtyInput = row.querySelector('input[type="number"]');  // Quantity
  const priceInput = row.querySelector('input[type="number"]'); // Unit price
  // These remain plain number inputs; total is auto-calculated
});
```

## Alternatives Considered

### 1. Use a shared utility file from `/shared/`
- **Rejected:** Violates ADR-013 (single-file PWAs, no framework). Every PWA is self-contained; importing from `/shared/` introduces a build step and external dependency.

### 2. Format in the database (store as string)
- **Rejected:** Complicates numeric aggregation, sorting, and filtering. Monetary columns must remain numeric for financial calculations and reporting.

### 3. Format only on display (list/ledger) not on input
- **Rejected:** Users need immediate visual feedback while typing to catch entry errors (e.g., `1,23,45,678` vs `1,234,5678`). Formatting on display only (after blur) is too late; the user has already moved to the next field.

### 4. Use `type="number"` with custom styling
- **Rejected:** `type="number"` input elements do not allow comma characters in the value, so formatting is impossible. Custom styling cannot change this browser-level constraint.

### 5. Support multiple regional formats (Western, Indian, European)
- **Rejected:** Out of scope for this platform. 360lm operates in India; Indian format is the standard. If multi-region support is needed in the future, it can be addressed in a separate ADR.

## Consequences

### Benefits

1. **Immediate user feedback:** Users see formatting as they type, catching entry errors early.
2. **Consistency across PWAs:** All monetary inputs use the same standard format, reducing cognitive load.
3. **Compliance with local convention:** Indian users expect Indian number format; the app matches their mental model.
4. **No DB complexity:** Plain numeric columns simplify queries, aggregation, and reporting.

### Trade-offs

1. **Copy-paste redundancy:** Three utility functions are duplicated across PWAs instead of being shared. This is intentional per ADR-013 (single-file, no framework).
2. **Manual adoption:** Each new PWA must explicitly copy the functions; there is no automatic reuse. Mitigation: template in `ADR-template.md` or `input_and_image_standards.md` documents the copy-paste step.
3. **Cursor positioning complexity:** The `fmtAmountInput()` function is more complex than a simple regex replacement because it must preserve cursor position. Mitigation: the function is well-tested in custodian v11; no further maintenance is expected.

### Risks and Mitigations

| Risk | Mitigation |
|------|-----------|
| Developer forgets to call `_parseAmt()` when reading formatted field, leading to NaN | Linting rule or code review checklist in VCC-PRE-BUILD requires checking all `parseFloat()` calls on formatted fields. |
| Developer applies formatting to line-item `type="number"` rows, breaking auto-calculation | Document exception in ADR and PWA-specific README. Code review catches this pattern. |
| Cursor jumps to end of field during editing, confusing users | Cursor position logic is tested in custodian; if issues arise in new PWAs, file a bug against `fmtAmountInput()` with reproduction steps. |
| DB schema mismatch between dev and prod (e.g., dev has string column, prod has numeric) | ADR-009 (each PWA owns schema) and ADR-015 (dev/prod two stacks) govern schema management. Migrations must be applied consistently to both stacks. |

## Related Decisions

- **ADR-013** (Single HTML File, No Framework): Governs why functions are copied, not imported from `/shared/`.
- **ADR-009** (Each PWA Owns Its DB Schema): Governs column types (numeric only, no string storage).
- **ADR-015** (Dev/Prod Two Stacks): Governs schema consistency between dev and prod.
- **input_and_image_standards.md** (Section 1): Canonical reference for implementation details (HTML, functions, cursor logic, paste handling).

## References

- **Canonical Implementation:** `/var/www/360lm/docs/input_and_image_standards.md`, Section 1 — "Amount Input — Indian Number Formatting"
- **Test Coverage:** Custodian PWA (`/var/www/360lm/custodian/index.html`), fields `#tf-amount`, `#edit-amount`, `#recon-amount` (live since 2026-06-17)
- **Related Issue:** Linked to expense PWA backlog (no ADR yet)

---

**Changed via:** adr-kit (360lm)  
**Last updated:** 2026-06-27
