Files
boc/quixzoom-capture-pipeline/api/zoomer-api.js
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

516 lines
12 KiB
JavaScript

/**
* QUIXZOOM Zoomer API
*
* Endpoints för Zoomer-hantering:
* - CRUD för Zoomers
* - Onboarding progress
* - Quality scores
* - Mission assignment
* - Regional coordinators
*/
const express = require('express');
const router = express.Router();
// Mock database - replace with real PostgreSQL
const zoomers = new Map();
const missions = new Map();
/**
* ============================================================
* ZOOMER CRUD
* ============================================================
*/
// List all zoomers
router.get('/zoomers', (req, res) => {
const { status, level, area } = req.query;
let results = Array.from(zoomers.values());
if (status) {
results = results.filter(z => z.status === status);
}
if (level) {
results = results.filter(z => z.level === parseInt(level));
}
if (area) {
results = results.filter(z => z.area === area);
}
res.json({
success: true,
count: results.length,
zoomers: results,
});
});
// Get single zoomer
router.get('/zoomers/:id', (req, res) => {
const zoomer = zoomers.get(req.params.id);
if (!zoomer) {
return res.status(404).json({
success: false,
error: 'Zoomer not found',
});
}
res.json({
success: true,
zoomer,
});
});
// Create zoomer
router.post('/zoomers', (req, res) => {
const { name, email, phone, location } = req.body;
const zoomer = {
id: `zoomer_${Date.now()}`,
name,
email,
phone,
location,
level: 0,
status: 'onboarding',
role: 'zoomer',
createdAt: new Date().toISOString(),
scores: {
overall: 0,
gps: [],
image: [],
corrections: 0,
approvedObservations: 0,
totalObservations: 0,
confirmations: 0,
responseTime: [],
completionRate: [],
},
};
zoomers.set(zoomer.id, zoomer);
res.status(201).json({
success: true,
zoomer,
});
});
// Update zoomer
router.patch('/zoomers/:id', (req, res) => {
const zoomer = zoomers.get(req.params.id);
if (!zoomer) {
return res.status(404).json({
success: false,
error: 'Zoomer not found',
});
}
Object.assign(zoomer, req.body);
zoomer.updatedAt = new Date().toISOString();
res.json({
success: true,
zoomer,
});
});
// Delete zoomer
router.delete('/zoomers/:id', (req, res) => {
const deleted = zoomers.delete(req.params.id);
if (!deleted) {
return res.status(404).json({
success: false,
error: 'Zoomer not found',
});
}
res.json({
success: true,
message: 'Zoomer deleted',
});
});
/**
* ============================================================
* ONBOARDING
* ============================================================
*/
// Get onboarding progress
router.get('/zoomers/:id/onboarding', (req, res) => {
const zoomer = zoomers.get(req.params.id);
if (!zoomer) {
return res.status(404).json({
success: false,
error: 'Zoomer not found',
});
}
const progress = {
level: zoomer.level,
status: zoomer.status,
completedLevels: zoomer.level >= 1,
currentLevel: zoomer.level < 4 ? zoomer.level + 1 : 4,
canStartNext: zoomer.level < 4,
};
res.json({
success: true,
progress,
});
});
// Complete level
router.post('/zoomers/:id/onboarding/complete', (req, res) => {
const { level } = req.body;
const zoomer = zoomers.get(req.params.id);
if (!zoomer) {
return res.status(404).json({
success: false,
error: 'Zoomer not found',
});
}
if (level !== zoomer.level + 1) {
return res.status(400).json({
success: false,
error: 'Invalid level progression',
});
}
zoomer.level = level;
if (level === 3) {
zoomer.status = 'active';
} else if (level === 4) {
zoomer.status = 'trusted';
}
res.json({
success: true,
zoomer,
});
});
/**
* ============================================================
* QUALITY SCORES
* ============================================================
*/
// Get quality score
router.get('/zoomers/:id/score', (req, res) => {
const zoomer = zoomers.get(req.params.id);
if (!zoomer) {
return res.status(404).json({
success: false,
error: 'Zoomer not found',
});
}
const score = calculateQualityScore(zoomer);
res.json({
success: true,
score,
});
});
// Update scores from observation
router.post('/zoomers/:id/scores', (req, res) => {
const { observation } = req.body;
const zoomer = zoomers.get(req.params.id);
if (!zoomer) {
return res.status(404).json({
success: false,
error: 'Zoomer not found',
});
}
// Update scores
if (observation.location?.accuracy) {
zoomer.scores.gps.push(observation.location.accuracy);
}
if (observation.quality?.overall) {
zoomer.scores.image.push(observation.quality.overall);
}
if (observation.aiCorrections) {
zoomer.scores.corrections += observation.aiCorrections;
}
zoomer.scores.totalObservations++;
if (observation.approved) {
zoomer.scores.approvedObservations++;
}
if (observation.responseTime) {
zoomer.scores.responseTime.push(observation.responseTime);
}
if (observation.missionCompleted !== undefined) {
zoomer.scores.completionRate.push(observation.missionCompleted ? 1 : 0);
}
const score = calculateQualityScore(zoomer);
res.json({
success: true,
score,
});
});
function calculateQualityScore(zoomer) {
const scores = zoomer.scores;
const avgGps = scores.gps.length > 0
? scores.gps.reduce((a, b) => a + b, 0) / scores.gps.length
: 999;
const gpsScore = Math.max(0, 1 - (avgGps / 10));
const avgImage = scores.image.length > 0
? scores.image.reduce((a, b) => a + b, 0) / scores.image.length
: 0;
const correctionRate = scores.totalObservations > 0
? scores.corrections / scores.totalObservations
: 0;
const correctionScore = Math.max(0, 1 - correctionRate);
const approvalRate = scores.totalObservations > 0
? scores.approvedObservations / scores.totalObservations
: 0;
const confirmationScore = Math.min(1, scores.confirmations / 10);
const avgResponse = scores.responseTime.length > 0
? scores.responseTime.reduce((a, b) => a + b, 0) / scores.responseTime.length
: 3600;
const responseScore = Math.max(0, 1 - (avgResponse / 3600));
const avgCompletion = scores.completionRate.length > 0
? scores.completionRate.reduce((a, b) => a + b, 0) / scores.completionRate.length
: 0;
const overall =
gpsScore * 0.20 +
avgImage * 0.20 +
correctionScore * 0.15 +
approvalRate * 0.20 +
confirmationScore * 0.10 +
responseScore * 0.10 +
avgCompletion * 0.05;
return {
overall: Math.round(overall * 100) / 100,
breakdown: {
gps: Math.round(gpsScore * 100) / 100,
image: Math.round(avgImage * 100) / 100,
corrections: Math.round(correctionScore * 100) / 100,
approval: Math.round(approvalRate * 100) / 100,
confirmations: Math.round(confirmationScore * 100) / 100,
response: Math.round(responseScore * 100) / 100,
completion: Math.round(avgCompletion * 100) / 100,
},
};
}
/**
* ============================================================
* MISSIONS
* ============================================================
*/
// Assign mission to zoomer
router.post('/zoomers/:id/missions', (req, res) => {
const { missionId } = req.body;
const zoomer = zoomers.get(req.params.id);
if (!zoomer) {
return res.status(404).json({
success: false,
error: 'Zoomer not found',
});
}
const mission = missions.get(missionId);
if (!mission) {
return res.status(404).json({
success: false,
error: 'Mission not found',
});
}
const score = calculateQualityScore(zoomer);
const suitability = calculateSuitability(score, mission);
res.json({
success: true,
assignment: {
zoomerId: zoomer.id,
missionId,
suitability,
recommended: suitability > 0.8,
},
});
});
function calculateSuitability(score, mission) {
if (!score) return 0;
let suitability = score.overall;
if (mission.complexity === 'high' && score.overall < 0.8) {
suitability *= 0.5;
}
if (mission.timeOfDay === 'night' && score.breakdown.image < 0.7) {
suitability *= 0.7;
}
return Math.round(suitability * 100) / 100;
}
/**
* ============================================================
* REGIONAL COORDINATORS
* ============================================================
*/
// List coordinators
router.get('/coordinators', (req, res) => {
const coordinators = Array.from(zoomers.values())
.filter(z => z.role === 'regional_coordinator');
res.json({
success: true,
count: coordinators.length,
coordinators,
});
});
// Promote to coordinator
router.post('/zoomers/:id/coordinator', (req, res) => {
const { area } = req.body;
const zoomer = zoomers.get(req.params.id);
if (!zoomer) {
return res.status(404).json({
success: false,
error: 'Zoomer not found',
});
}
const score = calculateQualityScore(zoomer);
if (score.overall < 0.8 || zoomer.level < 4) {
return res.status(400).json({
success: false,
error: 'Zoomer does not meet requirements for coordinator',
});
}
zoomer.role = 'regional_coordinator';
zoomer.area = area;
res.json({
success: true,
zoomer,
});
});
/**
* ============================================================
* STATS
* ============================================================
*/
// Get system stats
router.get('/stats', (req, res) => {
const allZoomers = Array.from(zoomers.values());
const stats = {
total: allZoomers.length,
byLevel: {
0: allZoomers.filter(z => z.level === 0).length,
1: allZoomers.filter(z => z.level === 1).length,
2: allZoomers.filter(z => z.level === 2).length,
3: allZoomers.filter(z => z.level === 3).length,
4: allZoomers.filter(z => z.level === 4).length,
},
byStatus: {
onboarding: allZoomers.filter(z => z.status === 'onboarding').length,
active: allZoomers.filter(z => z.status === 'active').length,
trusted: allZoomers.filter(z => z.status === 'trusted').length,
},
coordinators: allZoomers.filter(z => z.role === 'regional_coordinator').length,
avgScore: allZoomers.length > 0
? allZoomers.reduce((sum, z) => sum + calculateQualityScore(z).overall, 0) / allZoomers.length
: 0,
};
res.json({
success: true,
stats,
});
});
module.exports = router;
// Demo data
if (require.main === module) {
const app = express();
app.use(express.json());
app.use('/api/v1', router);
// Add demo zoomer
const demoZoomer = {
id: 'zoomer_bangkok_001',
name: 'Somchai',
email: 'somchai@example.com',
phone: '+66-81-234-5678',
location: 'Bangkok, Sukhumvit',
level: 4,
status: 'trusted',
role: 'regional_coordinator',
area: 'Sukhumvit',
createdAt: '2026-06-15',
scores: {
overall: 0.84,
gps: [3, 4, 3, 5, 4, 3, 4, 5, 3, 4],
image: [0.9, 0.85, 0.9, 0.8, 0.9, 0.85, 0.9, 0.9, 0.85, 0.9],
corrections: 2,
approvedObservations: 45,
totalObservations: 50,
confirmations: 8,
responseTime: [300, 450, 200, 600, 350, 400, 250, 500, 300, 350],
completionRate: [1, 1, 1, 1, 0, 1, 1, 1, 1, 1],
},
};
zoomers.set(demoZoomer.id, demoZoomer);
const PORT = 3003;
app.listen(PORT, () => {
console.log(`Zoomer API running on port ${PORT}`);
console.log('Endpoints:');
console.log(' GET /api/v1/zoomers');
console.log(' GET /api/v1/zoomers/:id');
console.log(' POST /api/v1/zoomers');
console.log(' GET /api/v1/zoomers/:id/score');
console.log(' GET /api/v1/stats');
});
}