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