# Phase 3.3 Implementation Plan: Session Topic Sensitivity

## Summary of Clarified Requirements

| Topic | Decision |
|---|---|
| Customer selection UX | Required bottom sheet after "Mulai Curhat", before pricing; two buttons (not toggle) |
| Customer flag mutability | Locked for session duration — customer cannot change |
| Mitra flag mutability | Can flip mid-session via app-bar toggle |
| Flip confirmation dialog | Default `true`, disableable via `app_config` key `sensitive_flip_confirmation_enabled` |
| One-way latch | Default `false`, enableable via `app_config` key `sensitive_flag_one_way_latch` |
| Visual cue for mitra | Label "Topik sensitif" + warning-yellow doodle background |
| Customer visual cue | None — customer UI stays pink always |
| Storage | `chat_sessions.topic_sensitivity` enum + mirrored on `chat_requests`-like flow (actual rows live in `chat_sessions` before acceptance; see Section 1.1) |
| Audit trail | New `session_sensitivity_log` table — logs every mitra flip |
| WS event to customer | Sent but silently ignored client-side (future-proofing) |
| Extension | Shows current flag; flag carries over unchanged on acceptance |
| Control center | Sensitive stats panel + filter on sessions page + 2 new config keys |
| Pricing | Unchanged — schema leaves room for future differentiation |
| Push notification content | Stays generic — label only surfaces in overlay |
| Multi-device mitra sync | Out of scope |
| Auto-moderation | Out of scope |

---

## Data Model Note

After reviewing the codebase, `chat_requests` is NOT a separate table — pairing requests live as rows in `chat_sessions` with `status` transitions (`SEARCHING` → `PENDING_ACCEPTANCE` → `ACTIVE` → ...). Mitra notifications are tracked in `chat_request_notifications`. Therefore, **one column on `chat_sessions` is sufficient** — no mirroring needed.

---

## Work Stream 1: Backend — Schema, Services, Routes

### 1.1 DB Migration

**File:** [backend/src/db/migrate.js](backend/src/db/migrate.js)

```sql
-- New column on chat_sessions
ALTER TABLE chat_sessions
  ADD COLUMN IF NOT EXISTS topic_sensitivity VARCHAR(16) NOT NULL DEFAULT 'regular';

-- Optional index if CC filter needs it
CREATE INDEX IF NOT EXISTS idx_chat_sessions_topic_sensitivity
  ON chat_sessions (topic_sensitivity);

-- New audit table
CREATE TABLE IF NOT EXISTS session_sensitivity_log (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  session_id UUID NOT NULL REFERENCES chat_sessions(id) ON DELETE CASCADE,
  changed_by_mitra_id UUID NOT NULL REFERENCES mitras(id),
  from_value VARCHAR(16) NOT NULL,
  to_value VARCHAR(16) NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_session_sensitivity_log_session
  ON session_sensitivity_log (session_id);
```

**Seed new `app_config` keys** (in the same migration or the seed script):

```sql
INSERT INTO app_config (key, value) VALUES
  ('sensitive_flip_confirmation_enabled', 'true'::jsonb),
  ('sensitive_flag_one_way_latch', 'false'::jsonb)
ON CONFLICT (key) DO NOTHING;
```

### 1.2 Constants

**File:** [backend/src/constants.js](backend/src/constants.js)

```js
const TopicSensitivity = Object.freeze({
  REGULAR: 'regular',
  SENSITIVE: 'sensitive',
});

const WsMessage = Object.freeze({
  // ... existing
  SESSION_TOPIC_UPDATED: 'session_topic_updated',
});
```

### 1.3 Config Service

**File:** [backend/src/services/config.service.js](backend/src/services/config.service.js)

Add:
- `getSensitiveFlipConfirmationEnabled()` → bool (default `true`)
- `getSensitiveFlagOneWayLatch()` → bool (default `false`)
- `setSensitiveConfig({ flipConfirmationEnabled, oneWayLatch })` — used by control center

### 1.4 Pairing Service — Accept Topic Flag From Customer

