Phase 3.1 WS2: Backend FCM fallback, ping config, unread API

- Add require_mitra_ping + mitra_ping_interval_seconds config keys (migration)
- Add getMitraPingConfig/setMitraPingConfig to config service
- Add GET/PATCH /internal/config/mitra-ping routes for control center
- Update mitra status service: honor ping config in auto-offline sweep,
  include ping config in GET /api/mitra/status response
- Enhance pairing FCM payload with action: 'open_accept' for deep-link
- Add FCM fallback to closure.service (initiateEarlyEnd, completeSession)
- Add FCM fallback to session-timer.service (onSessionExpired)
- Add unread count queries (getActiveSessionByCustomerWithUnread,
  getActiveSessionsByMitraWithUnread)
- Add GET /api/client/chat/session/active-with-unread route
- Add GET /api/mitra/chat-requests/sessions/active-with-unread route

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 14:22:41 +08:00
parent fa8c963d92
commit ed765d230c
11 changed files with 187 additions and 17 deletions

View File

@@ -1,6 +1,7 @@
import { getDb } from '../db/client.js'
import { publish } from '../plugins/valkey.js'
import { sendToSessionParticipant } from '../plugins/websocket.js'
import { sendPushNotification } from './notification.service.js'
import { UserType, SessionStatus, WsMessage } from '../constants.js'
const sql = getDb()
@@ -85,15 +86,29 @@ const onSessionExpired = async (sessionId) => {
`
if (!session) return
// Notify customer — sees extend/close dialog
// Notify customer — sees extend/close dialog; FCM fallback if WebSocket is down
const expiredData = { type: WsMessage.SESSION_EXPIRED, session_id: sessionId }
sendToSessionParticipant(sessionId, UserType.CUSTOMER, expiredData)
const customerSent = sendToSessionParticipant(sessionId, UserType.CUSTOMER, expiredData)
if (!customerSent) {
await sendPushNotification(UserType.CUSTOMER, session.customer_id, {
title: 'Waktu Sesi Habis',
body: 'Sesi curhat kamu telah habis. Ketuk untuk memperpanjang atau mengakhiri.',
data: { type: WsMessage.SESSION_CLOSING, session_id: sessionId },
})
}
// Notify mitra — sees expired + closing (waits for customer's decision or goodbye)
sendToSessionParticipant(sessionId, UserType.MITRA, expiredData)
const mitraSent = sendToSessionParticipant(sessionId, UserType.MITRA, expiredData)
sendToSessionParticipant(sessionId, UserType.MITRA, {
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 },
})
}
// Also publish via Valkey for any listeners
await publish(`session:${sessionId}:status`, expiredData)