Phase 3 testing fixes: Fastify 5, SSE→WebSocket+FCM, enums, security, session lifecycle
- Upgrade Fastify 4→5 with all plugins (@fastify/websocket 11, cors 11, sensible 6) - Migrate all SSE endpoints to WebSocket + FCM push (mitra chat requests, customer pairing status) - Add flutter_local_notifications for foreground push notifications with sound - Add splash screen to both apps (hide auth loading flash) - Introduce constants/enums across entire codebase (no raw string literals) - Move price tiers from hardcoded array to app_config DB (data-driven, includes 1-min test tier) - Add session ownership validation on all shared chat routes - Add ownership checks on endSession, respondToExtension, requestExtension - Fix session timer: auto-complete expired/stale sessions on server restart - Add 5-min grace period for abandoned closing sessions - Fix extension flow: proper session_resumed handling, clearExtensionRequest, closure grace timer cleanup - Fix chat screens: ConnectChat in initState, session status check on connect - Fix customer expired view: 5-min countdown, closure state priority over expired state - Fix mitra extension UI: loading spinner, disable buttons, handle EXTENSION_RESOLVED error - Fix GoRouter navigation consistency (no more Navigator.pushNamed) - Fix goodbye view keyboard overflow (SingleChildScrollView) - Add active session card on customer home screen with refresh on navigate back - Fix PricingBottomSheet extension mode (RequestExtension instead of new pairing) - Send session_resumed to both parties on extension accept Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { getDb } from '../db/client.js'
|
||||
import { publish } from '../plugins/valkey.js'
|
||||
import { UserType, SessionStatus, WsMessage } from '../constants.js'
|
||||
|
||||
const sql = getDb()
|
||||
|
||||
@@ -11,7 +12,7 @@ export const getActiveSessionByCustomer = async (customerId) => {
|
||||
FROM chat_sessions cs
|
||||
LEFT JOIN mitras m ON m.id = cs.mitra_id
|
||||
WHERE cs.customer_id = ${customerId}
|
||||
AND cs.status IN ('active', 'pending_payment', 'extending', 'closing')
|
||||
AND cs.status IN (${SessionStatus.ACTIVE}, ${SessionStatus.PENDING_PAYMENT}, ${SessionStatus.EXTENDING}, ${SessionStatus.CLOSING})
|
||||
ORDER BY cs.created_at DESC LIMIT 1
|
||||
`
|
||||
return session
|
||||
@@ -25,17 +26,20 @@ export const getActiveSessionsByMitra = async (mitraId) => {
|
||||
FROM chat_sessions cs
|
||||
INNER JOIN customers c ON c.id = cs.customer_id
|
||||
WHERE cs.mitra_id = ${mitraId}
|
||||
AND cs.status IN ('active', 'pending_payment', 'extending', 'closing')
|
||||
AND cs.status IN (${SessionStatus.ACTIVE}, ${SessionStatus.PENDING_PAYMENT}, ${SessionStatus.EXTENDING}, ${SessionStatus.CLOSING})
|
||||
ORDER BY cs.created_at DESC
|
||||
`
|
||||
return sessions
|
||||
}
|
||||
|
||||
export const endSession = async (sessionId, endedBy) => {
|
||||
export const endSession = async (sessionId, endedBy, userId) => {
|
||||
// Validate session belongs to this user
|
||||
const ownerCol = endedBy === UserType.CUSTOMER ? 'customer_id' : 'mitra_id'
|
||||
const [session] = await sql`
|
||||
UPDATE chat_sessions
|
||||
SET status = 'completed', ended_at = NOW(), ended_by = ${endedBy}
|
||||
WHERE id = ${sessionId} AND status IN ('active', 'pending_payment')
|
||||
SET status = ${SessionStatus.COMPLETED}, ended_at = NOW(), ended_by = ${endedBy}
|
||||
WHERE id = ${sessionId} AND status IN (${SessionStatus.ACTIVE}, ${SessionStatus.PENDING_PAYMENT})
|
||||
AND ${sql(ownerCol)} = ${userId}
|
||||
RETURNING id, customer_id, mitra_id, status, ended_at, ended_by
|
||||
`
|
||||
|
||||
@@ -47,7 +51,7 @@ export const endSession = async (sessionId, endedBy) => {
|
||||
|
||||
// Notify both parties
|
||||
await publish(`session:${sessionId}:status`, {
|
||||
type: 'session_ended',
|
||||
type: WsMessage.SESSION_ENDED,
|
||||
session_id: sessionId,
|
||||
ended_by: endedBy,
|
||||
})
|
||||
@@ -59,7 +63,7 @@ export const rerouteSession = async (sessionId, newMitraId) => {
|
||||
// Get current session
|
||||
const [current] = await sql`
|
||||
SELECT id, customer_id, mitra_id, status FROM chat_sessions
|
||||
WHERE id = ${sessionId} AND status IN ('active', 'pending_payment')
|
||||
WHERE id = ${sessionId} AND status IN (${SessionStatus.ACTIVE}, ${SessionStatus.PENDING_PAYMENT})
|
||||
`
|
||||
|
||||
if (!current) {
|
||||
@@ -94,7 +98,7 @@ export const rerouteSession = async (sessionId, newMitraId) => {
|
||||
|
||||
// Notify customer about reroute
|
||||
await publish(`session:${sessionId}:status`, {
|
||||
type: 'rerouted',
|
||||
type: WsMessage.REROUTED,
|
||||
session_id: sessionId,
|
||||
mitra_display_name: newMitra.display_name,
|
||||
})
|
||||
@@ -102,14 +106,14 @@ export const rerouteSession = async (sessionId, newMitraId) => {
|
||||
// Notify old mitra session removed
|
||||
if (oldMitraId) {
|
||||
await publish(`mitra:${oldMitraId}:requests`, {
|
||||
type: 'session_rerouted',
|
||||
type: WsMessage.SESSION_REROUTED,
|
||||
session_id: sessionId,
|
||||
})
|
||||
}
|
||||
|
||||
// Notify new mitra about new session
|
||||
await publish(`mitra:${newMitraId}:requests`, {
|
||||
type: 'session_assigned',
|
||||
type: WsMessage.SESSION_ASSIGNED,
|
||||
session_id: sessionId,
|
||||
})
|
||||
|
||||
@@ -157,17 +161,17 @@ export const getCustomerHistory = async (customerId, { page = 1, limit = 20 } =
|
||||
SELECT cs.id, cs.mitra_id, cs.status, cs.created_at, cs.paired_at, cs.ended_at,
|
||||
cs.duration_minutes, cs.price, cs.is_free_trial, cs.extended_minutes,
|
||||
m.display_name AS mitra_display_name,
|
||||
(SELECT message FROM session_closures WHERE session_id = cs.id AND user_type = 'mitra' LIMIT 1) AS mitra_closure_message,
|
||||
(SELECT message FROM session_closures WHERE session_id = cs.id AND user_type = 'customer' LIMIT 1) AS customer_closure_message
|
||||
(SELECT message FROM session_closures WHERE session_id = cs.id AND user_type = ${UserType.MITRA} LIMIT 1) AS mitra_closure_message,
|
||||
(SELECT message FROM session_closures WHERE session_id = cs.id AND user_type = ${UserType.CUSTOMER} LIMIT 1) AS customer_closure_message
|
||||
FROM chat_sessions cs
|
||||
LEFT JOIN mitras m ON m.id = cs.mitra_id
|
||||
WHERE cs.customer_id = ${customerId}
|
||||
AND cs.status = 'completed'
|
||||
AND cs.status = ${SessionStatus.COMPLETED}
|
||||
ORDER BY cs.ended_at DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`
|
||||
const [{ count }] = await sql`
|
||||
SELECT COUNT(*) FROM chat_sessions WHERE customer_id = ${customerId} AND status = 'completed'
|
||||
SELECT COUNT(*) FROM chat_sessions WHERE customer_id = ${customerId} AND status = ${SessionStatus.COMPLETED}
|
||||
`
|
||||
return { items, total: Number(count), page, limit }
|
||||
}
|
||||
@@ -178,17 +182,17 @@ export const getMitraHistory = async (mitraId, { page = 1, limit = 20 } = {}) =>
|
||||
SELECT cs.id, cs.customer_id, cs.status, cs.created_at, cs.paired_at, cs.ended_at,
|
||||
cs.duration_minutes, cs.price, cs.is_free_trial, cs.extended_minutes,
|
||||
c.display_name AS customer_display_name,
|
||||
(SELECT message FROM session_closures WHERE session_id = cs.id AND user_type = 'mitra' LIMIT 1) AS mitra_closure_message,
|
||||
(SELECT message FROM session_closures WHERE session_id = cs.id AND user_type = 'customer' LIMIT 1) AS customer_closure_message
|
||||
(SELECT message FROM session_closures WHERE session_id = cs.id AND user_type = ${UserType.MITRA} LIMIT 1) AS mitra_closure_message,
|
||||
(SELECT message FROM session_closures WHERE session_id = cs.id AND user_type = ${UserType.CUSTOMER} LIMIT 1) AS customer_closure_message
|
||||
FROM chat_sessions cs
|
||||
INNER JOIN customers c ON c.id = cs.customer_id
|
||||
WHERE cs.mitra_id = ${mitraId}
|
||||
AND cs.status = 'completed'
|
||||
AND cs.status = ${SessionStatus.COMPLETED}
|
||||
ORDER BY cs.ended_at DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`
|
||||
const [{ count }] = await sql`
|
||||
SELECT COUNT(*) FROM chat_sessions WHERE mitra_id = ${mitraId} AND status = 'completed'
|
||||
SELECT COUNT(*) FROM chat_sessions WHERE mitra_id = ${mitraId} AND status = ${SessionStatus.COMPLETED}
|
||||
`
|
||||
return { items, total: Number(count), page, limit }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user