LandveX Gateway v0.1.0 + Operations API + Communications Module — ALL RUNNING

This commit is contained in:
Bernt
2026-07-02 18:33:25 +00:00
parent 4f7152316c
commit 8dcca96185
20 changed files with 1754 additions and 81 deletions
+25
View File
@@ -0,0 +1,25 @@
/**
* LandveX Operations API
*
* Government customers, billing, contracts, self-service
* Port: 3005
*/
import http from 'http';
import { handleRequest } from './router.mjs';
const PORT = process.env.PORT || 3005;
const server = http.createServer(handleRequest);
server.listen(PORT, () => {
console.log(`🏛️ LandveX Operations API running on port ${PORT}`);
console.log(`📋 Endpoints:`);
console.log(` GET /health → Health check`);
console.log(` GET /api/v1/government/me → My profile`);
console.log(` PUT /api/v1/government/payment → Update payment settings`);
console.log(` GET /api/v1/government/budget → Budget overview`);
console.log(` POST /api/v1/government/areas → Add watch area`);
console.log(` GET /api/v1/government/areas → List watch areas`);
console.log(` POST /api/v1/government/users → Invite user`);
});
+25
View File
@@ -0,0 +1,25 @@
/**
* LandveX Operations API
*
* Government customers, billing, contracts, self-service
* Port: 3005
*/
import http from 'http';
import { handleRequest } from './router.js';
const PORT = process.env.PORT || 3005;
const server = http.createServer(handleRequest);
server.listen(PORT, () => {
console.log(`🏛️ LandveX Operations API running on port ${PORT}`);
console.log(`📋 Endpoints:`);
console.log(` GET /health → Health check`);
console.log(` GET /api/v1/government/me → My profile`);
console.log(` PUT /api/v1/government/payment → Update payment settings`);
console.log(` GET /api/v1/government/budget → Budget overview`);
console.log(` POST /api/v1/government/areas → Add watch area`);
console.log(` GET /api/v1/government/areas → List watch areas`);
console.log(` POST /api/v1/government/users → Invite user`);
});
+191
View File
@@ -0,0 +1,191 @@
/**
* LandveX Operations API — Router
* Zero-dependency HTTP router
*/
import http from 'http';
import { URL } from 'url';
// In-memory store (replace with PostgreSQL)
const customers = {};
const watchAreas = {};
const users = {};
export function handleRequest(req, res) {
// CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Content-Type', 'application/json');
if (req.method === 'OPTIONS') {
res.statusCode = 204;
res.end();
return;
}
const url = new URL(req.url || '/', `http://${req.headers.host}`);
const path = url.pathname;
// Health check
if (path === '/health') {
res.end(JSON.stringify({
status: 'ok',
version: '0.1.0-operations',
service: 'operations',
}));
return;
}
// Government customer endpoints
if (path === '/api/v1/government/me') {
handleGetProfile(req, res);
} else if (path === '/api/v1/government/payment' && req.method === 'PUT') {
handleUpdatePayment(req, res);
} else if (path === '/api/v1/government/budget') {
handleGetBudget(req, res);
} else if (path === '/api/v1/government/areas' && req.method === 'POST') {
handleAddArea(req, res);
} else if (path === '/api/v1/government/areas' && req.method === 'GET') {
handleListAreas(req, res);
} else if (path === '/api/v1/government/users' && req.method === 'POST') {
handleInviteUser(req, res);
} else {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Not Found', path }));
}
}
function handleGetProfile(req, res) {
const profile = {
id: 'gov-001',
orgNumber: '212000-0142',
name: 'Stockholms stad',
type: 'kommun',
address: 'Stockholms stadshus, 111 83 Stockholm',
contactEmail: 'inkop@stockholm.se',
paymentSettings: {
method: 'invoice',
invoiceAddress: 'Stockholms stad, FE 101, 838 73 Frösön',
reference: 'Avd. Gatu- och fastighetskontoret',
ocrNumber: '2120000142',
},
budgetLimits: {
monthly: 50000,
yearly: 600000,
alertAtPercent: 80,
blockAtPercent: 100,
},
createdAt: '2024-01-15T10:00:00Z',
};
res.end(JSON.stringify(profile));
}
function handleUpdatePayment(req, res) {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const data = JSON.parse(body);
res.end(JSON.stringify({
success: true,
message: 'Payment settings updated',
settings: data,
}));
} catch {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}
function handleGetBudget(req, res) {
const budget = {
monthlyLimit: 50000,
monthlyUsed: 32450,
monthlyRemaining: 17550,
percentUsed: 64.9,
yearlyLimit: 600000,
yearlyUsed: 187340,
yearlyRemaining: 412660,
yearlyPercentUsed: 31.2,
alertTriggered: false,
blockTriggered: false,
forecast: {
projectedYearly: 582000,
status: 'on_track',
},
breakdown: [
{ category: 'Road Inspection', amount: 12400 },
{ category: 'Bridge Survey', amount: 8900 },
{ category: 'Property Scan', amount: 6750 },
{ category: 'Emergency Call', amount: 4400 },
],
};
res.end(JSON.stringify(budget));
}
function handleAddArea(req, res) {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const data = JSON.parse(body);
const area = {
id: `area-${Date.now()}`,
...data,
createdAt: new Date().toISOString(),
};
const customerId = 'gov-001';
if (!watchAreas[customerId]) watchAreas[customerId] = [];
watchAreas[customerId].push(area);
res.statusCode = 201;
res.end(JSON.stringify(area));
} catch {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}
function handleListAreas(req, res) {
const customerId = 'gov-001';
const areas = watchAreas[customerId] || [];
res.end(JSON.stringify({
areas,
count: areas.length,
}));
}
function handleInviteUser(req, res) {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const data = JSON.parse(body);
const user = {
id: `user-${Date.now()}`,
email: data.email,
name: data.name,
role: data.role || 'viewer',
invitedAt: new Date().toISOString(),
status: 'pending',
};
const customerId = 'gov-001';
if (!users[customerId]) users[customerId] = [];
users[customerId].push(user);
res.statusCode = 201;
res.end(JSON.stringify(user));
} catch {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}
+192
View File
@@ -0,0 +1,192 @@
/**
* LandveX Operations API — Router
* Zero-dependency HTTP router
*/
import http from 'http';
import { URL } from 'url';
// In-memory store (replace with PostgreSQL)
const customers: Record<string, any> = {};
const watchAreas: Record<string, any[]> = {};
const users: Record<string, any[]> = {};
export function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
// CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Content-Type', 'application/json');
if (req.method === 'OPTIONS') {
res.statusCode = 204;
res.end();
return;
}
const url = new URL(req.url || '/', `http://${req.headers.host}`);
const path = url.pathname;
// Health check
if (path === '/health') {
res.end(JSON.stringify({
status: 'ok',
version: '0.1.0-operations',
service: 'operations',
}));
return;
}
// Government customer endpoints
if (path === '/api/v1/government/me') {
handleGetProfile(req, res);
} else if (path === '/api/v1/government/payment' && req.method === 'PUT') {
handleUpdatePayment(req, res);
} else if (path === '/api/v1/government/budget') {
handleGetBudget(req, res);
} else if (path === '/api/v1/government/areas' && req.method === 'POST') {
handleAddArea(req, res);
} else if (path === '/api/v1/government/areas' && req.method === 'GET') {
handleListAreas(req, res);
} else if (path === '/api/v1/government/users' && req.method === 'POST') {
handleInviteUser(req, res);
} else {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Not Found', path }));
}
}
function handleGetProfile(req: http.IncomingMessage, res: http.ServerResponse): void {
// Mock profile — in real implementation, extract from JWT
const profile = {
id: 'gov-001',
orgNumber: '212000-0142',
name: 'Stockholms stad',
type: 'kommun',
address: 'Stockholms stadshus, 111 83 Stockholm',
contactEmail: 'inkop@stockholm.se',
paymentSettings: {
method: 'invoice',
invoiceAddress: 'Stockholms stad, FE 101, 838 73 Frösön',
reference: 'Avd. Gatu- och fastighetskontoret',
ocrNumber: '2120000142',
},
budgetLimits: {
monthly: 50000,
yearly: 600000,
alertAtPercent: 80,
blockAtPercent: 100,
},
createdAt: '2024-01-15T10:00:00Z',
};
res.end(JSON.stringify(profile));
}
function handleUpdatePayment(req: http.IncomingMessage, res: http.ServerResponse): void {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const data = JSON.parse(body);
res.end(JSON.stringify({
success: true,
message: 'Payment settings updated',
settings: data,
}));
} catch {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}
function handleGetBudget(req: http.IncomingMessage, res: http.ServerResponse): void {
const budget = {
monthlyLimit: 50000,
monthlyUsed: 32450,
monthlyRemaining: 17550,
percentUsed: 64.9,
yearlyLimit: 600000,
yearlyUsed: 187340,
yearlyRemaining: 412660,
yearlyPercentUsed: 31.2,
alertTriggered: false,
blockTriggered: false,
forecast: {
projectedYearly: 582000,
status: 'on_track',
},
breakdown: [
{ category: 'Road Inspection', amount: 12400 },
{ category: 'Bridge Survey', amount: 8900 },
{ category: 'Property Scan', amount: 6750 },
{ category: 'Emergency Call', amount: 4400 },
],
};
res.end(JSON.stringify(budget));
}
function handleAddArea(req: http.IncomingMessage, res: http.ServerResponse): void {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const data = JSON.parse(body);
const area = {
id: `area-${Date.now()}`,
...data,
createdAt: new Date().toISOString(),
};
const customerId = 'gov-001';
if (!watchAreas[customerId]) watchAreas[customerId] = [];
watchAreas[customerId].push(area);
res.statusCode = 201;
res.end(JSON.stringify(area));
} catch {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}
function handleListAreas(req: http.IncomingMessage, res: http.ServerResponse): void {
const customerId = 'gov-001';
const areas = watchAreas[customerId] || [];
res.end(JSON.stringify({
areas,
count: areas.length,
}));
}
function handleInviteUser(req: http.IncomingMessage, res: http.ServerResponse): void {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const data = JSON.parse(body);
const user = {
id: `user-${Date.now()}`,
email: data.email,
name: data.name,
role: data.role || 'viewer',
invitedAt: new Date().toISOString(),
status: 'pending',
};
const customerId = 'gov-001';
if (!users[customerId]) users[customerId] = [];
users[customerId].push(user);
res.statusCode = 201;
res.end(JSON.stringify(user));
} catch {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}