diff --git a/src/lib/stundenkontoView.js b/src/lib/stundenkontoView.js index d003096..d9cd63d 100644 --- a/src/lib/stundenkontoView.js +++ b/src/lib/stundenkontoView.js @@ -14,9 +14,14 @@ const lesbar = (at) => typeof at === 'string' && at !== '' && !Number.isNaN(new const HINWEIS_HOSTING = 'Hosting-Abo: kein Stundenkonto.' const HINWEIS_KEINS = 'Kein Abo: kein Stundenkonto.' +// Vorschuss (Einrichtung ohne Deckung): mit laufendem Abo geht er in die nächsten Gutschriften, nach dem Ende des Abos verfällt er (E17: WEBklar trägt den Rest) +const SUB_VORSCHUSS = 'wird mit den nächsten Gutschriften verrechnet' +const SUB_VORSCHUSS_ENDE = 'verfällt, wird nicht berechnet' // null (keine brauchbare Antwort) | { art: 'konto'|'hosting'|'keins', hinweis, eintraege: [{ key, label, wert, sub, ton }] } // Reihenfolge: Guthaben, Vorschuss (nur > 0), Mehraufwand (nur > 0), Verfall (nur mit Minuten und lesbarem Datum). +// antwort.active (Portal, boolean, Abo läuft): nur ein ausdrückliches false ändert den Satz zum Vorschuss; fehlt das Feld (altes Portal) oder ist es +// etwas anderes, gilt wie bisher "wird mit den nächsten Gutschriften verrechnet". export function stundenkontoAnsicht(antwort) { if (!istObjekt(antwort)) return null const ledger = antwort.ledger @@ -29,7 +34,7 @@ export function stundenkontoAnsicht(antwort) { const eintraege = [{ key: 'guthaben', label: 'Guthaben', wert: stepLabel(minuten(ledger.balanceMinutes)), sub: '', ton: '' }] const vorschuss = minuten(ledger.vorschussMinutes) if (vorschuss > 0) { - eintraege.push({ key: 'vorschuss', label: 'Vorschuss (Einrichtung)', wert: stepLabel(vorschuss), sub: 'wird mit den nächsten Gutschriften verrechnet', ton: 'warn' }) + eintraege.push({ key: 'vorschuss', label: 'Vorschuss (Einrichtung)', wert: stepLabel(vorschuss), sub: antwort.active === false ? SUB_VORSCHUSS_ENDE : SUB_VORSCHUSS, ton: 'warn' }) } const mehraufwand = minuten(ledger.overageMinutes) if (mehraufwand > 0) { diff --git a/src/lib/teamApi.js b/src/lib/teamApi.js index 9c3bf71..ff64d92 100644 --- a/src/lib/teamApi.js +++ b/src/lib/teamApi.js @@ -140,5 +140,5 @@ export const getCustomerAboStatus = (customerId) => request(`/customers/${enc(cu // --- Stundenkonto (alle Mitarbeiter) --- -/** { produkt: 'hosting' | 'wartung' | null, ledger: { balanceMinutes, vorschussMinutes, overageMinutes, nextExpiry: { minutes, at } | null } | null } (Aufbereitung: stundenkontoView) */ +/** { produkt: 'hosting' | 'wartung' | null, active?: boolean (Abo läuft; fehlt bei einem älteren Portal), ledger: { balanceMinutes, vorschussMinutes, overageMinutes, nextExpiry: { minutes, at } | null } | null } (Aufbereitung: stundenkontoView) */ export const getCustomerStundenkonto = (customerId) => request(`/customers/${enc(customerId)}/stundenkonto`) diff --git a/tests/unit/stundenkontoView.test.mjs b/tests/unit/stundenkontoView.test.mjs index dce57c5..c70d1d2 100644 --- a/tests/unit/stundenkontoView.test.mjs +++ b/tests/unit/stundenkontoView.test.mjs @@ -22,6 +22,28 @@ test('Hosting, kein Abo, unbrauchbare Antworten und Minuten', () => { const k = stundenkontoAnsicht({ produkt: 'wartung', ledger: { balanceMinutes: 85, vorschussMinutes: -5, overageMinutes: 'abc', nextExpiry: { minutes: 0, at: 'kaputt' } } }) assert.deepEqual(k.eintraege.map((e) => [e.key, e.wert]), [['guthaben', '1 h 25 min']]) }) +test('Vorschuss bei beendetem Abo (active false): verfällt, wird nicht berechnet; sonst wie bisher', () => { + const ledger = { balanceMinutes: 0, vorschussMinutes: 90, overageMinutes: 30, nextExpiry: null } + const vorschuss = (antwort) => stundenkontoAnsicht(antwort).eintraege.find((e) => e.key === 'vorschuss') + assert.deepEqual(vorschuss({ produkt: 'wartung', active: false, ledger }), { key: 'vorschuss', label: 'Vorschuss (Einrichtung)', wert: '1 h 30 min', sub: 'verfällt, wird nicht berechnet', ton: 'warn' }) + const verrechnet = 'wird mit den nächsten Gutschriften verrechnet' + assert.equal(vorschuss({ produkt: 'wartung', active: true, ledger }).sub, verrechnet) + // Feld fehlt (altes Portal) oder ist kein boolean false: Verhalten wie bisher + for (const active of [undefined, null, 0, '', 'false', 'nein', [], {}]) assert.equal(vorschuss({ produkt: 'wartung', active, ledger }).sub, verrechnet, String(active)) + assert.equal(vorschuss({ produkt: 'wartung', ledger }).sub, verrechnet) +}) +test('beendetes Abo ändert nur den Satz zum Vorschuss: Guthaben, Mehraufwand und Verfall bleiben, ohne Vorschuss kein Eintrag', () => { + const ledger = { balanceMinutes: 60, vorschussMinutes: 90, overageMinutes: 30, nextExpiry: { minutes: 60, at: '2027-08-01T12:00:00.000Z' } } + const aktiv = stundenkontoAnsicht({ produkt: 'wartung', active: true, ledger }) + const beendet = stundenkontoAnsicht({ produkt: 'wartung', active: false, ledger }) + assert.deepEqual(beendet.eintraege.map(({ key, wert, ton }) => [key, wert, ton]), aktiv.eintraege.map(({ key, wert, ton }) => [key, wert, ton])) + assert.deepEqual(beendet.eintraege.map((e) => e.sub), ['', 'verfällt, wird nicht berechnet', '', 'am 01.08.2027']) + const ohne = stundenkontoAnsicht({ produkt: 'wartung', active: false, ledger: { ...ledger, vorschussMinutes: 0 } }) + assert.deepEqual(ohne.eintraege.map((e) => e.key), ['guthaben', 'mehraufwand', 'verfall']) + // Hosting und kein Abo: active spielt keine Rolle + assert.deepEqual(stundenkontoAnsicht({ produkt: 'hosting', active: false, ledger: null }), { art: 'hosting', hinweis: 'Hosting-Abo: kein Stundenkonto.', eintraege: [] }) + assert.deepEqual(stundenkontoAnsicht({ produkt: null, active: false, ledger: null }), { art: 'keins', hinweis: 'Kein Abo: kein Stundenkonto.', eintraege: [] }) +}) test('einrichtungText', () => { assert.equal(einrichtungText({ einrichtungAbrechnung: 'abo' }), 'Einrichtung aus dem Abo: Die Arbeitszeit geht aufs Stundenkonto des Kunden, auch wenn das Guthaben nicht reicht.') assert.equal(einrichtungText({ einrichtungAbrechnung: 'projekt' }), 'Einrichtung im Projekt enthalten: Die Arbeitszeit zählt nicht aufs Abo-Stundenkonto.')