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
/**
|
|
* 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}`);
|
|
});
|