import { getDb } from '../db/client.js' import { publish } from '../plugins/valkey.js' import { clearSessionTimer, clearClosureGraceTimer } from './session-timer.service.js' import { sendToSessionParticipant } from '../plugins/websocket.js' import { sendPushNotification } from './notification.service.js' import { UserType, SessionStatus, EndedBy, WsMessage } from '../constants.js' const sql = getDb() export const submitClosureMessage = async (sessionId, userType, userId, message) => { // Verify session is in closing or active state (for early end) const [session] = await sql` SELECT id, status FROM chat_sessions WHERE id = ${sessionId} AND status IN (${SessionStatus.CLOSING}, ${SessionStatus.ACTIVE}, ${SessionStatus.EXTENDING}) ` if (!session) { throw Object.assign(new Error('Session not found or already completed'), { code: 'SESSION_NOT_ACTIVE', statusCode: 409, }) } // Save closure message const [closure] = await sql` INSERT INTO session_closures (session_id, user_type, user_id, message) VALUES (${sessionId}, ${userType}, ${userId}, ${message}) ON CONFLICT DO NOTHING RETURNING id, session_id, user_type, message, created_at ` // Check if both parties have submitted const closures = await sql` SELECT user_type FROM session_closures WHERE session_id = ${sessionId} ` const hasCustomer = closures.some((c) => c.user_type === UserType.CUSTOMER) const hasMitra = closures.some((c) => c.user_type === UserType.MITRA) if (hasCustomer && hasMitra) { // Both submitted — complete the session await completeSession(sessionId) } return closure } export const completeSession = async (sessionId) => { clearSessionTimer(sessionId) clearClosureGraceTimer(sessionId) const [session] = await sql` UPDATE chat_sessions SET status = ${SessionStatus.COMPLETED}, ended_at = NOW(), ended_by = ${EndedBy.SYSTEM} WHERE id = ${sessionId} AND status IN (${SessionStatus.CLOSING}, ${SessionStatus.ACTIVE}, ${SessionStatus.EXTENDING}) RETURNING id, customer_id, mitra_id, status, ended_at ` if (!session) return null // Notify both parties, FCM fallback if WebSocket is down const data = { type: WsMessage.SESSION_COMPLETED, session_id: sessionId } const customerSent = sendToSessionParticipant(sessionId, UserType.CUSTOMER, data) const mitraSent = sendToSessionParticipant(sessionId, UserType.MITRA, data) if (!customerSent) { await sendPushNotification(UserType.CUSTOMER, session.customer_id, { title: 'Sesi Selesai', body: 'Sesi curhat kamu telah selesai.', data: { type: WsMessage.SESSION_COMPLETED, session_id: sessionId }, }) } if (!mitraSent) { await sendPushNotification(UserType.MITRA, session.mitra_id, { title: 'Sesi Selesai', body: 'Sesi curhat telah selesai.', data: { type: WsMessage.SESSION_COMPLETED, session_id: sessionId }, }) } await publish(`session:${sessionId}:status`, { type: WsMessage.SESSION_ENDED, session_id: sessionId }) return session } export const initiateEarlyEnd = async (sessionId, userType) => { // Check if early end is enabled for this user type const configKey = userType === UserType.MITRA ? 'early_end_mitra_enabled' : 'early_end_customer_enabled' const [configRow] = await sql`SELECT value FROM app_config WHERE key = ${configKey}` const enabled = configRow?.value?.value ?? false if (!enabled) { throw Object.assign(new Error('Early end is not enabled'), { code: 'EARLY_END_DISABLED', statusCode: 403, }) } // Move session to closing const [session] = await sql` UPDATE chat_sessions SET status = ${SessionStatus.CLOSING}, ended_by = ${userType} WHERE id = ${sessionId} AND status = ${SessionStatus.ACTIVE} RETURNING id, customer_id, mitra_id ` if (!session) { throw Object.assign(new Error('Session not active'), { code: 'SESSION_NOT_ACTIVE', statusCode: 409, }) } clearSessionTimer(sessionId) // Notify both parties to enter closure flow, FCM fallback if WebSocket is down const data = { type: WsMessage.SESSION_CLOSING, session_id: sessionId, ended_by: userType } const customerSent = sendToSessionParticipant(sessionId, UserType.CUSTOMER, data) const mitraSent = sendToSessionParticipant(sessionId, UserType.MITRA, data) if (!customerSent) { await sendPushNotification(UserType.CUSTOMER, session.customer_id, { title: 'Sesi Berakhir', body: 'Sesi curhat kamu telah berakhir. Ketuk untuk menulis pesan penutup.', data: { type: WsMessage.SESSION_CLOSING, session_id: sessionId }, }) } if (!mitraSent) { await sendPushNotification(UserType.MITRA, session.mitra_id, { title: 'Sesi Berakhir', body: 'Sesi curhat telah berakhir. Ketuk untuk menulis pesan penutup.', data: { type: WsMessage.SESSION_CLOSING, session_id: sessionId }, }) } return session } export const getSessionClosures = async (sessionId) => { return sql` SELECT user_type, message, created_at FROM session_closures WHERE session_id = ${sessionId} ORDER BY created_at ASC ` }