Garantiregimer, försäkringsvillkor och felanmälan i ärendet
---- Garantier (ALVA-SPEC-040) ----------------------------------------- Skiljelinjen mellan LAGSTADGAT och AVTALAT bär allt annat, och den förväxlas dagligen åt bägge håll. Överraskande lite av det verkstaden kallar garanti är lagstadgat: nybilsgaranti avtalad. Ingen lag i EU eller USA kräver den. rostskyddsgaranti avtalad. Ingen lag kräver den, någonstans. lackgaranti avtalad. drivlinegaranti avtalad. avgasgaranti LAGSTADGAD i bägge, och den enda som är det. Att kalla rostskyddet lagstadgat är fel; att kalla avgasgarantin en tillverkarutfästelse är fel åt andra hållet, och det andra felet kostar kunden pengar — en katalysator på en sexårig bil offereras rutinmässigt som en betald reparation när den inte är det. Varje lagstadgad post bär källa och kontrolldatum: 2019/771 (2 år, säljaren — inte tillverkaren), MVBER 461/2010 förlängd till 2028, 715/2007 (5 år/100 000 km), 2024/1257 (8 år/160 000 km, batteri 80 % vid 5 år och 72 % vid 8), CAA § 207 (2/24 000 och 8/80 000 miles), Magnuson–Moss § 2302(c), CARB (3/50 000 och 7/70 000), ACC II (70 % SoH vid 8 år/100 000 miles). Bedömningen skiljer INOM, UTANFÖR och OKLART. Saknas underlag blir det oklart, aldrig utanför — att påstå att en garanti gått ut när underlaget inte räcker är det enda felet här som kostar kunden pengar. ---- Försäkring (ALVA-SPEC-041) ---------------------------------------- Modulen avgör ingenting och kan inte avgöra något. Vad som gäller bestäms av kundens eget försäkringsbrev och villkoren vid tecknandet — inte av vad ett bolag skriver i dag och inte av vad vi noterat. Testerna låser att resultatet inte innehåller något fält som liknar ett utfall, och att ett fordon utanför samtliga noterade gränser ändå inte får ett nej: registret kan vara ofullständigt och produkten en annan. Det som står överst är de sju villkoren som avgör en maskinskadefråga. Ålder och sträcka är två av dem; de fem andra avgör oftare. Registret med If, Folksam, Trygg-Hansa, Svedea, WaterCircles och Hedvig ligger under, med källa och avläsningsdatum per post, och märks som föråldrat efter 180 dagar i stället för att se aktuellt ut. ---- Felanmälan i ärendet (ALVA-PROC-0050) ----------------------------- En anmälningsknapp som bara finns i en portalvy nås aldrig av den som står med händerna i en bil. Den ligger därför i varje ärende, och tar med sitt sammanhang självt: ärende, metodik, plattformsversion och spår-id. Teknikern minns inte plattformsversionen, och att fråga efter den är att lägga över vårt problem på verkstaden. Sammanhanget HÄRLEDS och kan inte smugglas: ett regnr i anropet släpps inte igenom. En supportanmälan är inte ett skäl att flytta personuppgifter till ett annat system. Bevisat i integrationstestet. Anmälan är oföränderlig och statusen en projektion av inläggen — samma skäl som för fakturan: en anmälan vars historia kan skrivas om är inte ett underlag när någon frågar hur länge felet var känt. Verkstaden svarar i sitt eget ärende; status sätts av supporten med egen nyckel. ---- Historikkontrollen ------------------------------------------------- Kravet fanns redan: klienten öppnar inte metodiken förrän pre-diagnostiken är besvarad, och ett nej kräver skriven motivering. Det var dock aldrig BEVISAT att spärren håller. Genomgången prövar nu att metodiken inte går att nå medan historikfrågan står obesvarad — i det enda ögonblick det går att pröva, eftersom svaret därefter redan är avgivet. 429 enhetstester · 157 integrationskontroller mot riktig Postgres · genomgången 4/4 · portalspärren 16/16. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
// TILLAT_INTERNA_UPPSLAG "true" tillåter leverantörsuppslag mot privata nät
|
||||
// ECM_REGLER_FIL / INTEGRATIONER_FIL sökvägar till utbytbar konfiguration
|
||||
// FAKTURERING_NYCKEL utfärdarens nyckel; utan den kan ingen faktura utfärdas
|
||||
// SUPPORT_NYCKEL vår egen supports nyckel; krävs för att svara och sätta status
|
||||
// PERSONNYCKEL_HUVUD 32 byte (hex/base64) — kuverterar personnycklarna så att
|
||||
// en återställd databasdump inte innehåller läsbara nycklar
|
||||
// PORT default 8080
|
||||
@@ -37,6 +38,7 @@ import { oversikt as statistikOversikt } from "./statistik.mjs";
|
||||
import { KATEGORIER, UTGAENDE, protokollTillHandelser, signeraLeverans } from "./integration.mjs";
|
||||
import { enrading, sammanfatta } from "./sammanfattning.mjs";
|
||||
import { fakturabeteckning, fakturastatus, fakturera, granskaPeriod, kreditera } from "./fakturering.mjs";
|
||||
import { STATUSAR, granskaAnmalan, sammanhang as byggSammanhang, supportbeteckning, supportstatus } from "./support.mjs";
|
||||
import {
|
||||
MASKERAT,
|
||||
gallringsdatum,
|
||||
@@ -131,16 +133,28 @@ const ROLLER = ["tekniker", "arbetsledare", "admin"];
|
||||
// ett ärende utan evidens inte kan avslutas.
|
||||
const FAKTURERING_NYCKEL = process.env.FAKTURERING_NYCKEL ?? "";
|
||||
|
||||
function arUtfardare(req) {
|
||||
if (!FAKTURERING_NYCKEL) return false;
|
||||
const given = String(req.headers["x-fakturering"] ?? "");
|
||||
const a = Buffer.from(given);
|
||||
const b = Buffer.from(FAKTURERING_NYCKEL);
|
||||
// Vår egen support. Verkstaden anmäler och svarar i sitt eget ärende;
|
||||
// att sätta status är vår sida av bordet, och den skiljs med en egen
|
||||
// nyckel av samma skäl som utfärdaren av fakturor gör det.
|
||||
const SUPPORT_NYCKEL = process.env.SUPPORT_NYCKEL ?? "";
|
||||
|
||||
function nyckelStammer(given, forvantad) {
|
||||
if (!forvantad) return false;
|
||||
const a = Buffer.from(String(given ?? ""));
|
||||
const b = Buffer.from(forvantad);
|
||||
// Jämförelsen är tidskonstant, och längden jämförs först eftersom
|
||||
// timingSafeEqual kastar på olika längd.
|
||||
return a.length === b.length && timingSafeEqual(a, b);
|
||||
}
|
||||
|
||||
function arUtfardare(req) {
|
||||
return nyckelStammer(req.headers["x-fakturering"], FAKTURERING_NYCKEL);
|
||||
}
|
||||
|
||||
function arSupport(req) {
|
||||
return nyckelStammer(req.headers["x-support"], SUPPORT_NYCKEL);
|
||||
}
|
||||
|
||||
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10 });
|
||||
|
||||
// Poolens uttömning är den vanligaste orsaken till att allt blir
|
||||
@@ -668,6 +682,27 @@ async function medFakturanummer(arbete) {
|
||||
}
|
||||
}
|
||||
|
||||
// Supportnummer, samma resonemang som fakturanumret: låset serialiserar
|
||||
// mot alla repliker, och en sequence hade lämnat luckor vid rollback.
|
||||
// Serien är dock inte ett bokföringsunderlag, så luckor vore uthärdliga —
|
||||
// det är enhetligheten som motiverar samma lösning, inte kravet.
|
||||
async function medSupportnummer(arbete) {
|
||||
const klient = await pool.connect();
|
||||
try {
|
||||
await klient.query("begin");
|
||||
await klient.query("lock table supportarenden in exclusive mode");
|
||||
const rad = await klient.query(`select coalesce(max(nummer), 0) + 1 as nasta from supportarenden`);
|
||||
const resultat = await arbete(klient, Number(rad.rows[0].nasta));
|
||||
await klient.query("commit");
|
||||
return resultat;
|
||||
} catch (fel) {
|
||||
await klient.query("rollback").catch(() => {});
|
||||
throw fel;
|
||||
} finally {
|
||||
klient.release();
|
||||
}
|
||||
}
|
||||
|
||||
// Fakturan med sitt härledda tillstånd. Statusen finns inte i tabellen
|
||||
// och kan inte finnas där: raden är oföränderlig.
|
||||
async function fakturamedStatus(id) {
|
||||
@@ -1301,6 +1336,123 @@ export function skapaServer() {
|
||||
return svara(res, 401, { error: "Sessionen gäller inte längre — logga in på nytt." });
|
||||
}
|
||||
|
||||
// ---- Support och felanmälan (ALVA-PROC-0050) -------------------
|
||||
//
|
||||
// ALLA inloggade får anmäla, inklusive teknikern. En tröskel här
|
||||
// ger färre anmälningar, inte färre fel — och de fel som inte
|
||||
// anmäls är de som lever längst.
|
||||
if (vag === "/api/support") {
|
||||
if (req.method === "GET") {
|
||||
const rader = await pool.query(
|
||||
`select s.id, s.beteckning, s.arende_id, s.typ, s.rubrik, s.beskrivning,
|
||||
s.sammanhang, s.skapad, a.namn as anmald_av,
|
||||
coalesce(json_agg(json_build_object(
|
||||
'typ', i.typ, 'text', i.text, 'status', i.status,
|
||||
'fran', i.fran, 'skapad', i.skapad) order by i.skapad)
|
||||
filter (where i.id is not null), '[]') as inlagg
|
||||
from supportarenden s
|
||||
left join anvandare a on a.id = s.skapad_av
|
||||
left join supportinlagg i on i.support_id = s.id
|
||||
where s.organisation_id = $1
|
||||
group by s.id, a.namn
|
||||
order by s.nummer desc limit 200`,
|
||||
[anspr.org],
|
||||
);
|
||||
return svara(res, 200, {
|
||||
arenden: rader.rows.map(({ inlagg, ...r }) => ({
|
||||
...r,
|
||||
inlagg,
|
||||
// Statusen lagras inte, den härleds. Se supportstatus().
|
||||
status: supportstatus(inlagg),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === "POST") {
|
||||
const kropp = await lasKropp(req);
|
||||
const fel = granskaAnmalan(kropp);
|
||||
if (fel) return svara(res, 400, { error: fel });
|
||||
|
||||
// Ärendet måste vara vårt om det anges. En anmälan är inte en
|
||||
// väg runt organisationsgränsen.
|
||||
if (kropp.arendeId && !(await arendeIOrg(kropp.arendeId, anspr.org))) {
|
||||
return svara(res, 404, { error: "Ärendet är inte tillgängligt." });
|
||||
}
|
||||
|
||||
// Sammanhanget HÄRLEDS. Klienten får skicka tekniska fält, men
|
||||
// byggSammanhang() släpper bara igenom de kända — ett fordon
|
||||
// eller en kund kan inte smugglas in i en supportanmälan.
|
||||
const kontext = byggSammanhang({
|
||||
...kropp.sammanhang,
|
||||
arendeId: kropp.arendeId,
|
||||
sparId: res.spår.spårId,
|
||||
plattformsversion: kropp.sammanhang?.plattformsversion,
|
||||
});
|
||||
|
||||
const skapat = await medSupportnummer(async (klient, nummer) => {
|
||||
const rad = await klient.query(
|
||||
`insert into supportarenden
|
||||
(organisation_id, arende_id, nummer, beteckning, typ, rubrik, beskrivning, sammanhang, skapad_av)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
returning id, beteckning, skapad`,
|
||||
[
|
||||
anspr.org,
|
||||
kropp.arendeId ?? null,
|
||||
nummer,
|
||||
supportbeteckning(nummer),
|
||||
kropp.typ,
|
||||
kropp.rubrik.trim(),
|
||||
kropp.beskrivning.trim(),
|
||||
JSON.stringify(kontext),
|
||||
anspr.sub,
|
||||
],
|
||||
);
|
||||
return rad.rows[0];
|
||||
});
|
||||
logga("info", "supportärende skapat", {
|
||||
beteckning: skapat.beteckning,
|
||||
typ: kropp.typ,
|
||||
organisation: anspr.org,
|
||||
spårId: res.spår.spårId,
|
||||
});
|
||||
return svara(res, 201, { ...skapat, status: "mottagen" });
|
||||
}
|
||||
}
|
||||
|
||||
// Svar och statusbyten. Verkstaden får svara i sitt eget ärende;
|
||||
// status sätts bara av vår support, med egen nyckel.
|
||||
const supportInlagg = vag.match(/^\/api\/support\/([0-9a-fA-F-]{36})\/inlagg$/);
|
||||
if (req.method === "POST" && supportInlagg) {
|
||||
const { text, status } = await lasKropp(req);
|
||||
const agare = await pool.query(
|
||||
`select organisation_id from supportarenden where id = $1`,
|
||||
[supportInlagg[1]],
|
||||
);
|
||||
if (agare.rowCount === 0 || agare.rows[0].organisation_id !== anspr.org) {
|
||||
return svara(res, 404, { error: "Supportärendet är inte tillgängligt." });
|
||||
}
|
||||
if (status !== undefined) {
|
||||
if (!arSupport(req)) return svara(res, 403, { error: "Status sätts av supporten." });
|
||||
if (!STATUSAR.includes(status)) return svara(res, 400, { error: "Okänd status." });
|
||||
}
|
||||
if (typeof text !== "string" || text.trim().length < 2) {
|
||||
return svara(res, 400, { error: "Skriv något." });
|
||||
}
|
||||
await pool.query(
|
||||
`insert into supportinlagg (support_id, typ, text, status, fran, skapad_av)
|
||||
values ($1, $2, $3, $4, $5, $6)`,
|
||||
[
|
||||
supportInlagg[1],
|
||||
status !== undefined ? "status" : "svar",
|
||||
text.trim(),
|
||||
status ?? null,
|
||||
arSupport(req) ? "Support" : anspr.namn,
|
||||
arSupport(req) ? null : anspr.sub,
|
||||
],
|
||||
);
|
||||
return svara(res, 200, { ok: true });
|
||||
}
|
||||
|
||||
// Organisationens egna fakturor. Läsning, aldrig mer: kunden ser
|
||||
// vad den ska betala och varför, men utfärdar inget och bokför
|
||||
// inget. Kommersiella uppgifter — därför administratör.
|
||||
|
||||
Reference in New Issue
Block a user