diff --git a/src/lib/financeRequest.js b/src/lib/financeRequest.js index bbe1d2b..ab2933b 100644 --- a/src/lib/financeRequest.js +++ b/src/lib/financeRequest.js @@ -67,6 +67,8 @@ export function createFinanceRequest({ baseUrl, createJWT, fetchImpl = (...args) if (typeof info.code === 'string') err.code = info.code // Funktion einschalten ohne passendes Abo (409 abo_fehlt): der Grund (kein_abo, falsches_abo, gekuendigt, nicht_aktiv …) bestimmt den Text if (typeof info.grund === 'string') err.grund = info.grund + // Bei grund beginnt_spaeter nennt das Portal den Beginn des Abos (ab, ISO-Zeitpunkt); featureFehlerText macht daraus das Datum + if (typeof info.ab === 'string') err.ab = info.ab if (info.fehler && typeof info.fehler === 'object') err.fehler = info.fehler throw err } diff --git a/src/lib/funktionenView.js b/src/lib/funktionenView.js index 9f8c185..e5081d3 100644 --- a/src/lib/funktionenView.js +++ b/src/lib/funktionenView.js @@ -312,8 +312,12 @@ export const fragtAbrechnung = (key, action, feature) => export const abrechnungBereit = ({ key, action, feature, abrechnung }) => !fragtAbrechnung(key, action, feature) || ABRECHNUNG_OPTIONEN.some((o) => o.value === abrechnung) -/** Text zu 409 abo_fehlt (Portal: grund = kein_abo | falsches_abo | gekuendigt | nicht_aktiv); unbekannter oder fehlender Grund -> allgemeiner Satz. */ -export function aboFehltText(grund, key) { +/** + * Text zu 409 abo_fehlt (Portal: grund = kein_abo | falsches_abo | gekuendigt | nicht_aktiv | beginnt_spaeter); unbekannter oder fehlender Grund + * -> allgemeiner Satz. ab (ISO-Zeitpunkt, nur bei beginnt_spaeter) nennt das Datum als Berliner Kalendertag; ohne brauchbares Datum bleibt es + * beim allgemeinen Satz. + */ +export function aboFehltText(grund, key, ab) { const name = FUNKTIONEN.find((f) => f.key === key)?.label || key switch (grund) { case 'kein_abo': @@ -324,13 +328,34 @@ export function aboFehltText(grund, key) { return `${name}: Das Abo des Kunden ist gekündigt. Eingeschaltet wird nicht, solange die Kündigung vorgemerkt ist.` case 'nicht_aktiv': return `${name}: Das Abo des Kunden läuft nicht (Status nicht aktiv). Bitte zuerst das Abo klären.` + case 'beginnt_spaeter': + if (typeof ab === 'string' && ab !== '' && !Number.isNaN(Date.parse(ab))) { + return `${name}: Das Abo beginnt am ${fmtBerlin(ab)}. Ab dann kann die Funktion eingeschaltet werden.` + } + return `${name}: Das passende Abo fehlt.` default: return `${name}: Das passende Abo fehlt.` } } -/** Fehlertext zu einer Aktion: bei abo_fehlt der Text zum Grund, sonst die Meldung des Fehlers, ohne beides "Speichern fehlgeschlagen.". */ -export const featureFehlerText = (err, key) => (err?.code === 'abo_fehlt' ? aboFehltText(err.grund, key) : err?.message || 'Speichern fehlgeschlagen.') +/** + * Fehlertext zu einer Aktion. 409 abo_fehlt: der Text zum Grund (bei beginnt_spaeter mit Datum). 409 abgeschaltet und bereits_eingeschaltet + * (Team-Aktivierung im Abo-Modell): eigene Texte fuers Team, nie der Kundentext des Portals. Sonst die Meldung des Fehlers, ohne beides + * "Speichern fehlgeschlagen.". + */ +export function featureFehlerText(err, key) { + const name = FUNKTIONEN.find((f) => f.key === key)?.label || key + switch (err?.code) { + case 'abo_fehlt': + return aboFehltText(err.grund, key, err.ab) + case 'abgeschaltet': + return `${name}: Die Funktion wurde für dieses Projekt abgeschaltet (der Grund steht in der Zeile). Sie lässt sich hier nicht wieder einschalten.` + case 'bereits_eingeschaltet': + return `${name}: Die Funktion ist für dieses Projekt schon eingeschaltet. Bitte die Seite neu laden.` + default: + return err?.message || 'Speichern fehlgeschlagen.' + } +} /** * Rumpf fuer POST /funktionen/:customerId/:key; Datum und Weg der Textform nur beim Aktivieren und nur, wenn beide da sind, @@ -356,13 +381,21 @@ export const K16_WARNUNG_OHNE_UNTERLAGEN = 'Eingeschaltet, aber die E-Mail „be const K16_OHNE_UNTERLAGEN = ['zeiten'] /** - * Statuszeile nach dem Speichern einer Aktion: Warnung statt "Gespeichert.", wenn die Antwort warnung: true traegt. Der Wortlaut der - * Warnung folgt der Funktion (key): Terminplaner und Webshop nennen Leistungsbeschreibung und AVV, Oeffnungszeiten live keinen AVV. + * Aufbauphase (Abo-Modell): das Team hat "Im Projekt enthalten" gewaehlt, obwohl der Kunde noch nie ein Abo hatte. Das Portal schaltet ein und + * antwortet mit 200 und warnung: 'abo_fehlt' (dazu grund: 'kein_abo'). Nicht zu verwechseln mit warnung: true (K16 ging nicht raus). */ -export const speicherStatus = (antwort, key) => - antwort?.warnung === true - ? { ok: false, text: K16_OHNE_UNTERLAGEN.includes(key) ? K16_WARNUNG_OHNE_UNTERLAGEN : K16_WARNUNG } - : { ok: true, text: 'Gespeichert.' } +export const ABO_FEHLT_WARNUNG = 'Eingeschaltet. Achtung: Diese Funktion läuft nur mit Wartungs-Abo. Bitte das Abo vor dem Livegang vereinbaren.' + +/** + * Statuszeile nach dem Speichern einer Aktion: Warnung statt "Gespeichert.", wenn die Antwort warnung: true (K16 nicht gesendet) oder + * warnung: 'abo_fehlt' (Aufbauphase ohne Abo) traegt. Der Wortlaut der K16-Warnung folgt der Funktion (key): Terminplaner und Webshop nennen + * Leistungsbeschreibung und AVV, Oeffnungszeiten live keinen AVV; die Abo-Warnung gilt fuer alle Funktionen gleich. + */ +export function speicherStatus(antwort, key) { + if (antwort?.warnung === true) return { ok: false, text: K16_OHNE_UNTERLAGEN.includes(key) ? K16_WARNUNG_OHNE_UNTERLAGEN : K16_WARNUNG } + if (antwort?.warnung === 'abo_fehlt') return { ok: false, text: ABO_FEHLT_WARNUNG } + return { ok: true, text: 'Gespeichert.' } +} // --- Webshop: Anfrage des Kunden und Stand des Shops (Portal: GET /shop/:customerId) --- diff --git a/tests/unit/financeRequest.test.mjs b/tests/unit/financeRequest.test.mjs index 10ebfaa..81483cc 100644 --- a/tests/unit/financeRequest.test.mjs +++ b/tests/unit/financeRequest.test.mjs @@ -151,3 +151,17 @@ test('Der Grund einer Abo-Sperre bleibt am Fehler (409 abo_fehlt mit grund)', as assert.deepEqual([a.status, a.code, a.grund], [409, 'abo_fehlt', 'kein_abo']) assert.equal((await fails(api('/funktionen/c1/news', { method: 'POST', body: {} }))).grund, undefined) }) + +test('Das Datum eines späteren Abo-Beginns bleibt am Fehler (409 abo_fehlt, beginnt_spaeter mit ab)', async () => { + const { api } = client({ + responses: [ + reply(409, { error: 'x', code: 'abo_fehlt', grund: 'beginnt_spaeter', ab: '2026-11-03T08:00:00.000Z' }), + reply(409, { error: 'y', code: 'abo_fehlt', grund: 'beginnt_spaeter', ab: 5 }), + reply(409, { error: 'z', code: 'abo_fehlt', grund: 'beginnt_spaeter' }), + ], + }) + const a = await fails(api('/funktionen/c6/news', { method: 'POST', body: {} })) + assert.deepEqual([a.status, a.code, a.grund, a.ab], [409, 'abo_fehlt', 'beginnt_spaeter', '2026-11-03T08:00:00.000Z']) + assert.equal((await fails(api('/funktionen/c6/news', { method: 'POST', body: {} }))).ab, undefined) // kein Text + assert.equal((await fails(api('/funktionen/c6/news', { method: 'POST', body: {} }))).ab, undefined) // fehlt +}) diff --git a/tests/unit/funktionenView.test.mjs b/tests/unit/funktionenView.test.mjs index d648ad1..3b86616 100644 --- a/tests/unit/funktionenView.test.mjs +++ b/tests/unit/funktionenView.test.mjs @@ -37,6 +37,7 @@ const { speicherStatus, K16_WARNUNG, K16_WARNUNG_OHNE_UNTERLAGEN, + ABO_FEHLT_WARNUNG, warenartLabel, artikelzahlLabel, anfrageStatus, @@ -896,3 +897,59 @@ test('aboFehltText und featureFehlerText: jeder Grund, unbekannter Grund, andere assert.equal(featureFehlerText(fehler('kein_abo', 'andere'), 'news'), 'Text des Portals') assert.equal(featureFehlerText(null, 'news'), 'Speichern fehlgeschlagen.') }) + +test('aboFehltText: beginnt_spaeter nennt den Berliner Tag des Beginns, ohne brauchbares Datum bleibt der allgemeine Satz', () => { + assert.equal(aboFehltText('beginnt_spaeter', 'termine', '2026-11-03T08:00:00.000Z'), 'Terminplaner: Das Abo beginnt am 03.11.2026. Ab dann kann die Funktion eingeschaltet werden.') + // Berliner Kalendertag, nicht UTC: 23:30 UTC am 3.11. ist in Berlin schon der 4.11. + assert.equal(aboFehltText('beginnt_spaeter', 'news', '2026-11-03T23:30:00.000Z'), 'News: Das Abo beginnt am 04.11.2026. Ab dann kann die Funktion eingeschaltet werden.') + for (const ab of [undefined, null, '', 'kein Datum', 5, {}]) assert.equal(aboFehltText('beginnt_spaeter', 'news', ab), 'News: Das passende Abo fehlt.', String(ab)) + // das Datum gehört nur zu beginnt_spaeter + for (const grund of ['kein_abo', 'falsches_abo', 'gekuendigt', 'nicht_aktiv', 'unbekannt']) { + assert.equal(aboFehltText(grund, 'news', '2026-11-03T08:00:00.000Z'), aboFehltText(grund, 'news'), grund) + } +}) + +test('featureFehlerText: beginnt_spaeter trägt das Datum des Fehlers, abgeschaltet und bereits_eingeschaltet haben eigene Texte fürs Team', () => { + const fehler = (code, extra = {}) => Object.assign(new Error('Text des Portals (in der Du-Form des Kunden)'), { status: 409, code, ...extra }) + assert.equal( + featureFehlerText(fehler('abo_fehlt', { grund: 'beginnt_spaeter', ab: '2026-11-03T08:00:00.000Z' }), 'shop'), + 'Webshop: Das Abo beginnt am 03.11.2026. Ab dann kann die Funktion eingeschaltet werden.' + ) + assert.equal(featureFehlerText(fehler('abo_fehlt', { grund: 'beginnt_spaeter' }), 'shop'), 'Webshop: Das passende Abo fehlt.') + assert.equal( + featureFehlerText(fehler('abgeschaltet'), 'news'), + 'News: Die Funktion wurde für dieses Projekt abgeschaltet (der Grund steht in der Zeile). Sie lässt sich hier nicht wieder einschalten.' + ) + assert.equal( + featureFehlerText(fehler('bereits_eingeschaltet'), 'zeiten'), + 'Öffnungszeiten live: Die Funktion ist für dieses Projekt schon eingeschaltet. Bitte die Seite neu laden.' + ) + assert.equal(featureFehlerText(fehler('abgeschaltet'), 'unbekannt'), 'unbekannt: Die Funktion wurde für dieses Projekt abgeschaltet (der Grund steht in der Zeile). Sie lässt sich hier nicht wieder einschalten.') + // nie der Kundentext des Portals ("du", "Schreib uns im Chat"), und kein Preis + for (const code of ['abgeschaltet', 'bereits_eingeschaltet']) { + const text = featureFehlerText(fehler(code), 'termine') + assert.doesNotMatch(text, /\bdu\b|\bdein|Schreib|Portal|€/i, code) + } + // ein anderer Code oder ein Fehler ohne Code zeigt wie bisher die Meldung des Portals + assert.equal(featureFehlerText(fehler('bestellung_laeuft'), 'news'), 'Text des Portals (in der Du-Form des Kunden)') + assert.equal(featureFehlerText(Object.assign(new Error('Netz weg'), { status: 0 }), 'news'), 'Netz weg') +}) + +test('speicherStatus: warnung "abo_fehlt" (Aufbauphase, noch kein Abo) ist eine eigene Warnung und wird nicht mit der K16-Warnung verwechselt', () => { + const aufbau = { projects: [], warnung: 'abo_fehlt', grund: 'kein_abo' } + for (const key of [undefined, 'news', 'termine', 'shop', 'zeiten', 'unterkunft']) { + assert.deepEqual(speicherStatus(aufbau, key), { ok: false, text: ABO_FEHLT_WARNUNG }, String(key)) + } + assert.equal(ABO_FEHLT_WARNUNG, 'Eingeschaltet. Achtung: Diese Funktion läuft nur mit Wartungs-Abo. Bitte das Abo vor dem Livegang vereinbaren.') + assert.doesNotMatch(ABO_FEHLT_WARNUNG, /K16|AVV|Unterlagen|€/) + assert.notEqual(ABO_FEHLT_WARNUNG, K16_WARNUNG) + assert.notEqual(ABO_FEHLT_WARNUNG, K16_WARNUNG_OHNE_UNTERLAGEN) + // die alte boolesche Warnung (K16 ging nicht raus) behält ihren Text, mit und ohne Grund + assert.deepEqual(speicherStatus({ projects: [], warnung: true }, 'termine'), { ok: false, text: K16_WARNUNG }) + assert.deepEqual(speicherStatus({ projects: [], warnung: true }, 'zeiten'), { ok: false, text: K16_WARNUNG_OHNE_UNTERLAGEN }) + assert.deepEqual(speicherStatus({ projects: [], warnung: true, grund: 'kein_abo' }, 'shop'), { ok: false, text: K16_WARNUNG }) + // ohne Warnung (oder mit einem Wert, den es nicht gibt) bleibt es bei "Gespeichert." + for (const antwort of [{ projects: [] }, { warnung: false }, { warnung: 'unbekannt' }, { warnung: 'abo_fehlt_nicht' }, undefined, null]) { + assert.deepEqual(speicherStatus(antwort, 'news'), { ok: true, text: 'Gespeichert.' }, JSON.stringify(antwort)) + } +})