Funktionen: Warnung der Aufbauphase, Abo-Beginn mit Datum, Texte für abgeschaltet und bereits eingeschaltet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 17:05:46 +00:00
1 parent 06166221ec
commit 1742fb254d
4 files changed
+116 -10

No files matched your search

+2
View File
@@ -67,6 +67,8 @@ export function createFinanceRequest({ baseUrl, createJWT, fetchImpl = (...args)
if (typeof info.code === 'string') err.code = info.code 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 // 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 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 if (info.fehler && typeof info.fehler === 'object') err.fehler = info.fehler
throw err throw err
} }
+43 -10
View File
@@ -312,8 +312,12 @@ export const fragtAbrechnung = (key, action, feature) =>
export const abrechnungBereit = ({ key, action, feature, abrechnung }) => export const abrechnungBereit = ({ key, action, feature, abrechnung }) =>
!fragtAbrechnung(key, action, feature) || ABRECHNUNG_OPTIONEN.some((o) => o.value === 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 const name = FUNKTIONEN.find((f) => f.key === key)?.label || key
switch (grund) { switch (grund) {
case 'kein_abo': 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.` return `${name}: Das Abo des Kunden ist gekündigt. Eingeschaltet wird nicht, solange die Kündigung vorgemerkt ist.`
case 'nicht_aktiv': case 'nicht_aktiv':
return `${name}: Das Abo des Kunden läuft nicht (Status nicht aktiv). Bitte zuerst das Abo klären.` 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: default:
return `${name}: Das passende Abo fehlt.` 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, * 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'] const K16_OHNE_UNTERLAGEN = ['zeiten']
/** /**
* Statuszeile nach dem Speichern einer Aktion: Warnung statt "Gespeichert.", wenn die Antwort warnung: true traegt. Der Wortlaut der * Aufbauphase (Abo-Modell): das Team hat "Im Projekt enthalten" gewaehlt, obwohl der Kunde noch nie ein Abo hatte. Das Portal schaltet ein und
* Warnung folgt der Funktion (key): Terminplaner und Webshop nennen Leistungsbeschreibung und AVV, Oeffnungszeiten live keinen AVV. * 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) => export const ABO_FEHLT_WARNUNG = 'Eingeschaltet. Achtung: Diese Funktion läuft nur mit Wartungs-Abo. Bitte das Abo vor dem Livegang vereinbaren.'
antwort?.warnung === true
? { ok: false, text: K16_OHNE_UNTERLAGEN.includes(key) ? K16_WARNUNG_OHNE_UNTERLAGEN : K16_WARNUNG } /**
: { ok: true, text: 'Gespeichert.' } * 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) --- // --- Webshop: Anfrage des Kunden und Stand des Shops (Portal: GET /shop/:customerId) ---
+14
View File
@@ -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.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) 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
})
+57
View File
@@ -37,6 +37,7 @@ const {
speicherStatus, speicherStatus,
K16_WARNUNG, K16_WARNUNG,
K16_WARNUNG_OHNE_UNTERLAGEN, K16_WARNUNG_OHNE_UNTERLAGEN,
ABO_FEHLT_WARNUNG,
warenartLabel, warenartLabel,
artikelzahlLabel, artikelzahlLabel,
anfrageStatus, 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(fehler('kein_abo', 'andere'), 'news'), 'Text des Portals')
assert.equal(featureFehlerText(null, 'news'), 'Speichern fehlgeschlagen.') 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))
}
})