# RBS Regal Business Suite — Changelog

## v12.43 — 2026-06-29 — Floating AI: Female Indian Voice Upgrade (TTS only)

> User mandate: Upgrade ONLY the AI voice — female, warm, soft, natural, Indian neutral accent, native pronunciation for every supported regional language, speed 0.95–1.0×. No other system changes. Pure TTS-layer refinement.

### Changes (2 files, voice path only — no UI, no backend, no DB, no module change)
- **NEW `frontend/src/lib/voicePicker.js`** (~190 lines) — focused helper:
  - `detectVoiceLang(text)` — Unicode-script sniffer that returns BCP-47 tag (hi/mr/pa/gu/ta/te/kn/ml/bn/or/ur/en) by counting Devanagari, Gurmukhi, Gujarati, Tamil, Telugu, Kannada, Malayalam, Bengali, Oriya, Arabic and Latin codepoints in the AI's reply. Reads the **reply** text, NOT the UI-language toggle, so a Tamil reply is read in a Tamil voice even if UI is English.
  - `resolveVoiceLang(detected, uiHint)` — overrides the ambiguous Devanagari → hi-IN to mr-IN if UI hint is Marathi/Konkani.
  - `getVoicesReady()` — promise that waits for `speechSynthesis.onvoiceschanged` (Chrome / Edge load voices async).
  - `pickFemaleVoice(voices, wantLang)` — ranks every available voice by (language match + female-name hint score + neural/online quality bonus) and returns the best female-leaning voice for the requested language. Walks a fallback chain (e.g. ta-IN → en-IN) when the OS doesn't ship the exact regional voice.
  - Recognises known neural female voice names: **Heera, Swara, Kalpana, Neerja, Aarohi, Kavya, Gargi, Ananya, Anjali** (Microsoft Edge / Win11), **Lekha, Veena, Priya** (Apple), **Aditi, Raveena, Asha, Tara, Tarini** (Google / Android), plus generic "Female / Woman / Girl" markers.
  - Penalises male-coded names (Ravi, Hemant, Prabhat, etc.) so they're picked only as a last resort.
- **`frontend/src/components/AiFloatingChat.jsx`** — TTS block (lines ~131-152) replaced:
  - Old: hardcoded BCP-47 map keyed by UI language, rate 1.0, pitch 1.0, default voice (often male / robotic).
  - New: `chooseVoiceForReply(data.reply, lang)` → female voice selected for detected reply language; **rate 0.95** (comfortable Indian speaking speed); **pitch 1.05** (slightly warmer / softer); utterance cap raised 400 → 600 chars (Indian languages are word-dense).

### Verified
- **Node-level unit tests on `voicePicker`** — 13 / 13 language-detection cases PASS:
  Devanagari → hi-IN, Devanagari + Marathi hint → mr-IN, Tamil → ta-IN, Telugu → te-IN, Kannada → kn-IN, Malayalam → ml-IN, Gujarati → gu-IN, Gurmukhi → pa-IN, Bengali → bn-IN, Arabic → ur-IN, Hinglish-Latin → en-IN, Pure English → en-IN, Empty → en-IN.
- **Female-voice picker test**: given `[Heera-Neural, Ravi-male, Google-हिन्दी]`, picker correctly returns **"Microsoft Heera Online Natural"** (highest combined score).
- **End-to-end Playwright (live browser)**: logged in, enabled TTS via localStorage, sent Hindi prompt → captured `SpeechSynthesisUtterance` showed `rate=0.95, pitch=1.05, volume=1.0`, language detected from reply script. Headless Chrome ships 0 voices so the actual female voice can only be observed in a real desktop browser, but the code path is verified end-to-end.
- ESLint clean on both files.

### What stayed untouched (per mandate)
- Web Speech RECOGNITION (input) — unchanged
- Cloud Whisper transcribe fallback — unchanged
- UI, dashboard, modules, workflow, login, translations, database, backend routes, feature flags — all unchanged

### Expected real-browser behaviour
- **Windows 11 / Edge**: picks Microsoft Heera / Swara (Hindi), Bashkar/Heera fallback for Marathi, Pallavi (Tamil), Kalpana, Neerja — all neural female voices.
- **macOS / iOS**: picks Lekha (Hindi/Marathi), Veena (Tamil), Aditi-female fallback.
- **Android / Chrome**: picks Google हिन्दी (female), Google தமிழ் (female).
- **Chrome on Linux with no Indian voices**: falls back to en-IN female voice, then en-GB/en-US female.

---

## v12.42 — 2026-06-29 — Floating AI: Universal Multilingual + Translation Engine

> User mandate: Upgrade the existing Floating AI into a true **Universal Multilingual Translation + Conversation Engine** — auto-detect any input language and mirror it, switch to a strict "Translation Mode" on explicit translate triggers, and preserve business/ERP terminology (Invoice, Purchase, GST, GSTIN, UPI, HSN, etc.) as English even inside foreign translations. **No new module, no new route, no new collection** — pure system-prompt + rule upgrade inside the existing `/api/ai/chat` endpoint.

### Changes
- **`backend/ai_assistant.py`** — added module-level `MULTILINGUAL_ENGINE` constant (40-line policy block) and injected it into the `/chat` system prompt above the existing RULES. Rule 1 now defers to the new engine instead of carrying a 5-language hard-coded list.
- The engine specifies three behaviours:
  1. **Language Auto-Detect** — mirror the user's language (any of 22 Indian + major foreign), preserve domain keywords as English, use ₹ + Indian commas.
  2. **Translation Mode** — triggered on `translate ...`, `... को ... में अनुवाद करो`, `... ka ... mein anuvad karo`, `convert this to ...`, `say this in ...`, etc. (multi-language detection). Emits a STRICT 3-line markdown block: `**Source:** <code> (<name>)` / blank line / `**Target:** ...` / blank line / `**Translated:** ...`. No code-fence wrapping, no preface, no chit-chat. Multi-target ("Hindi AND Marathi") → repeat blocks separated by `---`.
  3. **Keyword Preservation (mandatory, even for foreign targets)** — Invoice, Purchase, Sale, GST, GSTIN, HSN, UPI, MRP, Stock, Cash, Bank, Receipt, Payment, Party, Vendor, Customer, WhatsApp, OTP, KYC, ERP, POS, RGE REGALGOA stay English tokens. Example: Hindi→French translation of "naya purchase bill banao aur GST 18% lagao" must read "Créez un nouveau **Purchase bill** et appliquez **GST** 18 %" — NOT "facture d'achat" / "TVA".

### Live curl verification (8 scenarios)
- Hindi→English explicit translate → 3-line block ✅
- Marathi conversation → mirrored in Marathi, NO translate block ✅
- Hindi→French → `Purchase bill` + `GST` preserved as English ✅
- Hindi→Spanish → `GST` preserved ✅
- Konkani-ish input → mirrored in Devanagari ✅
- Multi-target (Hindi AND Marathi) → two blocks separated by `---` ✅
- Plain English business question → conversational reply, no Translation Mode ✅
- No code-fence wrapping (`\`\`\``) in any output ✅

### Verified
- **5 NEW pytest cases** in `tests/test_ai_translation.py`:
  1. `MULTILINGUAL_ENGINE` constant carries the policy markers ✅
  2. Explicit `Translate ... to English` returns 3-line block, no fences ✅
  3. Multi-target returns 2+ blocks with `---` separator ✅
  4. French target preserves `gst` + `purchase` keywords ✅
  5. Casual Marathi conversation does NOT emit Source/Target block ✅
- **AI regression sweep: 15/15 PASS** (`test_ai_assistant.py` 6/6 + `test_ai_health_search.py` 9/9) — no breakage to existing chat / Whisper / smart-search / health-score endpoints.
- DOM verification: `wait_for_function` confirmed `**Source:**`/`**Target:**`/`**Translated:**` render in the Floating AI bubble via existing `ReactMarkdown` (already used in `AiFloatingChat.jsx` and `AiAssistant.jsx`).

