Phase 2 scaffold: mitra online status & pairing logic
Add mitra online/offline status with heartbeat-based auto-offline, customer-mitra pairing via Valkey pub/sub blast, session management, and control center dashboard with real-time stats. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
149
backend/src/services/session.service.js
Normal file
149
backend/src/services/session.service.js
Normal file
@@ -0,0 +1,149 @@
|
||||
import { getDb } from '../db/client.js'
|
||||
import { publish } from '../plugins/valkey.js'
|
||||
|
||||
const sql = getDb()
|
||||
|
||||
export const getActiveSessionByCustomer = async (customerId) => {
|
||||
const [session] = await sql`
|
||||
SELECT cs.id, cs.customer_id, cs.mitra_id, cs.status, cs.created_at, cs.paired_at,
|
||||
m.display_name AS mitra_display_name
|
||||
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')
|
||||
ORDER BY cs.created_at DESC LIMIT 1
|
||||
`
|
||||
return session
|
||||
}
|
||||
|
||||
export const getActiveSessionsByMitra = async (mitraId) => {
|
||||
const sessions = await sql`
|
||||
SELECT cs.id, cs.customer_id, cs.status, cs.created_at, cs.paired_at,
|
||||
c.display_name AS customer_display_name
|
||||
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')
|
||||
ORDER BY cs.created_at DESC
|
||||
`
|
||||
return sessions
|
||||
}
|
||||
|
||||
export const endSession = async (sessionId, endedBy) => {
|
||||
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')
|
||||
RETURNING id, customer_id, mitra_id, status, ended_at, ended_by
|
||||
`
|
||||
|
||||
if (!session) {
|
||||
throw Object.assign(new Error('Session not found or already ended'), {
|
||||
code: 'SESSION_NOT_ACTIVE', statusCode: 409,
|
||||
})
|
||||
}
|
||||
|
||||
// Notify both parties
|
||||
await publish(`session:${sessionId}:status`, {
|
||||
type: 'session_ended',
|
||||
session_id: sessionId,
|
||||
ended_by: endedBy,
|
||||
})
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
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')
|
||||
`
|
||||
|
||||
if (!current) {
|
||||
throw Object.assign(new Error('Session not found or not active'), {
|
||||
code: 'SESSION_NOT_ACTIVE', statusCode: 409,
|
||||
})
|
||||
}
|
||||
|
||||
// Verify new mitra is online
|
||||
const [newMitraStatus] = await sql`
|
||||
SELECT is_online FROM mitra_online_status WHERE mitra_id = ${newMitraId}
|
||||
`
|
||||
if (!newMitraStatus?.is_online) {
|
||||
throw Object.assign(new Error('Target mitra is not online'), {
|
||||
code: 'MITRA_NOT_ONLINE', statusCode: 422,
|
||||
})
|
||||
}
|
||||
|
||||
const oldMitraId = current.mitra_id
|
||||
|
||||
// Update session with new mitra (forced assignment)
|
||||
const [session] = await sql`
|
||||
UPDATE chat_sessions
|
||||
SET mitra_id = ${newMitraId}
|
||||
WHERE id = ${sessionId}
|
||||
RETURNING id, customer_id, mitra_id, status
|
||||
`
|
||||
|
||||
const [newMitra] = await sql`
|
||||
SELECT display_name FROM mitras WHERE id = ${newMitraId}
|
||||
`
|
||||
|
||||
// Notify customer about reroute
|
||||
await publish(`session:${sessionId}:status`, {
|
||||
type: 'rerouted',
|
||||
session_id: sessionId,
|
||||
mitra_display_name: newMitra.display_name,
|
||||
})
|
||||
|
||||
// Notify old mitra session removed
|
||||
if (oldMitraId) {
|
||||
await publish(`mitra:${oldMitraId}:requests`, {
|
||||
type: 'session_rerouted',
|
||||
session_id: sessionId,
|
||||
})
|
||||
}
|
||||
|
||||
// Notify new mitra about new session
|
||||
await publish(`mitra:${newMitraId}:requests`, {
|
||||
type: 'session_assigned',
|
||||
session_id: sessionId,
|
||||
})
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
export const listSessions = async ({ page = 1, limit = 20, status } = {}) => {
|
||||
const offset = (page - 1) * limit
|
||||
const conditions = status
|
||||
? sql`WHERE cs.status = ${status}`
|
||||
: sql``
|
||||
|
||||
const items = await sql`
|
||||
SELECT cs.id, cs.customer_id, cs.mitra_id, cs.status, cs.created_at, cs.paired_at, cs.ended_at, cs.ended_by,
|
||||
c.display_name AS customer_display_name,
|
||||
m.display_name AS mitra_display_name
|
||||
FROM chat_sessions cs
|
||||
INNER JOIN customers c ON c.id = cs.customer_id
|
||||
LEFT JOIN mitras m ON m.id = cs.mitra_id
|
||||
${conditions}
|
||||
ORDER BY cs.created_at DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`
|
||||
const [{ count }] = await sql`SELECT COUNT(*) FROM chat_sessions cs ${conditions}`
|
||||
return { items, total: Number(count), page, limit }
|
||||
}
|
||||
|
||||
export const getSessionById = async (sessionId) => {
|
||||
const [session] = await sql`
|
||||
SELECT cs.id, cs.customer_id, cs.mitra_id, cs.status, cs.created_at, cs.paired_at, cs.ended_at, cs.ended_by,
|
||||
c.display_name AS customer_display_name,
|
||||
m.display_name AS mitra_display_name
|
||||
FROM chat_sessions cs
|
||||
INNER JOIN customers c ON c.id = cs.customer_id
|
||||
LEFT JOIN mitras m ON m.id = cs.mitra_id
|
||||
WHERE cs.id = ${sessionId}
|
||||
`
|
||||
return session
|
||||
}
|
||||
Reference in New Issue
Block a user