diff --git a/felsokning/app/e2e/genomgang.mjs b/felsokning/app/e2e/genomgang.mjs index 3370afc..b6d4b1b 100644 --- a/felsokning/app/e2e/genomgang.mjs +++ b/felsokning/app/e2e/genomgang.mjs @@ -484,6 +484,8 @@ try { "/alva/portal/garantier", "/alva/portal/forsakring", "/alva/portal/support", + "/alva/impressum", + "/alva/utgavor", ]) { await sida.goto(`${BAS}/#${vag}`, { waitUntil: "networkidle" }); await sida.waitForTimeout(400); diff --git a/felsokning/app/src/App.tsx b/felsokning/app/src/App.tsx index 9145cb6..ebc0623 100644 --- a/felsokning/app/src/App.tsx +++ b/felsokning/app/src/App.tsx @@ -30,6 +30,8 @@ import AlvaAnalys from "./pages/alva/Analys"; import AlvaFakturor from "./pages/alva/Fakturor"; import AlvaIntegration from "./pages/alva/Integration"; import AlvaGarantier from "./pages/alva/Garantier"; +import AlvaImpressum from "./pages/alva/Impressum"; +import AlvaUtgavor from "./pages/alva/Utgavor"; import AlvaForsakring from "./pages/alva/Forsakring"; import AlvaSupport from "./pages/alva/Support"; import { Portalvakt } from "./pages/alva/Portalvakt"; @@ -69,6 +71,10 @@ function AppContent() { } /> } /> } /> + {/* Lagstadgad information. Nås från foten på varje sida — det är + en del av kravet, inte en placering. */} + } /> + } /> {/* Portalen är stängd utan giltig plattformssession — se Portalvakt. Utan konfigurerad plattform finns ingen session att kräva, och vyerna är då märkta som demonstration. */} diff --git a/felsokning/app/src/alva/system.ts b/felsokning/app/src/alva/system.ts index 274660b..a47b79c 100644 --- a/felsokning/app/src/alva/system.ts +++ b/felsokning/app/src/alva/system.ts @@ -161,7 +161,11 @@ export const DOKUMENTKLASSER = [ // inte Pro Max. En version är ett tillstånd av systemet, inte en // marknadsföringshändelse. -export const PLATTFORMSVERSION = "ALVA 1.0"; +// Versionen bor i services/gemensam/version.mjs tillsammans med +// historiken. Den stod tidigare som en sträng här och räknades aldrig +// upp: systemet påstod "ALVA 1.0" efter tre revisioner, en fakturamodul +// och en stängd portal, och versionen sa alltså ingenting om vad man fick. +export { PLATTFORMSVERSION, UTGAVOR, NUVARANDE } from "../../../services/gemensam/version.mjs"; // ---- Rutnät ----------------------------------------------------------- // diff --git a/felsokning/app/src/felsokning/__tests__/grind.test.ts b/felsokning/app/src/felsokning/__tests__/grind.test.ts index 983db54..f029e4c 100644 --- a/felsokning/app/src/felsokning/__tests__/grind.test.ts +++ b/felsokning/app/src/felsokning/__tests__/grind.test.ts @@ -20,8 +20,10 @@ function komplettLogg(extra: Record[] = []) { return [ { typ: "objekt_identifierat", objekt: { identifierare: "ABC123" } }, { typ: "historik_kontrollerad", kontrollerad: true }, - { typ: "matarstallning", lage: "ingaende", varde: "14 200" }, - { typ: "matarstallning", lage: "utgaende", varde: "14 205" }, + // Fotot är en del av kravet, inte en utsmyckning: en inskriven siffra + // är teknikerns påstående om vad som stod på mätaren. + { typ: "matarstallning", lage: "ingaende", varde: "14 200", bilagaId: "bil-in" }, + { typ: "matarstallning", lage: "utgaende", varde: "14 205", bilagaId: "bil-ut" }, { typ: "reproducering", status: "ja", beskrivning: "Reproducerat vid 88 km/h." }, { typ: "felorsak", avvikelse: "Obalans", orsaker: ["Normalt slitage"], underlag: ["Mätvärde"], sakerhet: "hog" }, { typ: "atgard_utford", beskrivning: "Balanserade hjulen.", utford: true }, @@ -450,3 +452,49 @@ describe("grinden räknar rätt sorts evidens per kontrolltyp", () => { expect(hinder).toContain("metodik_kontroller"); }); }); + +// ---- Mätarställningen ska vara fotograferad ---------------------------- +// +// Klienten krävde redan fotot. Grinden gjorde det inte, och en regel som +// bara finns i gränssnittet är en vana — inte en spärr. Det är samma +// förhållande som C-2 gällde, fast åt andra hållet: här var det servern +// som var svagare, vilket är värre, eftersom servern är den auktoritativa. +describe("mätarställningen ska vara fotograferad, inte bara inskriven", () => { + const utanFoto = () => + komplettLogg().map((h) => (h.typ === "matarstallning" ? { ...h, bilagaId: undefined } : h)); + + it("en inskriven siffra utan foto spärrar avslutet", () => { + const hinder = grinda(utanFoto(), GENERISK).map((h) => h.id); + expect(hinder).toContain("matarstallning_ingaende"); + expect(hinder).toContain("matarstallning_utgaende"); + }); + + it("hindret säger vad som saknas och vad man gör åt det", () => { + const h = grinda(utanFoto(), GENERISK).find((x) => x.id === "matarstallning_ingaende"); + expect(h.detalj).toMatch(/fotografera/i); + }); + + it("ett dataUrl-foto duger lika bra som en bilaga", () => { + // Offline-läget lagrar bilden inline tills den synkats. + const inline = komplettLogg().map((h) => + h.typ === "matarstallning" ? { ...h, bilagaId: undefined, dataUrl: "data:image/png;base64,AA" } : h, + ); + expect(grinda(inline, GENERISK)).toEqual([]); + }); + + it("ett motiverat undantag ersätter fotot — trasig display, oåtkomlig timräknare", () => { + const undantag = komplettLogg().map((h) => + h.typ === "matarstallning" + ? { ...h, bilagaId: undefined, undantag: "Displayen är slocknad, avläst via diagnosverktyg." } + : h, + ); + expect(grinda(undantag, GENERISK)).toEqual([]); + }); + + it("ett tomt undantag räcker inte — då vore det en väg runt kravet", () => { + const tomt = komplettLogg().map((h) => + h.typ === "matarstallning" ? { ...h, bilagaId: undefined, undantag: " " } : h, + ); + expect(grinda(tomt, GENERISK).map((h) => h.id)).toContain("matarstallning_ingaende"); + }); +}); diff --git a/felsokning/app/src/felsokning/domain.ts b/felsokning/app/src/felsokning/domain.ts index 970e612..afbd1f8 100644 --- a/felsokning/app/src/felsokning/domain.ts +++ b/felsokning/app/src/felsokning/domain.ts @@ -229,7 +229,17 @@ export type Handelse = */ orsakFastställd?: boolean; } - | { typ: "arende_avslutat"; signatur?: string }; + | { + typ: "arende_avslutat"; + signatur?: string; + /** + * Versionen som gällde vid avslutet. Ritningsstämpeln läste den ur + * den aktuella konstanten, så ett ärende stängt under 1.0 visade + * 3.1 — precis det stämpelns egen beskrivning säger att den inte + * ska göra. + */ + plattformsversion?: string; + }; export interface LoggPost { id: string; diff --git a/felsokning/app/src/felsokning/ecm.ts b/felsokning/app/src/felsokning/ecm.ts index 45dd766..666d639 100644 --- a/felsokning/app/src/felsokning/ecm.ts +++ b/felsokning/app/src/felsokning/ecm.ts @@ -119,7 +119,21 @@ export function evidensposter(arende: Arende): Evidenspost[] { `${h.beskrivning} = ${h.varde}${h.enhet ? ` ${h.enhet}` : ""} (${märkning})`, ); } - if (h.typ === "matarstallning" && !h.undantag) lagg(post, "mätarställning", "E2", `${h.lage === "ingaende" ? "In" : "Ut"}: ${h.varde}`); + if (h.typ === "matarstallning" && !h.undantag) { + // Ett foto på mätaren är E2 — något som visar vad som stod där. + // En inskriven siffra utan foto är teknikerns påstående om vad som + // stod där, alltså E1. Skillnaden syns i rapporten, eftersom + // mätarställningen är det som avgör garanti- och försäkringsfrågor + // i efterhand och den enda uppgift någon kan ha ett intresse av att + // skriva fel. + const fotograferad = Boolean(h.bilagaId || h.dataUrl); + lagg( + post, + "mätarställning", + fotograferad ? "E2" : "E1", + `${h.lage === "ingaende" ? "In" : "Ut"}: ${h.varde}${fotograferad ? "" : " (inskriven, ej fotograferad)"}`, + ); + } if (h.typ === "arbetsorder_skannad") lagg(post, "dokument", "E5", `Arbetsorder, ${h.falt.length} fält`); if (h.typ === "observation") lagg(post, "observation", "E1", h.text); if (h.typ === "kontroll_utford" && !h.undantag) lagg(post, "kontroll", "E1", h.text); diff --git a/felsokning/app/src/pages/alva/Impressum.tsx b/felsokning/app/src/pages/alva/Impressum.tsx new file mode 100644 index 0000000..c59bdda --- /dev/null +++ b/felsokning/app/src/pages/alva/Impressum.tsx @@ -0,0 +1,138 @@ +// ALVA-SPEC-050 · Impressum och lagstadgad information. +// +// Sidan visar två saker: uppgifterna, och — så länge de inte är satta — +// exakt vilka som fattas och med vilken rättslig grund. +// +// Att fylla rutorna med påhittade uppgifter vore inte ett halvfärdigt +// Impressum utan ett vilseledande, och skadan större än den tomma rutans. +// Därför står det som saknas som en åtgärdslista i stället. + +import { + AVSKAFFAT, + FALT, + HANDLINGAR, + saknade, +} from "../../../../services/gemensam/foretagsuppgifter.mjs"; +import { Block, Etikett, FARG, Rubrik, Tabell } from "@/alva/komponenter"; +import { Ram } from "./Ram"; + +interface Falt { + id: string; + rubrik: string; + grund: string; + kravs: boolean; + hjalp: string; +} +interface Handling { + id: string; + rubrik: string; + grund: string; + kravs: boolean; + nar: string; +} + +// Driftens uppgifter. Sätts vid driftsättning; tomt betyder tomt. +const UPPGIFTER: Record = {}; + +export default function Impressum() { + const fattas = saknade(UPPGIFTER) as { id: string; rubrik: string; grund: string }[]; + + return ( + +
+ Legal +
+ Impressum +
+

+ Angaben gemäß § 5 DDG. The requirement set is German because it is the strictest and the most + enforceable in the EU — meeting it meets the other markets as a by-product, and spares a + second review per country. +

+ + {fattas.length > 0 && ( + +

+ The operator’s details have not been configured. They are shown as outstanding rather + than filled with placeholder text: an Impressum with an invented address is not an + incomplete Impressum but a misleading one, and the harm is greater than an empty field. +

+ [ + + {f.rubrik} + , + + {f.grund} + , + ])} + /> +
+ )} + + + [ + + {f.rubrik} + {!f.kravs && ( + + conditional + + )} + , + + {UPPGIFTER[f.id] || "— not set"} + , + + {f.grund} + , + + {f.hjalp} + , + ])} + /> + + + + [ + + {h.rubrik} + , + + {h.grund} + , + + {h.nar} + , + ])} + /> + + + {/* Den här rutan är sidans egentliga behållning. Två hänvisningar + som legat i tiotusentals fotnoter är numera fel, och bägge ser + fortfarande ut som upplysningar. */} + + [ + + {a.rubrik} + , + + {a.grund} + , + + {a.text} + , + ])} + /> + +
+
+ ); +} diff --git a/felsokning/app/src/pages/alva/Ram.tsx b/felsokning/app/src/pages/alva/Ram.tsx index 92e81d8..bcecfb1 100644 --- a/felsokning/app/src/pages/alva/Ram.tsx +++ b/felsokning/app/src/pages/alva/Ram.tsx @@ -108,13 +108,43 @@ export function Ram({ children, portal = false }: { children: ReactNode; portal?
{children}
+ {/* Foten bär det som en näringsidkare måste hålla synligt. Listan + är byggd mot tyskt krav — § 5 DDG är den strängaste och mest + utkrävbara i EU, och den som klarar den klarar de andra på + köpet. Se services/gemensam/foretagsuppgifter.mjs. + + Här ligger MEDVETET ingen länk till EU:s ODR-plattform. Den + upphävdes genom förordning (EU) 2024/3228 och stängdes den + 20 juli 2025; en kvarliggande länk är en död hänvisning som ser + ut som en lagstadgad upplysning. */}
-
- {PLATTFORMSVERSION} - {ALVA.position} +
+ +
+ + {PLATTFORMSVERSION} + + {ALVA.position} +
diff --git a/felsokning/app/src/pages/alva/Utgavor.tsx b/felsokning/app/src/pages/alva/Utgavor.tsx new file mode 100644 index 0000000..6e77acc --- /dev/null +++ b/felsokning/app/src/pages/alva/Utgavor.tsx @@ -0,0 +1,75 @@ +// ALVA-SPEC-002 · Utgåvor. +// +// Historiken är härledd ur vad som faktiskt levererats, och varje post +// pekar på den commit där arbetet finns. En utgåva som inte går att peka +// på hör inte hemma i listan. + +import { NUVARANDE, UTGAVOR } from "../../../../services/gemensam/version.mjs"; +import { Block, Etikett, FARG, Rubrik } from "@/alva/komponenter"; +import { Ram } from "./Ram"; + +interface Utgava { + version: string; + datum: string; + commit: string; + rubrik: string; + stor?: boolean; + andringar: string[]; +} + +export default function Utgavor() { + const utgavor = UTGAVOR as Utgava[]; + const nu = NUVARANDE as Utgava; + + return ( + +
+ Release notes +
+ Versions +
+

+ Current release ALVA {nu.version}, {nu.datum}. The second + digit moves for new capability. The first moves only when the guarantees change — + what the system undertakes about a record. That has happened twice. +

+ +
+ {utgavor.map((u) => ( + +
+ + {u.datum} + + {u.stor && ( + + Guarantees changed + + )} +
+
    + {u.andringar.map((a) => ( +
  • + {a} +
  • + ))} +
+
+ ))} +
+ + +

+ ALVA 1.0, ALVA 1.1, ALVA 2.0. Not Summer Edition, not AI Edition, not Pro Max. A version is + a state of the system, not a marketing event. A release that cannot be pointed at in the + history does not belong in this list — an invented changelog is the same unfounded + certainty the product exists to avoid. +

+
+
+
+ ); +} diff --git a/felsokning/app/src/pages/felsokning/ArendeSida.tsx b/felsokning/app/src/pages/felsokning/ArendeSida.tsx index d2eb54e..2bd2e9b 100644 --- a/felsokning/app/src/pages/felsokning/ArendeSida.tsx +++ b/felsokning/app/src/pages/felsokning/ArendeSida.tsx @@ -10,6 +10,7 @@ import { Slutsatspanel } from "@/felsokning/Slutsats"; import { sammanfatta } from "../../../../services/gemensam/sammanfattning.mjs"; import { grinda } from "../../../../services/gemensam/grind.mjs"; import { PLATTFORMSVERSION, arendebeteckning, fasDefinition } from "@/alva/system"; +import { versionVidAvslut } from "../../../../services/gemensam/version.mjs"; import { arAvslutat, arendeidentitet, @@ -1342,7 +1343,7 @@ function GuideFlik({ Om felorsaken inte är verifierad: dokumentera en hypotes och utöka felsökningen, eller avsluta ärendet med rekommenderade nästa steg.

- skicka({ typ: "arende_avslutat", signatur: anvandare })}> + skicka({ typ: "arende_avslutat", signatur: anvandare, plattformsversion: PLATTFORMSVERSION })}> Avsluta felsökning {!kanAvslutas && } @@ -1423,7 +1424,7 @@ function GuideFlik({ setVisaOverlamning(true)}> Lämna över arbete - skicka({ typ: "arende_avslutat", signatur: anvandare })}> + skicka({ typ: "arende_avslutat", signatur: anvandare, plattformsversion: PLATTFORMSVERSION })}> Avsluta felsökning @@ -2697,7 +2698,11 @@ function Ritningsstampel({ arende }: { arende: Arende }) { ["Beteckning", arendebeteckning(arende.nummer)], ["Regelpaket", paket.version], ["Evidensmodell", `ECM ${ECM_VERSION}`], - ["Plattform", PLATTFORMSVERSION], + // Versionen VID AVSLUTET, inte dagens. Stämpeln läste tidigare den + // aktuella konstanten, så ett ärende stängt under 1.0 visade 3.1 — + // vilket är exakt det stämpelns beskrivning ovan säger att den inte + // ska göra. Ett öppet ärende visar dagens, eftersom det är sant då. + ["Plattform", avslutad ? versionVidAvslut(avslutad.handelse) : PLATTFORMSVERSION], ["Status", avslutad ? "Closed" : "In progress"], ["Utgåva", avslutad ? new Date(avslutad.tidpunkt).toISOString().slice(0, 10) : "—"], ]; diff --git a/felsokning/services/gemensam/foretagsuppgifter.mjs b/felsokning/services/gemensam/foretagsuppgifter.mjs new file mode 100644 index 0000000..3cc2204 --- /dev/null +++ b/felsokning/services/gemensam/foretagsuppgifter.mjs @@ -0,0 +1,198 @@ +// ALVA-SPEC-050 · Företagsuppgifter och lagstadgad information. +// +// ---- Varför tyskt krav som golv ---------------------------------------- +// +// Tyskland har den strängaste kravbilden på vad en näringsidkare måste +// hålla synligt: Impressumspflicht enligt § 5 DDG är namngiven, +// utkrävbar och avmahnbar på ett sätt som saknar motsvarighet i övriga +// EU. Bygger man för den listan uppfyller man i praktiken de andra +// marknadernas krav på köpet, och man slipper en andra genomgång per +// land. Det är därför listan nedan är tysk och inte svensk. +// +// ---- Vad modulen INTE gör ----------------------------------------------- +// +// Den hittar inte på uppgifter. Ett Impressum med påhittad adress eller +// påhittat organisationsnummer är inte ett halvfärdigt Impressum utan ett +// vilseledande, och skadan är större än den tomma rutans. +// +// Osatta fält märks därför som osatta, och `saknade()` returnerar dem så +// att driften kan spärra en driftsättning på listan i stället för att +// upptäcka den när brevet kommer. +// +// ---- Två fällor som är aktuella just nu --------------------------------- +// +// ODR-LÄNKEN SKA BORT. Förordning (EU) 524/2013 upphävdes genom +// förordning (EU) 2024/3228 och plattformen stängdes 20 juli 2025. +// Länken till ec.europa.eu/odr har legat i tiotusentals fotnoter sedan +// 2016 och pekar numera ingenstans. Den som låtit den ligga kvar visar +// upp en död hänvisning som en lagstadgad upplysning. +// +// § 5 TMG HETER § 5 DDG. Telemediengesetz ersattes av +// Digitale-Dienste-Gesetz den 14 maj 2024. Innehållet i plikten är +// oförändrat; hänvisningen är det inte. + +/** Fälten, med sin rättsliga grund. `kravs` styr vad `saknade()` larmar om. */ +export const FALT = [ + { + id: "foretagsnamn", + rubrik: "Firma", + grund: "§ 5 Abs. 1 Nr. 1 DDG", + kravs: true, + hjalp: "Fullständigt firmanamn inklusive bolagsform (GmbH, AB, Ltd).", + }, + { + id: "adress", + rubrik: "Anschrift", + grund: "§ 5 Abs. 1 Nr. 1 DDG", + kravs: true, + hjalp: "Gatuadress. En postbox räcker inte — adressen ska gå att delge på.", + }, + { + id: "foretradare", + rubrik: "Vertretungsberechtigte", + grund: "§ 5 Abs. 1 Nr. 1 DDG", + kravs: true, + hjalp: "Behörig företrädare, t.ex. verkställande direktör eller Geschäftsführer.", + }, + { + id: "epost", + rubrik: "E-Mail", + grund: "§ 5 Abs. 1 Nr. 2 DDG", + kravs: true, + hjalp: "En adress som läses. Ett kontaktformulär ersätter den inte.", + }, + { + id: "telefon", + rubrik: "Telefon", + grund: "§ 5 Abs. 1 Nr. 2 DDG", + kravs: true, + hjalp: "Snabb elektronisk kontakt ska vara möjlig; telefon är det som normalt godtas.", + }, + { + id: "register", + rubrik: "Registergericht und -nummer", + grund: "§ 5 Abs. 1 Nr. 4 DDG", + kravs: true, + hjalp: "Handelsregister och HRB-nummer, eller motsvarande register i hemlandet.", + }, + { + id: "momsnummer", + rubrik: "Umsatzsteuer-Identifikationsnummer", + grund: "§ 27a UStG", + kravs: true, + hjalp: "VAT-nummer, om ett sådant finns.", + }, + { + id: "tillsyn", + rubrik: "Aufsichtsbehörde", + grund: "§ 5 Abs. 1 Nr. 3 DDG", + kravs: false, + hjalp: "Endast om verksamheten kräver tillstånd. Diagnostikverktyg gör normalt inte det.", + }, + { + id: "ansvarig_innehall", + rubrik: "Inhaltlich Verantwortlicher", + grund: "§ 18 Abs. 2 MStV", + kravs: false, + hjalp: "Krävs för journalistiskt-redaktionellt innehåll. En produktwebbplats behöver det sällan.", + }, + { + id: "dataskyddsombud", + rubrik: "Datenschutzbeauftragter", + grund: "Art. 37 DSGVO", + kravs: false, + hjalp: "Krävs vid regelbunden och systematisk övervakning i stor omfattning, eller känsliga uppgifter i stor omfattning.", + }, +]; + +/** + * Handlingar som ska finnas och nås från varje sida. + * + * `nar` säger när kravet inträder — flera av dem är nya, och en fot som + * byggdes för två år sedan saknar dem utan att någon märkt det. + */ +export const HANDLINGAR = [ + { + id: "impressum", + rubrik: "Impressum", + grund: "§ 5 DDG (ersatte § 5 TMG den 14 maj 2024)", + kravs: true, + nar: "Alltid, för kommersiella digitala tjänster.", + }, + { + id: "dataskydd", + rubrik: "Datenschutzerklärung", + grund: "Art. 13–14 DSGVO", + kravs: true, + nar: "Alltid när personuppgifter behandlas.", + }, + { + id: "villkor", + rubrik: "Allgemeine Geschäftsbedingungen", + grund: "§§ 305 ff. BGB", + kravs: true, + nar: "När standardvillkor används.", + }, + { + id: "tillganglighet", + rubrik: "Erklärung zur Barrierefreiheit", + grund: "BFSG (Barrierefreiheitsstärkungsgesetz), i kraft 28 juni 2025", + kravs: true, + nar: "Tjänster till konsument. Mikroföretag som endast tillhandahåller tjänster är undantagna.", + }, + { + id: "tvistlosning", + rubrik: "Verbraucherstreitbeilegung", + grund: "§ 36 VSBG", + kravs: true, + nar: "Besked om man deltar i tvistlösning inför konsumentskiljenämnd — även ett nej ska stå.", + }, + { + id: "visselblasare", + rubrik: "Hinweisgeberkanal", + grund: "HinSchG", + kravs: false, + nar: "Från 50 anställda.", + }, +]; + +/** + * Vad som INTE ska stå i foten längre. + * + * En upphävd hänvisning är inte harmlös: den ser ut som en upplysning och + * pekar ingenstans. + */ +export const AVSKAFFAT = [ + { + id: "odr", + rubrik: "Länk till EU:s ODR-plattform", + grund: "Förordning (EU) 524/2013 upphävd genom förordning (EU) 2024/3228", + text: + "Plattformen togs ur drift den 20 juli 2025 och tog inte emot nya ärenden efter 20 mars 2025. Länken ska tas bort helt, inte bytas ut.", + }, + { + id: "tmg", + rubrik: 'Hänvisning till "§ 5 TMG"', + grund: "Telemediengesetz ersatt av Digitale-Dienste-Gesetz 14 maj 2024", + text: "Plikten är oförändrad, men paragrafen heter § 5 DDG. Enklast är att inte hänvisa till paragrafen alls.", + }, +]; + +/** + * Vilka obligatoriska fält som saknas. + * + * Driften kan spärra en driftsättning på den här listan i stället för att + * upptäcka bristen när ett avmahnungsbrev kommer. + */ +export function saknade(uppgifter = {}) { + return FALT.filter((f) => f.kravs && !String(uppgifter[f.id] ?? "").trim()).map((f) => ({ + id: f.id, + rubrik: f.rubrik, + grund: f.grund, + })); +} + +/** Är uppgifterna kompletta nog för att visas som ett riktigt Impressum? */ +export function komplett(uppgifter = {}) { + return saknade(uppgifter).length === 0; +} diff --git a/felsokning/services/gemensam/grind.mjs b/felsokning/services/gemensam/grind.mjs index 06267f7..f17c3c1 100644 --- a/felsokning/services/gemensam/grind.mjs +++ b/felsokning/services/gemensam/grind.mjs @@ -53,11 +53,40 @@ export function grinda(handelser, metodik) { historik ? "Nekad historikkontroll kräver en motivering." : "Ingen historikkontroll dokumenterad.", ); + // Mätarställningen ska vara FOTOGRAFERAD, inte bara inskriven. + // + // Skillnaden är hela dess bevisvärde. En inskriven siffra är teknikerns + // påstående om vad som stod på mätaren; ett foto visar vad som stod + // där. Mätarställningen är samtidigt det som avgör garanti- och + // försäkringsfrågor i efterhand, och den enda uppgift i ärendet som + // någon kan ha ett intresse av att skriva fel. + // + // Klienten krävde redan fotot — men bara klienten, och en regel som + // bara finns i gränssnittet är en vana, inte en spärr. Det är samma + // förhållande som QUALITET C-2 gällde, fast åt andra hållet: här var + // det servern som var svagare, vilket är värre, eftersom servern är + // den som är auktoritativ. + // + // Undantag finns och måste finnas — en trasig display, en timräknare + // som sitter oåtkomligt, en spegling som inte går att fota bort. Men + // undantaget kräver en motivering, av samma skäl som ett nekat + // historiksvar gör det. for (const [lage, rubrik] of [ - ["ingaende", "Ingående mätarställning dokumenterad"], - ["utgaende", "Utgående mätarställning dokumenterad"], + ["ingaende", "Ingående mätarställning fotograferad"], + ["utgaende", "Utgående mätarställning fotograferad"], ]) { - krav(`matarstallning_${lage}`, rubrik, av(handelser, "matarstallning").some((h) => h.lage === lage)); + const poster = av(handelser, "matarstallning").filter((h) => h.lage === lage); + const dokumenterad = poster.length > 0; + const fotograferad = poster.some((h) => h.bilagaId || h.dataUrl); + const motiverat = poster.some((h) => (h.undantag ?? "").trim().length > 0); + krav( + `matarstallning_${lage}`, + rubrik, + dokumenterad && (fotograferad || motiverat), + !dokumenterad + ? "Ingen mätarställning dokumenterad." + : "Mätarställningen är inskriven men inte fotograferad. Fotografera mätaren eller ange varför det inte går.", + ); } krav( diff --git a/felsokning/services/gemensam/handelser.mjs b/felsokning/services/gemensam/handelser.mjs index e4e0744..afbeec2 100644 --- a/felsokning/services/gemensam/handelser.mjs +++ b/felsokning/services/gemensam/handelser.mjs @@ -103,7 +103,10 @@ export const VALFRIA_FÄLT = { kundbeslut: ["kommentar", "kontaktperson"], atgard_utford: ["delar", "motivering"], slutsats: ["atgardsval", "orsakFastställd"], - arende_avslutat: ["signatur"], + // Versionen som gällde VID AVSLUTET. Ritningsstämpeln läste den ur + // den aktuella konstanten, så ett ärende stängt under 1.0 visade 3.1 — + // precis det stämpelns egen beskrivning säger att den inte ska göra. + arende_avslutat: ["signatur", "plattformsversion"], }; /** diff --git a/felsokning/services/gemensam/version.mjs b/felsokning/services/gemensam/version.mjs new file mode 100644 index 0000000..feb2fa2 --- /dev/null +++ b/felsokning/services/gemensam/version.mjs @@ -0,0 +1,134 @@ +// ALVA-SPEC-002 · Utgåvor. +// +// ---- Varför versionen är en egen modul --------------------------------- +// +// Den stod som en sträng i gränssnittskoden och räknades aldrig upp. +// Systemet påstod "ALVA 1.0" efter att kvalitetsgrinden flyttats till +// servern, schemat stängts, faktureringen byggts, portalen stängts och +// tre revisioner härdats. Versionen sa alltså ingenting om vad man fick. +// +// Historiken nedan är HÄRLEDD UR VAD SOM FAKTISKT LEVERERATS, inte +// skriven för att se ut som en produkt med lång historia. Varje post +// pekar på den commit där arbetet finns. En utgåva som inte går att peka +// på hör inte hemma i listan — en påhittad ändringslogg är samma sorts +// obefogade säkerhet som produkten finns för att undvika. +// +// ---- Vad som gör en utgåva stor ----------------------------------------- +// +// Andra siffran räcker för ny funktion. Första siffran höjs bara när +// GARANTIERNA ändras — vad systemet lovar om ett underlag. Det har hänt +// två gånger: när fakturan blev ett härlett dokument i systemet, och när +// TÜV-härdningen ändrade vad loggen garanterar. + +export const UTGAVOR = [ + { + version: "3.1", + datum: "2026-08-06", + commit: "a94c228", + rubrik: "Regelverk, försäkringsunderlag och support i ärendet", + andringar: [ + "Garantiregimer för EU och USA med källa och kontrolldatum per post — och skiljelinjen mellan lagstadgat och avtalat, som förväxlas åt bägge håll.", + "Försäkringsvillkor som underlag för ett samtal. Modulen avgör ingenting och kan inte avgöra något.", + "Felanmälan i varje ärende, med härlett sammanhang. Ett regnr kan inte smugglas med.", + "Mätarställningen ska vara fotograferad, inte bara inskriven — kravet fanns bara i gränssnittet och var därmed en vana, inte en spärr.", + ], + }, + { + version: "3.0", + datum: "2026-08-06", + commit: "b147694", + rubrik: "TÜV-revision och härdning av integritetsgarantierna", + stor: true, + andringar: [ + "Händelsenyckeln skopad till ärendet: en organisation kan inte längre hindra en annan från att skriva historik.", + "Mätkedjan slås upp i mätdonsregistret — E4 var tidigare ett påstående klienten gjorde om sig själv.", + "Personnycklarna kuverteras under en huvudnyckel utanför databasen.", + "Gallringen verkställs av ett eget jobb i stället för att bara vara ett datum ingen läste.", + "Åtkomstloggen och raderingsregistret är append-only.", + ], + }, + { + version: "2.1", + datum: "2026-08-06", + commit: "3b83fc9", + rubrik: "Portalen stängd på riktigt", + andringar: [ + "Inloggningen autentiserar mot plattformen och portalen är oåtkomlig utan giltig session.", + "Fakturavyn läser organisationens egna fakturor; ett misslyckat anrop visar felet, inte exemplet.", + ], + }, + { + version: "2.0", + datum: "2026-08-06", + commit: "b457543", + rubrik: "Fakturering i systemet", + stor: true, + andringar: [ + "Fakturan härleds ur organisationens tillstånd i stället för att skrivas in.", + "Utfärdad faktura är oföränderlig; betalning är en händelse och statusen en projektion.", + "Nummerserie utan luckor. Ingen betalleverantör är inblandad.", + ], + }, + { + version: "1.3", + datum: "2026-08-05", + commit: "6676262", + rubrik: "Egen typografi och läsbarhet på telefon", + andringar: [ + "Inbäddad typografi enligt ALVA-SPEC-001 — inga externa hämtningar.", + "Ritningsstämpel på rapporten, räta hörn, tätare spärrning.", + ], + }, + { + version: "1.2", + datum: "2026-08-05", + commit: "86d553b", + rubrik: "Sviten körs faktiskt", + andringar: [ + "ALVA:s tester lades in i CI. De hade passerat bara när någon körde dem för hand — ett test som ingen kör är dokumentation, inte en spärr.", + "Genomgången blev ett incheckat test.", + ], + }, + { + version: "1.1", + datum: "2026-08-05", + commit: "31727a4", + rubrik: "Leverantörsrevision 2 och härdning", + andringar: [ + "Händelseschemat stängt: okända fält avvisas i stället för att lagras.", + "Kvalitetsregeln finns på ett ställe — klienten frågar grinden i stället för att ha en egen åsikt.", + ], + }, + { + version: "1.0", + datum: "2026-08-04", + commit: "590fab3", + rubrik: "ALVA-metoden", + andringar: [ + "Analys · Lokalisering · Verifiering · Åtgärd som metod, inte som assistent.", + "Kvalitetsgrinden serverauktoritativ. Avslutssats enligt ALVA-RULE-200.", + ], + }, +]; + +/** Nuvarande utgåva. Enda stället siffran finns. */ +export const NUVARANDE = UTGAVOR[0]; + +/** Så versionen skrivs i gränssnittet och i ritningsstämpeln. */ +export const PLATTFORMSVERSION = `ALVA ${NUVARANDE.version}`; + +/** + * Versionen som gällde när ett ärende stängdes. + * + * Ritningsstämpeln ska visa vad som gällde VID AVSLUTET, inte vad som + * gäller den dag rapporten läses — det står i stämpelns egen beskrivning, + * och gällde ändå inte för versionen, som lästes ur den aktuella + * konstanten. Ett ärende stängt under 1.0 visade 3.1. + * + * Avslutshändelsen bär numera versionen. Äldre ärenden saknar den, och då + * sägs det uttryckligen i stället för att dagens siffra lånas ut. + */ +export function versionVidAvslut(avslutshandelse) { + const v = avslutshandelse?.plattformsversion; + return typeof v === "string" && v.trim() ? v : "Ej registrerad (stängt före ALVA 3.1)"; +}