### Production-safe posture
- Backwards-compatible: prior behaviour for English / Hindi / Hinglish / Marathi / Konkani remains identical (the engine simply *adds* universal language support + explicit Translation Mode on top).
- No DB migration, no env variable, no flag — ships immediately.
- All prior AI capabilities (security policy, app guide, live snapshot, expense categorise, GST suggest, vision identify, health score, smart search, parse-invoice) untouched.

---

## v12.41 — 2026-06-24 — Smart Serial (Expense) + Pilot Customer Activation Wizard

> User mandate (combined): (a) Apply Vyapar-style "Smart Serial" numbering to every existing document type, especially Expense (which was the only one missing auto-numbering). NO new module, NO new dashboard, NO toggle button — just an editable Expense-No field that auto-fills with the next number on form open AND bumps the counter forward on manual override. (b) Add a single super-admin Pilot Customer Activation Wizard inside the existing `/admin/features` page that enables Phase 1 (Offline) + Phase 2 (Auto Transaction Messages) for ONE user with a 48h soak timer, plus a one-time welcome modal at the next login.

### Part A — Smart Serial for Expense (closes the Vyapar-style numbering parity gap)
- **`backend/routes.py::create_expense`** — wired into the existing per-user atomic counter engine in `txn_prefixes.py`. Behaviour exactly matches invoices:
  - Empty `expense_no` → `resolve_next_invoice_no_per_user(..., txn_type="expense")` atomically reserves the next number.
  - Manual `expense_no` → uniqueness check on `(company_id, expense_no, created_by)` then `bump_user_counter_for_manual(...)` so the next auto continues from `manual+1` (Smart Continue).
  - The endpoint stamps `prefix_id`, `financial_year`, and `created_by` on the doc — fully backward-compatible with legacy expenses that have no number.
- **`frontend/src/pages/ExpenseForm.jsx`** — calls `GET /api/txn-prefixes/preview-number?type=expense` on mount (when creating, not editing) and pre-fills the `expense_no` input. Field remains editable; no toggle button. Mirrors the existing NewInvoice UX exactly.
- **No new module, no new route, no schema change.** Existing `txn_prefixes`, `prefix_user_counters`, and `prefix_audit` collections already supported `type="expense"`; we just turned the lights on.

End-to-end curl verification (admin session):
```
preview            → EXP/26/00001
create (no number) → EXP/26/00001
create EXP/26/00099 (manual) → EXP/26/00099
create (no number) → EXP/26/00100  ✓ Smart Continue
create EXP/26/00099 again → HTTP 400 "already exists"  ✓ Duplicate protection
```

### Part B — Pilot Customer Activation Wizard
- **NEW `backend/pilot_activation.py`** (~220 lines) — thin orchestrator that reuses the existing feature-flag engine. Endpoints:
  - `GET /api/pilot` — admin list active pilots (with hours_remaining countdown)
  - `POST /api/pilot/activate` — flips `enabled_users[]` of 3 flags (`service-worker`, `mutation-queue`, `auto-transaction-messages`) for the chosen user_id; stamps `expires_at = now + soak_hours`
  - `POST /api/pilot/revoke` — removes user_id from all 3 flags + marks status=revoked
  - `POST /api/pilot/promote` — sets `enabled_global=True` on all 3 flags + closes pilot
  - `GET /api/pilot/me` — current user's pilot status + `show_welcome` flag for the gate component
  - `POST /api/pilot/welcome-ack` — clears `show_welcome` after the modal is dismissed
  - `enforce_expired_activations()` — lazy enforcer: every `/me` and admin-list call demotes any pilot whose 48h window has elapsed (auto-rollback)
- **NEW collection** `pilot_activations` — `{user_id, activated_at, activated_by, expires_at, status, welcomed_at, promoted_at, note, soak_hours}`.
- **NEW `frontend/src/components/PilotActivationWizard.jsx`** (~190 lines) — single card mounted at the top of the existing `/admin/features` page (NO new route):
  - User dropdown (loads from `/api/admin/users`)
  - Soak-hours input (1-168, default 48)
  - Note textfield (optional, e.g. "pilot for Goa branch")
  - "Activate Pilot" button (amber, prominent)
  - Active-pilots list with live `hours_remaining` countdown (color-coded: green > 12h, amber 6-12h, rose < 6h)
  - Per-pilot **Revoke** + **Promote** buttons (Promote sets `enabled_global=True` for all 3 flags globally — confirmation prompt)
- **NEW `frontend/src/components/PilotWelcomeGate.jsx`** — invisible global component mounted in `App.js` next to `InstallPrompt`. Polls `/api/pilot/me` once after login; if `active && show_welcome` → renders a celebratory `Dialog` with feature checklist + remaining-hours badge + "Let's go!" button that calls `/api/pilot/welcome-ack`. Renders nothing for non-pilots.

### Verified
- **8 new pytest cases** in `test_pilot_activation.py` — all pass: activate flips all 3 flags ✅, /me round-trip + ack ✅, revoke clears all 3 ✅, promote sets enabled_global ✅, invalid soak_hours rejected ✅, anonymous denied ✅, expense auto-numbers ✅, manual override bumps counter ✅, duplicate rejected ✅.
- **Full Phase 1+2+3 + Pilot pytest sweep: 50 / 50 PASS, 0 regressions**.
- E2E Playwright: Pilot wizard renders on `/admin/features` with all 5 controls ✅; ExpenseForm auto-prefills `expense_no` with `EXP/26/…` on mount ✅; field remains editable.
- End-to-end curl: activate → all 3 flags' `enabled_users` contain admin uid AND `/feature-flags/resolved` returns True for all three; revoke → all back to false.

### Production-safe posture
- All flags remain `enabled_global=False` after this release. Pilot wizard ships but no pilots active. Welcome modal never fires until a super admin activates someone.
- Expense numbering is backward-compatible: legacy expenses with no `expense_no` keep working; only NEW expenses get auto-numbered.
- Auto-demote enforcer means a forgotten 48h pilot can't drift forever — fail-safe.

### Files touched (additive only)
- NEW: `backend/pilot_activation.py`, `frontend/src/components/PilotActivationWizard.jsx`, `frontend/src/components/PilotWelcomeGate.jsx`, `backend/tests/test_pilot_activation.py`
- MOD: `backend/server.py` (router mount), `backend/routes.py::create_expense` (Smart Serial wiring), `frontend/src/pages/ExpenseForm.jsx` (preview-number prefill), `frontend/src/pages/admin/AdminPages.jsx` (mount wizard above Maintenance Mode), `frontend/src/App.js` (mount PilotWelcomeGate globally)

### Intentionally NOT done
- No new admin route — wizard is a card on existing `/admin/features` per user's "no new module" mandate.
- No expense renumbering of legacy data — backward-compat only.
- Welcome modal is a one-shot per activation (re-activation re-shows it).



## v12.40 — 2026-06-22 — Universal Back Button + Error Dashboard Auto-Refresh

> User mandate (`EXISTING MODULE NAVIGATION PATCH`): no new modules, no new pages, no new routes, no schema changes. Just (a) a universal Back button in the header for every screen that's missing one, (b) an Auto-Refresh engine on the Error Dashboard with safety pauses, and (c) global keyboard shortcuts (Alt+Left = Back, ESC = close panel via existing Radix behaviour, F5 = browser default).

### What ships
- **NEW `frontend/src/components/HeaderBackButton.jsx`** — compact `← Back` button that:
  - Lives in the right-side header cluster, immediately LEFT of Refresh (spec order: Back → Refresh → AI → Profile).
  - Auto-hides on root routes (`/`, `/login`, `/register`, `/forgot-password`, `/admin` root).
  - Click → `navigate(-1)` (preserves filters, scroll, tabs, drafts — no reload, no data clear).
  - Falls back to `navigate("/")` when the history stack length is 1 (fresh-tab scenario).
  - Hotkey `Alt + ArrowLeft` triggers the same behaviour. Hotkey is silenced while the user is typing in an input/textarea/contenteditable to match the OS-default back-key semantics.
