diff --git a/felsokning/app/src/felsokning/__tests__/grind.test.ts b/felsokning/app/src/felsokning/__tests__/grind.test.ts index f029e4c..b89e389 100644 --- a/felsokning/app/src/felsokning/__tests__/grind.test.ts +++ b/felsokning/app/src/felsokning/__tests__/grind.test.ts @@ -10,6 +10,7 @@ import { describe, expect, it } from "vitest"; import { granskaHändelse, tillPost } from "../../../../services/gemensam/handelser.mjs"; import { SPARRFRAGOR as SPARR_SERVER, grinda, grindaArendetyp } from "../../../../services/gemensam/grind.mjs"; import { ALLA_METODIKER } from "../../../../services/gemensam/metodiker.mjs"; +import { t } from "../../../../services/gemensam/sprak/index.mjs"; import { SPARRFRAGOR } from "../metodik"; const GENERISK = ALLA_METODIKER.at(-1)!; @@ -203,7 +204,8 @@ describe("ärendetypens regelpaket", () => { it("en okänd kravtyp spärrar i stället för att tolkas som uppfylld", () => { const trasigt = { arendetyper: { Garanti: { krav: [{ typ: "hittepa" }] } } }; const logg = [{ typ: "arendetyp_satt", arendetyp: "Garanti" }]; - expect(grindaArendetyp(logg, trasigt)[0].rubrik).toMatch(/Okänt krav/); + expect(grindaArendetyp(logg, trasigt)[0].nyckel).toBe("grind.arendetyp.okant"); + expect(grindaArendetyp(logg, trasigt)[0].rubrik).toContain("hittepa"); }); }); @@ -470,8 +472,24 @@ describe("mätarställningen ska vara fotograferad, inte bara inskriven", () => }); it("hindret säger vad som saknas och vad man gör åt det", () => { + // Prövas mot nyckeln, inte mot texten: texten är översatt och byter + // språk med organisationen. Att pröva svenska ord här hade gjort + // testet till ett test av vilket språk som råkar vara standard. const h = grinda(utanFoto(), GENERISK).find((x) => x.id === "matarstallning_ingaende"); - expect(h.detalj).toMatch(/fotografera/i); + expect(h.detaljNyckel).toBe("grind.matarstallning.ej_foto"); + expect(h.detalj).toBe(t("en", "grind.matarstallning.ej_foto")); + }); + + it("hindret följer organisationens språk", () => { + // Det som faktiskt betyder något i drift: en tysk verkstad ska få + // veta på tyska varför avslutet nekas. Ett hinder ingen förstår är + // en spärr utan väg förbi. + const engelska = grinda(utanFoto(), GENERISK).find((x) => x.id === "matarstallning_ingaende"); + const tyska = grinda(utanFoto(), GENERISK, "de").find((x) => x.id === "matarstallning_ingaende"); + expect(tyska.id).toBe(engelska.id); + expect(tyska.nyckel).toBe(engelska.nyckel); + expect(tyska.rubrik).not.toBe(engelska.rubrik); + expect(tyska.rubrik).toBe(t("de", "grind.matarstallning.ingaende")); }); it("ett dataUrl-foto duger lika bra som en bilaga", () => { diff --git a/felsokning/services/gemensam/grind.mjs b/felsokning/services/gemensam/grind.mjs index f17c3c1..8209d63 100644 --- a/felsokning/services/gemensam/grind.mjs +++ b/felsokning/services/gemensam/grind.mjs @@ -16,6 +16,7 @@ // samma utfall, vilket är vad som gör en spärr granskbar. import { granskaSlutsats } from "./motivering.mjs"; +import { STANDARD, t } from "./sprak/index.mjs"; const UNDANTAG_MOTIVERAT = (h) => typeof h.undantag === "string" && h.undantag.trim().length > 0; @@ -23,8 +24,8 @@ const UNDANTAG_MOTIVERAT = (h) => typeof h.undantag === "string" && h.undantag.t // Speglar SPARRFRAGOR i app/src/felsokning/metodik.ts; ett test jämför // listorna så de inte kan glida isär. export const SPARRFRAGOR = { - "hogvolt/sakerhet/behorighet": "Behörighet för högvoltsarbete bekräftad", - "hogvolt/sakerhet/avstangt": "Fordonet spänningslöst enligt tillverkarens rutin", + "hogvolt/sakerhet/behorighet": "grind.hogvolt.behorighet", + "hogvolt/sakerhet/avstangt": "grind.hogvolt.spanningslos", }; /** Händelser av en viss typ. */ @@ -35,22 +36,42 @@ const av = (handelser, typ) => handelser.filter((h) => h.typ === typ); * * @returns lista med hinder; tom lista betyder att ärendet får avslutas. */ -export function grinda(handelser, metodik) { +export function grinda(handelser, metodik, sprak = STANDARD) { const hinder = []; - const krav = (id, rubrik, ok, detalj) => { - if (!ok) hinder.push(detalj ? { id, rubrik, detalj } : { id, rubrik }); + + // Hindret bär både nyckeln och den färdiga texten. Nyckeln därför att + // en klient som byter språk ska kunna rendera om utan att fråga + // servern; texten därför att ett hinder som passerar genom en logg + // eller en PDF måste vara läsbart utan katalogen. + const krav = (id, nyckel, ok, detaljNyckel, variabler) => { + if (ok) return; + const post = { id, nyckel, rubrik: t(sprak, nyckel, variabler) }; + if (detaljNyckel) { + post.detaljNyckel = detaljNyckel; + post.detalj = t(sprak, detaljNyckel, variabler); + } + hinder.push(post); }; - krav("objekt", "Fordons-/objektidentifiering verifierad", handelser.some((h) => h.typ === "objekt_identifierat")); + // Fritext som kommer ur data, inte ur katalogen — namnen på metodikens + // kontroller. De står på metodikens språk och kan inte översättas här. + const kravMedData = (id, nyckel, ok, detalj) => { + if (ok) return; + const post = { id, nyckel, rubrik: t(sprak, nyckel) }; + if (detalj) post.detalj = detalj; + hinder.push(post); + }; + + krav("objekt", "grind.objekt", handelser.some((h) => h.typ === "objekt_identifierat")); // Historiken får besvaras nekande, men då krävs en motivering — annars // vore "Nej" ett sätt att hoppa över kontrollen utan att det syns. const historik = av(handelser, "historik_kontrollerad").at(-1); krav( "historik", - "Fordonshistorik kontrollerad eller motiverad", + "grind.historik", Boolean(historik) && (historik.kontrollerad || (historik.kommentar ?? "").trim().length > 0), - historik ? "Nekad historikkontroll kräver en motivering." : "Ingen historikkontroll dokumenterad.", + historik ? "grind.historik.nekad" : "grind.historik.saknas", ); // Mätarställningen ska vara FOTOGRAFERAD, inte bara inskriven. @@ -71,9 +92,9 @@ export function grinda(handelser, metodik) { // 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 fotograferad"], - ["utgaende", "Utgående mätarställning fotograferad"], + for (const [lage, nyckel] of [ + ["ingaende", "grind.matarstallning.ingaende"], + ["utgaende", "grind.matarstallning.utgaende"], ]) { const poster = av(handelser, "matarstallning").filter((h) => h.lage === lage); const dokumenterad = poster.length > 0; @@ -81,50 +102,35 @@ export function grinda(handelser, metodik) { const motiverat = poster.some((h) => (h.undantag ?? "").trim().length > 0); krav( `matarstallning_${lage}`, - rubrik, + nyckel, 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.", + dokumenterad ? "grind.matarstallning.ej_foto" : "grind.matarstallning.saknas", ); } - krav( - "reproducering", - "Symptomverifiering: reproducerat eller dokumenterat ej reproducerbart", - handelser.some((h) => h.typ === "reproducering"), - ); + krav("reproducering", "grind.reproducering", handelser.some((h) => h.typ === "reproducering")); - krav("felorsak", "Felorsaksanalys dokumenterad", handelser.some((h) => h.typ === "felorsak")); + krav("felorsak", "grind.felorsak", handelser.some((h) => h.typ === "felorsak")); // Åtgärdskedjan. En utebliven åtgärd är ett giltigt utfall — men bara // när skälet står i loggen. const atgarder = av(handelser, "atgard_utford"); const utfordArbete = atgarder.some((h) => h.utford === true); - krav( - "atgard", - "Åtgärd dokumenterad eller motiverad", - atgarder.length > 0, - "Varken utförd åtgärd eller skäl till att den uteblev finns dokumenterat.", - ); + krav("atgard", "grind.atgard", atgarder.length > 0, "grind.atgard.saknas"); if (utfordArbete) { const beslut = av(handelser, "kundbeslut"); - krav("kundbeslut", "Kundens besked på åtgärdsförslaget registrerat", beslut.length > 0); + krav("kundbeslut", "grind.kundbeslut", beslut.length > 0); // Hårt fel, inte en varning: arbete utfört trots att kunden avböjt. krav( "avbojt_men_utfort", - "Utfört arbete trots avböjt åtgärdsförslag", + "grind.kundbeslut.avbojt", !beslut.some((h) => h.beslut === "avbojt"), - "Kunden avböjde förslaget men arbete har dokumenterats som utfört.", + "grind.kundbeslut.avbojt.detalj", ); - krav( - "kvalitetskontroll", - "Kvalitetskontroll genomförd — symptomet verifierat", - handelser.some((h) => h.typ === "kvalitetskontroll"), - ); + krav("kvalitetskontroll", "grind.kvalitetskontroll", handelser.some((h) => h.typ === "kvalitetskontroll")); } // Metodikens kontroller: varje kontroll ska bära evidens eller ett @@ -156,18 +162,11 @@ export function grinda(handelser, metodik) { if (!(h.resultat ?? "").trim()) saknade.push(`${steg.rubrik} · ${kontroll.text} (utan resultat)`); } } - krav( - "metodik_kontroller", - "Metodikens kontroller: evidens eller dokumenterat undantag", - saknade.length === 0, - saknade.slice(0, 5).join(" · "), - ); - krav( - "foton", - "Foton finns för fotokrävande kontroller", - foton >= fotokravande.length, - fotokravande.length ? `${fotokravande.length} kontroller kräver foto, ${foton} foton i loggen.` : undefined, - ); + kravMedData("metodik_kontroller", "grind.kontroller", saknade.length === 0, saknade.slice(0, 5).join(" · ")); + krav("foton", "grind.foton", foton >= fotokravande.length, fotokravande.length ? "grind.foton.detalj" : undefined, { + kontroller: fotokravande.length, + foton, + }); // ALVA-RULE-200 · Slutsatsen. Ett ärende stängs aldrig utan att // teknikern lämnat ett varför — motiveringen som knyter slutsatsen @@ -185,22 +184,22 @@ export function grinda(handelser, metodik) { // Säkerhetsspärrar. Ett nekande svar på en spärrfråga får aldrig // passera som "besvarad" — se SPARRFRAGOR i klientens metodik.ts. // Klientens spärr är vägledning; den här är hindret (QUALITET M-2). - for (const [nyckel, text] of Object.entries(SPARRFRAGOR)) { - const [metodikId, stegId, frageId] = nyckel.split("/"); + for (const [fraga, nyckel] of Object.entries(SPARRFRAGOR)) { + const [metodikId, stegId, frageId] = fraga.split("/"); if (metodik?.id !== metodikId) continue; const svaret = av(handelser, "fraga_besvarad").findLast( (h) => h.stegId === stegId && h.frageId === frageId, ); - krav(`sparr_${frageId}`, text, svaret?.svar === "Ja", "Säkerhetskravet är inte uppfyllt."); + krav(`sparr_${frageId}`, nyckel, svaret?.svar === "Ja", "grind.sparr.ej_uppfyllt"); } // Evidensnivån måste överstiga E0 — annars avslutas ett ärende utan // att någonting alls har dokumenterats. krav( "evidens", - "Evidensnivå över E0", + "grind.evidens", handelser.some((h) => ["foto", "video", "matvarde", "observation", "kontroll_utford"].includes(h.typ)), - "Ingen evidens av något slag finns i loggen.", + "grind.evidens.saknas", ); return hinder; @@ -210,7 +209,7 @@ export function grinda(handelser, metodik) { * Extra krav som följer av ärendetypen. Reglerna kommer från regelpaketet * (ECM Knowledge Library) och är alltså data, inte kod. */ -export function grindaArendetyp(handelser, regelpaket) { +export function grindaArendetyp(handelser, regelpaket, sprak = STANDARD) { const typ = av(handelser, "arendetyp_satt").at(-1)?.arendetyp; const regler = regelpaket?.arendetyper?.[typ]?.krav ?? []; const hinder = []; @@ -227,10 +226,21 @@ export function grindaArendetyp(handelser, regelpaket) { const kontroll = uppfyllt[regel.typ ?? regel]; // En okänd kravtyp får aldrig tolkas som uppfylld. Ett regelpaket som // inte går att utvärdera ska stoppa avslutet, inte släppa igenom det. + const krav = regel.typ ?? regel; if (!kontroll) { - hinder.push({ id: `arendetyp_${regel.typ ?? regel}`, rubrik: `Okänt krav i regelpaketet: ${regel.typ ?? regel}` }); + hinder.push({ + id: `arendetyp_${krav}`, + nyckel: "grind.arendetyp.okant", + rubrik: t(sprak, "grind.arendetyp.okant", { krav }), + }); } else if (!kontroll()) { - hinder.push({ id: `arendetyp_${regel.typ ?? regel}`, rubrik: regel.rubrik ?? `Krav för ärendetypen: ${regel.typ ?? regel}` }); + // Regelpaketets egen rubrik står på regelpaketets språk och går + // inte att översätta här — den är data från kunden, inte vår text. + hinder.push({ + id: `arendetyp_${krav}`, + nyckel: regel.rubrik ? undefined : "grind.arendetyp.krav", + rubrik: regel.rubrik ?? t(sprak, "grind.arendetyp.krav", { krav }), + }); } } return hinder; diff --git a/felsokning/services/gemensam/sprak/de.mjs b/felsokning/services/gemensam/sprak/de.mjs index 0d1e065..c0098c8 100644 --- a/felsokning/services/gemensam/sprak/de.mjs +++ b/felsokning/services/gemensam/sprak/de.mjs @@ -46,6 +46,13 @@ export const DE = { "grind.hogvolt.spanningslos": "Fahrzeug nach Herstellerverfahren spannungsfrei geschaltet", "grind.regelpaket": "Signatur des Regelpakets stimmt nicht — Abschluss gesperrt.", "grind.regelpaket.osignerat": "Ein externes Regelpaket wird ohne Signatur verwendet — Abschluss gesperrt.", + "grind.evidens": "Evidenzgrad über E0", + "grind.evidens.saknas": "Es liegt keinerlei Evidenz im Protokoll vor.", + "grind.foton.detalj": "{kontroller} Prüfpunkte erfordern ein Foto, {foton} Fotos im Protokoll.", + "grind.sparr.ej_uppfyllt": "Die Sicherheitsanforderung ist nicht erfüllt.", + "grind.arendetyp.okant": "Unbekannte Anforderung im Regelpaket: {krav}", + "grind.arendetyp.krav": "Anforderung für diese Vorgangsart: {krav}", + // ---- Abschlussfeststellung (ALVA-RULE-200) ------------------------------ "slutsats.rubrik": "Abschlussfeststellung", diff --git a/felsokning/services/gemensam/sprak/en.mjs b/felsokning/services/gemensam/sprak/en.mjs index e0634b5..5e62e5e 100644 --- a/felsokning/services/gemensam/sprak/en.mjs +++ b/felsokning/services/gemensam/sprak/en.mjs @@ -73,6 +73,13 @@ export const EN = { "grind.hogvolt.spanningslos": "Vehicle de-energised per the manufacturer's procedure", "grind.regelpaket": "Rule package signature does not match — closing blocked.", "grind.regelpaket.osignerat": "An external rule package is in use without a signature — closing blocked.", + "grind.evidens": "Evidence above E0", + "grind.evidens.saknas": "No evidence of any kind is present in the log.", + "grind.foton.detalj": "{kontroller} checks require a photograph, {foton} photographs in the log.", + "grind.sparr.ej_uppfyllt": "The safety requirement is not met.", + "grind.arendetyp.okant": "Unknown requirement in the rule package: {krav}", + "grind.arendetyp.krav": "Requirement for this case type: {krav}", + // ---- The closing statement (ALVA-RULE-200) ------------------------------ "slutsats.rubrik": "Closing statement", diff --git a/felsokning/services/gemensam/sprak/es.mjs b/felsokning/services/gemensam/sprak/es.mjs index c7e0ba8..7645cfb 100644 --- a/felsokning/services/gemensam/sprak/es.mjs +++ b/felsokning/services/gemensam/sprak/es.mjs @@ -45,6 +45,13 @@ export const ES = { "grind.hogvolt.spanningslos": "Vehículo sin tensión según el procedimiento del fabricante", "grind.regelpaket": "La firma del paquete de reglas no coincide — cierre bloqueado.", "grind.regelpaket.osignerat": "Se utiliza un paquete de reglas externo sin firma — cierre bloqueado.", + "grind.evidens": "Nivel de evidencia superior a E0", + "grind.evidens.saknas": "No hay ninguna evidencia de ningún tipo en el registro.", + "grind.foton.detalj": "{kontroller} controles exigen fotografía, {foton} fotografías en el registro.", + "grind.sparr.ej_uppfyllt": "El requisito de seguridad no se cumple.", + "grind.arendetyp.okant": "Requisito desconocido en el paquete de reglas: {krav}", + "grind.arendetyp.krav": "Requisito para este tipo de expediente: {krav}", + // ---- La conclusión (ALVA-RULE-200) -------------------------------------- "slutsats.rubrik": "Conclusión", diff --git a/felsokning/services/gemensam/sprak/fr.mjs b/felsokning/services/gemensam/sprak/fr.mjs index 8e7e81b..eb1ce93 100644 --- a/felsokning/services/gemensam/sprak/fr.mjs +++ b/felsokning/services/gemensam/sprak/fr.mjs @@ -46,6 +46,13 @@ export const FR = { "grind.hogvolt.spanningslos": "Véhicule consigné hors tension selon la procédure du constructeur", "grind.regelpaket": "La signature du paquet de règles ne correspond pas — clôture bloquée.", "grind.regelpaket.osignerat": "Un paquet de règles externe est utilisé sans signature — clôture bloquée.", + "grind.evidens": "Niveau de preuve supérieur à E0", + "grind.evidens.saknas": "Aucune preuve d'aucune sorte ne figure dans le journal.", + "grind.foton.detalj": "{kontroller} contrôles exigent une photographie, {foton} photographies dans le journal.", + "grind.sparr.ej_uppfyllt": "L'exigence de sécurité n'est pas satisfaite.", + "grind.arendetyp.okant": "Exigence inconnue dans le paquet de règles : {krav}", + "grind.arendetyp.krav": "Exigence pour ce type de dossier : {krav}", + // ---- La conclusion (ALVA-RULE-200) -------------------------------------- "slutsats.rubrik": "Conclusion", diff --git a/felsokning/services/gemensam/sprak/it.mjs b/felsokning/services/gemensam/sprak/it.mjs index 7644b92..db0ce3c 100644 --- a/felsokning/services/gemensam/sprak/it.mjs +++ b/felsokning/services/gemensam/sprak/it.mjs @@ -45,6 +45,13 @@ export const IT = { "grind.hogvolt.spanningslos": "Veicolo messo fuori tensione secondo la procedura del costruttore", "grind.regelpaket": "La firma del pacchetto di regole non corrisponde — chiusura bloccata.", "grind.regelpaket.osignerat": "È in uso un pacchetto di regole esterno privo di firma — chiusura bloccata.", + "grind.evidens": "Livello di evidenza superiore a E0", + "grind.evidens.saknas": "Nel registro non è presente alcuna evidenza.", + "grind.foton.detalj": "{kontroller} controlli richiedono una fotografia, {foton} fotografie nel registro.", + "grind.sparr.ej_uppfyllt": "Il requisito di sicurezza non è soddisfatto.", + "grind.arendetyp.okant": "Requisito sconosciuto nel pacchetto di regole: {krav}", + "grind.arendetyp.krav": "Requisito per questo tipo di pratica: {krav}", + // ---- La conclusione (ALVA-RULE-200) ------------------------------------- "slutsats.rubrik": "Conclusione", diff --git a/felsokning/services/gemensam/sprak/nl.mjs b/felsokning/services/gemensam/sprak/nl.mjs index 0c7c511..4cceb15 100644 --- a/felsokning/services/gemensam/sprak/nl.mjs +++ b/felsokning/services/gemensam/sprak/nl.mjs @@ -45,6 +45,13 @@ export const NL = { "grind.hogvolt.spanningslos": "Voertuig spanningsloos gemaakt volgens de procedure van de fabrikant", "grind.regelpaket": "Handtekening van het regelpakket komt niet overeen — afsluiten geblokkeerd.", "grind.regelpaket.osignerat": "Er wordt een extern regelpakket zonder handtekening gebruikt — afsluiten geblokkeerd.", + "grind.evidens": "Bewijsgraad boven E0", + "grind.evidens.saknas": "Er is geen enkel bewijs in het logboek aanwezig.", + "grind.foton.detalj": "{kontroller} controles vereisen een foto, {foton} foto's in het logboek.", + "grind.sparr.ej_uppfyllt": "Aan de veiligheidseis is niet voldaan.", + "grind.arendetyp.okant": "Onbekende eis in het regelpakket: {krav}", + "grind.arendetyp.krav": "Eis voor dit zaaktype: {krav}", + // ---- De eindconclusie (ALVA-RULE-200) ----------------------------------- "slutsats.rubrik": "Eindconclusie", diff --git a/felsokning/services/gemensam/sprak/pl.mjs b/felsokning/services/gemensam/sprak/pl.mjs index b85a42b..a4d4be0 100644 --- a/felsokning/services/gemensam/sprak/pl.mjs +++ b/felsokning/services/gemensam/sprak/pl.mjs @@ -47,6 +47,13 @@ export const PL = { "grind.hogvolt.spanningslos": "Pojazd odłączony od napięcia zgodnie z procedurą producenta", "grind.regelpaket": "Podpis pakietu reguł nie zgadza się — zamknięcie zablokowane.", "grind.regelpaket.osignerat": "Używany jest zewnętrzny pakiet reguł bez podpisu — zamknięcie zablokowane.", + "grind.evidens": "Poziom dowodu powyżej E0", + "grind.evidens.saknas": "W dzienniku nie ma żadnego dowodu.", + "grind.foton.detalj": "{kontroller} kontroli wymaga fotografii, {foton} fotografii w dzienniku.", + "grind.sparr.ej_uppfyllt": "Wymóg bezpieczeństwa nie jest spełniony.", + "grind.arendetyp.okant": "Nieznany wymóg w pakiecie reguł: {krav}", + "grind.arendetyp.krav": "Wymóg dla tego rodzaju sprawy: {krav}", + // ---- Wniosek końcowy (ALVA-RULE-200) ------------------------------------ "slutsats.rubrik": "Wniosek końcowy", diff --git a/felsokning/services/gemensam/sprak/pt.mjs b/felsokning/services/gemensam/sprak/pt.mjs index ab92182..635779f 100644 --- a/felsokning/services/gemensam/sprak/pt.mjs +++ b/felsokning/services/gemensam/sprak/pt.mjs @@ -45,6 +45,13 @@ export const PT = { "grind.regelpaket": "A assinatura do pacote de regras não corresponde — encerramento bloqueado.", "grind.regelpaket.osignerat": "Está a ser utilizado um pacote de regras externo sem assinatura — encerramento bloqueado.", + "grind.evidens": "Nível de evidência superior a E0", + "grind.evidens.saknas": "Não existe qualquer evidência no registo.", + "grind.foton.detalj": "{kontroller} controlos exigem fotografia, {foton} fotografias no registo.", + "grind.sparr.ej_uppfyllt": "O requisito de segurança não está cumprido.", + "grind.arendetyp.okant": "Requisito desconhecido no pacote de regras: {krav}", + "grind.arendetyp.krav": "Requisito para este tipo de processo: {krav}", + // ---- A conclusão (ALVA-RULE-200) ---------------------------------------- "slutsats.rubrik": "Conclusão", diff --git a/felsokning/services/gemensam/sprak/ro.mjs b/felsokning/services/gemensam/sprak/ro.mjs index 79ddfc5..3b3eaed 100644 --- a/felsokning/services/gemensam/sprak/ro.mjs +++ b/felsokning/services/gemensam/sprak/ro.mjs @@ -51,6 +51,13 @@ export const RO = { "grind.regelpaket": "Semnătura pachetului de reguli nu corespunde — închiderea este blocată.", "grind.regelpaket.osignerat": "Se utilizează un pachet de reguli extern fără semnătură — închiderea este blocată.", + "grind.evidens": "Nivel de dovadă peste E0", + "grind.evidens.saknas": "În jurnal nu există nicio dovadă.", + "grind.foton.detalj": "{kontroller} controale necesită o fotografie, {foton} fotografii în jurnal.", + "grind.sparr.ej_uppfyllt": "Cerința de siguranță nu este îndeplinită.", + "grind.arendetyp.okant": "Cerință necunoscută în pachetul de reguli: {krav}", + "grind.arendetyp.krav": "Cerință pentru acest tip de caz: {krav}", + // ---- Concluzia finală (ALVA-RULE-200) ----------------------------------- "slutsats.rubrik": "Concluzie finală", diff --git a/felsokning/services/gemensam/sprak/sv.mjs b/felsokning/services/gemensam/sprak/sv.mjs index ba58a3a..95e7cb4 100644 --- a/felsokning/services/gemensam/sprak/sv.mjs +++ b/felsokning/services/gemensam/sprak/sv.mjs @@ -44,6 +44,13 @@ export const SV = { "grind.hogvolt.spanningslos": "Fordonet spänningslöst enligt tillverkarens metod", "grind.regelpaket": "Regelpaketets signatur stämmer inte — avslut spärrat.", "grind.regelpaket.osignerat": "Ett externt regelpaket används utan signatur — avslut spärrat.", + "grind.evidens": "Evidensnivå över E0", + "grind.evidens.saknas": "Ingen evidens av något slag finns i loggen.", + "grind.foton.detalj": "{kontroller} kontroller kräver foto, {foton} foton i loggen.", + "grind.sparr.ej_uppfyllt": "Säkerhetskravet är inte uppfyllt.", + "grind.arendetyp.okant": "Okänt krav i regelpaketet: {krav}", + "grind.arendetyp.krav": "Krav för ärendetypen: {krav}", + // ---- Slutsatsen (ALVA-RULE-200) ----------------------------------------- "slutsats.rubrik": "Slutsats", diff --git a/felsokning/services/plattform/integrationstest.sh b/felsokning/services/plattform/integrationstest.sh index 2b2f583..6110e5a 100755 --- a/felsokning/services/plattform/integrationstest.sh +++ b/felsokning/services/plattform/integrationstest.sh @@ -860,6 +860,53 @@ kontroll "utan huvudnyckel maskeras uppgiften" \ "$(echo "$SVAR" | falt '.handelser[0].handelse.objekt.identifierare')" "[raderat på begäran]" kontroll "men anropet gar fortfarande igenom" "$(echo "$SVAR" | falt .handelser.length)" "1" +# 18b. Grinden talar organisationens språk (ALVA-SPEC-060) +# +# Det som prövas är inte att en översättning finns — det gör testerna i +# app/ — utan att språket följer HELA vägen: organisationens inställning, +# genom servern, ut i det 409-svar som faktiskt nekar ett avslut. +# +# En spärr på fel språk är en spärr utan väg förbi. Teknikern ser att +# systemet vägrar men inte varför, och den enda utvägen blir att ringa +# någon. Det är då regeln slutar vara en regel. +curl -s -X POST "$BAS/api/arenden" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \ + -d '{"id":"arende-sprak","nummer":77,"skapad":"2026-08-06T09:00:00Z"}' >/dev/null +AVSLUT='{"handelser":[{"id":"h-sprak-avslut","tidpunkt":"2026-08-06T09:05:00Z","anvandare":"Anna","handelse":{"typ":"arende_avslutat"}}]}' + +SVAR=$(curl -s -X POST "$BAS/api/arenden/arende-sprak/handelser" -H "Authorization: Bearer $TOKEN_A" \ + -H 'Content-Type: application/json' -d "$AVSLUT") +kontroll "avslut utan underlag nekas" "$(echo "$SVAR" | falt '.hinder.length > 0')" "true" +kontroll "hindret star pa engelska som standard" \ + "$(echo "$SVAR" | falt '.hinder.find(h=>h.id==="objekt").rubrik')" "Vehicle or object identification verified" +kontroll "hindret bar sin nyckel" \ + "$(echo "$SVAR" | falt '.hinder.find(h=>h.id==="objekt").nyckel')" "grind.objekt" + +# Organisationen byter dokumentationsspråk. +curl -s -X POST "$BAS/api/organisation/installningar" -H "Authorization: Bearer $TOKEN_A" \ + -H 'Content-Type: application/json' \ + -d '{"objekttyper":["Personbil"],"identifieringsmetoder":["Registreringsnummer"],"sprak":"de"}' >/dev/null + +SVAR=$(curl -s -X POST "$BAS/api/arenden/arende-sprak/handelser" -H "Authorization: Bearer $TOKEN_A" \ + -H 'Content-Type: application/json' -d "$AVSLUT") +kontroll "hindret foljer organisationens sprak" \ + "$(echo "$SVAR" | falt '.hinder.find(h=>h.id==="objekt").rubrik')" \ + "Identität des Fahrzeugs oder Objekts verifiziert" +# Nyckeln ar oforandrad: identiteten pa hindret ar inte sprakberoende, +# vilket ar hela poangen med att bara texten oversatts. +kontroll "nyckeln ar densamma pa bada spraken" \ + "$(echo "$SVAR" | falt '.hinder.find(h=>h.id==="objekt").nyckel')" "grind.objekt" + +# En felstavad sprakkod far inte hindra nagon fran att spara sina +# objekttyper — sprakinstallningen ar en etikett, inte ett krav. +KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/organisation/installningar" \ + -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \ + -d '{"objekttyper":["Personbil"],"identifieringsmetoder":["Registreringsnummer"],"sprak":"klingon"}') +kontroll "okand sprakkod avvisas inte" "$KOD" "200" +SVAR=$(curl -s -X POST "$BAS/api/arenden/arende-sprak/handelser" -H "Authorization: Bearer $TOKEN_A" \ + -H 'Content-Type: application/json' -d "$AVSLUT") +kontroll "okand sprakkod blir engelska" \ + "$(echo "$SVAR" | falt '.hinder.find(h=>h.id==="objekt").rubrik')" "Vehicle or object identification verified" + # 19. Support och felanmälan (ALVA-PROC-0050) kill "$SERVER_PID" 2>/dev/null || true wait "$SERVER_PID" 2>/dev/null || true diff --git a/felsokning/services/plattform/server.mjs b/felsokning/services/plattform/server.mjs index 76d5c21..6d61f9f 100644 --- a/felsokning/services/plattform/server.mjs +++ b/felsokning/services/plattform/server.mjs @@ -41,6 +41,7 @@ import { fakturabeteckning, fakturastatus, fakturera, granskaPeriod, kreditera } import { STATUSAR, granskaAnmalan, sammanhang as byggSammanhang, supportbeteckning, supportstatus } from "./support.mjs"; import { NIVAER, abonnemangstillstand, behorighet, besked, forfallenFakturering, nivaFor } from "./abonnemang.mjs"; import { fakturaPdf } from "./fakturapdf.mjs"; +import { STANDARD, valjSprak, t } from "./sprak/index.mjs"; import { MASKERAT, gallringsdatum, @@ -444,12 +445,35 @@ function nyKod(langd = 16) { return ut.join(""); } +/** + * Organisationens dokumentationsspråk. + * + * Organisationen och inte användaren, därför att grindens hinder hamnar + * i ärendets logg och i rapporten. En verkstad i Tyskland med en polsk + * tekniker ska ha ett gemensamt dokumentationsspråk — rapporten ska + * inte byta språk beroende på vem som råkade stänga ärendet. + * + * Faller tillbaka på engelska, tyst. Att neka ett avslut för att + * språkinställningen inte gick att läsa vore att låta en etikett stoppa + * arbetet. + */ +async function orgSprak(pool, orgId) { + try { + const rader = await pool.query(`select installningar->>'sprak' as sprak from organisationer where id = $1`, [ + orgId, + ]); + return valjSprak({ organisation: rader.rows[0]?.sprak }); + } catch { + return STANDARD; + } +} + /** * Avslutsspärren. Läser hela loggen ur databasen, lägger de inkommande * händelserna ovanpå och utvärderar grinden mot det samlade underlaget — * annars skulle ett avslut i samma anrop som evidensen felaktigt nekas. */ -async function grindHinder(pool, arendeId, nya) { +async function grindHinder(pool, arendeId, nya, sprak = STANDARD) { const rader = await pool.query( `select h.handelse, a.metodik_id from felsokning_handelser h join felsokning_arenden a on a.id = h.arende_id @@ -462,14 +486,17 @@ async function grindHinder(pool, arendeId, nya) { let regelpaket; if (REGELPAKET_STATUS === "ogiltig signatur") { - return [{ id: "regelpaket", rubrik: "Regelpaketets signatur stämmer inte — avslut spärrat." }]; + return [{ id: "regelpaket", nyckel: "grind.regelpaket", rubrik: t(sprak, "grind.regelpaket") }]; } if (REGELPAKET_STATUS === "osignerat externt") { return [ { id: "regelpaket", - rubrik: "Ett externt regelpaket används utan signatur — avslut spärrat.", - detalj: "Sätt ECM_REGLER_NYCKEL och ECM_REGLER_SIGNATUR, eller ta bort ECM_REGLER_FIL.", + nyckel: "grind.regelpaket.osignerat", + rubrik: t(sprak, "grind.regelpaket.osignerat"), + // Driftanvisning till den som satt upp servern, inte text till + // teknikern. Den står därför på engelska och översätts inte. + detalj: "Set ECM_REGLER_NYCKEL and ECM_REGLER_SIGNATUR, or remove ECM_REGLER_FIL.", }, ]; } @@ -477,9 +504,9 @@ async function grindHinder(pool, arendeId, nya) { regelpaket = JSON.parse(ECM_REGLER); } catch { // Ett trasigt regelpaket får aldrig tolkas som "inga extra krav". - return [{ id: "regelpaket", rubrik: "Regelpaketet gick inte att läsa — avslut spärrat." }]; + return [{ id: "regelpaket", nyckel: "grind.regelpaket", rubrik: t(sprak, "grind.regelpaket") }]; } - return [...grinda(handelser, metodik), ...grindaArendetyp(handelser, regelpaket)]; + return [...grinda(handelser, metodik, sprak), ...grindaArendetyp(handelser, regelpaket, sprak)]; } @@ -1893,7 +1920,7 @@ export function skapaServer() { if (req.method === "POST" && vag === "/api/organisation/installningar") { if (anspr.roll !== "admin") return svara(res, 403, { error: "Kräver administratörsbehörighet." }); - const { objekttyper, identifieringsmetoder, ai_tillaten } = await lasKropp(req); + const { objekttyper, identifieringsmetoder, ai_tillaten, sprak } = await lasKropp(req); const giltigLista = (lista) => Array.isArray(lista) && lista.length > 0 && lista.length <= 50 && lista.every((v) => typeof v === "string" && v.length <= 100); @@ -1912,6 +1939,11 @@ export function skapaServer() { // överföringen till modelleverantören kan ändå använda // produkten (QUALITET C-4). ai_tillaten: ai_tillaten !== false, + // Dokumentationsspråket. Ett okänt värde blir engelska i + // stället för ett fel: språket är en etikett, och en + // felstavad landskod ska inte hindra en organisation från + // att spara sina objekttyper. + sprak: valjSprak({ organisation: sprak }), }), ], ); @@ -2459,7 +2491,7 @@ export function skapaServer() { // tidigare bara i webbläsaren, vilket gjorde hela ECM till ett // råd i stället för en spärr (QUALITET C-2). if (attSkriva.some((p) => p.handelse.typ === "arende_avslutat")) { - const hinder = await grindHinder(pool, handelserVag[1], attSkriva); + const hinder = await grindHinder(pool, handelserVag[1], attSkriva, await orgSprak(pool, anspr.org)); if (hinder.length > 0) { return svara(res, 409, { error: "Ärendet kan inte avslutas — kvalitetsgrinden är inte passerad.", diff --git a/felsokning/services/plattform/sprak b/felsokning/services/plattform/sprak new file mode 120000 index 0000000..ae03652 --- /dev/null +++ b/felsokning/services/plattform/sprak @@ -0,0 +1 @@ +../gemensam/sprak \ No newline at end of file