**File:** [backend/src/services/pairing.service.js](backend/src/services/pairing.service.js)

- `createPairingRequest({ customerId, durationMinutes, isFreeTrial, topicSensitivity })` — new param, validated against `TopicSensitivity` enum; defaults to `regular` if omitted.
- Insert `topic_sensitivity` column when creating the `chat_sessions` row.
- When broadcasting the `chat_request` WS message to candidate mitras, include `topic_sensitivity` in the payload (alongside `duration_minutes`, `is_free_trial`).

### 1.5 New Service: Sensitivity Service

**New file:** `backend/src/services/sensitivity.service.js`

Exports:
- `flipSessionSensitivity({ sessionId, mitraId, toValue })`
  - Load session; verify ownership (mitra_id matches).
  - Verify session status is `ACTIVE` or `EXTENDING` (not flippable if `CLOSING`, `COMPLETED`, etc.).
  - Read `sensitive_flag_one_way_latch` from config. If `true` and current value is `sensitive`, reject with 409.
  - No-op if `fromValue === toValue`.
  - Transaction: UPDATE `chat_sessions.topic_sensitivity`, INSERT `session_sensitivity_log` row.
  - Broadcast WS message `session_topic_updated` to both participants: `{ session_id, topic_sensitivity, changed_at }`.
  - Return updated session.
- `getSessionSensitivityLog(sessionId)` — used by control center session detail (future).

### 1.6 Extension Service — Expose Current Flag

**File:** [backend/src/services/extension.service.js](backend/src/services/extension.service.js)

- `requestExtension` does not change the flag; flag carries over unchanged.
- When broadcasting the extension request WS message to the mitra, include `topic_sensitivity` from the session row.

### 1.7 New Routes

**File:** [backend/src/routes/public/shared.chat.routes.js](backend/src/routes/public/shared.chat.routes.js) (or the existing shared chat routes file — confirm name at implementation)

| Method | Path | Purpose | Auth |
|---|---|---|---|
| `PATCH` | `/api/shared/chat/sessions/:sessionId/topic` | Mitra flips flag | Firebase (mitra) |

**Body:** `{ topic_sensitivity: 'regular' \| 'sensitive' }`

**Response:** `{ session_id, topic_sensitivity, changed_at }`

### 1.8 Client Chat Request Route — Accept Topic Param

**File:** [backend/src/routes/public/client.chat.routes.js](backend/src/routes/public/client.chat.routes.js) — `POST /api/client/chat/request`

Extend request body schema:
```js
{
  duration_minutes: Joi/ajv,
  is_free_trial: bool,
  topic_sensitivity: enum('regular', 'sensitive') // required
}
```
Pass through to `createPairingRequest`.

### 1.9 Internal Routes — Config + Stats

**File:** [backend/src/routes/internal/config.routes.js](backend/src/routes/internal/config.routes.js)

Add:
- `GET /internal/config/sensitivity` → `{ flip_confirmation_enabled, one_way_latch }`
- `PATCH /internal/config/sensitivity` → accept same shape

**File:** [backend/src/routes/internal/session.routes.js](backend/src/routes/internal/session.routes.js)

- Extend session list query to accept `topic_sensitivity` filter param (`all` / `regular` / `sensitive`).
- Extend session detail response to include `topic_sensitivity` and recent `session_sensitivity_log` entries.

**File:** [backend/src/routes/internal/dashboard.routes.js](backend/src/routes/internal/dashboard.routes.js) (or wherever dashboard stats live)

- Add `sensitive_session_stats` to the dashboard payload: `{ total_sessions, sensitive_count, sensitive_percent }` over the last N days (match existing stat windows).

### 1.10 WebSocket — New Message Type

**File:** [backend/src/plugins/websocket.js](backend/src/plugins/websocket.js)

No plugin change needed if broadcast is done via existing `sendToSessionParticipant` helper — just a new `WsMessage.SESSION_TOPIC_UPDATED` constant.

---

## Work Stream 2: client_app — Topic Selection

