diff --git a/src/components/customer/CustomerFunktionen.jsx b/src/components/customer/CustomerFunktionen.jsx
index 95c4382..ef6feaf 100644
--- a/src/components/customer/CustomerFunktionen.jsx
+++ b/src/components/customer/CustomerFunktionen.jsx
@@ -4,7 +4,7 @@ import { Card, Badge, Button, EmptyState } from '../ui'
import { copyText } from '../CopyableCredential'
import { getFunktionen, setFeature, getTermineAnleitung, getShop, entscheideShopAnfrage, getUnterkunft, entscheideUnterkunftAnfrage } from '../../lib/teamApi'
import {
- FUNKTIONEN, ZUSTAND, AKTION_LABEL, funktionOf, aktionen, mergeFunktion, siteKeyOf, abgeschaltetText,
+ FUNKTIONEN, ZUSTAND, AKTION_LABEL, funktionOf, aktionen, aboModellFunktion, mergeFunktion, siteKeyOf, abgeschaltetText,
hindernisText, panelModell, anleitungOffen, textformNoetig, textformAnsicht, textformBereit, speicherStatus,
anfrageZeilen, anfrageStatus, stripeText, stripeTon, shopHatEintrag, begruendungOk, BEGRUENDUNG_MAX, entscheidungStatus,
ABRECHNUNG_OPTIONEN, ABRECHNUNG_HINWEIS, fragtAbrechnung, abrechnungBereit, featureFehlerText,
@@ -418,7 +418,7 @@ export default function CustomerFunktionen({ customerId }) {
{!isConfirming && (
- {aktionen(feature.state, f.key, { ticketMissing: feature.ticketMissing, hindernis: feature.hindernis }).map((action) => (
+ {aktionen(feature.state, f.key, { ticketMissing: feature.ticketMissing, hindernis: feature.hindernis, aboModell: aboModellFunktion(feature) }).map((action) => (
diff --git a/src/lib/funktionenView.js b/src/lib/funktionenView.js
index e5081d3..29faf49 100644
--- a/src/lib/funktionenView.js
+++ b/src/lib/funktionenView.js
@@ -12,6 +12,7 @@ export const FUNKTIONEN = [
bestaetigen: {
activate: 'News ohne Zahlung aktivieren?',
disable: 'News abschalten? Die Website zeigt dann keine Beiträge mehr.',
+ ticket: 'Fehlendes Einrichtungs-Ticket neu anlegen?',
},
},
{
@@ -112,8 +113,11 @@ export const AKTION_LABEL = {
* Abschalten, auch wenn das Ersatz-Ticket beim Abschalten fehlschlug (das Portal merkt sich das nicht; die Antwort nennt es nur einmal).
* hindernis (Portal: feature.hindernis): beim Terminplaner bleibt die Abnahme sichtbar und das Portal sperrt sie mit Begruendung
* (hindernisText); bei Oeffnungszeiten live entfaellt sie, solange der Kunde noch keine Zeiten eingetragen hat ('zeiten_fehlen').
+ * aboModell (aboModellFunktion(feature)): News, die das Abo-Modell eingeschaltet hat und die in "setup" stehen, bieten kein "Aktivieren
+ * (ohne Zahlung)": das Portal antwortet darauf mit 409 bereits_eingeschaltet. Es bleibt Abschalten, bei fehlendem Ticket davor "Ticket nachlegen"
+ * (die Route heilt auch ein fehlendes Ticket; ticketFreischaltung gilt im Portal auch fuer News). Alle anderen Faelle bleiben unveraendert.
*/
-export function aktionen(state, key, { ticketMissing = false, hindernis = null } = {}) {
+export function aktionen(state, key, { ticketMissing = false, hindernis = null, aboModell = false } = {}) {
if (key === 'termine' || key === 'shop' || key === 'zeiten' || key === 'unterkunft') {
if (state === 'ordered' || state === 'active') return ['disable']
if (state === 'setup') {
@@ -122,12 +126,20 @@ export function aktionen(state, key, { ticketMissing = false, hindernis = null }
}
return ['activate']
}
+ if (state === 'setup' && aboModell === true) return [...(ticketMissing ? ['ticket'] : []), 'disable']
const list = []
if (state !== 'active') list.push('activate')
if (['ordered', 'setup', 'active'].includes(state)) list.push('disable')
return list
}
+/**
+ * true, wenn das Abo-Modell diese Funktion eingeschaltet hat: Portal feature.unlockedBy ist "abo" oder "projekt" (Marker von einschalten,
+ * Kauf-Bestand traegt "kauf", das Team "team", eine ausgeschaltete Funktion null). Das Portal nennt den Kunden nicht als Ganzen "im
+ * Abo-Modell"; das Dokument der Funktion ist die Stelle, an der es entscheidet. Fehlt das Feld (aelteres Portal), ist es false.
+ */
+export const aboModellFunktion = (feature) => feature?.unlockedBy === 'abo' || feature?.unlockedBy === 'projekt'
+
/** Warum der Terminplaner in "setup" noch nicht freigeschaltet werden kann (Portal: feature.hindernis) -> Hinweistext; sonst ''. */
const HINDERNIS_TEXT = {
kein_abo: 'Kein laufendes Wartungs-Abo: Freischalten ist gesperrt.',
diff --git a/tests/unit/funktionenView.test.mjs b/tests/unit/funktionenView.test.mjs
index 3b86616..d21b367 100644
--- a/tests/unit/funktionenView.test.mjs
+++ b/tests/unit/funktionenView.test.mjs
@@ -9,6 +9,7 @@ const {
AKTION_LABEL,
funktionOf,
aktionen,
+ aboModellFunktion,
mergeFunktion,
siteKeyOf,
abgeschaltetText,
@@ -59,6 +60,7 @@ test('FUNKTIONEN: News, Terminplaner, Webshop und Öffnungszeiten live (in diese
bestaetigen: {
activate: 'News ohne Zahlung aktivieren?',
disable: 'News abschalten? Die Website zeigt dann keine Beiträge mehr.',
+ ticket: 'Fehlendes Einrichtungs-Ticket neu anlegen?',
},
},
{
@@ -168,6 +170,69 @@ test('aktionen: News ignoriert ticketMissing in jedem Zustand', () => {
}
})
+test('aboModellFunktion: nur Funktionen, die das Abo-Modell eingeschaltet hat (unlockedBy "abo" oder "projekt")', () => {
+ assert.equal(aboModellFunktion({ state: 'setup', unlockedBy: 'abo' }), true)
+ assert.equal(aboModellFunktion({ state: 'setup', unlockedBy: 'projekt' }), true)
+ for (const unlockedBy of ['kauf', 'team', null, undefined, '', 'Abo', 'ABO', ' abo', 'projekt ', 5, true, ['abo']]) {
+ assert.equal(aboModellFunktion({ state: 'setup', unlockedBy }), false, String(unlockedBy))
+ }
+ for (const feature of [undefined, null, {}, 'abo', 5]) assert.equal(aboModellFunktion(feature), false, String(feature))
+})
+
+test('aktionen W2: News im Abo-Modell in "Einrichtung" bietet kein Aktivieren (der Server antwortet 409 bereits_eingeschaltet), nur Abschalten', () => {
+ assert.deepEqual(aktionen('setup', 'news', { aboModell: true }), ['disable'])
+ assert.deepEqual(aktionen('setup', undefined, { aboModell: true }), ['disable'], 'ohne Schlüssel wie News')
+ assert.deepEqual(aktionen('setup', 'news', { aboModell: true, hindernis: 'kein_abo' }), ['disable'])
+})
+
+test('aktionen W2: fehlt das Einrichtungs-Ticket, steht an der Stelle von Aktivieren "Ticket nachlegen" (dafür gibt es die Route, Aktivieren nicht)', () => {
+ assert.deepEqual(aktionen('setup', 'news', { aboModell: true, ticketMissing: true }), ['ticket', 'disable'])
+ assert.deepEqual(aktionen('setup', 'news', { aboModell: true, ticketMissing: false }), ['disable'])
+ for (const action of aktionen('setup', 'news', { aboModell: true, ticketMissing: true })) {
+ assert.ok(FUNKTIONEN.find((f) => f.key === 'news').bestaetigen[action], `Text für ${action}`)
+ }
+ assert.equal(FUNKTIONEN.find((f) => f.key === 'news').bestaetigen.ticket, 'Fehlendes Einrichtungs-Ticket neu anlegen?')
+})
+
+test('aktionen W2: außerhalb von "Einrichtung" und ohne Abo-Modell bleibt alles wie bisher', () => {
+ // andere Zustände: das Abo-Modell ändert nichts
+ for (const state of ['off', 'ordered', 'active', 'disabled', undefined]) {
+ for (const ticketMissing of [false, true]) {
+ assert.deepEqual(aktionen(state, 'news', { aboModell: true, ticketMissing }), aktionen(state, 'news'), String(state))
+ }
+ }
+ // ohne Abo-Modell (Kauf, Team, altes Portal ohne unlockedBy): Aktivieren in "Einrichtung" bleibt, ticketMissing wird ignoriert
+ assert.deepEqual(aktionen('setup', 'news', { aboModell: false }), ['activate', 'disable'])
+ assert.deepEqual(aktionen('setup', 'news', { aboModell: false, ticketMissing: true }), ['activate', 'disable'])
+ assert.deepEqual(aktionen('setup', 'news', {}), ['activate', 'disable'])
+ assert.deepEqual(aktionen('setup', 'news', { aboModell: 'abo' }), ['activate', 'disable'], 'nur ein echtes true zählt')
+ // ticketbasierte Funktionen sind vom Abo-Modell unberührt (dort gibt es in "Einrichtung" nie Aktivieren)
+ for (const key of ['termine', 'shop', 'zeiten', 'unterkunft']) {
+ for (const state of ['off', 'ordered', 'setup', 'active', 'disabled', undefined]) {
+ for (const ticketMissing of [false, true]) {
+ assert.deepEqual(aktionen(state, key, { aboModell: true, ticketMissing }), aktionen(state, key, { ticketMissing }), `${key} ${String(state)}`)
+ }
+ }
+ }
+})
+
+test('aktionen: jede angebotene Aktion hat Beschriftung und Bestätigungstext (alle Funktionen, Zustände und Optionen, auch im Abo-Modell)', () => {
+ for (const f of FUNKTIONEN) {
+ for (const state of ['off', 'ordered', 'setup', 'active', 'disabled', undefined]) {
+ for (const aboModell of [false, true]) {
+ for (const ticketMissing of [false, true]) {
+ for (const hindernis of [null, 'zeiten_fehlen']) {
+ for (const action of aktionen(state, f.key, { aboModell, ticketMissing, hindernis })) {
+ assert.ok(AKTION_LABEL[action], `${f.key} ${String(state)}: Beschriftung ${action}`)
+ assert.ok(f.bestaetigen[action], `${f.key} ${String(state)}: Text ${action}`)
+ }
+ }
+ }
+ }
+ }
+ }
+})
+
test('aktionen: Terminplaner – bestellt nur Abschalten (kein Aktivieren ohne Zahlung), Einrichtung mit Abnahme', () => {
assert.deepEqual(aktionen('off', 'termine'), ['activate'])
assert.deepEqual(aktionen('ordered', 'termine'), ['disable'], 'Aktivieren würde eine spätere Zahlung im Portal verschlucken')
|