# 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