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. */}
- 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)";
+}