bae705aa97
- 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
220 lines
6.9 KiB
JavaScript
220 lines
6.9 KiB
JavaScript
/**
|
|
* QUIXZOOM Zoomer API (Simple - no dependencies)
|
|
*
|
|
* Endpoints för Zoomer-hantering:
|
|
* - CRUD för Zoomers
|
|
* - Onboarding progress
|
|
* - Quality scores
|
|
* - Mission assignment
|
|
* - Regional coordinators
|
|
*/
|
|
|
|
const http = require('http');
|
|
const url = require('url');
|
|
|
|
// Mock database
|
|
const zoomers = new Map();
|
|
const missions = new Map();
|
|
|
|
// Demo data
|
|
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);
|
|
|
|
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,
|
|
},
|
|
};
|
|
}
|
|
|
|
const server = http.createServer((req, res) => {
|
|
const parsedUrl = url.parse(req.url, true);
|
|
const path = parsedUrl.pathname;
|
|
const method = req.method;
|
|
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
|
|
// GET /api/v1/zoomers
|
|
if (path === '/api/v1/zoomers' && method === 'GET') {
|
|
const results = Array.from(zoomers.values());
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
count: results.length,
|
|
zoomers: results,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// GET /api/v1/zoomers/:id
|
|
const zoomerMatch = path.match(/^\/api\/v1\/zoomers\/([^\/]+)$/);
|
|
if (zoomerMatch && method === 'GET') {
|
|
const zoomer = zoomers.get(zoomerMatch[1]);
|
|
if (zoomer) {
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({ success: true, zoomer }));
|
|
} else {
|
|
res.writeHead(404);
|
|
res.end(JSON.stringify({ success: false, error: 'Zoomer not found' }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// GET /api/v1/zoomers/:id/score
|
|
const scoreMatch = path.match(/^\/api\/v1\/zoomers\/([^\/]+)\/score$/);
|
|
if (scoreMatch && method === 'GET') {
|
|
const zoomer = zoomers.get(scoreMatch[1]);
|
|
if (zoomer) {
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
score: calculateQualityScore(zoomer),
|
|
}));
|
|
} else {
|
|
res.writeHead(404);
|
|
res.end(JSON.stringify({ success: false, error: 'Zoomer not found' }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// GET /api/v1/stats
|
|
if (path === '/api/v1/stats' && method === 'GET') {
|
|
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.writeHead(200);
|
|
res.end(JSON.stringify({ success: true, stats }));
|
|
return;
|
|
}
|
|
|
|
// GET /api/v1/coordinators
|
|
if (path === '/api/v1/coordinators' && method === 'GET') {
|
|
const coordinators = Array.from(zoomers.values())
|
|
.filter(z => z.role === 'regional_coordinator');
|
|
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
count: coordinators.length,
|
|
coordinators,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// Health check
|
|
if (path === '/health' && method === 'GET') {
|
|
res.writeHead(200);
|
|
res.end(JSON.stringify({ status: 'ok', service: 'zoomer-api' }));
|
|
return;
|
|
}
|
|
|
|
// 404
|
|
res.writeHead(404);
|
|
res.end(JSON.stringify({ success: false, error: 'Not found' }));
|
|
});
|
|
|
|
const PORT = 3003;
|
|
server.listen(PORT, () => {
|
|
console.log(`╔════════════════════════════════════════════════════════════╗`);
|
|
console.log(`║ ZOOMER API RUNNING ON PORT ${PORT} ║`);
|
|
console.log(`╚════════════════════════════════════════════════════════════╝\n`);
|
|
console.log('Endpoints:');
|
|
console.log(' GET /api/v1/zoomers - List all zoomers');
|
|
console.log(' GET /api/v1/zoomers/:id - Get zoomer details');
|
|
console.log(' GET /api/v1/zoomers/:id/score - Get quality score');
|
|
console.log(' GET /api/v1/stats - System stats');
|
|
console.log(' GET /api/v1/coordinators - List coordinators');
|
|
console.log(' GET /health - Health check');
|
|
console.log('\nDemo zoomer: zoomer_bangkok_001');
|
|
});
|