### 2.1 New Widget: Topic Selection Bottom Sheet

**New file:** `client_app/lib/features/chat/widgets/topic_selection_bottom_sheet.dart`

A non-dismissible `DraggableScrollableSheet` (or `showModalBottomSheet` with `isDismissible: false` and `enableDrag: false`) containing:

- Title, body, sub-question (copy as in PRD Section 1)
- Two equal-width buttons: "Topik umum" (primary) + "Topik sensitif" (secondary style, equal height)
- Helper line below buttons
- System back / back button cancels and returns to home

**Return:** `Future<TopicSensitivity?>` — null if cancelled, otherwise the selected enum.

### 2.2 Topic Sensitivity Enum

**New file:** `client_app/lib/core/constants/topic_sensitivity.dart`

```dart
enum TopicSensitivity {
  regular('regular'),
  sensitive('sensitive');

  final String value;
  const TopicSensitivity(this.value);

  static TopicSensitivity fromString(String v) =>
    values.firstWhere((e) => e.value == v, orElse: () => TopicSensitivity.regular);
}
```

### 2.3 Home Screen — Wire Bottom Sheet Into Flow

**File:** [client_app/lib/features/home/home_screen.dart](client_app/lib/features/home/home_screen.dart)

Current "Mulai Curhat" → opens pricing sheet directly.

New flow:
1. Tap "Mulai Curhat"
2. Show `TopicSelectionBottomSheet` → await result
3. If result is null → no-op
4. If result is non-null → open existing `PricingBottomSheet`, passing selected topic as param

### 2.4 Pricing Bottom Sheet — Carry Topic

**File:** [client_app/lib/features/chat/widgets/pricing_bottom_sheet.dart](client_app/lib/features/chat/widgets/pricing_bottom_sheet.dart)

- Accept `TopicSensitivity topicSensitivity` constructor param.
- When firing the pairing request (via `PairingNotifier`), pass through `topic_sensitivity` to the request body.

### 2.5 Pairing Notifier — Pass Topic to Backend

**File:** [client_app/lib/core/pairing/pairing_notifier.dart](client_app/lib/core/pairing/pairing_notifier.dart)

- Extend `startPairing({ int durationMinutes, bool isFreeTrial, required TopicSensitivity topicSensitivity })` to include `topic_sensitivity` in the API call body.

### 2.6 No Customer UI Changes Beyond Selection