- **MOD `frontend/src/components/Header.jsx`** — mounts `<HeaderBackButton />` inside the existing right-side sync cluster, just before `<RefreshButton />`. **Zero existing markup re-ordered** — preserves the v12.36 layout exactly.
- **MOD `frontend/src/pages/admin/AdminLayout.jsx`** — admin chrome has its own sidebar but no desktop top bar, so the button is rendered inside the `<main>` content wrapper, top-right, with `hidden lg:flex` and `-mt-2 mb-2` so it doesn't push other content. Mobile already had a back button — unchanged.
- **MOD `frontend/src/pages/admin/AdminErrors.jsx` — Error Dashboard Auto-Refresh engine**:
  - New `autoRefresh` toggle (Switch). Default OFF, **persisted across sessions** in `localStorage.rge.errors.autoRefresh`.
  - When ON → silent `setInterval(load, 10_000)`. Spinner pill shows "10s"; OFF pill shows pause icon.
  - **Safety pauses** (per spec "Skip refresh while editing/saving/uploading/payment/UPI"):
    - `inFlightRef` — skips when an earlier call hasn't returned (no duplicate calls).
    - `document.hidden` — pauses when the tab is in the background; resumes on focus.
    - `window.__RGE_BUSY__` — global flag any page can set (e.g. payment / UPI / upload screens) to suspend background refresh during critical flows.
  - "Last sync: X ago" label with live tick (Just now / 5s ago / 1m ago …).
  - All silent ticks skip the loading spinner + the error toast (no flicker, no screen jump).
  - Manual Refresh button + Clear-old button untouched.

### Verified
- E2E Playwright sweep (full keyboard + click coverage):
  - Root `/` → Back hidden ✅
  - `/parties` → Back visible, click navigates to `/` ✅
  - `Alt + ArrowLeft` from `/items` → navigates to `/parties` ✅
  - `/admin` root → Back hidden ✅
  - `/admin/errors` → Back visible + auto-refresh toggle + last-sync label all present ✅
  - `/admin/features` → Back visible ✅
  - Auto-refresh toggle ON → status pill shows "10s" with spinning icon ✅
- Backend regression (Phase 1 + 2 + 3): **41 / 41 PASS, 0 regressions**.
- All new files lint clean.

### Safety lock (verbatim per user spec)
- ❌ NO new modules, pages, routes, schema migrations, data overwrites.
- ❌ NO changes to customer records, ERP tables, UPI flows, admin permissions, dashboard logic, APK system, authentication.
- ❌ NO changes to mobile layout (mobile back button in AdminLayout untouched).
- ❌ NO redesign. Every existing gradient, border, color, dimension preserved.
- ✅ Additive-only: 1 new component + 2 edited components + 1 edited admin shell.

### Keyboard shortcuts shipped
- `Alt + ←` → Back (in header AND globally)
- `F5` → Refresh (browser default, no override)
- `ESC` → Close panel (handled by existing Radix Dialog/Popover/Sheet — no new wiring)

### Files touched
- NEW: `frontend/src/components/HeaderBackButton.jsx`
- MOD: `frontend/src/components/Header.jsx`, `frontend/src/pages/admin/AdminLayout.jsx`, `frontend/src/pages/admin/AdminErrors.jsx`



## v12.39 — 2026-06-20 — Admin Dashboard + Module Switches: Full Clickable Activation

> User mandate (`FULL CLICKABLE MODULE ACTIVATION`): every visible tile / card / row / icon / switch on the Admin Dashboard and Module Switches screens must open its linked screen. STRICT: no UI redesign, no layout change, no rename, no route break, no API change, no mobile regression.

### Approach
This release is **additive-only**: existing markup, styles and dimensions are preserved verbatim; the only changes are (a) wrapping a handful of static `<div>`s in `<Link>` and (b) extending the toggle handler with a confirm-undo-loading flow that the spec required.

### What ships
- **NEW `frontend/src/lib/moduleRoutes.js`** — single source of truth that maps the catalog `key` returned by `GET /api/admin/features/catalog` (47 entries today) to the actual in-app route the user expects when they click the module card. Modules with no route yet (`coming_soon`, future) map to `null` so the card stays non-navigating instead of producing 404s. Also exports `CRITICAL_MODULES` (sales, purchases, billing, payment_in, payment_out, inventory, customers, suppliers, expense) — disabling these requires confirmation.
- **MOD `pages/admin/AdminDashboard.jsx`** — all 6 KPI tiles now wrapped in `<Link>`:
  - Total Users → `/admin/users`
  - Admins → `/admin/users?role=admin`
  - Devices → `/admin/devices`
  - Logins (7d) → `/admin/audit`
  - License → `/license`
  - Invoices → `/invoices`
  - The four QuickAction tiles already navigated; left untouched.
- **MOD `pages/admin/AdminPages.jsx::AdminFeatures` (Module Switches)** — full toggle engine + click-to-open:
  - **Card click** → navigates to the module's route from `moduleRoutes.js` (via `useNavigate`). The whole row becomes a `role="link"` keyboard-accessible target; clicks on the switch / settings icon / status badge are excluded via `data-stop-nav="1"`.
  - **NEW SettingsIcon button** on every navigable row — explicit "Open module" affordance (mirrors the card click, but visually discoverable). Hidden for `coming_soon` rows.
  - **NEW status-badge popover** — clicking the small status pill opens a Popover with label, key, status, category, current state, and a warning when the module is critical.
  - **NEW confirmation modal** (`AlertDialog`) — disabling any module in `CRITICAL_MODULES` shows "Yeh ek critical module hai. Staff is module ki screens, shortcuts aur API access nahi use kar payenge." with Cancel / Disable Module buttons. Enabling a critical module is unconditional.
  - **NEW loading spinner** per-row — the switch is replaced by a `Loader2` while the PUT is in flight, preventing double-clicks.
  - **NEW Undo action** on the success toast — pressing Undo reverses the toggle. Implemented via `sonner` `action: {label, onClick}`.
  - **NEW optimistic update + automatic rollback** on API failure — the state mutates immediately for snappy UX, and reverts if the PUT throws (with an error toast explaining the revert).
