Files
boc/aamos-ledger-refactor/index-v2.mjs
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

253 lines
12 KiB
JavaScript

/**
* ═══════════════════════════════════════════════════════════════════════════
* AAMOS Ledger Engine — Ekonomimodulens sanning
* Port: 3250
* Etapp 1 — Vertikal stängning
* Refactored: Routes split, shared pool, API v1 versioning
*
* Principer (BYGGPLAN 0.001):
* • Determinism före AI — alla beslut reproducerbara
* • Audit First — varje operation genererar event + auditpost
* • Tenant Isolation — kunddata korsas aldrig
* • Hermes — alla ekonomiska händelser publiceras via event fabric
* • Inga direktberoenden till andra moduler
* ═══════════════════════════════════════════════════════════════════════════
*/
import express from 'express';
import cors from 'cors';
import pg from 'pg';
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { randomUUID } from 'crypto';
import hermes from './hermes.mjs';
import { authMiddleware } from './auth.mjs';
import jwt from 'jsonwebtoken';
import { register as registerPeriodWorkflow } from './period-workflow.mjs';
import { register as registerReconciliation } from './reconciliation.mjs';
import { register as registerSie4Export } from './sie4-export.mjs';
import { registerCheckpoints, initCheckpointEngine } from './checkpoint-engine.mjs';
// Route modules
import registerAccounts from './routes/accounts.mjs';
import registerJournal from './routes/journal.mjs';
import registerPeriods from './routes/periods.mjs';
import registerReports from './routes/reports.mjs';
import registerInvoices from './routes/invoices.mjs';
import registerExport from './routes/export.mjs';
import registerHealth from './routes/health.mjs';
const __dir = dirname(fileURLToPath(import.meta.url));
const PORT = process.env.AAMOS_LEDGER_PORT || process.env.PORT || 3250;
const { Pool } = pg;
// ── Database ─────────────────────────────────────────────────────────────────
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false },
max: 10,
idleTimeoutMillis: 30000,
});
// Initialize checkpoint engine with shared pool (fixes duplicate pool)
initCheckpointEngine(pool);
async function initSchema() {
const sql = readFileSync(join(__dir, 'schema.sql'), 'utf8');
await pool.query(sql);
console.log('[ledger] Schema initialiserat');
}
// ── App ───────────────────────────────────────────────────────────────────────
const app = express();
app.use(cors());
app.use((req, res, next) => { if (req.path.endsWith('/upload')) return next(); express.json()(req, res, next); });
// ── Auth Validate (unauthed, local RS256 verify) ─────────────────────────────
let _ledgerPubKey = null;
try { _ledgerPubKey = readFileSync('/opt/amos/data/keys/jwt-public.pem', 'utf8'); } catch {}
const _ledgerJwtSecret = process.env.AMOS_JWT_SECRET || process.env.JWT_SECRET || 'y8Lf__vnWpImX2lxyPo3R0WYFEgCrtmQ3r8FcCwKjAWTNsLBKhaqIrP5oGAdt31E';
app.get('/api/auth/validate', (req, res) => {
const token = req.query.token;
if (!token) return res.status(400).json({ ok: false, error: 'token query param required' });
try {
const verifyKey = _ledgerPubKey || _ledgerJwtSecret;
const algos = _ledgerPubKey ? ['RS256'] : ['HS256'];
const claims = jwt.verify(token, verifyKey, { algorithms: algos });
res.json({ ok: true, claims });
} catch (e) {
res.status(401).json({ ok: false, error: e.message });
}
});
app.use(authMiddleware); // E1-AUTH: JWT validation för alla övriga endpoints
// ── Context helper ────────────────────────────────────────────────────────────
function buildCtx(req, entityType = null, entityId = null, decisionSource = 'user') {
return {
trace_id: req.headers['x-trace-id'] || randomUUID(),
correlation_id: req.headers['x-correlation-id'] || randomUUID(),
tenant_id: req.headers['x-tenant-id'] || req.body?.tenant_id || 'wavult-group',
user_id: req.headers['x-user-id'] || req.body?.user_id || 'system',
entity_type: entityType,
entity_id: entityId,
decision_source: decisionSource,
};
}
// ── Audit helper ──────────────────────────────────────────────────────────────
async function writeAudit(ctx, action, before = null, after = null, client = pool) {
await client.query(
`INSERT INTO ledger_audit_log
(tenant_id, trace_id, correlation_id, user_id,
entity_type, entity_id, action, decision_source, before_state, after_state)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
[
ctx.tenant_id, ctx.trace_id, ctx.correlation_id, ctx.user_id,
ctx.entity_type, ctx.entity_id, action, ctx.decision_source,
before ? JSON.stringify(before) : null,
after ? JSON.stringify(after) : null,
]
);
}
// ══════════════════════════════════════════════════════════════════════════════
// ROUTE REGISTRATION (Refactored into modules)
// ══════════════════════════════════════════════════════════════════════════════
// Health (unauthed)
registerHealth(app, pool, PORT);
// Core ledger routes
registerAccounts(app, pool, buildCtx, writeAudit, hermes);
registerJournal(app, pool, buildCtx, writeAudit, hermes);
registerPeriods(app, pool);
registerReports(app, pool);
registerInvoices(app, pool);
registerExport(app, pool);
// ══════════════════════════════════════════════════════════════════════════════
// KUNDREGISTER — LandveX Customers & Contracts
// ══════════════════════════════════════════════════════════════════════════════
// GET /api/ledger/customers — lista kunder med avtal
app.get('/api/ledger/customers', async (req, res) => {
try {
const orgId = req.query.org_id || req.headers['x-tenant-id'] || 'landvex';
const { rows } = await pool.query(`
SELECT
c.id, c.name, c.org_number, c.vat_number,
c.contact_name, c.contact_email, c.contact_phone,
c.address, c.postal_code, c.city, c.country,
c.payment_terms_days, c.default_vat_rate, c.default_account,
c.currency, c.notes, c.active, c.created_at,
json_agg(
json_build_object(
'id', ct.id,
'title', ct.title,
'contract_ref', ct.contract_ref,
'amount_excl_vat', ct.amount_excl_vat,
'vat_rate', ct.vat_rate,
'billing_period', ct.billing_period,
'payment_terms_days', ct.payment_terms_days,
'invoice_description', ct.invoice_description,
'account_credit', ct.account_credit,
'status', ct.status,
'start_date', ct.start_date,
'end_date', ct.end_date
) ORDER BY ct.created_at DESC
) FILTER (WHERE ct.id IS NOT NULL) as contracts
FROM landvex_customers c
LEFT JOIN landvex_contracts ct ON ct.customer_id = c.id AND ct.status = 'active'
WHERE c.org_id = $1 AND c.active = true
GROUP BY c.id
ORDER BY c.name
`, [orgId]);
res.json({ ok: true, customers: rows });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// POST /api/ledger/customers — skapa ny kund
app.post('/api/ledger/customers', async (req, res) => {
try {
const {
name, org_number, vat_number, contact_name, contact_email, contact_phone,
address, postal_code, city, country, payment_terms_days,
default_vat_rate, default_account, currency, notes, org_id
} = req.body;
if (!name) return res.status(400).json({ ok: false, error: 'name required' });
const tenantOrgId = org_id || req.headers['x-tenant-id'] || 'landvex';
const { rows } = await pool.query(
`INSERT INTO landvex_customers
(org_id, name, org_number, vat_number, contact_name, contact_email, contact_phone,
address, postal_code, city, country, payment_terms_days,
default_vat_rate, default_account, currency, notes)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
RETURNING *`,
[
tenantOrgId, name,
org_number || null, vat_number || null,
contact_name || null, contact_email || null, contact_phone || null,
address || null, postal_code || null, city || null,
country || 'SE',
payment_terms_days != null ? payment_terms_days : 30,
default_vat_rate != null ? default_vat_rate : 25,
default_account || '3000', currency || 'SEK',
notes || null
]
);
res.json({ ok: true, customer: rows[0] });
} catch (err) {
res.status(500).json({ ok: false, error: err.message });
}
});
// ── E1-004 Reconciliation Engine ─────────────────────────────────────────────
registerReconciliation(app, pool, buildCtx, writeAudit, hermes);
// ── E1-005 Period Close Workflow ──────────────────────────────────────────────
registerPeriodWorkflow(app, pool, buildCtx, writeAudit, hermes);
// ── E1-006 SIE4 Export ─────────────────────────────────────────────────────────────
registerSie4Export(app, pool, buildCtx, hermes);
// Checkpoint Engine (L2-001)
registerCheckpoints(app);
// ══════════════════════════════════════════════════════════════════════════════
// API v1 PREFIX (backward compatibility)
// All existing /api/ledger/* routes are also available under /api/v1/ledger/*
// ══════════════════════════════════════════════════════════════════════════════
// Create a sub-app for v1 routes
const v1App = express.Router();
// Re-register all routes on v1 router
registerHealth(v1App, pool, PORT);
registerAccounts(v1App, pool, buildCtx, writeAudit, hermes);
registerJournal(v1App, pool, buildCtx, writeAudit, hermes);
registerPeriods(v1App, pool);
registerReports(v1App, pool);
registerInvoices(v1App, pool);
registerExport(v1App, pool);
// Mount v1 routes
app.use('/api/v1', v1App);
// ── Start ─────────────────────────────────────────────────────────────────────
try {
await initSchema();
app.listen(PORT, () => {
console.log(`[aamos-ledger] Ledger Engine på :${PORT}`);
console.log(`[aamos-ledger] Hermes → redis://127.0.0.1:6379 (kanal: aamos:hermes)`);
console.log(`[aamos-ledger] Endpoints: /api/ledger/* · /api/v1/ledger/* (versioned)`);
});
} catch (e) {
console.error('[aamos-ledger] Startup misslyckades:', e.message);
process.exit(1);
}