Grinden talar organisationens språk — engelska som standard

Kvalitetsgrinden formulerade sina hinder på svenska, hårdkodat. I en
produkt där engelska är standardspråket betyder det att en spärr kan
neka ett avslut på ett språk teknikern inte läser. Det ä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
och blir ett hinder man lär sig kringgå.

Varje hinder bär nu både nyckel och färdig text. 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 hamnar i en logg eller en PDF måste gå
att läsa utan katalogen. Hindrets id och nyckel är språkoberoende — det
är bara texten som byter.

Språket är organisationens, inte användarens. 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. Det
lagras i organisationens inställningar, och en felstavad landskod blir
engelska i stället för ett fel — språkinställningen är en etikett och
ska inte hindra någon från att spara sina objekttyper.

SPARRFRAGOR bär nu nycklar i stället för svensk text. Testet som jämför
serverns lista med klientens jämför nycklar, så listorna kan fortfarande
inte glida isär.

Två tester prövade svenska ord i grindens utdata och gick sönder, vilket
var rätt av dem. De prövar nu nyckeln — texten är översatt och byter
språk med organisationen, och att pröva svenska ord hade gjort dem till
tester av vilket språk som råkar vara standard. Ett nytt test prövar det
som faktiskt betyder något: samma id, samma nyckel, olika text.

Integrationstestet följer språket hela vägen — organisationens
inställning, genom servern, ut i det 409-svar som nekar avslutet: engelska
som standard, tyska efter byte, engelska igen vid okänd kod.

582 tester, 186 integrationskontroller mot riktig Postgres, genomgång och
portalspärr gröna.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-06 16:36:53 +00:00
parent 995858174d
commit be10704349
15 changed files with 243 additions and 65 deletions
@@ -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
+40 -8
View File
@@ -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.",
+1
View File
@@ -0,0 +1 @@
../gemensam/sprak