Files
0pt/lib/data.ts
T
adminandClaude Opus 4.8 125b2e1bbe feat(i18n): localize data-layer labels (lib/data + lib/services) — English base complete
All display labels in MARKETS/ENTITY_TYPES/FIELDS/ENRICHMENTS/FREQUENCIES/DELIVERY_FORMATS + SERVICES/FLOW_LABEL are now {en,sv} with stable IDs/values (pricing logic, EntityType/Frequency unions, config keys unchanged). Added entityLabel()/FREQUENCY_LABEL/FORMAT_LABEL. Updated all render sites (configurator, tjanster, beskriv, katalog/[id]) to resolve via useT(). Also fixed 2 pre-existing tsc nits (auth BufferSource cast, orders-store sql.json param type). tsc 0, build green. 0pt is now fully English-by-default with SV toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:47:42 +02:00

194 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Central mock data + the live-estimate pricing model shared by the order flow.
// No real backend yet — the configurator's price/timeline/confidence come from
// computeEstimate(); replace with a call to the FastAPI /estimates later.
//
// i18n: IDs/values (used as keys + in pricing logic) stay stable; only DISPLAY
// labels are localized as { en, sv } and resolved with useT() at the render site.
export type ProvLevel = 0 | 1 | 2 | 3; // 0 licence/none · 1 one source · 2 partial · 3 multi-source
export type Loc = { en: string; sv: string };
export interface Market {
code: string;
name: Loc;
live: boolean;
coverage: number; // %
}
// 20 European markets (Nordics live first, rest coming).
export const MARKETS: Market[] = [
{ code: "SE", name: { en: "Sweden", sv: "Sverige" }, live: true, coverage: 98 },
{ code: "NO", name: { en: "Norway", sv: "Norge" }, live: true, coverage: 96 },
{ code: "DK", name: { en: "Denmark", sv: "Danmark" }, live: true, coverage: 94 },
{ code: "FI", name: { en: "Finland", sv: "Finland" }, live: true, coverage: 89 },
{ code: "IS", name: { en: "Iceland", sv: "Island" }, live: false, coverage: 0 },
{ code: "NL", name: { en: "Netherlands", sv: "Nederländerna" }, live: false, coverage: 0 },
{ code: "DE", name: { en: "Germany", sv: "Tyskland" }, live: false, coverage: 0 },
{ code: "BE", name: { en: "Belgium", sv: "Belgien" }, live: false, coverage: 0 },
{ code: "IE", name: { en: "Ireland", sv: "Irland" }, live: false, coverage: 0 },
{ code: "FR", name: { en: "France", sv: "Frankrike" }, live: false, coverage: 0 },
{ code: "GB", name: { en: "United Kingdom", sv: "Storbritannien" }, live: false, coverage: 0 },
{ code: "AT", name: { en: "Austria", sv: "Österrike" }, live: false, coverage: 0 },
{ code: "CH", name: { en: "Switzerland", sv: "Schweiz" }, live: false, coverage: 0 },
{ code: "IT", name: { en: "Italy", sv: "Italien" }, live: false, coverage: 0 },
{ code: "ES", name: { en: "Spain", sv: "Spanien" }, live: false, coverage: 0 },
{ code: "PT", name: { en: "Portugal", sv: "Portugal" }, live: false, coverage: 0 },
{ code: "PL", name: { en: "Poland", sv: "Polen" }, live: false, coverage: 0 },
{ code: "CZ", name: { en: "Czechia", sv: "Tjeckien" }, live: false, coverage: 0 },
{ code: "HU", name: { en: "Hungary", sv: "Ungern" }, live: false, coverage: 0 },
{ code: "LU", name: { en: "Luxembourg", sv: "Luxemburg" }, live: false, coverage: 0 },
];
// Stable IDs (also the stored value + pricing key). Display via entityLabel().
export type EntityType = "Bolag" | "Beslutsfattare" | "Fordon";
export const ENTITY_TYPES: { id: EntityType; label: Loc; mult: number }[] = [
{ id: "Bolag", label: { en: "Companies", sv: "Bolag" }, mult: 1.0 },
{ id: "Beslutsfattare", label: { en: "Decision-makers", sv: "Beslutsfattare" }, mult: 1.4 },
{ id: "Fordon", label: { en: "Vehicles", sv: "Fordon" }, mult: 1.2 },
];
export function entityLabel(id: EntityType): Loc {
return ENTITY_TYPES.find((e) => e.id === id)?.label ?? { en: id, sv: id };
}
export interface Field {
id: string;
name: Loc;
sub: Loc;
level: ProvLevel;
source: Loc;
freshness: Loc;
weight: number;
locked?: boolean;
}
// Each field carries its honest provenance: source · freshness · confidence level.
export const FIELDS: Field[] = [
{ id: "identity", name: { en: "Identity & registered office", sv: "Identitet & säte" }, sub: { en: "reg. no., name, address", sv: "org.nr, namn, adress" }, level: 3, source: { en: "GLEIF · Brreg", sv: "GLEIF · Brreg" }, freshness: { en: "daily", sv: "daglig" }, weight: 1.0, locked: true },
{ id: "lei", name: { en: "LEI / cross-border key", sv: "LEI / cross-border-nyckel" }, sub: { en: "GLEIF golden copy", sv: "GLEIF golden copy" }, level: 3, source: { en: "GLEIF", sv: "GLEIF" }, freshness: { en: "daily", sv: "daglig" }, weight: 0.8 },
{ id: "financials", name: { en: "Financials", sv: "Finansiellt" }, sub: { en: "revenue, profit, key ratios", sv: "omsättning, resultat, nyckeltal" }, level: 2, source: { en: "register", sv: "register" }, freshness: { en: "annual", sv: "årlig" }, weight: 1.6 },
{ id: "ubo", name: { en: "Beneficial owner (UBO)", sv: "Verklig huvudman (UBO)" }, sub: { en: "ownership chain", sv: "ägarkedja" }, level: 2, source: { en: "register", sv: "register" }, freshness: { en: "on change", sv: "vid ändring" }, weight: 1.4 },
{ id: "contact", name: { en: "Contact: email & phone", sv: "Kontakt: e-post & telefon" }, sub: { en: "licence required in DE/FR", sv: "licens krävs i DE/FR" }, level: 1, source: { en: "public web", sv: "publik webb" }, freshness: { en: "weekly", sv: "veckovis" }, weight: 1.8 },
{ id: "intent", name: { en: "Buying signals & intent", sv: "Köpsignaler & intent" }, sub: { en: "job ads, news, tech", sv: "jobbannonser, nyheter, tech" }, level: 1, source: { en: "web", sv: "webb" }, freshness: { en: "weekly", sv: "veckovis" }, weight: 1.5 },
];
export interface Enrichment {
id: string;
label: Loc;
weight: number;
}
export const ENRICHMENTS: Enrichment[] = [
{ id: "dedup", label: { en: "Dedup against my file", sv: "Dedup mot min fil" }, weight: 0.1 },
{ id: "crm", label: { en: "Match & enrich my CRM export", sv: "Matcha & berika min CRM-export" }, weight: 0.2 },
{ id: "sanctions", label: { en: "Sanctions / PEP flag", sv: "Sanktioner / PEP-flagg" }, weight: 0.15 },
{ id: "nace", label: { en: "Industry class (NACE/SNI)", sv: "Branschklassning (NACE/SNI)" }, weight: 0.1 },
{ id: "tech", label: { en: "Website & tech stack", sv: "Webbplats & teknikstack" }, weight: 0.15 },
];
// Stable values (stored + used by computeEstimate); display via FREQUENCY_LABEL.
export type Frequency = "Engångs" | "Veckovis" | "Dagligt flöde";
export const FREQUENCIES: Frequency[] = ["Engångs", "Veckovis", "Dagligt flöde"];
export const FREQUENCY_LABEL: Record<Frequency, Loc> = {
"Engångs": { en: "One-time", sv: "Engångs" },
"Veckovis": { en: "Weekly", sv: "Veckovis" },
"Dagligt flöde": { en: "Daily feed", sv: "Dagligt flöde" },
};
// Stable values (stored in config.formats); display via FORMAT_LABEL.
export const DELIVERY_FORMATS = [
"CSV",
"Tabellvy (NocoDB)",
"Parquet",
"JSON",
"Dashboard (Metabase)",
"API / webhook",
"Push → FiveCRM",
];
export const FORMAT_LABEL: Record<string, Loc> = {
"CSV": { en: "CSV", sv: "CSV" },
"Tabellvy (NocoDB)": { en: "Table view (NocoDB)", sv: "Tabellvy (NocoDB)" },
"Parquet": { en: "Parquet", sv: "Parquet" },
"JSON": { en: "JSON", sv: "JSON" },
"Dashboard (Metabase)": { en: "Dashboard (Metabase)", sv: "Dashboard (Metabase)" },
"API / webhook": { en: "API / webhook", sv: "API / webhook" },
"Push → FiveCRM": { en: "Push → FiveCRM", sv: "Push → FiveCRM" },
};
const FIELD_RATE = 240; // kr per field-weight per 1000 rows per market
export interface EstimateInput {
markets: string[];
type: EntityType;
fieldIds: string[];
enrichmentIds: string[];
volume: number;
frequency: Frequency;
confidence: number; // target 80–99
serviceMult?: number; // service-type price multiplier (see lib/services)
}
export interface Estimate {
price: number;
deposit: number;
remainder: number;
timelineDays: number;
estConfidence: number;
recurring: boolean;
}
export function computeEstimate(input: EstimateInput): Estimate {
const markets = Math.max(1, input.markets.length);
const fields = FIELDS.filter((f) => input.fieldIds.includes(f.id) || f.locked);
const fieldWeight = fields.reduce((s, f) => s + f.weight, 0) || 1;
const entityMult = ENTITY_TYPES.find((e) => e.id === input.type)?.mult ?? 1;
const confidenceMult = 1 + Math.max(0, input.confidence - 90) * 0.03;
const enrichWeight = ENRICHMENTS.filter((e) => input.enrichmentIds.includes(e.id)).reduce((s, e) => s + e.weight, 0);
const serviceMult = input.serviceMult ?? 1;
const base = fieldWeight * markets * FIELD_RATE * (input.volume / 1000) * entityMult * confidenceMult * serviceMult;
const price = Math.round((base * (1 + enrichWeight)) / 5) * 5; // round to 5 kr
const deposit = Math.round(price / 2);
const timelineDays = Math.min(
14,
Math.max(1, Math.round(1 + markets * 0.4 + input.volume / 6000 + fields.length * 0.25)),
);
// Honest estimated confidence: bounded by the provenance of the chosen fields.
const avgLevel = fields.reduce((s, f) => s + f.level, 0) / fields.length; // 0..3
const fieldCeiling = Math.round(82 + avgLevel * 5); // ~82..97
const estConfidence = Math.min(input.confidence, fieldCeiling);
return {
price,
deposit,
remainder: price - deposit,
timelineDays,
estConfidence,
recurring: input.frequency !== "Engångs",
};
}
export function formatSEK(n: number): string {
return new Intl.NumberFormat("sv-SE").format(Math.round(n)) + " kr";
}
// Sample Nordic rows reused by the validation table.
export interface SampleRow {
orgnr: string;
bolag: string;
sate: string;
sni: string;
anstallda: number;
oms: number; // tkr
epost: string;
epostLevel: ProvLevel;
lei: string;
}
export const SAMPLE_ROWS: SampleRow[] = [
{ orgnr: "556016-0680", bolag: "Volvo Group", sate: "Göteborg", sni: "29100", anstallda: 95000, oms: 553000000, epost: "info@volvo.com", epostLevel: 2, lei: "5493000XYZ..." },
{ orgnr: "556036-0793", bolag: "Spotify AB", sate: "Stockholm", sni: "63110", anstallda: 9800, oms: 132000000, epost: "press@spotify.com", epostLevel: 2, lei: "549300ABC..." },
{ orgnr: "915933149", bolag: "Equinor ASA", sate: "Stavanger", sni: "06100", anstallda: 22000, oms: 980000000, epost: "kontakt@equinor.no", epostLevel: 1, lei: "549300NOR..." },
{ orgnr: "25052943", bolag: "Maersk A/S", sate: "København", sni: "50200", anstallda: 100000, oms: 510000000, epost: "info@maersk.dk", epostLevel: 1, lei: "549300DNK..." },
{ orgnr: "0112038-9", bolag: "Nokia Oyj", sate: "Espoo", sni: "26300", anstallda: 86000, oms: 221000000, epost: "", epostLevel: 0, lei: "549300FIN..." },
];