Files
0pt/lib/data.ts
T
adminandClaude Opus 4.8 24035dec8b Build 0pt ordering flow: configurator, deposit/escrow, production tracker, validation
Customer order lifecycle on the mono design system: red + provenance tokens, useReveal hook, lib/data pricing model, ProvenanceBadge/CellProvenance, OrderStepper, and 4 routes with live pricing, 50/50 escrow, simulated production, 4-view validation. tsc clean; next build green (5 routes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 17:28:24 +02:00

168 lines
6.9 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.
export type ProvLevel = 0 | 1 | 2 | 3; // 0 licens/ej · 1 en källa · 2 delvis · 3 flera källor
export interface Market {
code: string;
name: string;
live: boolean;
coverage: number; // %
}
// 20 European markets (Nordics live first, rest "kommande").
export const MARKETS: Market[] = [
{ code: "SE", name: "Sverige", live: true, coverage: 98 },
{ code: "NO", name: "Norge", live: true, coverage: 96 },
{ code: "DK", name: "Danmark", live: true, coverage: 94 },
{ code: "FI", name: "Finland", live: true, coverage: 89 },
{ code: "IS", name: "Island", live: false, coverage: 0 },
{ code: "NL", name: "Nederländerna", live: false, coverage: 0 },
{ code: "DE", name: "Tyskland", live: false, coverage: 0 },
{ code: "BE", name: "Belgien", live: false, coverage: 0 },
{ code: "IE", name: "Irland", live: false, coverage: 0 },
{ code: "FR", name: "Frankrike", live: false, coverage: 0 },
{ code: "GB", name: "Storbritannien", live: false, coverage: 0 },
{ code: "AT", name: "Österrike", live: false, coverage: 0 },
{ code: "CH", name: "Schweiz", live: false, coverage: 0 },
{ code: "IT", name: "Italien", live: false, coverage: 0 },
{ code: "ES", name: "Spanien", live: false, coverage: 0 },
{ code: "PT", name: "Portugal", live: false, coverage: 0 },
{ code: "PL", name: "Polen", live: false, coverage: 0 },
{ code: "CZ", name: "Tjeckien", live: false, coverage: 0 },
{ code: "HU", name: "Ungern", live: false, coverage: 0 },
{ code: "LU", name: "Luxemburg", live: false, coverage: 0 },
];
export type EntityType = "Bolag" | "Beslutsfattare" | "Fordon";
export const ENTITY_TYPES: { id: EntityType; mult: number }[] = [
{ id: "Bolag", mult: 1.0 },
{ id: "Beslutsfattare", mult: 1.4 },
{ id: "Fordon", mult: 1.2 },
];
export interface Field {
id: string;
name: string;
sub: string;
level: ProvLevel;
source: string;
freshness: string;
weight: number;
locked?: boolean;
}
// Each field carries its honest provenance: source · freshness · confidence level.
export const FIELDS: Field[] = [
{ id: "identity", name: "Identitet & säte", sub: "org.nr, namn, adress", level: 3, source: "GLEIF · Brreg", freshness: "daglig", weight: 1.0, locked: true },
{ id: "lei", name: "LEI / cross-border-nyckel", sub: "GLEIF golden copy", level: 3, source: "GLEIF", freshness: "daglig", weight: 0.8 },
{ id: "financials", name: "Finansiellt", sub: "omsättning, resultat, nyckeltal", level: 2, source: "register", freshness: "årlig", weight: 1.6 },
{ id: "ubo", name: "Verklig huvudman (UBO)", sub: "ägarkedja", level: 2, source: "register", freshness: "vid ändring", weight: 1.4 },
{ id: "contact", name: "Kontakt: e-post & telefon", sub: "licens krävs i DE/FR", level: 1, source: "publik webb", freshness: "veckovis", weight: 1.8 },
{ id: "intent", name: "Köpsignaler & intent", sub: "jobbannonser, nyheter, tech", level: 1, source: "webb", freshness: "veckovis", weight: 1.5 },
];
export interface Enrichment {
id: string;
label: string;
weight: number;
}
export const ENRICHMENTS: Enrichment[] = [
{ id: "dedup", label: "Dedup mot min fil", weight: 0.1 },
{ id: "crm", label: "Matcha & berika min CRM-export", weight: 0.2 },
{ id: "sanctions", label: "Sanktioner / PEP-flagg", weight: 0.15 },
{ id: "nace", label: "Branschklassning (NACE/SNI)", weight: 0.1 },
{ id: "tech", label: "Webbplats & teknikstack", weight: 0.15 },
];
export type Frequency = "Engångs" | "Veckovis" | "Dagligt flöde";
export const FREQUENCIES: Frequency[] = ["Engångs", "Veckovis", "Dagligt flöde"];
export const DELIVERY_FORMATS = [
"CSV",
"Tabellvy (NocoDB)",
"Parquet",
"JSON",
"Dashboard (Metabase)",
"API / webhook",
"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
}
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 base = fieldWeight * markets * FIELD_RATE * (input.volume / 1000) * entityMult * confidenceMult;
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..." },
];