Phase 3 scaffold: chat engine (WebSocket, FCM, pricing, timer, extension, history)
- Backend: WebSocket plugin, chat/pricing/timer/extension/closure/notification services - Client app: ChatBloc, pricing dialog, chat screen with message status, extension/goodbye flow, history - Mitra app: MitraChatBloc, ExtensionBloc, chat screen, extension accept/reject, history - Control center: free trial, extension timeout, early end config toggles - DB migration: chat_messages, session_closures, session_extensions, customer_transactions tables Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { authenticate } from '../../plugins/auth.js'
|
||||
import { getCustomerByFirebaseUid } from '../../services/customer.service.js'
|
||||
import { createPairingRequest, cancelPairingRequest, getSessionStatus } from '../../services/pairing.service.js'
|
||||
import { getActiveSessionByCustomer, endSession } from '../../services/session.service.js'
|
||||
import { getActiveSessionByCustomer, endSession, getCustomerHistory } from '../../services/session.service.js'
|
||||
import { subscribe } from '../../plugins/valkey.js'
|
||||
import { getPricingForCustomer, isValidTier, isCustomerEligibleForFreeTrial, getFreeTrial } from '../../services/pricing.service.js'
|
||||
import { requestExtension } from '../../services/extension.service.js'
|
||||
|
||||
const resolveCustomer = async (request, reply) => {
|
||||
const customer = await getCustomerByFirebaseUid(request.firebaseUser.uid)
|
||||
@@ -16,8 +18,48 @@ const resolveCustomer = async (request, reply) => {
|
||||
}
|
||||
|
||||
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 session = await createPairingRequest(request.customer.id)
|
||||
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 (!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 })
|
||||
})
|
||||
|
||||
@@ -72,4 +114,27 @@ export const clientChatRoutes = async (app) => {
|
||||
const session = await endSession(request.params.sessionId, 'customer')
|
||||
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 })
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user