Files
boc/aamos-bank-import/index.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

220 lines
6.9 KiB
JavaScript

/**
* AAMOS Bank CSV Import Service
* E1-003
* Port: 3252
*
* Importerar banktransaktioner (CSV) från svenska banker och skapar
* draft journal entries i Ledger Engine (port 3250).
*/
import express from 'express';
import cors from 'cors';
import { randomUUID } from 'crypto';
import { parseCSV, detectBank, SUPPORTED_FORMATS } from './parsers.mjs';
const PORT = parseInt(process.env.PORT || '3252', 10);
const LEDGER_BASE = process.env.LEDGER_URL || 'http://127.0.0.1:3250';
const app = express();
app.use(cors());
app.use(express.json({ limit: '20mb' }));
// ── Health ────────────────────────────────────────────────────────────────────
app.get('/health', (_req, res) => {
res.json({ ok: true, service: 'aamos-bank-import', port: PORT, ledger: LEDGER_BASE });
});
// ── GET /api/bank/formats ─────────────────────────────────────────────────────
app.get('/api/bank/formats', (_req, res) => {
res.json({ ok: true, formats: SUPPORTED_FORMATS });
});
// ── POST /api/bank/import ─────────────────────────────────────────────────────
app.post('/api/bank/import', async (req, res) => {
const {
bank = 'auto',
csv_content,
account_number = '1910',
tenant_id,
dry_run = false,
debit_account_positive = '3000', // kredit vid positiva belopp (inkomst)
credit_account_negative = '4000', // debet vid negativa belopp (utgift)
} = req.body ?? {};
if (!csv_content || typeof csv_content !== 'string') {
return res.status(400).json({ ok: false, error: 'csv_content (string) krävs' });
}
const correlation_id = randomUUID();
const trace_id = randomUUID();
const resolved_tenant = tenant_id || req.headers['x-tenant-id'] || 'wavult-group';
// ── Parse CSV ──────────────────────────────────────────────────────────────
let parsed;
let resolvedBank;
try {
parsed = parseCSV(bank, csv_content);
resolvedBank = parsed.bank;
} catch (err) {
return res.status(400).json({
ok: false,
error: `CSV parse-fel: ${err.message}`,
correlation_id,
trace_id,
});
}
const rows = parsed.rows;
const rows_parsed = rows.length;
if (rows_parsed === 0) {
return res.json({
ok: true,
bank: resolvedBank,
rows_parsed: 0,
rows_imported: 0,
skipped: 0,
errors: [],
correlation_id,
trace_id,
dry_run,
});
}
// ── Dry run — returnera utan att importera ─────────────────────────────────
if (dry_run) {
return res.json({
ok: true,
dry_run: true,
bank: resolvedBank,
rows_parsed,
rows_imported: 0,
skipped: 0,
errors: [],
preview: rows.slice(0, 5),
correlation_id,
trace_id,
});
}
// ── Live import — posta varje rad som en journal entry ─────────────────────
let rows_imported = 0;
let skipped = 0;
const errors = [];
for (const row of rows) {
try {
// Validera datum
if (!row.date || !/^\d{4}-\d{2}-\d{2}/.test(row.date)) {
skipped++;
errors.push({ row: row.raw, reason: `Ogiltigt datum: "${row.date}"` });
continue;
}
const amount = row.amount;
if (amount == null || amount === 0) {
skipped++;
errors.push({ row: row.raw, reason: 'Belopp är noll eller saknas' });
continue;
}
const absAmount = Math.abs(amount).toFixed(2);
/*
* Positiva belopp (inkomst/insättning):
* Debet: 1910 (bank), Kredit: 3000 (intäkt)
*
* Negativa belopp (utgift/uttag):
* Debet: 4000 (kostnad), Kredit: 1910 (bank)
*
* Ledger kräver ANTINGEN debit ELLER credit per rad (ej båda).
*/
let lines;
if (amount > 0) {
lines = [
{ account_number: account_number, debit: absAmount, description: row.description },
{ account_number: debit_account_positive, credit: absAmount, description: row.description },
];
} else {
lines = [
{ account_number: credit_account_negative, debit: absAmount, description: row.description },
{ account_number: account_number, credit: absAmount, description: row.description },
];
}
// Extrahera period från datum (YYYY-MM)
const entryDate = row.date.substring(0, 10);
const fy = entryDate.substring(0, 4);
const period = entryDate.substring(0, 7);
const body = {
entry_date: entryDate,
description: row.description || `Banktransaktion ${entryDate}`,
reference: row.raw?.substring(0, 200),
source_type: 'import_csv',
source_id: `${resolvedBank}:${correlation_id}:${rows_imported + skipped}`,
lines,
period,
fiscal_year: parseInt(fy, 10),
metadata: {
bank: resolvedBank,
original_amount: amount,
balance: row.balance,
correlation_id,
import_service: 'aamos-bank-import',
},
};
const response = await fetch(`${LEDGER_BASE}/api/ledger/journal`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Trace-Id': trace_id,
'X-User-Id': 'bank-import',
'X-Tenant-Id': resolved_tenant,
'X-Correlation-Id': correlation_id,
},
body: JSON.stringify(body),
});
const result = await response.json();
if (!response.ok || !result.ok) {
skipped++;
errors.push({
row: row.raw,
reason: result.error || `HTTP ${response.status}`,
details: result,
});
} else {
rows_imported++;
}
} catch (err) {
skipped++;
errors.push({ row: row.raw, reason: err.message });
}
}
res.json({
ok: true,
bank: resolvedBank,
rows_parsed,
rows_imported,
skipped,
errors: errors.slice(0, 50), // max 50 errors i svaret
correlation_id,
trace_id,
dry_run: false,
});
});
// ── Start ─────────────────────────────────────────────────────────────────────
app.listen(PORT, () => {
console.log(`[aamos-bank-import] Lyssnar på port ${PORT}`);
console.log(`[aamos-bank-import] Ledger Engine: ${LEDGER_BASE}`);
});