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
180 lines
4.8 KiB
JavaScript
180 lines
4.8 KiB
JavaScript
/**
|
|
* LandveX Finance — API Layer
|
|
* Sprint 3: UI-refaktorering
|
|
*
|
|
* Centraliserad fetch-wrapper med auth, timeout, retry och felhantering.
|
|
*/
|
|
|
|
const API_BASE = '';
|
|
const TENANT = 'landvex';
|
|
const DEFAULT_TIMEOUT = 10000;
|
|
const MAX_RETRIES = 1;
|
|
|
|
// ── Helpers ──
|
|
function getToken() {
|
|
// Försök hämta JWT från cookie eller localStorage
|
|
const match = document.cookie.match(/wavult_token=([^;]+)/);
|
|
if (match) return decodeURIComponent(match[1]);
|
|
try {
|
|
return localStorage.getItem('wavult:token');
|
|
} catch { return null; }
|
|
}
|
|
|
|
function buildHeaders() {
|
|
const h = {
|
|
'x-internal-source': 'ouroboros-system',
|
|
'x-tenant-id': TENANT,
|
|
};
|
|
const tok = getToken();
|
|
if (tok) h['Authorization'] = `Bearer ${tok}`;
|
|
return h;
|
|
}
|
|
|
|
// ── Core fetch ──
|
|
export async function api(path, method = 'GET', body = null, opts = {}) {
|
|
const url = path.startsWith('http') ? path : `${API_BASE}${path}`;
|
|
const timeout = opts.timeout || DEFAULT_TIMEOUT;
|
|
const retries = opts.retries ?? MAX_RETRIES;
|
|
|
|
const headers = { ...buildHeaders() };
|
|
if (body && method !== 'GET') {
|
|
headers['Content-Type'] = 'application/json';
|
|
}
|
|
|
|
let lastError;
|
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
const ctrl = new AbortController();
|
|
const t = setTimeout(() => ctrl.abort(), timeout);
|
|
|
|
try {
|
|
const options = {
|
|
method,
|
|
headers,
|
|
signal: ctrl.signal,
|
|
};
|
|
if (body && method !== 'GET') {
|
|
options.body = JSON.stringify(body);
|
|
}
|
|
|
|
const r = await fetch(url, options);
|
|
clearTimeout(t);
|
|
|
|
if (!r.ok) {
|
|
const err = new Error(`HTTP ${r.status}: ${r.statusText}`);
|
|
err.status = r.status;
|
|
err.code = `HTTP_${r.status}`;
|
|
throw err;
|
|
}
|
|
|
|
// Tomt svar (204)
|
|
if (r.status === 204) return null;
|
|
|
|
const contentType = r.headers.get('content-type') || '';
|
|
if (contentType.includes('application/json')) {
|
|
return await r.json();
|
|
}
|
|
return await r.text();
|
|
|
|
} catch (e) {
|
|
clearTimeout(t);
|
|
lastError = e;
|
|
|
|
// Retry vid nätverksfel eller 5xx
|
|
const shouldRetry = attempt < retries && (
|
|
e.name === 'AbortError' ||
|
|
e.name === 'TypeError' ||
|
|
(e.status >= 500 && e.status < 600)
|
|
);
|
|
|
|
if (!shouldRetry) break;
|
|
|
|
// Exponential backoff
|
|
await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
|
|
}
|
|
}
|
|
|
|
throw lastError;
|
|
}
|
|
|
|
// ── Convenience methods ──
|
|
export const get = (path, opts) => api(path, 'GET', null, opts);
|
|
export const post = (path, body, opts) => api(path, 'POST', body, opts);
|
|
export const put = (path, body, opts) => api(path, 'PUT', body, opts);
|
|
export const del = (path, opts) => api(path, 'DELETE', null, opts);
|
|
|
|
// ── Typed API wrappers ──
|
|
export async function getJournal(params = {}) {
|
|
const qs = new URLSearchParams(params).toString();
|
|
return get(`/api/ledger/journal?${qs}`);
|
|
}
|
|
|
|
export async function getTrialBalance(fiscalYear, period) {
|
|
const qs = new URLSearchParams({ fiscal_year: fiscalYear, period }).toString();
|
|
return get(`/api/ledger/trial-balance?${qs}`);
|
|
}
|
|
|
|
export async function getAccounts() {
|
|
return get('/api/ledger/accounts');
|
|
}
|
|
|
|
export async function getPeriods(fiscalYear) {
|
|
return get(`/api/ledger/periods?fiscal_year=${fiscalYear}`);
|
|
}
|
|
|
|
export async function getInvoices(fiscalYear) {
|
|
return get(`/api/ledger/invoices?fiscal_year=${fiscalYear}`);
|
|
}
|
|
|
|
export async function createJournalEntry(entry) {
|
|
return post('/api/ledger/journal', entry);
|
|
}
|
|
|
|
export async function postJournalEntry(id) {
|
|
return post(`/api/ledger/journal/${id}/post`, {});
|
|
}
|
|
|
|
export async function createInvoice(invoice) {
|
|
return post('/api/ledger/invoices', invoice);
|
|
}
|
|
|
|
export async function getTasks() {
|
|
return get('/api/ledger/tasks');
|
|
}
|
|
|
|
export async function uploadReceipt(entryId, file) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const url = `/api/ledger/journal/${entryId}/upload`;
|
|
const headers = buildHeaders();
|
|
delete headers['Content-Type']; // Låt browser sätta boundary
|
|
|
|
const r = await fetch(url, {
|
|
method: 'POST',
|
|
headers,
|
|
body: formData,
|
|
});
|
|
|
|
if (!r.ok) throw new Error(`Upload failed: ${r.status}`);
|
|
return r.json();
|
|
}
|
|
|
|
export async function getReceipts(entryId) {
|
|
return get(`/api/ledger/journal/${entryId}/receipts`);
|
|
}
|
|
|
|
export async function exportSIE4(fiscalYear) {
|
|
return get(`/api/ledger/export/sie4?fiscal_year=${fiscalYear}`);
|
|
}
|
|
|
|
// ── Health ──
|
|
export async function getHealth() {
|
|
return get('/health');
|
|
}
|
|
|
|
export async function getFinanceHealth() {
|
|
return get('/health/finance');
|
|
}
|
|
|
|
export default { api, get, post, put, del, getJournal, getTrialBalance, getAccounts, getPeriods, getInvoices, createJournalEntry, postJournalEntry, createInvoice, getTasks, uploadReceipt, getReceipts, exportSIE4, getHealth, getFinanceHealth };
|