Files
halobestie-clone/requirement/phase3-plan.md
ramadhan sjamsani b4efcf14c2 Phase 3 scaffold: chat engine (WebSocket, FCM, pricing, timer, extension, history)
- Backend: WebSocket plugin, chat/pricing/timer/extension/closure/notification services
- Client app: ChatBloc, pricing dialog, chat screen with message status, extension/goodbye flow, history
- Mitra app: MitraChatBloc, ExtensionBloc, chat screen, extension accept/reject, history
- Control center: free trial, extension timeout, early end config toggles
- DB migration: chat_messages, session_closures, session_extensions, customer_transactions tables

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:58:11 +08:00

476 lines
20 KiB
Markdown

# Phase 3 Implementation Plan: Chat Engine
## Summary of Clarified Requirements
| Topic | Decision |
|---|---|
| Chat opening | Time/price selection dialog before pairing |
| Price tiers (mock) | 15min/30k, 30min/60k, 45min/100k, 60min/150k, 24jam/250k |
| Free trial | One-time per customer; enabled/disabled globally via Control Center |
| Free trial duration | Single global config value (minutes) in Control Center |
| Payment timing | After pairing starts, but mocked in this phase |
| Chat transport | WebSocket (real-time) + FCM (background push notifications) |
| Message types | Text-only now; schema supports image/voice/video later |
| Emoji | Works natively (unicode text), no special handling needed |
| Message status | Sent (server ack), Delivered (client ack), Read (client opened) |
| Typing indicator | 3-second timeout, throttled (send at most once per 2-3s) |
| Chat history storage | Backend API as source of truth; no local cache |
| Session timer | Backend-authoritative (server-side countdown) |
| Timer warning | Show remaining time at 1 minute left |
| Extension timeout | 1 minute for customer to decide + mitra to confirm (configurable) |
| Chat during extension | Paused — no messages until extension confirmed or rejected |
| Early end | Mechanism built, disabled by default, configurable per role |
| Closing message | Free-text goodbye message from both parties |
| Chat history view | Full read-only transcript, kept forever |
| Deletion requests | Deferred to later phase |
| Sessions per customer | One active session at a time |
| WSS termination | Cloud Run handles TLS; backend uses plain `ws://` |
| Control center transcripts | Viewable by certain roles only (deferred to later phase) |
| Notification permission | Request on first app launch |
---
## 1. Database Changes
### 1.1 New table: `chat_messages`
Stores all chat messages.
| Column | Type | Notes |
|---|---|---|
| `id` | `SERIAL PRIMARY KEY` | |
| `session_id` | `INT REFERENCES chat_sessions(id)` | |
| `sender_type` | `VARCHAR` | `'customer'` or `'mitra'` |
| `sender_id` | `INT` | customer or mitra ID |
| `type` | `VARCHAR DEFAULT 'text'` | `'text'` now; `'image'`, `'voice'`, `'video'` later |
| `content` | `TEXT` | Message text (or file URL for future media) |
| `metadata` | `JSONB` | Nullable; for future media (file size, duration, thumbnail) |
| `status` | `VARCHAR DEFAULT 'sent'` | `'sent'`, `'delivered'`, `'read'` |
| `delivered_at` | `TIMESTAMPTZ` | When recipient's client acknowledged |
| `read_at` | `TIMESTAMPTZ` | When recipient opened/read the message |
| `created_at` | `TIMESTAMPTZ DEFAULT now()` | |
**Indexes:**
- `(session_id, created_at)` — fetch messages in chronological order
- `(session_id, status)` — query undelivered/unread messages
### 1.2 New table: `session_closures`
Stores goodbye messages when a session ends.
| Column | Type | Notes |
|---|---|---|
| `id` | `SERIAL PRIMARY KEY` | |
| `session_id` | `INT REFERENCES chat_sessions(id)` | |
| `user_type` | `VARCHAR` | `'customer'` or `'mitra'` |
| `user_id` | `INT` | customer or mitra ID |
| `message` | `TEXT` | Free-text goodbye message |
| `created_at` | `TIMESTAMPTZ DEFAULT now()` | |
### 1.3 New table: `session_extensions`
Tracks extension requests and their outcomes.
| Column | Type | Notes |
|---|---|---|
| `id` | `SERIAL PRIMARY KEY` | |
| `session_id` | `INT REFERENCES chat_sessions(id)` | |
| `requested_duration_minutes` | `INT` | Duration customer selected |
| `requested_price` | `INT` | Mock price (in IDR) |
| `status` | `VARCHAR` | `'pending'`, `'accepted'`, `'rejected'`, `'timeout'` |
| `requested_at` | `TIMESTAMPTZ DEFAULT now()` | |
| `responded_at` | `TIMESTAMPTZ` | |
### 1.4 Alter table: `chat_sessions`
Add columns to support timed sessions:
| Column | Type | Notes |
|---|---|---|
| `duration_minutes` | `INT` | Selected duration (15/30/45/60/1440) |
| `price` | `INT` | Mock price in IDR (0 for free trial) |
| `is_free_trial` | `BOOLEAN DEFAULT false` | |
| `expires_at` | `TIMESTAMPTZ` | Computed: `paired_at + duration_minutes` |
| `extended_minutes` | `INT DEFAULT 0` | Total extended time |
Add new session statuses:
- `extending` — customer requested extension, waiting for mitra confirmation
- `closing` — session ended, waiting for goodbye messages
### 1.5 New table: `customer_transactions`
Tracks whether a customer has had any transaction (for free trial eligibility).
| Column | Type | Notes |
|---|---|---|
| `id` | `SERIAL PRIMARY KEY` | |
| `customer_id` | `INT REFERENCES customers(id)` | |
| `session_id` | `INT REFERENCES chat_sessions(id)` | |
| `type` | `VARCHAR` | `'free_trial'`, `'paid'`, `'extension'` |
| `amount` | `INT` | 0 for free trial |
| `created_at` | `TIMESTAMPTZ DEFAULT now()` | |
### 1.6 Extend `app_config`
New config keys:
| Key | Value (JSONB) | Purpose |
|---|---|---|
| `free_trial_enabled` | `{ "value": true }` | Enable/disable free trial globally |
| `free_trial_duration_minutes` | `{ "value": 5 }` | Free trial session duration |
| `extension_timeout_seconds` | `{ "value": 60 }` | Time limit for extension negotiation |
| `early_end_mitra_enabled` | `{ "value": false }` | Allow mitra to end session early |
| `early_end_customer_enabled` | `{ "value": false }` | Allow customer to end session early |
---
## 2. Backend Changes
### 2.1 WebSocket Setup
- Add `@fastify/websocket` plugin (`src/plugins/websocket.js`)
- Single WebSocket endpoint: `GET /api/shared/ws`
- Connection authenticated via Firebase token (sent as query param or first message)
- After auth, server identifies user as customer or mitra and joins them to their session channel
- Valkey pub/sub remains the backend message bus; WebSocket is the client-facing transport
- Architecture: `Client ↔ WebSocket ↔ Backend ↔ Valkey pub/sub ↔ Backend ↔ WebSocket ↔ Other client`
**WebSocket message types (JSON):**
| Type | Direction | Purpose |
|---|---|---|
| `auth` | Client → Server | Authenticate with Firebase token |
| `auth_ok` | Server → Client | Authentication successful |
| `message` | Client → Server | Send a chat message |
| `message` | Server → Client | Receive a chat message |
| `message_status` | Server → Client | Delivery/read status update |
| `message_ack` | Server → Client | Server acknowledges sent message (sent status) |
| `typing` | Client → Server | User is typing |
| `typing` | Server → Client | Other user is typing |
| `session_timer` | Server → Client | Timer warning (1 min left) |
| `session_expired` | Server → Client | Session time is up |
| `extension_request` | Server → Client | Extension request notification (to mitra) |
| `extension_response` | Server → Client | Extension accepted/rejected (to customer) |
| `session_paused` | Server → Client | Chat paused during extension negotiation |
| `session_resumed` | Server → Client | Chat resumed after extension accepted |
| `session_closing` | Server → Client | Session ending, prompt for goodbye message |
| `early_end` | Client → Server | Request to end session early |
| `delivered` | Client → Server | Client acknowledges message delivery |
| `read` | Client → Server | Client marks messages as read |
### 2.2 FCM Push Notification Setup
- Use existing `firebase-admin` plugin for sending push notifications
- Store FCM device token per user (new columns on `customers` and `mitras` tables)
- Send push notification when recipient's WebSocket is not connected
- FCM payload includes: message preview, session ID, sender name
- On notification tap: deep-link to specific chat screen
New columns:
| Table | Column | Type |
|---|---|---|
| `customers` | `fcm_token` | `VARCHAR` |
| `mitras` | `fcm_token` | `VARCHAR` |
New endpoint:
| Method | Path | Purpose |
|---|---|---|
| `POST` | `/api/shared/device-token` | Register/update FCM device token |
### 2.3 New Public Routes — Chat Opening
| Method | Path | Purpose |
|---|---|---|
| `GET` | `/api/client/chat/pricing` | Get mock price tiers + free trial eligibility |
| `POST` | `/api/client/chat/request` | Start pairing with selected duration/price (updated from Phase 2) |
### 2.4 New Public Routes — Chat Messages
| Method | Path | Purpose |
|---|---|---|
| `GET` | `/api/shared/chat/:sessionId/messages` | Fetch message history (paginated, for reconnect) |
| `GET` | `/api/shared/chat/:sessionId/info` | Get session info (timer, status, participants) |
### 2.5 New Public Routes — Session Closure
| Method | Path | Purpose |
|---|---|---|
| `POST` | `/api/client/sessions/:sessionId/extend` | Customer requests extension with selected duration |
| `POST` | `/api/mitra/sessions/:sessionId/extend-response` | Mitra accepts/rejects extension |
| `POST` | `/api/shared/sessions/:sessionId/close-message` | Submit goodbye message |
### 2.6 New Public Routes — Chat History
| Method | Path | Purpose |
|---|---|---|
| `GET` | `/api/client/chat/history` | List past sessions (with mitra name, closure messages) |
| `GET` | `/api/mitra/chat/history` | List past sessions (with customer name, closure messages) |
| `GET` | `/api/shared/chat/:sessionId/transcript` | Full read-only chat transcript |
### 2.7 New Internal Routes — Control Center
| Method | Path | Purpose |
|---|---|---|
| `GET` | `/internal/config/free-trial` | Get free trial config |
| `PATCH` | `/internal/config/free-trial` | Update free trial enabled + duration |
| `GET` | `/internal/config/extension-timeout` | Get extension timeout config |
| `PATCH` | `/internal/config/extension-timeout` | Update extension timeout |
| `GET` | `/internal/config/early-end` | Get early end config |
| `PATCH` | `/internal/config/early-end` | Update early end per role |
### 2.8 New Services
| Service | Responsibilities |
|---|---|
| `chat.service.js` | Send message, update delivery/read status, fetch message history, typing event relay |
| `session-timer.service.js` | Backend-authoritative countdown, expiry check, 1-min warning trigger, extension timeout |
| `extension.service.js` | Extension request/response flow, session pause/resume, timeout handling |
| `closure.service.js` | Goodbye message submission, session completion, transaction recording |
| `notification.service.js` | FCM push notification sending, device token management, online/offline detection |
| `pricing.service.js` | Mock price tiers, free trial eligibility check |
### 2.9 Chat Flow (Backend Detail)
```
Chat Opening:
1. Customer opens "Mulai Curhat"
2. Backend returns pricing tiers + free trial eligibility
- Free trial shown if: free_trial_enabled AND customer has 0 records in customer_transactions
3. Customer selects duration/price (or free trial)
4. POST /api/client/chat/request with { duration_minutes, price, is_free_trial }
5. Backend creates chat_session with duration_minutes, price, is_free_trial
6. Existing Phase 2 pairing flow proceeds (blast, accept, etc.)
7. On successful pairing:
- Set expires_at = now() + duration_minutes
- Create customer_transactions record
- Start server-side timer
Chat Messaging:
1. Both parties connect via WebSocket after pairing
2. Client sends { type: 'message', content: '...' }
3. Backend saves to chat_messages (status: 'sent'), publishes via Valkey
4. Backend sends message_ack to sender (sent ✓)
5. Recipient's WebSocket receives message
6. Recipient sends { type: 'delivered' } → backend updates status, notifies sender (delivered ✓✓)
7. Recipient views message → sends { type: 'read' } → backend updates status (read ✓✓ blue)
8. If recipient offline → backend sends FCM push notification instead
Session Expiry:
1. Backend timer fires 1 min before expires_at
2. Send session_timer to both clients via WebSocket
3. Both apps show countdown timer
4. At expires_at, backend sends session_expired
5. Customer gets extend/close dialog
6. If extend: POST /api/client/sessions/:id/extend
- Session status → extending, chat paused
- Mitra gets extension_request via WebSocket
- Mitra accepts/rejects within timeout (default 60s)
- If accept: extend expires_at, resume chat
- If reject or timeout: proceed to closure
7. If close (or after rejected extension):
- Session status → closing
- Both parties submit goodbye message
- Session status → completed
Early End (when enabled):
1. User sends { type: 'early_end' } via WebSocket
2. Backend checks if early end is enabled for that role
3. If enabled: skip to closure flow (step 7 above)
4. If disabled: reject with error
```
### 2.10 Typing Indicator (Backend Detail)
```
1. Client sends { type: 'typing' } via WebSocket
2. Backend relays to other party via Valkey pub/sub → WebSocket
3. Receiving client shows typing indicator
4. Receiving client auto-hides after 3 seconds of no new typing event
5. Sending client throttles: at most one typing event per 2 seconds
```
---
## 3. Client App Changes
### 3.1 New BLoC: `ChatBloc`
Manages active chat messaging.
**Events:** `ConnectWebSocket`, `DisconnectWebSocket`, `SendMessage`, `MessageReceived`, `MessageStatusUpdate`, `TypingStarted`, `TypingStopped`, `SessionTimerWarning`, `SessionExpired`
**States:** `ChatInitial`, `ChatConnecting`, `ChatConnected(messages)`, `ChatTimerWarning(remaining)`, `ChatSessionExpired`, `ChatError`
- On `ConnectWebSocket` → authenticate, load message history from API, listen for incoming messages
- On `SendMessage` → send via WebSocket, add to local message list with "sending" state
- On `MessageReceived` → add to list, send delivery acknowledgment
- On `MessageStatusUpdate` → update message status (sent → delivered → read)
### 3.2 New BLoC: `ChatOpeningBloc`
Manages pricing selection and free trial.
**Events:** `LoadPricing`, `SelectTier`, `SelectFreeTrial`, `ConfirmSelection`
**States:** `PricingLoading`, `PricingLoaded(tiers, freeTrialEligible)`, `TierSelected(tier)`, `PricingError`
- On `LoadPricing` → call `/api/client/chat/pricing`
- On `ConfirmSelection` → trigger existing `PairingBloc.RequestPairing` with duration/price
### 3.3 New BLoC: `SessionClosureBloc`
Manages extension and goodbye flow.
**Events:** `SessionExpired`, `RequestExtension`, `ExtensionResult`, `SubmitGoodbye`
**States:** `ClosureInitial`, `ShowExtendDialog`, `ExtendingWaitingMitra`, `ExtensionAccepted`, `ExtensionRejected`, `ShowGoodbyeInput`, `ClosureComplete`
### 3.4 Screen Changes
| Screen | Changes |
|---|---|
| Home screen | "Mulai Curhat" opens pricing dialog instead of directly pairing |
| New: Pricing dialog | Bottom sheet with 5 price tiers + free trial option (if eligible) |
| New: Chat screen | Full chat UI: message list, text input, send button, typing indicator |
| Chat screen | Message bubbles with status icons (✓ sent, ✓✓ delivered, ✓✓ blue read) |
| Chat screen | Countdown timer overlay at 1 minute remaining |
| New: Extension dialog | "Extend session?" with price tier selection |
| New: Waiting mitra dialog | "Menunggu konfirmasi Bestie..." with timeout |
| New: Goodbye screen | Free-text input for closing message |
| New: Chat history list | List of past sessions (bestie name, date, goodbye message) |
| New: Chat transcript screen | Read-only scrollable chat history |
### 3.5 Navigation Updates
New routes in GoRouter:
- `/chat/pricing` — pricing selection dialog/screen
- `/chat/session/:sessionId` — active chat screen (updated from Phase 2)
- `/chat/history` — chat history list
- `/chat/history/:sessionId` — read-only transcript
### 3.6 FCM Setup
- Request notification permission on first app launch
- Register device token via `POST /api/shared/device-token`
- Handle incoming notifications: tap → navigate to specific chat screen
- Update token on app launch (tokens can rotate)
---
## 4. Mitra App Changes
### 4.1 New BLoC: `ChatBloc`
Same as client app — manages active chat messaging. Shared message type structure.
**Events/States:** Same as client app ChatBloc.
### 4.2 New BLoC: `ExtensionBloc`
Handles incoming extension requests from customers.
**Events:** `ExtensionReceived`, `AcceptExtension`, `RejectExtension`, `ExtensionTimeout`
**States:** `ExtensionIdle`, `ExtensionPending(duration, price)`, `ExtensionAccepted`, `ExtensionRejected`
### 4.3 Screen Changes
| Screen | Changes |
|---|---|
| Home screen | Active sessions list → tap to open chat |
| New: Chat screen | Full chat UI (same as client app) |
| Chat screen | Extension request overlay when customer requests extension |
| New: Extension dialog | "Customer ingin perpanjang X menit" with Accept/Reject buttons |
| New: Goodbye screen | Free-text input for closing message |
| New: Chat history list | List of past sessions (customer name, date, goodbye message) |
| New: Chat transcript screen | Read-only scrollable chat history |
### 4.4 Navigation Updates
New routes in GoRouter:
- `/chat/session/:sessionId` — active chat screen
- `/chat/history` — chat history list
- `/chat/history/:sessionId` — read-only transcript
### 4.5 FCM Setup
- Same as client app: request permission on launch, register token
- Push notification on incoming message when app is backgrounded
- Tap notification → navigate directly to specific customer's chat screen (not just chat list)
---
## 5. Control Center Changes
### 5.1 Updated Pages
| Page | Changes |
|---|---|
| Settings page | Add: free trial toggle + duration, extension timeout, early end toggles (mitra/customer) |
| Session detail page | Add: view chat transcript link (role-restricted, deferred) |
### 5.2 No New Pages This Phase
Chat transcript viewing for admins is deferred. Config controls are added to the existing Settings page.
---
## 6. Implementation Order
| Step | What | Apps affected |
|---|---|---|
| 1 | Database migration (new tables, altered columns, new config keys) | Backend |
| 2 | WebSocket plugin setup (`@fastify/websocket`) | Backend |
| 3 | Pricing service + free trial eligibility + chat opening API | Backend |
| 4 | Chat message service + WebSocket message handling | Backend |
| 5 | Message delivery/read status tracking | Backend |
| 6 | Session timer service (backend-authoritative countdown) | Backend |
| 7 | Extension service (request/response/timeout) | Backend |
| 8 | Closure service (goodbye messages, session completion) | Backend |
| 9 | FCM notification service + device token endpoint | Backend |
| 10 | Client app: FCM setup + pricing dialog + ChatOpeningBloc | Client app |
| 11 | Client app: ChatBloc + chat screen + message status UI | Client app |
| 12 | Client app: timer warning + extension dialog + SessionClosureBloc | Client app |
| 13 | Client app: goodbye screen + chat history screens | Client app |
| 14 | Mitra app: FCM setup + ChatBloc + chat screen | Mitra app |
| 15 | Mitra app: ExtensionBloc + extension dialog | Mitra app |
| 16 | Mitra app: goodbye screen + chat history screens | Mitra app |
| 17 | Control center: settings page updates (free trial, extension, early end) | Control center |
| 18 | Typing indicator (WebSocket relay, throttle, 3s timeout) | Backend + both apps |
---
## 7. New Dependencies
| App | Package | Purpose |
|---|---|---|
| Backend | `@fastify/websocket` | WebSocket support (built on `ws`) |
| Backend | `firebase-admin` (existing) | FCM push notifications |
| Client app | `web_socket_channel` | WebSocket client |
| Client app | `firebase_messaging` | FCM push notifications |
| Mitra app | `web_socket_channel` | WebSocket client |
| Mitra app | `firebase_messaging` | FCM push notifications |
---
## 8. Notes for Future Phases
**Media messages (image, voice clip, video clip):**
- Add file upload endpoint (Cloud Storage)
- Add new `type` values to `chat_messages`
- Add media bubble widgets in Flutter
- No architectural changes — same WebSocket transport, same delivery status, same history
**Chat transcript for Control Center:**
- Add role-based access check
- Reuse existing `/api/shared/chat/:sessionId/transcript` endpoint on internal routes
**Deletion requests:**
- Add customer request flow + admin approval
- Soft-delete or anonymize messages in `chat_messages`
**Payment integration (Xendit):**
- Replace mock pricing with real payment flow
- Integrate at the `pending_payment` status transition point
- Extension payments follow same flow