Merge remote-tracking branch 'origin/test' into feature/oeffnungszeiten
Zusammenführung von TP2 und Webshop im Ticketsystem (origin/test00fa229) mit Öffnungszeiten live. Die Funktionen stehen in der Reihenfolge news, termine, shop, zeiten. Konfliktdateien (6, erwartet war nur CustomerFunktionen.jsx; origin/test enthält seit der Vorbereitung auch die beiden Webshop-Commits0a2eab8und00fa229): scripts/setup-legal.mjs, src/components/customer/CustomerFunktionen.jsx, src/lib/funktionenView.js, src/lib/teamApi.js, tests/setup-legal.test.mjs, tests/unit/funktionenView.test.mjs. Entscheidungen: ersatzTicket-Behandlung der Zeiten und speicherStatus/K16_WARNUNG von TP2 bleiben beide in save() (Abschalten der Zeiten zeigt abschaltMeldung, jede andere Aktion speicherStatus); Rechtliches-Karten Webshop (sort 10, 11) vor der Karte der Öffnungszeiten (sort 12, DOCUMENTS hat 12 Einträge); siteKeyOf kennt termine, shop und zeiten; aktionen behandelt shop und zeiten wie den Terminplaner (ticketbasiert, Abnahme der Zeiten mit hindernis); teamApi trägt Webshop- und Zeiten-Aufrufe nebeneinander. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
8319084930
19 files changed
+906
-165
No files matched your search
@@ -20,12 +20,28 @@ const {
|
||||
textformNoetig,
|
||||
termineTextformLabel,
|
||||
termineChatTemplate,
|
||||
shopTextformLabel,
|
||||
shopChatTemplate,
|
||||
TEXTFORM_FUNKTIONEN,
|
||||
textformAnsicht,
|
||||
textformBereit,
|
||||
featureBody,
|
||||
speicherStatus,
|
||||
K16_WARNUNG,
|
||||
warenartLabel,
|
||||
artikelzahlLabel,
|
||||
anfrageStatus,
|
||||
anfrageZeilen,
|
||||
anfrageText,
|
||||
stripeText,
|
||||
stripeTon,
|
||||
shopHatEintrag,
|
||||
begruendungOk,
|
||||
entscheidungBody,
|
||||
entscheidungStatus,
|
||||
} = await loadModule('src/lib/funktionenView.js')
|
||||
|
||||
test('FUNKTIONEN: News, Terminplaner und Öffnungszeiten live (in dieser Reihenfolge) mit den Bestätigungstexten', () => {
|
||||
test('FUNKTIONEN: News, Terminplaner, Webshop und Öffnungszeiten live (in dieser Reihenfolge) mit den Bestätigungstexten', () => {
|
||||
assert.deepEqual(FUNKTIONEN, [
|
||||
{
|
||||
key: 'news',
|
||||
@@ -45,6 +61,16 @@ test('FUNKTIONEN: News, Terminplaner und Öffnungszeiten live (in dieser Reihenf
|
||||
ticket: 'Fehlendes Einrichtungs-Ticket neu anlegen?',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'shop',
|
||||
label: 'Webshop',
|
||||
bestaetigen: {
|
||||
activate: 'Webshop ohne Zahlung aktivieren?',
|
||||
disable: 'Webshop abschalten? Auf der Website sind dann keine Bestellungen mehr möglich.',
|
||||
abnahme: 'Webshop freischalten? Der Kunde bekommt die Mail „bereit“ und der Shop kann geöffnet werden.',
|
||||
ticket: 'Fehlendes Einrichtungs-Ticket neu anlegen?',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'zeiten',
|
||||
label: 'Öffnungszeiten live',
|
||||
@@ -59,7 +85,7 @@ test('FUNKTIONEN: News, Terminplaner und Öffnungszeiten live (in dieser Reihenf
|
||||
})
|
||||
|
||||
test('FUNKTIONEN: Schlüssel sind eindeutig und jede Funktion hat Aktivieren und Abschalten bestätigt', () => {
|
||||
assert.deepEqual(FUNKTIONEN.map((f) => f.key), ['news', 'termine', 'zeiten'])
|
||||
assert.deepEqual(FUNKTIONEN.map((f) => f.key), ['news', 'termine', 'shop', 'zeiten'])
|
||||
for (const f of FUNKTIONEN) {
|
||||
assert.ok(f.bestaetigen.activate && f.bestaetigen.disable, f.key)
|
||||
}
|
||||
@@ -434,6 +460,71 @@ test('textformNoetig: nur "Aktivieren (ohne Zahlung)" des Terminplaners und nur,
|
||||
assert.equal(textformNoetig(undefined, 'activate', termine()), false)
|
||||
})
|
||||
|
||||
// Feld "shop" der Portal-Antwort GET /funktionen/:customerId (Fassung = config.shop.fassung)
|
||||
const shopFeld = (extra = {}) => ({
|
||||
textformNoetig: true,
|
||||
avvVersion: '2026-10-05',
|
||||
links: {
|
||||
lb: 'https://project.webklar.com/rechtliches/leistungsbeschreibung-webshop-2026-10-05.html',
|
||||
avv: 'https://project.webklar.com/rechtliches/avv-webshop-2026-10-05.html',
|
||||
},
|
||||
...extra,
|
||||
})
|
||||
|
||||
test('textformNoetig: auch das Aktivieren (ohne Zahlung) des Webshops, mit dem Feld shop; die Funktion kommt aus dem Schlüssel', () => {
|
||||
assert.deepEqual(TEXTFORM_FUNKTIONEN, ['termine', 'shop'])
|
||||
assert.equal(textformNoetig('shop', 'activate', shopFeld()), true)
|
||||
assert.equal(textformNoetig('shop', 'activate', shopFeld({ textformNoetig: false })), false)
|
||||
assert.equal(textformNoetig('shop', 'activate', undefined), false, 'altes Portal ohne Feld shop: wie bisher')
|
||||
assert.equal(textformNoetig('shop', 'activate', null), false)
|
||||
assert.equal(textformNoetig('shop', 'activate', shopFeld({ textformNoetig: 'true' })), false, 'nur echtes true')
|
||||
for (const action of ['disable', 'abnahme', 'ticket']) assert.equal(textformNoetig('shop', action, shopFeld()), false, action)
|
||||
assert.equal(textformNoetig('news', 'activate', shopFeld()), false)
|
||||
})
|
||||
|
||||
test('shopTextformLabel und shopChatTemplate: wörtlich, mit Fassung, den Erklärungen des Nachweises und den versionierten Adressen', () => {
|
||||
const l = shopTextformLabel('2026-10-05')
|
||||
assert.equal(l.lb, 'Leistungsbeschreibung Webshop')
|
||||
assert.equal(l.avv, 'Auftragsverarbeitungsvertrag Webshop')
|
||||
assert.equal(
|
||||
l.before + l.lb + l.between + l.avv + l.after,
|
||||
'Der Kunde hat der Leistungsbeschreibung Webshop und dem Auftragsverarbeitungsvertrag Webshop (Fassung 2026-10-05) ' +
|
||||
'in Textform zugestimmt und bestätigt, dass er als Unternehmer in eigenem Namen verkauft, für seine Produkte, Preise, Produktangaben und Rechtstexte selbst verantwortlich ist, ' +
|
||||
'keine der in der Leistungsbeschreibung ausgeschlossenen Waren verkauft und die Zahlungen seiner Käufer über sein eigenes Stripe-Konto laufen.',
|
||||
)
|
||||
const { lb, avv } = shopFeld().links
|
||||
const text = shopChatTemplate('2026-10-05', { lb, avv })
|
||||
assert.ok(text.startsWith('Damit wir den Webshop für Dich einschalten können, brauchen wir Deine Zustimmung in Textform. '))
|
||||
assert.ok(text.includes(`in der Fassung 2026-10-05: ${lb} · ${avv}. `))
|
||||
assert.match(text, /leistungsbeschreibung-webshop-2026-10-05\.html/)
|
||||
assert.match(text, /avv-webshop-2026-10-05\.html/)
|
||||
assert.match(text, /Ich stimme zu/)
|
||||
assert.match(text, /eigenes Stripe-Konto/)
|
||||
assert.ok(!text.includes('undefined') && !text.includes('{') && !text.includes('Terminplaner'))
|
||||
})
|
||||
|
||||
test('textformAnsicht(feld, "shop"): Label, Vorlage und Links des Webshops; ohne Schlüssel bleibt es der Terminplaner', () => {
|
||||
const a = textformAnsicht(shopFeld(), 'shop')
|
||||
assert.equal(a.version, '2026-10-05')
|
||||
assert.deepEqual(a.links, shopFeld().links)
|
||||
assert.deepEqual(a.label, shopTextformLabel('2026-10-05'))
|
||||
assert.equal(a.vorlage, shopChatTemplate('2026-10-05', shopFeld().links))
|
||||
assert.deepEqual(textformAnsicht(termine()).label, termineTextformLabel('2026-10-05'))
|
||||
// Adressen ohne https werden nie Link, ohne beide Adressen keine Vorlage
|
||||
const schlecht = textformAnsicht(shopFeld({ links: { lb: 'javascript:alert(1)', avv: 'http://x.example/a.html' } }), 'shop')
|
||||
assert.deepEqual(schlecht.links, { lb: '', avv: '' })
|
||||
assert.equal(schlecht.vorlage, '')
|
||||
assert.equal(textformAnsicht(undefined, 'shop').version, '')
|
||||
})
|
||||
|
||||
test('textformBereit mit dem Feld shop: Datum zwischen Fassung und heute, ohne Fassung nie bereit', () => {
|
||||
const ok = { confirmed: true, date: '2026-10-06', channel: 'email', today: '2026-10-07' }
|
||||
assert.equal(textformBereit(ok, shopFeld()), true)
|
||||
assert.equal(textformBereit({ ...ok, date: '2026-10-04' }, shopFeld()), false, 'vor der Fassung')
|
||||
assert.equal(textformBereit(ok, shopFeld({ avvVersion: '' })), false)
|
||||
assert.equal(textformBereit(ok, undefined), false)
|
||||
})
|
||||
|
||||
test('termineChatTemplate: wörtlich, mit Fassung und den beiden versionierten Adressen', () => {
|
||||
const { lb, avv } = termine().links
|
||||
assert.equal(
|
||||
@@ -524,3 +615,124 @@ test('featureBody: Aktivieren mit Textform trägt Datum und Weg, sonst bleibt de
|
||||
'halbe Angaben gehen nicht mit',
|
||||
)
|
||||
})
|
||||
|
||||
test('speicherStatus: warnung true (K16 nicht gesendet) wird zur Warnung, sonst "Gespeichert."', () => {
|
||||
assert.deepEqual(speicherStatus({ projects: [], warnung: true }), { ok: false, text: K16_WARNUNG })
|
||||
assert.match(K16_WARNUNG, /K16/)
|
||||
assert.match(K16_WARNUNG, /von Hand/)
|
||||
assert.deepEqual(speicherStatus({ projects: [] }), { ok: true, text: 'Gespeichert.' })
|
||||
assert.deepEqual(speicherStatus(undefined), { ok: true, text: 'Gespeichert.' })
|
||||
})
|
||||
|
||||
test('aktionen: Webshop verhält sich wie der Terminplaner (ticketbasiert)', () => {
|
||||
assert.deepEqual(aktionen('off', 'shop'), ['activate'])
|
||||
assert.deepEqual(aktionen('disabled', 'shop'), ['activate'])
|
||||
assert.deepEqual(aktionen('ordered', 'shop'), ['disable'])
|
||||
assert.deepEqual(aktionen('active', 'shop'), ['disable'])
|
||||
assert.deepEqual(aktionen('setup', 'shop'), ['abnahme', 'disable'])
|
||||
assert.deepEqual(aktionen('setup', 'shop', { ticketMissing: true }), ['ticket', 'abnahme', 'disable'])
|
||||
for (const state of ['off', 'ordered', 'active', 'disabled']) assert.deepEqual(aktionen(state, 'shop', { ticketMissing: true }), aktionen(state, 'shop'))
|
||||
for (const action of aktionen('setup', 'shop', { ticketMissing: true })) assert.ok(FUNKTIONEN.find((f) => f.key === 'shop').bestaetigen[action], `Text für ${action}`)
|
||||
})
|
||||
|
||||
test('siteKeyOf: auch der Webshop zeigt seinen Site-Key', () => {
|
||||
assert.equal(siteKeyOf({ state: 'active', siteKey: ' abcdefghijklmnop ' }, 'shop'), 'abcdefghijklmnop')
|
||||
assert.equal(siteKeyOf({ state: 'active' }, 'shop'), '')
|
||||
})
|
||||
|
||||
test('warenartLabel und artikelzahlLabel: Beschriftungen, Unbekanntes bleibt sichtbar', () => {
|
||||
assert.equal(warenartLabel('waren'), 'Waren')
|
||||
assert.equal(warenartLabel('lebensmittel'), 'Lebensmittel')
|
||||
assert.equal(warenartLabel('kleidung'), 'Kleidung')
|
||||
assert.equal(warenartLabel('kunst'), 'Kunst/Handgemachtes')
|
||||
assert.equal(warenartLabel('sonstiges'), 'Sonstiges')
|
||||
assert.equal(warenartLabel('xyz'), 'xyz')
|
||||
assert.equal(warenartLabel(undefined), '–')
|
||||
assert.equal(warenartLabel('toString'), 'toString', 'kein Treffer über die Prototypkette')
|
||||
assert.equal(artikelzahlLabel('bis20'), 'bis 20')
|
||||
assert.equal(artikelzahlLabel('bis100'), 'bis 100')
|
||||
assert.equal(artikelzahlLabel('mehr'), 'mehr als 100')
|
||||
assert.equal(artikelzahlLabel(''), '–')
|
||||
})
|
||||
|
||||
test('anfrageStatus: Ton und Beschriftung', () => {
|
||||
assert.deepEqual(anfrageStatus('offen'), ['warn', 'Offen'])
|
||||
assert.deepEqual(anfrageStatus('angenommen'), ['ok', 'Angenommen'])
|
||||
assert.deepEqual(anfrageStatus('abgelehnt'), ['danger', 'Abgelehnt'])
|
||||
assert.deepEqual(anfrageStatus('zurueckgezogen'), ['muted', 'Zurückgezogen'])
|
||||
assert.deepEqual(anfrageStatus(undefined), ['muted', '–'])
|
||||
})
|
||||
|
||||
const ANFRAGE = { warenart: 'kunst', beschreibung: ' Handgemachte Kerzen ', artikelzahl: 'bis20', versand: true, abholung: false, unternehmer: true, keineAusgeschlossenen: true, nachricht: '' }
|
||||
|
||||
test('anfrageZeilen: alle Angaben der Anfrage als Zeilen', () => {
|
||||
assert.deepEqual(anfrageZeilen(ANFRAGE), [
|
||||
{ label: 'Warenart', text: 'Kunst/Handgemachtes' },
|
||||
{ label: 'Beschreibung', text: 'Handgemachte Kerzen' },
|
||||
{ label: 'Artikelzahl', text: 'bis 20' },
|
||||
{ label: 'Übergabe', text: 'Versand' },
|
||||
{ label: 'Unternehmer', text: 'Ja' },
|
||||
{ label: 'Keine ausgeschlossenen Waren', text: 'Ja' },
|
||||
{ label: 'Nachricht', text: '–' },
|
||||
])
|
||||
assert.equal(anfrageZeilen({ ...ANFRAGE, abholung: true }).find((z) => z.label === 'Übergabe').text, 'Versand und Abholung')
|
||||
assert.equal(anfrageZeilen({ ...ANFRAGE, versand: false }).find((z) => z.label === 'Übergabe').text, '–')
|
||||
assert.deepEqual(anfrageZeilen(null), [])
|
||||
})
|
||||
|
||||
test('anfrageText: eine Zeile aus Warenart, Menge und Übergabe', () => {
|
||||
assert.equal(anfrageText(ANFRAGE), 'Kunst/Handgemachtes, bis 20 Artikel, Versand')
|
||||
assert.equal(anfrageText({ ...ANFRAGE, abholung: true }), 'Kunst/Handgemachtes, bis 20 Artikel, Versand und Abholung')
|
||||
assert.equal(anfrageText({ warenart: 'waren' }), 'Waren')
|
||||
assert.equal(anfrageText(null), '')
|
||||
})
|
||||
|
||||
test('stripeText: nicht verbunden, aktiv, Angaben fehlen', () => {
|
||||
assert.equal(stripeText(null, false), 'nicht verbunden')
|
||||
assert.equal(stripeText(undefined), 'nicht verbunden')
|
||||
assert.equal(stripeText(null, true), 'Konto angelegt, Stand noch nicht geprüft')
|
||||
assert.equal(stripeText({ charges_enabled: true, payouts_enabled: true }, true), 'Zahlungen aktiv')
|
||||
assert.equal(stripeText({ charges_enabled: true, payouts_enabled: false }, true), 'Zahlungen aktiv, Auszahlungen noch nicht freigegeben')
|
||||
assert.equal(stripeText({ charges_enabled: false, currently_due: ['external_account', 'individual.dob.day'], past_due: [] }, true), 'Angaben fehlen: external_account, individual.dob.day')
|
||||
assert.equal(stripeText({ charges_enabled: false, past_due: ['a'], currently_due: ['a', 'b'] }, true), 'Angaben fehlen: a, b', 'doppelte Einträge nur einmal')
|
||||
assert.equal(stripeText({ charges_enabled: false, currently_due: ['a', 'b', 'c', 'd', 'e', 'f'] }, true), 'Angaben fehlen: a, b, c, d und 2 weitere')
|
||||
assert.equal(stripeText({ charges_enabled: false, currently_due: [], disabled_reason: 'requirements.pending_verification' }, true), 'Angaben fehlen: requirements.pending_verification')
|
||||
assert.equal(stripeText({ charges_enabled: false, details_submitted: true }, true), 'Angaben fehlen: Prüfung durch Stripe läuft')
|
||||
assert.equal(stripeText({ charges_enabled: false, details_submitted: false }, true), 'Angaben fehlen: Einrichtung nicht abgeschlossen')
|
||||
})
|
||||
|
||||
test('stripeTon: ok, warn, muted', () => {
|
||||
assert.equal(stripeTon({ charges_enabled: true }, true), 'ok')
|
||||
assert.equal(stripeTon({ charges_enabled: false }, true), 'warn')
|
||||
assert.equal(stripeTon(null, true), 'warn')
|
||||
assert.equal(stripeTon(null, false), 'muted')
|
||||
})
|
||||
|
||||
test('shopHatEintrag: Anfrage oder Zustand außer "off"', () => {
|
||||
assert.equal(shopHatEintrag({ state: 'off', anfrage: null }), false)
|
||||
assert.equal(shopHatEintrag({ state: 'off', anfrage: { status: 'offen' } }), true)
|
||||
assert.equal(shopHatEintrag({ state: 'active', anfrage: null }), true)
|
||||
assert.equal(shopHatEintrag({ state: 'disabled' }), true)
|
||||
assert.equal(shopHatEintrag(null), false)
|
||||
})
|
||||
|
||||
test('begruendungOk: Pflicht, höchstens 1000 Zeichen', () => {
|
||||
assert.equal(begruendungOk(''), false)
|
||||
assert.equal(begruendungOk(' '), false)
|
||||
assert.equal(begruendungOk(undefined), false)
|
||||
assert.equal(begruendungOk('Passt nicht'), true)
|
||||
assert.equal(begruendungOk('x'.repeat(1000)), true)
|
||||
assert.equal(begruendungOk('x'.repeat(1001)), false)
|
||||
assert.equal(begruendungOk(` ${'x'.repeat(1000)} `), true, 'ohne Rand gezählt')
|
||||
})
|
||||
|
||||
test('entscheidungBody: Begründung nur beim Ablehnen, getrimmt', () => {
|
||||
assert.deepEqual(entscheidungBody({ projectId: 'p1', entscheidung: 'annehmen', begruendung: 'egal' }), { projectId: 'p1', entscheidung: 'annehmen' })
|
||||
assert.deepEqual(entscheidungBody({ projectId: 'p1', entscheidung: 'ablehnen', begruendung: ' Zu groß ' }), { projectId: 'p1', entscheidung: 'ablehnen', begruendung: 'Zu groß' })
|
||||
})
|
||||
|
||||
test('entscheidungStatus: Warnung, wenn die Mail an den Kunden fehlschlug', () => {
|
||||
assert.deepEqual(entscheidungStatus({ ok: true }), { ok: true, text: 'Gespeichert.' })
|
||||
assert.deepEqual(entscheidungStatus({ ok: true, mailFehler: true }), { ok: false, text: 'Entscheidung gespeichert, Mail an den Kunden fehlgeschlagen.' })
|
||||
assert.deepEqual(entscheidungStatus(null), { ok: true, text: 'Gespeichert.' })
|
||||
})
|
||||
Reference in new issue
Block a user