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
253 lines
12 KiB
JavaScript
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);
|
|
}
|