- 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>
109 lines
4.7 KiB
JavaScript
109 lines
4.7 KiB
JavaScript
import { authenticate } from '../../plugins/auth.js'
|
|
import { getCustomerByFirebaseUid } from '../../services/customer.service.js'
|
|
import { createPairingRequest, cancelPairingRequest } from '../../services/pairing.service.js'
|
|
import { getActiveSessionByCustomer, getActiveSessionByCustomerWithUnread, endSession, getCustomerHistory } from '../../services/session.service.js'
|
|
import { getPricingForCustomer, isValidTier, isCustomerEligibleForFreeTrial, getFreeTrial } from '../../services/pricing.service.js'
|
|
import { requestExtension } from '../../services/extension.service.js'
|
|
import { EndedBy } from '../../constants.js'
|
|
|
|
const resolveCustomer = async (request, reply) => {
|
|
const customer = await getCustomerByFirebaseUid(request.firebaseUser.uid)
|
|
if (!customer) {
|
|
return reply.code(404).send({
|
|
success: false,
|
|
error: { code: 'ACCOUNT_NOT_FOUND', message: 'Customer account not found' },
|
|
})
|
|
}
|
|
request.customer = customer
|
|
}
|
|
|
|
export const clientChatRoutes = async (app) => {
|
|
// Get pricing tiers + free trial eligibility
|
|
app.get('/pricing', { preHandler: [authenticate, resolveCustomer] }, async (request, reply) => {
|
|
const pricing = await getPricingForCustomer(request.customer.id)
|
|
return reply.send({ success: true, data: pricing })
|
|
})
|
|
|
|
app.post('/request', { preHandler: [authenticate, resolveCustomer] }, async (request, reply) => {
|
|
const { duration_minutes, price, is_free_trial } = request.body || {}
|
|
|
|
// Validate selection
|
|
if (is_free_trial) {
|
|
const eligible = await isCustomerEligibleForFreeTrial(request.customer.id)
|
|
if (!eligible) {
|
|
return reply.code(403).send({
|
|
success: false,
|
|
error: { code: 'FREE_TRIAL_INELIGIBLE', message: 'Not eligible for free trial' },
|
|
})
|
|
}
|
|
const freeTrial = await getFreeTrial()
|
|
const session = await createPairingRequest(request.customer.id, {
|
|
duration_minutes: freeTrial.duration_minutes,
|
|
price: 0,
|
|
is_free_trial: true,
|
|
})
|
|
return reply.code(201).send({ success: true, data: session })
|
|
}
|
|
|
|
if (!duration_minutes || price === undefined) {
|
|
return reply.code(400).send({
|
|
success: false,
|
|
error: { code: 'BAD_REQUEST', message: 'duration_minutes and price are required' },
|
|
})
|
|
}
|
|
|
|
if (!(await isValidTier(duration_minutes, price))) {
|
|
return reply.code(400).send({
|
|
success: false,
|
|
error: { code: 'INVALID_TIER', message: 'Invalid price tier selection' },
|
|
})
|
|
}
|
|
|
|
const session = await createPairingRequest(request.customer.id, { duration_minutes, price, is_free_trial: false })
|
|
return reply.code(201).send({ success: true, data: session })
|
|
})
|
|
|
|
app.post('/request/:sessionId/cancel', { preHandler: [authenticate, resolveCustomer] }, async (request, reply) => {
|
|
const session = await cancelPairingRequest(request.params.sessionId, request.customer.id)
|
|
return reply.send({ success: true, data: session })
|
|
})
|
|
|
|
app.get('/session/active', { preHandler: [authenticate, resolveCustomer] }, async (request, reply) => {
|
|
const session = await getActiveSessionByCustomer(request.customer.id)
|
|
return reply.send({ success: true, data: session ?? null })
|
|
})
|
|
|
|
app.get('/session/active-with-unread', { preHandler: [authenticate, resolveCustomer] }, async (request, reply) => {
|
|
const session = await getActiveSessionByCustomerWithUnread(request.customer.id)
|
|
return reply.send({ success: true, data: session ?? null })
|
|
})
|
|
|
|
app.post('/session/:sessionId/end', { preHandler: [authenticate, resolveCustomer] }, async (request, reply) => {
|
|
const session = await endSession(request.params.sessionId, EndedBy.CUSTOMER, request.customer.id)
|
|
return reply.send({ success: true, data: session })
|
|
})
|
|
|
|
// Request session extension
|
|
app.post('/session/:sessionId/extend', { preHandler: [authenticate, resolveCustomer] }, async (request, reply) => {
|
|
const { duration_minutes, price } = request.body || {}
|
|
if (!duration_minutes || price === undefined) {
|
|
return reply.code(400).send({
|
|
success: false,
|
|
error: { code: 'BAD_REQUEST', message: 'duration_minutes and price are required' },
|
|
})
|
|
}
|
|
const extension = await requestExtension(request.params.sessionId, request.customer.id, { duration_minutes, price })
|
|
return reply.send({ success: true, data: extension })
|
|
})
|
|
|
|
// Chat history
|
|
app.get('/history', { preHandler: [authenticate, resolveCustomer] }, async (request, reply) => {
|
|
const { page, limit } = request.query
|
|
const history = await getCustomerHistory(request.customer.id, {
|
|
page: page ? parseInt(page) : 1,
|
|
limit: limit ? parseInt(limit) : 20,
|
|
})
|
|
return reply.send({ success: true, data: history })
|
|
})
|
|
}
|