161 lines
5.2 KiB
JavaScript
161 lines
5.2 KiB
JavaScript
|
|
/**
|
||
|
|
* AAMOS SIE4 Import Service
|
||
|
|
* Port: 3251
|
||
|
|
*/
|
||
|
|
|
||
|
|
import express from 'express';
|
||
|
|
import { randomUUID } from 'crypto';
|
||
|
|
import { parseSIE4 } from './parser.mjs';
|
||
|
|
|
||
|
|
const PORT = parseInt(process.env.PORT || '3251', 10);
|
||
|
|
const LEDGER_BASE = process.env.LEDGER_URL || 'http://127.0.0.1:3250';
|
||
|
|
|
||
|
|
const app = express();
|
||
|
|
app.use(express.json({ limit: '10mb' }));
|
||
|
|
|
||
|
|
// ── Health ───────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
app.get('/health', (_req, res) => {
|
||
|
|
res.json({ ok: true, service: 'aamos-sie-import', port: PORT });
|
||
|
|
});
|
||
|
|
|
||
|
|
// ── SIE4 Import ──────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
app.post('/api/sie/import', async (req, res) => {
|
||
|
|
const { sie_content, tenant_id, dry_run = false } = req.body ?? {};
|
||
|
|
|
||
|
|
if (!sie_content || typeof sie_content !== 'string') {
|
||
|
|
return res.status(400).json({ ok: false, error: 'sie_content (string) is required' });
|
||
|
|
}
|
||
|
|
|
||
|
|
const trace_id = randomUUID();
|
||
|
|
const correlation_id = randomUUID();
|
||
|
|
|
||
|
|
// ── Parse ────────────────────────────────────────────────────────────────
|
||
|
|
let parsed;
|
||
|
|
try {
|
||
|
|
parsed = parseSIE4(sie_content);
|
||
|
|
} catch (err) {
|
||
|
|
return res.status(400).json({
|
||
|
|
ok: false,
|
||
|
|
error: `SIE4 parse error: ${err.message}`,
|
||
|
|
trace_id,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const { journal_entries } = parsed;
|
||
|
|
|
||
|
|
// ── Dry run ───────────────────────────────────────────────────────────────
|
||
|
|
if (dry_run) {
|
||
|
|
return res.json({
|
||
|
|
ok: true,
|
||
|
|
dry_run: true,
|
||
|
|
imported_count: 0,
|
||
|
|
skipped_count: journal_entries.length,
|
||
|
|
parsed_entries: journal_entries.length,
|
||
|
|
company: parsed.company,
|
||
|
|
org_nr: parsed.org_nr,
|
||
|
|
fiscal_year: parsed.fiscal_year,
|
||
|
|
accounts_count: parsed.accounts.length,
|
||
|
|
errors: [],
|
||
|
|
trace_id,
|
||
|
|
correlation_id,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Live import ───────────────────────────────────────────────────────────
|
||
|
|
const errors = [];
|
||
|
|
let imported_count = 0;
|
||
|
|
let skipped_count = 0;
|
||
|
|
|
||
|
|
const baseHeaders = {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'X-Tenant-Id': tenant_id || '',
|
||
|
|
'X-User-Id': 'sie-import',
|
||
|
|
'X-Trace-Id': trace_id,
|
||
|
|
'X-Correlation-Id': correlation_id,
|
||
|
|
};
|
||
|
|
|
||
|
|
for (const entry of journal_entries) {
|
||
|
|
const entryRef = `${entry.series}${entry.number}`;
|
||
|
|
|
||
|
|
const journalBody = {
|
||
|
|
date: entry.date,
|
||
|
|
description: entry.description,
|
||
|
|
source_type: 'import_sie',
|
||
|
|
series: entry.series,
|
||
|
|
series_number: entry.number,
|
||
|
|
correlation_id,
|
||
|
|
lines: entry.transactions.map(t => ({
|
||
|
|
account: t.account,
|
||
|
|
amount: t.amount,
|
||
|
|
date: t.date,
|
||
|
|
description: t.description,
|
||
|
|
})),
|
||
|
|
};
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 1. Create journal entry
|
||
|
|
const createResp = await fetch(`${LEDGER_BASE}/api/ledger/journal`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: baseHeaders,
|
||
|
|
body: JSON.stringify(journalBody),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!createResp.ok) {
|
||
|
|
const text = await createResp.text().catch(() => createResp.status.toString());
|
||
|
|
errors.push({ entry: entryRef, step: 'create', error: text });
|
||
|
|
skipped_count++;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
const created = await createResp.json().catch(() => null);
|
||
|
|
const journalId =
|
||
|
|
created?.id ??
|
||
|
|
created?.journal_id ??
|
||
|
|
created?.data?.id ??
|
||
|
|
null;
|
||
|
|
|
||
|
|
if (!journalId) {
|
||
|
|
errors.push({ entry: entryRef, step: 'create', error: 'No journal ID in response' });
|
||
|
|
skipped_count++;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. Post / commit the journal entry
|
||
|
|
const postResp = await fetch(`${LEDGER_BASE}/api/ledger/journal/${journalId}/post`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: baseHeaders,
|
||
|
|
body: JSON.stringify({}),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!postResp.ok) {
|
||
|
|
const text = await postResp.text().catch(() => postResp.status.toString());
|
||
|
|
errors.push({ entry: entryRef, step: 'post', error: text });
|
||
|
|
skipped_count++;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
imported_count++;
|
||
|
|
} catch (err) {
|
||
|
|
errors.push({ entry: entryRef, step: 'network', error: err.message });
|
||
|
|
skipped_count++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
ok: errors.length === 0,
|
||
|
|
imported_count,
|
||
|
|
skipped_count,
|
||
|
|
errors,
|
||
|
|
trace_id,
|
||
|
|
correlation_id,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
app.listen(PORT, '127.0.0.1', () => {
|
||
|
|
console.log(`[aamos-sie-import] Listening on 127.0.0.1:${PORT}`);
|
||
|
|
});
|