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

20 KiB

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