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
212 lines
7.7 KiB
JavaScript
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;
|
|
}
|