Extension request: WS→FCM fallback + chat-recovery on connect
Today the customer's "Perpanjang" only reaches the mitra via session- scoped WS. If the mitra is on Home/Undangan, in a different session, or backgrounded, the WS send no-ops and the 10s safeguard timeout fires auto-reject (or auto-approve if the mitra happens to also have an active general WS, depending on config) — either way the mitra never saw the request. Backend: - extension.service.js::requestExtension now falls back to FCM via notification.service when the mitra isn't on the session WS. Mirrors the pairing notifyMitra pattern (Curhat Baru). Customer display name is pulled into the session lookup for the FCM body. - shared.chat.routes.js: /chat/:sessionId/info now returns pending_extension (extension_id, duration_minutes, price, requested_at, expires_at, timeout_seconds) so the chat screen can rehydrate the accept/reject UI after a cold-start FCM tap. expires_at is derived from requested_at + extension_timeout_seconds config. Mitra app: - mitra_chat_notifier.dart::connect parses pending_extension from /info and seeds MitraChatConnectedData.extensionRequest — the existing _buildExtensionView renders unchanged. - notification_service.dart::_navigateFromMessage handles type=extension_request → pushes /chat/session/<id>. Composes with the new /info pending_extension to bring the mitra straight into the accept/reject view. Verified end-to-end on dev backend (FCM call returned sent=true; /info returns pending_extension when within timeout window). Visual delivery on emulator-5556 deferred — API 24 AVD queues FCM 5-30 min per feedback-emulator-avd-versions. Out of scope (follow-ups): - Customer-side FCM for EXTENSION_RESPONSE (accepted/rejected/timeout) - Perpanjang tab list endpoint + Flutter provider + UI Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,35 @@ export const sharedChatRoutes = async (app) => {
|
||||
return reply.code(404).send({ success: false, error: { code: 'NOT_FOUND', message: 'Session not found' } })
|
||||
}
|
||||
const goodbyeSubmittedByMe = await hasUserSubmittedClosure(sessionId, request.userType)
|
||||
return reply.send({ success: true, data: { ...session, goodbye_submitted_by_me: goodbyeSubmittedByMe } })
|
||||
// Surface any pending extension so the mitra chat screen can recover the
|
||||
// _buildExtensionView state after a cold-start via FCM tap — without this,
|
||||
// the WS EXTENSION_REQUEST frame fired earlier has nothing to bind to.
|
||||
const [pendingExt] = await sql`
|
||||
SELECT id, requested_duration_minutes, requested_price, requested_at
|
||||
FROM session_extensions
|
||||
WHERE session_id = ${sessionId} AND status = 'pending'
|
||||
ORDER BY requested_at DESC
|
||||
LIMIT 1
|
||||
`
|
||||
let pending_extension = null
|
||||
if (pendingExt) {
|
||||
const { getExtensionTimeoutConfig } = await import('../../services/config.service.js')
|
||||
const { extension_timeout_seconds } = await getExtensionTimeoutConfig()
|
||||
const requestedAtMs = new Date(pendingExt.requested_at).getTime()
|
||||
const expiresAtMs = requestedAtMs + extension_timeout_seconds * 1000
|
||||
pending_extension = {
|
||||
extension_id: pendingExt.id,
|
||||
duration_minutes: pendingExt.requested_duration_minutes,
|
||||
price: pendingExt.requested_price,
|
||||
requested_at: pendingExt.requested_at,
|
||||
expires_at: new Date(expiresAtMs).toISOString(),
|
||||
timeout_seconds: extension_timeout_seconds,
|
||||
}
|
||||
}
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: { ...session, goodbye_submitted_by_me: goodbyeSubmittedByMe, pending_extension },
|
||||
})
|
||||
})
|
||||
|
||||
// Get full transcript (read-only, for history)
|
||||
|
||||
Reference in New Issue
Block a user