// 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 = { "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 = { "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..." }, ];