- Chat screen remains pink doodle — **no conditional coloring**.
- WS listener for `session_topic_updated` is wired but **silently no-ops** (so the server-side event doesn't cause errors).

**File:** [client_app/lib/core/chat/chat_notifier.dart](client_app/lib/core/chat/chat_notifier.dart) (or wherever WS messages are dispatched)

Add a switch case for `session_topic_updated` that logs and returns — no state update.

---

## Work Stream 3: mitra_app — Flag Display + Mid-Session Flip

### 3.1 Shared Enum + Widget: Sensitivity Badge

**New file:** `mitra_app/lib/core/constants/topic_sensitivity.dart` — same shape as client_app version.

**New file:** `mitra_app/lib/core/chat/widgets/sensitivity_badge.dart`

A small pill widget rendering "Topik sensitif" on yellow background. Configurable size. Reused across overlay, chat header, extension card, history list.

### 3.2 Chat Request Overlay — Label + Color

**File:** [mitra_app/lib/core/chat/widgets/chat_request_overlay.dart](mitra_app/lib/core/chat/widgets/chat_request_overlay.dart)

- Extend `ChatRequestIncomingData` (in [mitra_app/lib/core/chat/chat_request_notifier.dart](mitra_app/lib/core/chat/chat_request_notifier.dart)) with `TopicSensitivity topicSensitivity` field.
- Parse from WS `chat_request` message payload (`topic_sensitivity`).
- When sensitive: show yellow accent (e.g., left-border or top-strip) + `SensitivityBadge` near session metadata.

### 3.3 Chat Request Notifier — Parse Topic

**File:** [mitra_app/lib/core/chat/chat_request_notifier.dart](mitra_app/lib/core/chat/chat_request_notifier.dart)

Extend `ChatRequestIncomingData` constructor + WS handler to populate `topicSensitivity`.

### 3.4 Mitra Chat Notifier — Track + Flip Topic

**File:** [mitra_app/lib/core/chat/mitra_chat_notifier.dart](mitra_app/lib/core/chat/mitra_chat_notifier.dart)

- Add `TopicSensitivity topicSensitivity` to the chat state (populated from session fetch on connect).
- Handle incoming WS `session_topic_updated` → update state (authoritative from backend — handles the case where mitra flipped on another device in the future).
- New method `flipTopic()`:
  - Read `sensitive_flip_confirmation_enabled` from app_config provider (fetched on app start or via dedicated provider)
  - Show confirmation dialog if enabled
  - Call `PATCH /api/shared/chat/sessions/:sessionId/topic` via existing `ApiClient`
  - Show toast on success; show error dialog on failure (e.g., one-way latch violation → "Sesi sudah ditandai sensitif dan tidak bisa diubah kembali.")

### 3.5 Mitra Chat Screen — Yellow Doodle + Toggle

**File:** [mitra_app/lib/features/chat/screens/mitra_chat_screen.dart](mitra_app/lib/features/chat/screens/mitra_chat_screen.dart)

- Conditional background: watch `topicSensitivity` from `mitraChatNotifier`; swap doodle asset (pink ↔ yellow).
- Header banner: persistent small strip showing "Topik sensitif" label when sensitive.
- App-bar toggle icon (e.g., `Icons.flag` / `Icons.flag_outlined`) wired to `flipTopic()`.
  - If `one_way_latch` is enabled and current is `sensitive`, disable the icon and show tooltip "Sesi sudah terkunci sensitif."

### 3.6 Doodle Assets

**New files:** `mitra_app/assets/doodle_yellow.png` (or SVG) — warning-yellow version of existing pink doodle pattern.

- Exact yellow hex: proposed `#FFC107` (Material amber) or `#F7B500` — confirm with designer at implementation. Choose whichever contrasts best with white text/dark badges.
- Update `mitra_app/pubspec.yaml` assets section.

### 3.7 Extension UI — Current Flag

**File:** [mitra_app/lib/features/chat/widgets/extension_request_card.dart](mitra_app/lib/features/chat/widgets/extension_request_card.dart) (or wherever the extension request UI lives)

- Read `topicSensitivity` from the extension WS payload / session state.
- If sensitive: yellow accent + `SensitivityBadge` on the extension card.

### 3.8 Chat History List + Transcript

**File:** [mitra_app/lib/features/chat/screens/chat_history_screen.dart](mitra_app/lib/features/chat/screens/chat_history_screen.dart) (confirm name)

- List row: add `SensitivityBadge` when session is sensitive.
- Transcript detail: yellow doodle background for sensitive sessions (pink otherwise).

### 3.9 Mitra API Client

**File:** [mitra_app/lib/core/network/api_client.dart](mitra_app/lib/core/network/api_client.dart) (confirm name)

Add:
```dart
Future<Session> flipSessionTopic(String sessionId, TopicSensitivity to);
```

---

## Work Stream 4: control_center — Config + Stats + Filter

### 4.1 Settings Page — New Sensitivity Section

**File:** [control_center/src/pages/settings/SettingsPage.jsx](control_center/src/pages/settings/SettingsPage.jsx)

Add new section "Sensitivitas Topik":
- Checkbox: "Aktifkan dialog konfirmasi saat Mitra menandai topik sensitif" (`sensitive_flip_confirmation_enabled`)
- Checkbox: "Kunci searah — Mitra tidak bisa membatalkan tanda topik sensitif" (`sensitive_flag_one_way_latch`)

Wire to new endpoints: `GET/PATCH /internal/config/sensitivity`.

### 4.2 Sessions Page — Filter

**File:** [control_center/src/pages/sessions/SessionsPage.jsx](control_center/src/pages/sessions/SessionsPage.jsx)

- Add dropdown filter: "Semua / Umum / Sensitif".
- Pass `topic_sensitivity` query param to session list endpoint.
- Add column "Topik" with badge (green "Umum" / yellow "Sensitif").

### 4.3 Session Detail — Show Audit Trail

**File:** `control_center/src/pages/sessions/SessionDetailPage.jsx` (confirm name)

- Show current `topic_sensitivity`.
- Show compact timeline of `session_sensitivity_log` entries: "Mitra X menandai topik sebagai sensitif pada HH:MM".

### 4.4 Dashboard — Sensitive Stats Panel

**File:** [control_center/src/pages/dashboard/DashboardPage.jsx](control_center/src/pages/dashboard/DashboardPage.jsx)

- New card: "Sesi Sensitif" with count + percentage over selected date range.

### 4.5 Mitra Activity — Per-Mitra Sensitive Breakdown

**Backend file:** [backend/src/services/mitra-activity.service.js](backend/src/services/mitra-activity.service.js)

Extend `getMitraActivitySummary` SQL to join `chat_request_notifications` → `chat_sessions` on `session_id` and group per-mitra, returning additional columns:
- `sensitive_total` — count of notifications where joined session had `topic_sensitivity = 'sensitive'`
- `sensitive_accepted` — count of those with `response = 'accepted'`
- `sensitive_rejected` — count with `response = 'rejected'`
- `sensitive_acceptance_rate` — `sensitive_accepted / sensitive_total` (null if `sensitive_total = 0`)

This is a pure join — no new column needed on `chat_request_notifications` because the flag is derivable from the parent session row.

**Backend file:** [backend/src/routes/internal/mitra-activity.routes.js](backend/src/routes/internal/mitra-activity.routes.js)

`GET /internal/mitra-activity/summary` response includes the new fields above.

**Control center file:** [control_center/src/pages/mitra-activity/MitraActivityPage.jsx](control_center/src/pages/mitra-activity/MitraActivityPage.jsx)

Extend summary table with additional columns (may need a sub-grouping to keep it readable):

| Mitra | Total | Accepted | Rejected | Missed | Ignored | Rate (%) | Avg Response (s) | **Sensitive Total** | **Sensitive Accepted** | **Sensitive Rate (%)** |

Optionally: color the "Sensitive Rate" cell red when significantly lower than the overall rate — useful for spotting mitras who accept regular requests but consistently skip sensitive ones (potential QC signal).

Optional detail view: on the detail log table, add a "Topik" column with the session's sensitivity badge.

---

## 5. Implementation Order

| Step | What | Apps Affected | Dependencies |
|---|---|---|---|
| **Work Stream 1: Backend** | | | |
| 1 | DB migration: `topic_sensitivity` column, `session_sensitivity_log` table, 2 new `app_config` keys | Backend | None |
| 2 | Constants: `TopicSensitivity`, `WsMessage.SESSION_TOPIC_UPDATED` | Backend | None |
| 3 | Config service: getters + setter for sensitivity flags | Backend | Step 1 |
| 4 | Pairing service: accept + persist + broadcast `topic_sensitivity` | Backend | Steps 1–2 |
| 5 | New sensitivity service: `flipSessionSensitivity`, `getSessionSensitivityLog` | Backend | Steps 1–4 |
| 6 | Extension service: include `topic_sensitivity` in WS payload | Backend | Step 4 |
| 7 | New route: `PATCH /api/shared/chat/sessions/:sessionId/topic` | Backend | Step 5 |
| 8 | Extend `POST /api/client/chat/request` schema | Backend | Step 4 |
| 9 | Internal routes: `/internal/config/sensitivity`, filter on sessions, dashboard sensitive stats | Backend | Step 3, 5 |
| **Work Stream 2: client_app** | | | |
| 10 | `TopicSensitivity` enum | client_app | None |
| 11 | `TopicSelectionBottomSheet` widget | client_app | Step 10 |
| 12 | Home screen: new flow (topic sheet → pricing) | client_app | Step 11 |
| 13 | Pricing sheet + PairingNotifier: accept + forward flag | client_app | Steps 10, 12 |
| 14 | ChatNotifier: silently handle `session_topic_updated` WS | client_app | None |
| **Work Stream 3: mitra_app** | | | |
| 15 | `TopicSensitivity` enum + `SensitivityBadge` widget + yellow doodle asset | mitra_app | None |
| 16 | ChatRequestNotifier + overlay: parse topic, show badge + yellow accent | mitra_app | Step 15 |
| 17 | MitraChatNotifier: track topic, handle `session_topic_updated`, `flipTopic()` | mitra_app | Step 15, backend step 7 |
| 18 | Mitra chat screen: yellow doodle + header + app-bar toggle + config-driven dialog/latch | mitra_app | Step 17 |
| 19 | Extension card: show current flag | mitra_app | Step 15, backend step 6 |
| 20 | History list + transcript: badge + yellow doodle | mitra_app | Step 15 |
| **Work Stream 4: control_center** | | | |
| 21 | Settings page: sensitivity config section | control_center | Backend step 9 |
| 22 | Sessions list: filter + column | control_center | Backend step 9 |
| 23 | Session detail: audit trail timeline | control_center | Backend step 9 |
| 24 | Dashboard: sensitive stats card | control_center | Backend step 9 |
| 25 | Mitra Activity: extend summary SQL + API + table with sensitive columns | Backend + control_center | Backend step 1 |
| **Testing** | | | |
| 26 | E2E: customer selects sensitive → mitra sees label + yellow → mitra flips → audit + stats reflect | All | All above |

---

## 6. New Files

| File | Purpose |
|---|---|
| `backend/src/services/sensitivity.service.js` | Flip + audit + one-way-latch enforcement |
| `client_app/lib/core/constants/topic_sensitivity.dart` | Enum |
| `client_app/lib/features/chat/widgets/topic_selection_bottom_sheet.dart` | Customer topic-selection UI |
| `mitra_app/lib/core/constants/topic_sensitivity.dart` | Enum |
| `mitra_app/lib/core/chat/widgets/sensitivity_badge.dart` | Reusable badge |
| `mitra_app/assets/doodle_yellow.png` | Yellow doodle for sensitive sessions |

## 7. Modified Files (Primary)

| File | Change |
|---|---|
| `backend/src/db/migrate.js` | Migration + seed |
| `backend/src/constants.js` | `TopicSensitivity` + `WsMessage.SESSION_TOPIC_UPDATED` |
| `backend/src/services/config.service.js` | Getters/setter |
| `backend/src/services/pairing.service.js` | Accept + persist + broadcast topic |
| `backend/src/services/extension.service.js` | Include topic in WS payload |
| `backend/src/routes/public/client.chat.routes.js` | Request body schema |
| `backend/src/routes/public/shared.chat.routes.js` | New PATCH topic route |
| `backend/src/routes/internal/config.routes.js` | GET/PATCH sensitivity config |
| `backend/src/routes/internal/session.routes.js` | Filter + audit in detail |
| `backend/src/routes/internal/dashboard.routes.js` | Sensitive stats |
| `client_app/lib/features/home/home_screen.dart` | New pre-pricing step |
| `client_app/lib/features/chat/widgets/pricing_bottom_sheet.dart` | Accept + forward topic |
| `client_app/lib/core/pairing/pairing_notifier.dart` | API body |
| `client_app/lib/core/chat/chat_notifier.dart` | No-op WS handler |
| `mitra_app/lib/core/chat/chat_request_notifier.dart` | Parse topic |
| `mitra_app/lib/core/chat/widgets/chat_request_overlay.dart` | Badge + yellow accent |
| `mitra_app/lib/core/chat/mitra_chat_notifier.dart` | State + flip method + WS handler |
| `mitra_app/lib/features/chat/screens/mitra_chat_screen.dart` | Yellow doodle + app-bar toggle |
| `mitra_app/lib/features/chat/widgets/extension_request_card.dart` | Badge + accent |
| `mitra_app/lib/features/chat/screens/chat_history_screen.dart` | Badge on row + yellow transcript |
| `mitra_app/pubspec.yaml` | Register new asset |
| `control_center/src/pages/settings/SettingsPage.jsx` | Sensitivity config section |
| `control_center/src/pages/sessions/SessionsPage.jsx` | Filter + column |
| `control_center/src/pages/dashboard/DashboardPage.jsx` | Sensitive stats card |

---

## 8. Risks & Mitigations

| Risk | Mitigation |
|---|---|
| Existing sessions have no `topic_sensitivity` value after migration | Column has `DEFAULT 'regular'` + NOT NULL — backfill automatic |
| Mitra flips topic but WS delivery fails to customer | Customer silently ignores anyway; backend state is source of truth, no user-facing consequence |
| Race: mitra flips to `sensitive`, then to `regular` while latch config toggles mid-session | `flipSessionSensitivity` reads latch config at flip-time; flip is atomic in a DB transaction |
| Customer cancels topic-selection sheet → home screen flashes | Confirm sheet result is `null` and return cleanly to home — no pairing request created |
| Yellow color clashes with existing pink UI elements | Restrict yellow to chat screen background + extension card; test contrast with message bubbles and badges |
| Mitra flips flag after session is `CLOSING` | Backend rejects with 409; mitra UI gracefully shows toast "Sesi sudah berakhir" |
| Old sessions in history have no audit log entries | No migration needed — history just shows current value with no log entries |

---

## 9. Testing Checklist

**Customer flow (client_app):**
- [ ] Tap "Mulai Curhat" → topic sheet appears, cannot dismiss by tap-outside
- [ ] System back cancels entire flow
- [ ] Topik umum → pricing sheet shows, pairing request sent with `topic_sensitivity: regular`
- [ ] Topik sensitif → pricing sheet shows, pairing request sent with `topic_sensitivity: sensitive`
- [ ] Chat screen stays pink regardless of flag
- [ ] Customer receives `session_topic_updated` WS message after mitra flip → no UI change, no error

**Mitra flow (mitra_app):**
- [ ] Sensitive incoming request → yellow accent + "Topik sensitif" badge on overlay
- [ ] Regular incoming request → no badge, no color change
- [ ] Sensitive active session → yellow doodle background + header label
- [ ] Flip toggle (confirmation enabled): dialog shows, Batal cancels, Tandai flips
- [ ] Flip toggle (confirmation disabled via CC): flips instantly with toast
- [ ] One-way latch enabled + session already sensitive: toggle disabled with tooltip
- [ ] One-way latch enabled + session regular: can flip to sensitive, then toggle locks
- [ ] Mitra requests extension on sensitive session → extension card shows badge + yellow
- [ ] Mitra requests extension after mid-session flip to sensitive → extension card reflects current flag
- [ ] History list: sensitive sessions show badge
- [ ] Transcript: sensitive sessions show yellow doodle

**Control center:**
- [ ] Settings page: toggle `sensitive_flip_confirmation_enabled` → mitra app behavior changes on next flip
- [ ] Settings page: toggle `sensitive_flag_one_way_latch` → mitra app behavior changes
- [ ] Sessions page: filter "Sensitif" only shows sensitive sessions
- [ ] Session detail: audit trail shows all flips with mitra name + timestamp
- [ ] Dashboard: sensitive session count + % matches manual query
- [ ] Mitra Activity: per-mitra sensitive total / accepted / rate columns populate correctly
- [ ] Mitra with 0 sensitive requests shows `—` (not 0%) for sensitive rate

**Backend:**
- [ ] `flipSessionSensitivity` logs every call in `session_sensitivity_log`
- [ ] Ownership check: mitra A cannot flip mitra B's session
- [ ] Status check: flip on `CLOSING`/`COMPLETED` session → 409
- [ ] Latch violation returns 409 with clear error code
- [ ] No-op flip (same value) does not create log entry
