Files
Bernt 04e0a1e8ec
Guidad Felsökning / test-och-bygg (push) Has been cancelled
Guidad Felsökning / tjanster (push) Has been cancelled
Guidad Felsökning / terraform (push) Has been cancelled
Guidad Felsökning / leverantorskedja (push) Has been cancelled
Guidad Felsökning / aterstallning (push) Has been cancelled
Guidad Felsökning / publicera (push) Has been cancelled
Guidad Felsökning / driftsatt (push) Has been cancelled
ALVA deployment config
2026-08-08 13:12:13 +00:00

212 lines
7.7 KiB
JavaScript

// ALVA-PROC-0002 · Abonnemang.
//
// ---- Hållningen ---------------------------------------------------------
//
// Vi litar på kunden. Det är inte en artighet utan ett designbeslut med
// följder: inget kort krävs, ingen förskottsbetalning, ingen spärr innan
// någon fått veta att något är obetalt. Kunden är en verkstad med
// anställda och kunder i sin tur — den försvinner inte över natten, och
// att behandla den som om den kunde det gör produkten sämre för alla som
// betalar i tid.
//
// Vid utebliven betalning varnar vi, räknar ned synligt, och låser sedan.
//
// ---- Vad en låsning ALDRIG får göra -------------------------------------
//
// Låsning stänger nya ärenden. Den rör inte historiken, och den får inte
// göra det.
//
// Skälet är inte generositet. Ärendeloggen är verkstadens underlag i en
// garanti- eller försäkringstvist som gäller DERAS kund — en tredje part
// som inte har någon del i vår obetalda faktura. Att göra det underlaget
// oåtkomligt vore att använda någon annans rättsliga ställning som
// påtryckningsmedel. Läsning och export förblir därför öppna i låst läge,
// och det är inte förhandlingsbart.
//
// ---- Gratis är en nivå, inte en provperiod ------------------------------
//
// Kontot startar på Free och stannar där tills någon väljer annat. En
// provperiod som tyst börjar kosta är samma sorts fälla som produkten
// finns för att undvika. Fakturan skapas ändå vid registreringen — på
// noll kronor — därför att den etablerar serien och visar kunden exakt
// vad den inte betalar för.
import { PRISLISTA } from "./fakturering.mjs";
/** Dagar efter förfallodagen innan låsning. Synlig nedräkning hela tiden. */
export const RESPITDAGAR = 14;
/**
* Nivåerna.
*
* `basavgift` och `anvandaravgift` är i öre, som resten av faktureringen.
* Värdena är PLATSHÅLLARE på samma villkor som PRISLISTA — mekanismen är
* oberoende av dem.
*/
export const NIVAER = [
{
id: "free",
namn: "Free",
basavgift: 0,
anvandaravgift: 0,
maxAnvandare: 2,
text: "Full method, full evidence model, full report. Two accounts.",
ingar: [
"The complete ALVA methodology and quality gate",
"Append-only case log and evidence report",
"Two active accounts",
],
saknas: ["Fleet analytics", "Outgoing integrations", "Knowledge source connectors"],
},
{
id: "standard",
namn: "Standard",
basavgift: PRISLISTA.plattform_ar / 12,
anvandaravgift: PRISLISTA.anvandare_manad,
maxAnvandare: null,
text: "Platform fee plus a fee per active account.",
ingar: [
"Everything in Free, without the account limit",
"Fleet analytics and fault-cause statistics",
"Live Share with customer, partner and internal levels",
"Instrument register with calibration tracking",
],
saknas: ["Outgoing integrations", "Knowledge source connectors"],
},
{
id: "premium",
namn: "Premium",
basavgift: (PRISLISTA.plattform_ar / 12) * 2,
anvandaravgift: PRISLISTA.anvandare_manad,
maxAnvandare: null,
text: "For workshops that report into someone else's system.",
ingar: [
"Everything in Standard",
"Outgoing integrations and event subscriptions",
"Protocol ingest from external test equipment",
"Knowledge source connectors",
],
saknas: ["Dedicated instance", "Custom methodology authoring"],
},
{
id: "enterprise",
namn: "Enterprise",
basavgift: null, // Avtalas. Inte "kontakta oss" som prissignal — det finns inget listpris.
anvandaravgift: null,
maxAnvandare: null,
text: "Chains and fleets. Priced per agreement because the scope is.",
ingar: [
"Everything in Premium",
"Own methodologies and own rule package",
"Dedicated instance, own key management",
"Signed rule package and audit support",
],
saknas: [],
},
];
export const nivaFor = (id) => NIVAER.find((n) => n.id === id) ?? NIVAER[0];
/** Abonnemangets tillstånd. Härlett, aldrig lagrat. */
export const TILLSTAND = ["aktiv", "varning", "last"];
/**
* Tillståndet, härlett ur fakturorna.
*
* Tre utfall och inget fjärde:
*
* aktiv Inget är förfallet.
* varning Något är förfallet men respiten löper. Nedräkningen visas.
* last Respiten är slut.
*
* En faktura på noll kronor kan inte försätta någon i varning. Free-nivån
* fakturerar noll, och en obetald nollfaktura är inte en skuld — den är
* en bokföringspost. Att låsa någon för den vore absurt, och det är
* precis den sortens absurditet som uppstår när tillståndet räknas fram
* utan att någon tänkt på specialfallet.
*
* @param fakturor [{ forfaller, totalt, status }]
* @param nu tidpunkt; injicerad för testbarhet
*/
export function abonnemangstillstand(fakturor = [], nu = new Date()) {
const obetalda = fakturor.filter(
(f) => f.status === "utfardad" && Number(f.totalt) > 0 && new Date(f.forfaller) < nu,
);
if (obetalda.length === 0) return { tillstand: "aktiv", dagarKvar: null, aldsta: null };
// Den äldsta förfallna styr. Betalar kunden den senaste men inte den
// första har respiten ändå löpt sedan den första.
const aldsta = obetalda.reduce((a, b) => (new Date(a.forfaller) < new Date(b.forfaller) ? a : b));
const forfalloDygn = Math.floor((nu - new Date(aldsta.forfaller)) / 86_400_000);
const dagarKvar = RESPITDAGAR - forfalloDygn;
return {
tillstand: dagarKvar > 0 ? "varning" : "last",
dagarKvar: Math.max(0, dagarKvar),
aldsta: aldsta.beteckning ?? null,
belopp: obetalda.reduce((s, f) => s + Number(f.totalt), 0),
};
}
/**
* Vad ett tillstånd tillåter.
*
* Läsning och export är sanna i ALLA tillstånd. Se filhuvudet — det är
* verkstadens kunds underlag, inte verkstadens, och inte vårt.
*/
export function behorighet(tillstand) {
const last = tillstand === "last";
return {
lasa: true,
exportera: true,
nyttArende: !last,
dokumentera: !last,
avsluta: !last,
};
}
/**
* Beskedet till kunden. Rak text, ingen förhandling, inget hot.
*
* Statusspråket gäller även här: ett konstaterande och vad som händer
* härnäst.
*/
export function besked(status) {
if (status.tillstand === "aktiv") return null;
if (status.tillstand === "varning") {
return (
`Invoice ${status.aldsta} is past due. New cases can be started for ${status.dagarKvar} more ` +
`day${status.dagarKvar === 1 ? "" : "s"}. Reading and export are never affected.`
);
}
return (
`Invoice ${status.aldsta} is past due and the grace period has ended. New cases cannot be started. ` +
"Existing cases remain readable and exportable — that record belongs to your customer, not to us."
);
}
/**
* Nästa fakturaperiod.
*
* Månadsvis från registreringsdagen, inte från den första i månaden: en
* organisation som registrerar sig den 20:e ska inte få en faktura på
* elva dagar som sin första upplevelse av produkten.
*/
export function nastaPeriod(registrerad, senastFakturerad = null) {
const start = new Date(senastFakturerad ?? registrerad);
if (Number.isNaN(start.getTime())) return null;
const fran = new Date(start);
if (senastFakturerad) fran.setMonth(fran.getMonth() + 1);
const till = new Date(fran);
till.setMonth(till.getMonth() + 1);
till.setDate(till.getDate() - 1);
return { fran: fran.toISOString().slice(0, 10), till: till.toISOString().slice(0, 10) };
}
/** Är det dags att fakturera perioden? */
export function forfallenFakturering(registrerad, senastFakturerad, nu = new Date()) {
const period = nastaPeriod(registrerad, senastFakturerad);
if (!period) return null;
return new Date(period.fran) <= nu ? period : null;
}