- **External-link icon** next to each navigable module label (purely a hint — no layout change since it's inline-flex).

### Sidebar
The existing `AdminLayout.jsx` already implements every requirement of section 2 of the spec — `NavLink` with `isActive` highlighting, instant navigation, sticky position, mobile dock. **No code change needed.**

### Verified
- E2E Playwright sweep:
  - `kpi-users` → `/admin/users` ✅
  - `kpi-devices` → `/admin/devices` ✅
  - `kpi-invoices` → `/invoices` ✅
  - `kpi-license` → `/license` ✅
  - `feature-open-sales` (settings cog) → `/invoices/sale/new` ✅
  - `feature-toggle-sales` OFF → confirmation modal opens (Cancel + Confirm buttons) ✅
  - `feature-badge-sales` → popover with full details ✅
- Targeted backend pytest (Phase 1 + 2 + 3): **41/41 PASS, 0 regressions**.
- Full pytest sweep: 419/431 PASS — the 12 failing tests are pre-existing (auth/backup/whatsapp test pollution), unrelated to this release. Fixed one of them (`test_new_fields_default_off`) by hardening the seed to be self-contained.

### Files touched (additive only)
- NEW: `frontend/src/lib/moduleRoutes.js`
- MOD: `frontend/src/pages/admin/AdminDashboard.jsx` (Kpi component accepts optional `to`), `frontend/src/pages/admin/AdminPages.jsx` (AdminFeatures toggle engine + ModuleRow card-click + settings icon + status popover + confirm dialog), `backend/tests/test_txn_messages.py` (hardened defaults test).

### Intentionally NOT done
- No UI redesign or restyling — every existing dimension, color, gradient, glass-card border is preserved.
- No mobile layout changes — all new interactivity uses existing breakpoints.
- No new admin route — the spec called these out but they're already wired in `App.js` lines 160-171.
- Sidebar item rename — explicitly forbidden by the spec ("No module rename"). Sidebar labels untouched.



## v12.38 — 2026-06-20 — Auto Transaction Message Center (full 9-section spec)

> User explicit mandate: do NOT create a new module — UPGRADE the existing WhatsApp Bulk Sender page into the Auto Transaction Message Center. Reuse `_send_twilio`, `_interp`, `WhatsAppBulkSender.jsx`, `activity_logs`. NO duplicate sender / template engine / communication module.

### What ships
- **Page renamed** in sidebar + page header (`pages/WhatsAppBulkSender.jsx`):
  - Sidebar label: "WhatsApp Bulk Sender" → "**Auto Transaction Message**" (i18n en + hi)
  - Page header: "**Auto Transaction Message — WhatsApp Bulk Sender**"
  - Eyebrow text: "Auto Transaction Message Center"
  - Description updated to match the user spec verbatim
  - `<AutoTransactionMessages />` mounted at the top of the page; the existing 3-tab bulk-send workflow (Compose / Send / History) preserved below under a "Manual Bulk Send" subheading.
- **`backend/txn_messages.py`** (v12.38 extensions):
  - DEFAULT_SETTINGS gained 5 new fields: `send_to_party` (default TRUE), `send_on_update` (FALSE), `send_copy_to_self` (FALSE), `self_copy_phone` (""), `last_synced_at` (None)
  - `SettingsIn` model accepts all 5 new optional fields
  - `fire_event(...)` extended with `is_update: bool=False`. Builds a recipient list (party + self-copy depending on toggles), dispatches per-recipient, logs one row per send with `recipient_kind` = `party` | `self_copy`, stamps `last_synced_at` after every fire. Append " (Updated)" to the body when `is_update=True`. Block update events when `send_on_update` is OFF.
  - New endpoint `POST /api/txn-messages/test-self?company_id=…` — admin sends a smoke test to their own `self_copy_phone` (bypasses 4-tier gate; this is a config-time sanity check).
  - New endpoint `POST /api/txn-messages/sync-now?company_id=…` — refreshes the `last_synced_at` timestamp for the "Last synced: X min ago" UI label.
- **`backend/routes.py`** — added `fire_event(..., is_update=True)` hooks to `update_invoice` (10 invoice types) and `update_expense`. Existing create-side hooks untouched. All hooks wrapped in `try/except` so a message failure NEVER blocks the save.
- **`backend/payments.py`** — added `fire_event(..., is_update=True)` hook to `update_payment`.
- **`frontend/src/components/AutoTransactionMessages.jsx`** — extended with:
  - **Section 1 — Message Recipient Settings**: 3 toggles (Send to Party / Send on Update / Send Copy to Self), self-copy phone input with Test Message + Save buttons, "Last synced: X ago" timestamp with Sync Now action button
  - **Section 3 — Channels** — `Email (soon)` and `Future channels` disabled placeholder buttons next to WhatsApp + SMS
  - **Section 4 — Per-event Template Picker** — each enabled event row now has a `<select>` for picking one of the 7 marketing templates (Festival, Diwali, Thank You, Reminder, Offer, New Arrival, Birthday) plus a per-event channel override (WhatsApp / SMS)
  - Templates list fetched from existing `GET /api/marketing/templates` — no new template store
  - Version badge bumped to v12.38

### Verified
- **4 new pytest cases** in `test_txn_messages.py::TestV1238Settings` — all pass:
  - `test_new_fields_default_off` — defaults: `send_to_party=True`, others `False`
  - `test_new_fields_persist_and_validate` — all 5 fields round-trip
  - `test_sync_now_stamps_timestamp` — `/sync-now` returns ISO-8601
  - `test_test_self_requires_phone` — 400 when phone empty
- Full pytest sweep: **41 / 41 PASS, 0 regressions** (smoke + offline-engine + txn-messages).
- E2E Playwright on `/whatsapp-bulk`:
  - Page header reads "Auto Transaction Message — WhatsApp Bulk Sender" ✅
  - All 9 spec sections render correctly ✅
  - 3 recipient toggles + phone input + Test + Save + Sync Now ✅
  - Template picker dropdown shows 7 marketing templates ✅
  - Per-event channel override dropdown ✅
  - Email + Future channels placeholders (disabled) ✅
  - Manual Bulk Send section preserved below ✅

### Rollout posture shipped
- Platform flag `auto-transaction-messages` reset to **`enabled_global=False`** (default OFF).
- The 5 new customer settings default to safe values: `send_to_party=True`, `send_on_update=False`, `send_copy_to_self=False`, `self_copy_phone=""`.
- Customer Admins who pilot the feature MUST explicitly enter their self-copy number AND tick `Send Copy to Self` for blind-copies to flow.

### Files touched
- MOD: `backend/txn_messages.py`, `backend/routes.py` (2 update hooks), `backend/payments.py` (1 update hook), `backend/tests/test_txn_messages.py` (+4 cases), `frontend/src/components/AutoTransactionMessages.jsx` (Sections 1, 3, 4 added), `frontend/src/pages/WhatsAppBulkSender.jsx` (rename header + mount AutoTransactionMessages on top), `frontend/src/lib/i18n.js` (sidebar label rename en + hi).

### Intentionally NOT done (separate user approval)
- Email channel actual dispatch (placeholder shown — SendGrid/Resend integration needed).
- Per-event custom_body editor (template_id picker shipped; freeform text edit deferred).
- Bulk "Assign Access to Staff" UI inside Settings (the backend `enabled_users[]` already works via API; UI surfaces only the admin's view).



## v12.37.1 — 2026-06-20 — WhatsApp Floating Shortcut wired into all three forms

> Continuation of v12.37: the backend `/api/txn-messages/wa-shortcut` endpoint was already built + tested. This patch wires the frontend "Send WhatsApp now?" CTA into NewInvoice, Payments, and ExpenseForm so that immediately after a successful save the user sees a sonner toast with a one-click action.

### What ships
- **NEW `frontend/src/lib/waShortcut.js`** — single source of truth:
  - `offerWaShortcut({ companyId, eventKey, txnId, party })` — async fire-and-forget helper. Calls `/api/txn-messages/wa-shortcut`. On 200 → opens a sonner toast with a body preview + "Send WhatsApp" action that does `window.open(wa.me URL)`. On 403 / 404 / 400 → silently no-ops (so a misconfigured customer never sees a broken UI).
  - `INVOICE_TYPE_TO_EVENT` — single canonical map from `payload.type` (sale, purchase, sale_order, purchase_order, quotation, delivery_challan, credit_note, debit_note, sales_return, purchase_return) to backend event_key. Shared by all call-sites.

- **MOD `pages/NewInvoice.jsx`** — `offerWaShortcut(...)` called on both branches of save (create + edit). Covers all 10 invoice types via the type→event map.
- **MOD `pages/Payments.jsx`** — `offerWaShortcut(...)` called on both branches of save. event_key resolves to `payment_in.created` or `payment_out.created` based on the page's `type` prop.
- **MOD `pages/ExpenseForm.jsx`** — `offerWaShortcut(...)` called only on the CREATE branch (no party means the backend returns 400 and the helper silently no-ops anyway — but most expenses go to a vendor who DOES have a phone).

### Toast UX
```
┌──────────────────────────────────────────────────────┐
│ Send WhatsApp to Acme Traders?                       │
│ Namaste Acme Traders, aapka bill RM/2026-27/52…   [SEND] │
└──────────────────────────────────────────────────────┘
```
Duration: 12 seconds. Action button opens `wa.me/<digits>?text=<urlencoded>` in a new tab.

### Verified
- Curl test: 4-tier gate disabled (default) → HTTP 403 (silent no-op in UI). 4-tier gate enabled + `whatsapp_shortcut_enabled=true` → HTTP 200 with `{wa_url, phone, body}`.
- Full pytest sweep: **37 / 37 PASS, 0 regressions**.

### Files touched
- NEW: `frontend/src/lib/waShortcut.js`
- MOD: `frontend/src/pages/NewInvoice.jsx`, `frontend/src/pages/Payments.jsx`, `frontend/src/pages/ExpenseForm.jsx`

### Rollout
Backend platform flag + customer `whatsapp_shortcut_enabled` setting are BOTH still default OFF. So no live user sees any new behaviour until the Super Admin promotes a pilot user AND the Customer Admin flips "WhatsApp Shortcut" ON in Settings → Auto Transaction Messages. Both gates are honoured by the same `/wa-shortcut` endpoint.



## v12.37 — 2026-06-20 — Auto Transaction Message Trigger Layer (License-Gated, 4-Tier)

> User mandate: do NOT create a new module — UPGRADE existing messaging.py / marketing.py / reminders.py with an event-driven trigger layer so that **save → auto-WhatsApp/SMS** works for 13 transaction types. Must be license-gated end-to-end (Platform → License → Customer → User). Default OFF everywhere, Super Admin promotes one licensed customer at a time.

### What ships (additive only — no duplication)
- **NEW `backend/txn_messages.py`** (~600 lines) — the thin trigger layer that:
  - Owns the 4-tier `check_access()` gate (platform flag → license active → customer setting → user permission)
  - Exposes `fire_event(db, user, event_key, txn_doc, request)` — non-blocking hook that is wrapped in a broad `try/except` so a failed message NEVER breaks an invoice/payment/expense save
  - 13 supported events: `sale.created`, `purchase.created`, `sale_order.created`, `purchase_order.created`, `quotation.created`, `delivery_challan.created`, `credit_note.created`, `debit_note.created`, `sales_return.created`, `purchase_return.created`, `payment_in.created`, `payment_out.created`, `expense.created`
  - Reuses `messaging._send_twilio` (dispatcher), `marketing._interp` (interpolation), `marketing.TEMPLATES` (template library), `licensing._days_left` (license gate)
  - Per-event `DEFAULT_BODIES` so the system is usable on day one with zero configuration
  - Three new Mongo collections: `txn_message_settings`, `txn_message_rules`, `txn_message_logs`
  - Endpoints: `/events`, `/access`, `/settings` (GET+PUT), `/rules` (GET+PUT+DELETE), `/logs` (GET+DELETE), `/test-fire`, `/wa-shortcut`
- **NEW feature flag `auto-transaction-messages`** (BUILT_IN_FLAGS, default OFF) — the platform-level kill switch. Super Admin flips it from existing `/admin/feature-flags` (NO new admin panel built).
- **MODIFIED `backend/routes.py`** — added 2 hook call-sites at the end of `create_invoice` (covers 10 events via type mapping) and `create_expense` (1 event). Each hook is wrapped in `try/except` to ensure save flow is never blocked.
- **MODIFIED `backend/payments.py`** — added 1 hook call-site at the end of `create_payment` (covers `payment_in.created` + `payment_out.created`).
- **MODIFIED `backend/server.py`** — wired the `txn_messages_router`.

### Frontend
- **NEW `components/AutoTransactionMessages.jsx`** (~370 lines) — drops into Settings as a self-contained card. UI behaviour follows the user's spec exactly:
  - Renders NOTHING when `access.platform_enabled === false` (platform OFF → entire module hidden)
  - Renders NOTHING for non-admin users (staff cannot configure)
  - Shows an "License inactive" amber banner when license is expired (controls disabled)
  - Three top-line toggles: Module Enable / Auto Send / WhatsApp Shortcut
  - Default channel selector (WhatsApp / SMS)
  - 13 per-event toggles grouped (Sales / Purchases / Payments-Expenses)
  - Test-fire panel (admin-only dry-run with sample doc)
  - Recent dispatches list (last 30, color-coded by status)
- **MODIFIED `pages/Settings.jsx`** — mounted `<AutoTransactionMessages />` between the About card and the YouTube card.

### Verified
- **9 new pytest cases** in `test_txn_messages.py` — all pass: 13-event taxonomy, default-denied posture, platform-only-still-denied, full-chain-allows, settings guard (403 when platform OFF), anonymous denied, rule upsert + invalid event rejection, logs listing.
- **Full pytest sweep: 37 tests pass, 0 regressions** (smoke + offline-engine + txn-messages).
- **E2E Playwright**:
  - Flag OFF → `settings-txn-messages-card` count = 0 (hidden) ✅
  - Flag ON → card visible, 3 top toggles render, settings save → toast "Settings updated" ✅
- **End-to-end curl** confirmed full 4-tier chain returns `allowed: true` only when ALL four layers are ON.

### Production-safe posture shipped
After this release the `auto-transaction-messages` flag is **`enabled_global=False`**, NO customers have customer-settings turned on, NO users in `enabled_users`. To roll out:
```
1. POST /api/feature-flags/auto-transaction-messages/toggle-user
       { user_id: "<pilot admin id>", enable: true }
2. The pilot admin logs in → Settings → sees the Auto Transaction Messages card
3. They flip "Module Enable" + "Auto Send" + pick channel + events
4. Soak 24-48h with that one customer
5. Promote to role / global via the same /admin/feature-flags UI
```

### Files touched (additive only)
- NEW: `backend/txn_messages.py`, `frontend/src/components/AutoTransactionMessages.jsx`, `backend/tests/test_txn_messages.py`
- MODIFIED: `backend/feature_flags.py` (added 1 BUILT_IN flag entry), `backend/server.py` (mounted router), `backend/routes.py` (2 fire_event hooks), `backend/payments.py` (1 fire_event hook), `frontend/src/pages/Settings.jsx` (1 component import + mount point)

### Intentionally NOT done (separate user approval)
- WhatsApp Floating shortcut (post-save toast with one-click wa.me) — endpoint `/wa-shortcut` is built and tested, frontend handler not yet hooked into invoice/payment save UI
- Per-event template-id picker UI in the Settings card (rules CRUD works via API; UI surfaces only the on/off toggle for now)
- Email channel (only WhatsApp + SMS shipped per user's spec)
- Update events (`.updated` variants exist in the schema but no default bodies — opt-in via custom_body)



## v12.36 — 2026-06-20 — Phase 1 Offline Engine (Controlled Rollout)

> User mandate: Continue safe-upgrade plan. Phase 1 deliverables — Service Worker gated by `service-worker` flag, mutation queue UI gated by `mutation-queue` flag, offline-write wrapper for the Expense form. Defaults OFF. Super-admin can pilot users one by one.

### Frontend additions (all gated by feature flags, default OFF)
- **NEW `lib/offlineGate.js`** — single coordinator that:
  - Polls `/api/feature-flags/resolved` every 60s and on login
  - Registers `/service-worker.js` (scope `/`) when `service-worker` flag is ON for the user
  - Unregisters every SW + wipes all caches when flag flips OFF (60s soft-rollback, instant via the audited kill switch)
  - Exposes `isServiceWorkerEnabled()`, `isMutationQueueEnabled()`, `onFlagsChange(cb)`, `kickOfflineGate()`
- **NEW `lib/offlineWrite.js`** — drop-in `tryOfflineWrite({...})` helper. Only enqueues failed network ops when `mutation-queue` flag is ON; 4xx/5xx are NOT queued (they are real validation errors). Forms continue to work unchanged when the flag is OFF.
- **NEW `components/MutationQueueBadge.jsx`** — header badge with:
  - Live count of pending + errored items
  - Color tone (green/amber/rose) reflecting status
  - Popover: per-item summary, kind, attempt count, last error
  - "Sync now" button (manual replay, respects per-device kill switch)
  - Auto-hides when `mutation-queue` flag is OFF
- **REWROTE `public/service-worker.js`** to **v3-2026-06-20**:
  - Triple-cache split: shell (network-first) / static `/static/*` (cache-first) / images (cache-first SWR)
  - Versioned cache names → activation event wipes any older cache
  - Hard NEVER_CACHE list: `/api/*`, `/login`, `/register`, `/forgot-password`, `/admin/*`, `/portal/login`
  - Skips requests carrying `Authorization` headers
  - `postMessage({type:'KILL'})` handler — fully tears itself down for emergency rollback
- **MODIFIED `index.js`** — replaced unconditional `navigator.serviceWorker.register()` with `bootstrapOfflineGate()` after `window.load`.
- **MODIFIED `context/AuthContext.jsx`** — calls `kickOfflineGate()` on login + on `/auth/me` refresh so the gate evaluates the new user's per-user/per-role flag scope immediately. Also wipes the feature-flag sessionStorage cache on logout.
- **MODIFIED `components/Header.jsx`** — inserts `<MutationQueueBadge />` next to `<SystemHealthBadge />`.
- **MODIFIED `pages/ExpenseForm.jsx`** — Save / Update wrapped in `tryOfflineWrite` so the user sees "Expense queued — will sync when online" instead of an error toast when offline + flag ON. Other modules can opt in incrementally (Party / Item / Invoice / Payment).

### Backend
- **No new endpoints** — the existing feature-flag engine (`backend/feature_flags.py`) already covered per-user / per-role / kill-switch resolution.
- **NEW `tests/test_offline_engine.py`** (7 tests, all passing) — verifies the resolved endpoint, default-OFF posture, super-admin global flip, kill-switch overrides everything, per-user toggle promotes only that user, anonymous read & write are both rejected.

### End-to-end verified (Playwright + curl)
| Test | Expected | Actual |
|------|----------|--------|
| Flag OFF → SW count | 0 | 0 ✅ |
| Flag ON → SW count | 1 (scope `/`, state `activated`) | 1 ✅ |
| Flag flip OFF mid-session | SW unregisters | unregistered ✅ |
| Flag OFF → MutationQueueBadge | hidden | hidden ✅ |
| Flag ON → MutationQueueBadge | visible + clickable | visible ✅ |
| Badge popover | shows "All synced", Sync now button | confirmed ✅ |
| Kill switch | overrides global + user + role | confirmed via pytest ✅ |
| 28 pytest tests (smoke + offline engine) | all pass | all pass ✅ |

### Rollout posture
After this release, `service-worker` AND `mutation-queue` are BOTH set to `enabled_global=False`, `enabled_users=[]`, `enabled_roles=[]`. The next operator action is to add the first pilot user (e.g. the super-admin's own user_id) via:
```
POST /api/feature-flags/service-worker/toggle-user { user_id: "<admin>", enable: true }
```
and observe for 24-48h before promoting to a role and then to global ON.

### Files touched (additive only)
- NEW: `frontend/src/lib/offlineGate.js`, `frontend/src/lib/offlineWrite.js`, `frontend/src/components/MutationQueueBadge.jsx`, `backend/tests/test_offline_engine.py`
- REWRITTEN: `frontend/public/service-worker.js`, `frontend/src/index.js`
- MODIFIED: `frontend/src/components/Header.jsx`, `frontend/src/pages/ExpenseForm.jsx`, `frontend/src/context/AuthContext.jsx`



## v12.3 — 2026-06-13 — Backup Trash + Migration Engine Upgrade

> User asked: (1) Backup delete should NEVER hard-purge — must go to trash first with restore option. (2) Migration engine should support more entity types + Marg/Busy/Tally aliases. STRICT: no new modules, extend existing only.

### Phase 1 — Backup Trash / Recycle Bin
- **`DELETE /api/backup/{id}`** now soft-deletes by default — moves file to `/app/backend/_backups/trash/` and marks the DB record with `is_deleted=true`, `deleted_at`, `deleted_by`, `restore_path`, `retention_days=30`. Pass `?permanent=true` for legacy hard-delete.
- **New endpoints**:
  - `GET /api/backup/trash` — list soft-deleted backups with `days_remaining` countdown
  - `POST /api/backup/trash/{id}/restore` — restore file + clear flags
  - `POST /api/backup/trash/empty` — purge ALL trash (irrecoverable)
  - `POST /api/backup/trash/auto-clean` — purge only items whose retention expired (safe for daily cron)
- **Scheduler cleanup** updated — `_run_scheduler` now soft-deletes (instead of hard-delete) auto-backups exceeding `keep_last_n`. They still hit retention before disappearing.
- **`/backup/list`** filters out `is_deleted` records by default.
- **`/backup/download/{id}`** now falls back to trash directory when a record is soft-deleted (useful for audit / pre-restore inspection).
- **Frontend BackupCenter** — new "Backup Trash" card below Restore Points showing:
  - Item count badge
  - Auto-Clean Expired button + Empty Trash button
  - Per-item: size · row count · ENCRYPTED badge · days-remaining badge (or EXPIRED red badge) · Download · Restore · Delete Forever
- **Confirmations updated** — delete now reads "Move this backup to Trash? You can restore it within 30 days." instead of the old scary "Delete permanently?".

### Phase 2 — Migration Engine Upgrade (no new module)
- **CSV/Excel import — Expenses entity ADDED**. New `EXPENSE_HEADER_MAP` recognises Vyapar/Tally/Marg column aliases: date, voucher date, category, head, amount, payment mode, paid by, vendor, supplier, narration, voucher no, bill no, gst, gst amount.
- **Auto date-format normalisation** — `DD-MM-YYYY` / `DD/MM/YYYY` rows are converted to ISO `YYYY-MM-DD` on import (silent best-effort).
- **PARTY_HEADER_MAP expanded** — added Tally / Marg aliases (ledger name, account name, balance type, credit limit, shipping address, phone no., email id).
- **ITEM_HEADER_MAP expanded** — added Vyapar/Marg aliases (alias, product code, EAN, max retail price, stock group, low stock, batch, batch no, expiry, expiry date, mfg, mfg date, wholesale price).
- **Currency-aware coercion** — `1,000.50` strings now correctly parsed (comma stripped before float).
- **Opening Stock mirroring** — when `opening_stock` is set on import, `current_stock` is auto-populated to match.
- **Party auto-typing** — when a CSV row has "Type=Supplier" / "Vendor", party type is set correctly (was hard-coded customer before).
- **Frontend** — Universal Import dropdown in BackupCenter now offers: Parties, Customers, Vendors, Items, **Expenses**.

### Bug fix (regression)
- `GET /api/txn-prefixes` now hides archived series by default — pass `?include_archived=true` for management screens. This fixed the InvoicePrefixPicker showing broken/old series in the dropdown and the test failure in test_invoice_with_explicit_prefix_id.

### Tests
- All 27 pytest still green (21 smoke + 6 Vyapar parity).
- E2E curl test: backup soft-delete → trash list → restore → permanent-delete cycle PASS.
- E2E curl test: Expense CSV with mixed-format dates → preview correctly auto-maps + normalises.

### Files touched
- `/app/backend/backup_engine.py` — DEFAULT_RETENTION_DAYS, BACKUP_TRASH_DIR, soft-delete branch, trash endpoints (list/restore/empty/auto-clean), download trash fallback, scheduler cleanup.
- `/app/backend/data_io.py` — EXPENSE_HEADER_MAP, expanded PARTY/ITEM maps, preview & commit accept "expenses".
- `/app/backend/txn_prefixes.py` — list_prefixes hides archived by default.
- `/app/frontend/src/pages/BackupCenter.jsx` — Backup Trash card, trash handlers, IMPORT_ENTITY_OPTS with Expenses.



## v12.2 — 2026-06-12 — Billing Polish · WhatsApp AI · Inventory Batch/Expiry · Accounting Verified

> User asked for system-wide partial-feature completion (a/b/c/d). No new modules — only enhancements to existing ones.

### (a) Billing Polish — New Sale Invoice
- **QuickAddItemModal** (`/app/frontend/src/pages/invoice/QuickAddItemModal.jsx`) — Vyapar-style inline "Add Item" modal launched when an item search yields no match. Captures name, code, barcode, HSN, unit, sale price, purchase price, GST %, opening stock + collapsible Batch/Expiry section (Batch No, Mfg Date, Expiry Date).
- Wired into `NewInvoice.jsx` — `ItemPicker.onAddNew` invokes `openQuickAddItem(lineIdx, seed)`. On save, item auto-fills the active invoice line.
- **Quick Bill keyboard shortcuts** (Vyapar parity): Ctrl+S (Save), Ctrl+Shift+P (Save & Print), Ctrl+Shift+W (Save & WhatsApp), Alt+I (Add Row), Alt+C (Focus Customer), Alt+N (Focus first item), F2 (Toggle GST/Non-GST).

### (b) WhatsApp Bulk Sender Upgrade
- **AI Quick Composer** panel — 4 greeting buttons (Good Morning, Festival, Monday Motivation, Thank You) + 4 business tip buttons (Growth, Finance, Customer, GST). Each calls a backend AI endpoint and loads result into the message body.
  - Backend: `POST /api/marketing/ai-greeting` (returns `{message, emoji_used}`).
  - Backend: `POST /api/marketing/ai-business-tip` (returns `{title, tip, action}`).
  - Both use Emergent LLM key via `ai_assistant._llm_call`.
- **Smart Segments** — Recipients section now has 6 pill chips: All / Customers / Vendors / **Due ₹** (outstanding > 0) / **High Value** (outstanding > ₹10,000) / **Advance ₹** (negative outstanding — you owe them). Each filters the party list client-side from the existing `outstanding` field.
- **Defensive UX**: `composeWithAI` now shows toast.error on 502, empty response, or network failure (was previously silent on edge cases).

### (c) Inventory — Batch & Expiry Tracking + Alerts
- **`ItemIn` extended** with `expiry_date`, `mfg_date`, `mfg_lot` (string ISO dates).
- **`GET /api/items/alerts?company_id=…&expiry_within_days=30`** — returns 4 buckets: `low_stock`, `out_of_stock`, `expiring_soon`, `expired` plus `as_of`, `expiry_within_days`.
- **Dashboard widget** — when expired/expiring items exist, a 2-card row appears below Low Stock: red "Expired Stock" + amber "Expiring Soon (30 days)". Each lists top 8 items with deep-link to filtered Items page.

### (d) Accounting — Endpoint Verification
- Confirmed working:
  - `GET /api/accounting/profit-loss` (revenue · cogs · expenses · gross_profit · net_profit) ✅
  - `GET /api/accounting/balance-sheet` (assets · liabilities · owners_equity · total_*) ✅
  - `GET /api/accounting/trial-balance` ✅
  - `GET /api/gst-returns/gstr-3b` and `GET /api/gst-returns/gstr-9` ✅
- No code changes needed — module is complete.

### Tests
- Added: `/app/backend/tests/test_v12_2_enhancements.py` (11 tests).
- Combined suite: **38/38 pytest PASS** (21 smoke + 6 Vyapar parity + 11 v12.2).
- Testing agent iteration #20: 100% backend, 100% frontend.

### Spec deviations noted (non-blocking)
- Balance sheet returns `owners_equity` as a scalar (not `equity[]` list). UI consumers can adapt.
- GST returns are namespaced under `/api/gst-returns/` (not `/api/gstr-…`). Canonical paths documented.

### What was deferred (out of scope this round)
- **QR Connect** for WhatsApp Web automation — needs server-side WhatsApp Web library (whatsapp-web.js / Baileys) + websocket. Major scope.
- **Daily Auto Greeting cron** — manual one-tap broadcast works today; scheduled cron requires APScheduler integration.



## v12.1 — 2026-06-12 — Vyapar Parity Layout Pass #2

> User instruction (Hindi/Hinglish): Logistics & Doc Copy ko Description ke neeche move karo, duplicate Terms & Conditions hatao, Invoice Number ko inline editable banao (Auto/Custom mode).

### Frontend
- **Layout reorder** — `Logistics & Document Copy` section moved from above the line-items table to between `Description (prints on invoice)` and `Terms & Conditions`. New flow:
  `Customer → Billing/Shipping → Line Items → Add Row → Description → Logistics & Document Copy → Terms & Conditions → Internal Notes → Totals (right panel)`. This is the canonical Vyapar layout.
- **Duplicate Terms & Conditions removed** — there is now exactly ONE T&C textarea on the screen (with the existing template picker). The redundant T&C that used to live inside the Logistics card is gone.
- **Invoice Number Custom Mode** — header now shows an `Auto / Custom` toggle pill next to the invoice number. In Auto mode the inline prefix picker is used (default). In Custom mode the user can type any invoice number directly into an input field; on save the backend stores that exact value without incrementing any series counter.
  - `data-testid="invoice-no-mode-toggle"`, `invoice-no-mode-auto`, `invoice-no-mode-custom`, `custom-invoice-no-input`.

### Backend
- `InvoiceIn` — new optional field `invoice_no_override: Optional[str] = None`.
- `create_invoice` — when `invoice_no_override` is non-empty, it is used as-is (no series increment). Duplicate detection on `(company_id, type, invoice_no)` returns HTTP 400 with a friendly message instead of silently overwriting.
- Tested end-to-end via curl: POST with `invoice_no_override="VYAPAR/2026-27/0099"` returns invoice with that exact `invoice_no`; second POST with the same number returns `400 — Invoice number 'VYAPAR/2026-27/0099' already exists`.

### Tests
- 27/27 pytest still green (21 smoke + 6 Vyapar parity tests).

### Files touched
- `/app/frontend/src/pages/NewInvoice.jsx` — layout move + Custom mode UI + payload override.
- `/app/backend/routes.py` — InvoiceIn override field + create_invoice override branch + duplicate guard.



## v12.0 — 2026-06-12 — Vyapar Parity Pass for New Sale Invoice (UI only)

> User asked (Hindi): "जितना बताया है वह सब चेंज करो — Vyapar जैसा simple, fast और user friendly". No new modules created, only enhanced existing New Sale Invoice / Purchase / Quotation flow.

### New Components
- `/app/frontend/src/pages/invoice/InvoicePrefixPicker.jsx` — clickable inline prefix dropdown right next to the Invoice Number preview. Lists every configured prefix series for the active company+txn-type (Default sale, Regal Goa Sales, RM/2026-27/, …), shows DEFAULT / LOCKED badges and `Next #N`, and has a `Manage Series & Customize Format` shortcut to `/admin/prefix-management`.
- `/app/frontend/src/pages/invoice/OldBillSearch.jsx` — header "Search Old Bill" popover that hits `GET /api/invoices?q=…` with 250 ms debounce; each result row carries View / Edit / Print quick-action icons routed via the canonical sale/purchase/quotation paths.

### PartyPicker (Vyapar parity)
- `+ Add Party` is now **pinned at the TOP** of the dropdown (visually like Vyapar) with a primary-tinted CTA and a "Create new customer/vendor without leaving invoice" subtitle.
- Every party row now shows an outstanding-balance pill (red = party owes you, green = you owe them) using the `outstanding` field already computed by `/api/parties`.

### ItemPicker (Vyapar parity)
- Hand-rolled dropdown now auto-flips **above** the input when there is < 300 px of space below — guarantees the list never overlaps the `Add Row` button or runs off-screen (`useLayoutEffect` measures viewport).
- Optional `onAddNew` prop renders a `+ Add Item "<query>"` row at the TOP when search yields no matches.

### Quick Add Party Modal — full Vyapar field set
- New fields wired both UI ↔ backend `PartyIn` ↔ MongoDB:
  - `email` (string, optional)
  - `shipping_address` + "Same as billing" auto-sync checkbox (default ON)
  - `opening_balance` numeric + `opening_balance_type` `"debit" | "credit"` ("To Receive" / "To Pay" Vyapar-style toggle)
  - `credit_limit` numeric
- Optional fields wrapped in a collapsible "Add Opening Balance / Credit Limit" section (closed by default) to keep the modal compact for fast billing.

### Rate Column — "With Tax / Without Tax" header
- In GST mode, the line-items `Rate (₹)` column header is now a `Select` dropdown ("Without Tax" / "With Tax") at the column header — same Vyapar UX. Switching flips the global `taxInclusive` calc.

### Backend
- `routes.py` `PartyIn` — added `shipping_address`, `opening_balance_type` (literal `"debit"|"credit"`). No legacy regression — both fields default to `""` / `"debit"`.
- `routes.py` `InvoiceIn` — added `prefix_id` so a user can pin a specific series for a single invoice (e.g. switch a sale to the "Regal Goa Sales" series). `create_invoice` now honours it via `resolve_next_invoice_no_by_id`.
- `routes.py` `list_invoices` — accepts a new `q` query parameter; case-insensitive `$regex` across `invoice_no` OR `party_name`. Powers OldBillSearch with sub-2-second response on 500+ invoices.
- `routes.py` `create_invoice` — defensive guard: auto-fills `party_name / party_gstin / party_state` from a party-id lookup if the client omitted them (keeps q-search and reports accurate for future API callers).
- `txn_prefixes.py` — added `resolve_next_invoice_no_by_id(db, prefix_id)` that atomically `$inc`s a specific series counter (instead of always using the default for the company+type pair).

### Tests
- `/app/backend/tests/test_invoice_vyapar_parity.py` — 6 new tests covering: party extended fields round-trip, invoice with explicit prefix_id, invoice with null prefix_id falling back to default, q-search on invoice_no, q-search on party_name, defensive party-name auto-fill.
- Existing `test_smoke.py` (21 tests) — still 21/21 green. Total smoke + Vyapar parity suite: 27/27 PASS.

### Frontend ↔ Backend Verification (testing agent iteration 19)
- 14/14 data-testid hooks present and wired (`invoice-prefix-picker`, `prefix-manage-link`, `old-bill-search-trigger/input`, `tax-mode-toggle`, `rate-tax-mode-header`, `party-picker`, `party-add-new-btn`, `quick-add-party-modal`, `party-{email|address|shipping-address|same-as-billing|opening-balance|balance-debit|balance-credit|credit-limit}-input`).
- End-to-end party→invoice creation flow PASSED — party with full payload auto-selected, invoice INV/26/00013 saved.

### What was strictly NOT changed
- No module renamed, removed, or moved.
- All existing data-testids preserved.
- Existing dashboards, OCR scan, AI Mode, sync engine, sidebar, error tracking: zero changes.



## v11.0 — 2026-06-09 — Phase-1 Stability Architecture

> User instruction: **"Fix root causes, not symptoms. Never apply temporary patches."** — saved to `/app/memory/AGENTS.md` and now enforced for every future agent.

### New: Global Error Tracking
- `/app/backend/error_tracking.py` — backend module with:
  - `ErrorCaptureMiddleware` (auto-captures uncaught exceptions + every 5xx response)
  - `POST /api/errors/log` — anonymous-safe endpoint for frontend to report errors
  - `GET /api/errors/list` (admin) — paginated, filterable error log
  - `GET /api/errors/stats` (admin) — aggregations by kind/source/path
  - `POST /api/errors/clear` (admin) — prune old logs
- `/app/frontend/src/components/ErrorBoundary.jsx` — React ErrorBoundary at app root; never blank screen, shows recovery card with Copy/Home/Try-again buttons
- `/app/frontend/src/lib/errorReporter.js` — global `window.onerror` + `unhandledrejection` handlers + 30s deduplication + offline-safe no-op
- `/app/frontend/src/pages/admin/AdminErrors.jsx` — admin UI at `/admin/errors` with filters (source/kind/path/since), expandable stack traces, copy details

### New: System Health Dashboard
- `GET /api/admin/health` (admin) — aggregate snapshot endpoint
- `/app/frontend/src/pages/admin/AdminHealth.jsx` — admin UI at `/admin/health`:
  - Performance score ring (0-100, formula: 100 - 5xx×2 - DB>100ms:5 - DB-down:50)
  - Uptime · Database status (with ping latency in ms) · Active users (24h)
  - Errors 1h/24h with backend-5xx breakdown
  - Last backup · Storage breakdown across 10 key collections
  - Top failing endpoints bar chart (24h)
  - Auto-refresh toggle (15s interval)

### New: Phase-1 Smoke Test Suite
- `/app/backend/tests/test_smoke.py` — 21 regression tests across 8 layers:
  1. Login/Session (3 tests)
  2. System Health endpoint (2)
  3. Error Tracking (3)
  4. Permissions (anon-blocked) (2)
  5. Inventory CRUD (2)
  6. Invoice CRUD + totals (2)
  7. DB Consistency / ObjectId serialization (3)
  8. PDF/Print/Label template generation (3)
  9. Summary marker (1)
- Run with `pytest tests/test_smoke.py -v` from `/app/backend` (uses existing `conftest.py` fixtures)
- `/app/backend/pytest.ini` — registers the `critical` marker, silences deprecation warnings

### Polish: Invoice PDF
- `InvoiceView.jsx` — PDF + Thermal buttons now show `toast.loading` → `toast.success/error` with disabled state during generation (same UX pattern as labels)

### Polish: Sidebar
- Added `System Health` (Activity icon) and `Error Dashboard` (Bug icon) under `Admin → System` group with `data-testid="sidebar-health-link"` / `"sidebar-errors-link"`
- Added 13-language i18n entries for `nav.health` / `nav.errors`

### Files touched
- **New backend**: `error_tracking.py`, `tests/test_smoke.py`, `pytest.ini`
- **Modified backend**: `server.py` (router + middleware wiring)
- **New frontend**: `components/ErrorBoundary.jsx`, `lib/errorReporter.js`, `pages/admin/AdminErrors.jsx`, `pages/admin/AdminHealth.jsx`
- **Modified frontend**: `App.js` (wiring), `components/Sidebar.jsx` (links), `lib/i18n.js` (translations), `pages/InvoiceView.jsx` (loading toasts)
- **New memory**: `/app/memory/AGENTS.md` (engineering protocol + golden rule)

### Test results
- Smoke suite: **21/21 PASS in 2.86s**
- Lint: 0 blocking (React Compiler `react-hooks/set-state-in-effect` false positives documented as known issue per handoff; webpack compiles cleanly)
- Backend imports: clean (`python -c "import error_tracking"`)
- Live verification: `/admin/health` shows Performance 100, DB Online 1ms ping; `/admin/errors` shows 6 smoke test entries

### Remaining risks
- React Compiler lint rules flag `set-state-in-effect` as false positives across the codebase (SyncCenter, NewInvoice, others). Documented in handoff; webpack/babel compile cleanly. No action needed.
- Auto-backup cron, CI/CD deployment-blocker gates, and multi-environment (staging/dev) require Emergent platform support.

### Dashboard URLs
- Error Dashboard: `/admin/errors`
- Health Dashboard: `/admin/health`
- Admin sidebar → Admin → System group → System Health / Error Dashboard
