Terminplaner: Kunden-Schnittstelle
/api/termine: Einstellungen (Vorlage, PATCH mit Version, Feed-Link), Buchungen (Liste, Zeiten, von Hand, Sperre, Aktionen), CSV-Export, Protokoll der Kundenansicht. Pflichtangaben fuer Gaeste (Telefon immer, Anschrift bei Widerrufsbelehrung) in normalisiereEinstellung, Entwurf aus Vorlagen, kontextPerSiteKey ohne Pflichtangaben nicht buchbar; speichereEinstellung nimmt eine Funktion unter der Sperre. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e8fd33713
commit
e3e198fb1a
10 files changed
+1766
-40
No files matched your search
+118
-7
@@ -82,17 +82,17 @@ const eingabe = (over = {}) => ({
|
||||
zahlung: { modus: 'vorort', vorOrtArten: ['bar', 'ec'] },
|
||||
...over,
|
||||
})
|
||||
const einstellung = (over) => {
|
||||
const erg = normalisiereEinstellung(eingabe(over), { now: JETZT })
|
||||
const einstellung = (over, optionen = {}) => {
|
||||
const erg = normalisiereEinstellung(eingabe(over), { now: JETZT, ...optionen })
|
||||
assert.equal(erg.fehler, undefined, JSON.stringify(erg.fehler))
|
||||
return erg.einstellung
|
||||
}
|
||||
const betriebMit = (felder) => ({ ...eingabe().betrieb, ...felder })
|
||||
|
||||
const ctxMit = (over = {}, projekt = { $id: PROJEKT, subdomain: 'salon' }) => ({
|
||||
const ctxMit = (over = {}, projekt = { $id: PROJEKT, subdomain: 'salon' }, optionen = {}) => ({
|
||||
feature: feature(),
|
||||
project: projekt,
|
||||
einstellung: einstellung(over),
|
||||
einstellung: einstellung(over, optionen),
|
||||
version: 1,
|
||||
})
|
||||
|
||||
@@ -264,6 +264,88 @@ test('ladeEinstellung: unlesbares JSON gilt als nicht gespeichert (mit Log); feh
|
||||
})
|
||||
})
|
||||
|
||||
test('R40 speichereEinstellung mit Funktion: laeuft nach der Versionspruefung, bekommt die bisherige Einstellung (erst null), Ergebnis wird gespeichert', async () => {
|
||||
const aufrufe = []
|
||||
const erste = einstellung({ betrieb: betriebMit({ name: 'Erste' }) })
|
||||
const erg = await speichereEinstellung(feature(), async (bisher) => {
|
||||
aufrufe.push(bisher)
|
||||
return erste
|
||||
}, { version: 0, by: 'kunde', now: JETZT })
|
||||
assert.deepEqual(aufrufe, [null], 'noch nichts gespeichert')
|
||||
assert.equal(erg.version, 1)
|
||||
assert.deepEqual(erg.einstellung, erste)
|
||||
assert.equal(gespeichert().updatedBy, 'kunde')
|
||||
|
||||
const zweite = einstellung({ betrieb: betriebMit({ name: 'Zweite' }) })
|
||||
const erg2 = await speichereEinstellung(feature(), (bisher) => {
|
||||
aufrufe.push(bisher) // auch eine synchrone Funktion
|
||||
return zweite
|
||||
}, { version: 1 })
|
||||
assert.deepEqual(aufrufe[1], erste, 'bisher = die gespeicherte Einstellung')
|
||||
assert.equal(erg2.version, 2)
|
||||
assert.equal((await ladeEinstellung(FEATURE)).einstellung.betrieb.name, 'Zweite')
|
||||
})
|
||||
|
||||
test('R40 speichereEinstellung mit Funktion: veraltete Version ruft die Funktion nie auf; ein Fehler der Funktion geht unveraendert durch und schreibt nichts', async () => {
|
||||
await speichereEinstellung(feature(), einstellung(), { version: 0 })
|
||||
let aufrufe = 0
|
||||
await assert.rejects(
|
||||
() => speichereEinstellung(feature(), async () => { aufrufe += 1; return einstellung() }, { version: 0 }),
|
||||
(err) => err.status === 409 && err.code === 'EINSTELLUNG_VERALTET',
|
||||
)
|
||||
assert.equal(aufrufe, 0, 'Version zuerst')
|
||||
|
||||
const eigener = Object.assign(new Error('Eingabe'), { status: 400, fehler: { 'leistungen.s_x': 'Noch 2 künftige Termine.' } })
|
||||
await assert.rejects(() => speichereEinstellung(feature(), async () => { throw eigener }, { version: 1 }), (err) => err === eigener)
|
||||
const geladen = await ladeEinstellung(FEATURE)
|
||||
assert.equal(geladen.version, 1, 'nichts geschrieben')
|
||||
// das Sperrschloss ist wieder frei
|
||||
assert.equal((await speichereEinstellung(feature(), async () => einstellung(), { version: 1 })).version, 2)
|
||||
})
|
||||
|
||||
test('R40 speichereEinstellung mit Funktion: Ergebnis ohne Objekt => TypeError und nichts geschrieben; Eingaben ohne Funktion bleiben TypeError', async () => {
|
||||
for (const unsinn of [null, undefined, 'x', 5, []]) {
|
||||
await assert.rejects(() => speichereEinstellung(feature(), async () => unsinn, { version: 0 }), TypeError, String(unsinn))
|
||||
}
|
||||
assert.equal(aw.docs('termineConfig').size, 0)
|
||||
await assert.rejects(() => speichereEinstellung({}, async () => einstellung(), { version: 0 }), TypeError)
|
||||
})
|
||||
|
||||
test('R40 speichereEinstellung mit version null: keine Versionspruefung, Version zaehlt trotzdem hoch (Feed-Link neu erzeugen)', async () => {
|
||||
await speichereEinstellung(feature(), einstellung(), { version: 0 })
|
||||
await speichereEinstellung(feature(), einstellung({ betrieb: betriebMit({ name: 'Zwei' }) }), { version: 1 })
|
||||
const erg = await speichereEinstellung(feature(), async (bisher) => ({ ...bisher, farbe: '#112233' }), { version: null })
|
||||
assert.equal(erg.version, 3)
|
||||
const geladen = await ladeEinstellung(FEATURE)
|
||||
assert.equal(geladen.einstellung.farbe, '#112233')
|
||||
assert.equal(geladen.einstellung.betrieb.name, 'Zwei', 'baut auf dem aktuellen Stand auf')
|
||||
// nur null spart die Pruefung; undefined und falsche Versionen bleiben 409
|
||||
await assert.rejects(() => speichereEinstellung(feature(), async (b) => b, {}), (err) => err.status === 409)
|
||||
await assert.rejects(() => speichereEinstellung(feature(), async (b) => b, { version: 99 }), (err) => err.status === 409)
|
||||
// ohne Dokument sieht die Funktion null (mit der Einstellung als Objekt bleibt version null auch erlaubt)
|
||||
await deleteDocument('termineConfig', FEATURE)
|
||||
let gesehen
|
||||
await speichereEinstellung(feature(), async (bisher) => { gesehen = bisher; return einstellung() }, { version: null })
|
||||
assert.equal(gesehen, null)
|
||||
})
|
||||
|
||||
test('R40 speichereEinstellung mit Funktion: gleichzeitige Aufrufe laufen nacheinander, der zweite sieht das Ergebnis des ersten', async () => {
|
||||
await speichereEinstellung(feature(), einstellung({ betrieb: betriebMit({ name: 'Start' }) }), { version: 0 })
|
||||
const gesehen = []
|
||||
const aendere = (name) => async (bisher) => {
|
||||
gesehen.push(bisher.betrieb.name)
|
||||
await new Promise((resolve) => setTimeout(resolve, 20)) // der Zweite haette Zeit, dazwischenzufunken
|
||||
return { ...bisher, betrieb: { ...bisher.betrieb, name } }
|
||||
}
|
||||
await Promise.all([
|
||||
speichereEinstellung(feature(), aendere('A'), { version: null }),
|
||||
speichereEinstellung(feature(), aendere('B'), { version: null }),
|
||||
])
|
||||
assert.equal(gesehen[0], 'Start')
|
||||
assert.ok(['A', 'B'].includes(gesehen[1]), 'der Zweite baut auf dem Ergebnis des Ersten auf')
|
||||
assert.equal((await ladeEinstellung(FEATURE)).version, 3)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Kontext
|
||||
|
||||
test('kontextPerSiteKey: aktive Funktion mit Einstellung => ctx { feature, project, einstellung, version }', async () => {
|
||||
@@ -279,6 +361,32 @@ test('kontextPerSiteKey: aktive Funktion mit Einstellung => ctx { feature, proje
|
||||
assert.equal(ctx.version, 1)
|
||||
})
|
||||
|
||||
test('R41 kontextPerSiteKey: solange Pflichtangaben fehlen (Telefon; Anschrift bei Widerrufsbelehrung) ist die Funktion oeffentlich nicht buchbar (null); das Portal sieht sie weiter', async () => {
|
||||
seedFeature()
|
||||
seedProjekt()
|
||||
const entwurf = { entwurf: true }
|
||||
const ohneTelefon = einstellung({ betrieb: betriebMit({ telefon: '' }) }, entwurf)
|
||||
await speichereEinstellung(feature(), ohneTelefon, { version: 0 })
|
||||
assert.equal(await kontextPerSiteKey(SITE_KEY, T0), null, 'Telefon fehlt')
|
||||
const portal = await kontextPerFeature(FEATURE)
|
||||
assert.ok(portal && portal.einstellung, 'kontextPerFeature (Portal, Gaesteseiten per Token) bleibt erreichbar')
|
||||
assert.deepEqual(portal.einstellung.betrieb.telefon, '')
|
||||
|
||||
await speichereEinstellung(feature(), einstellung({ betrieb: betriebMit({ adresse: '' }) }, entwurf), { version: 1 })
|
||||
assert.equal(await kontextPerSiteKey(SITE_KEY, SEKUNDEN(1)), null, 'Anschrift fehlt bei Verbraucher + sofort')
|
||||
|
||||
// ohne Widerrufsbelehrung (nur Unternehmer) reicht das Telefon
|
||||
await speichereEinstellung(feature(), einstellung({ betrieb: betriebMit({ adresse: '' }), recht: { gaesteSind: 'unternehmer', freizeit: false } }, entwurf), { version: 2 })
|
||||
assert.ok(await kontextPerSiteKey(SITE_KEY, SEKUNDEN(2)), 'Unternehmer-Betrieb ohne Anschrift ist buchbar')
|
||||
|
||||
// vollstaendig: buchbar; die Speicherung leert den gemerkten null-Eintrag sofort
|
||||
await speichereEinstellung(feature(), einstellung({ betrieb: betriebMit({ adresse: '', telefon: '' }) }, entwurf), { version: 3 })
|
||||
assert.equal(await kontextPerSiteKey(SITE_KEY, SEKUNDEN(3)), null)
|
||||
await speichereEinstellung(feature(), einstellung(), { version: 4 })
|
||||
const ctx = await kontextPerSiteKey(SITE_KEY, SEKUNDEN(4))
|
||||
assert.equal(ctx.einstellung.betrieb.telefon, '06301 1234')
|
||||
})
|
||||
|
||||
test('kontextPerSiteKey: Modus off => null ohne Anfrage; ungueltiger siteKey => null ohne Anfrage', async () => {
|
||||
seedFeature()
|
||||
await speichereEinstellung(feature(), einstellung(), { version: 0 })
|
||||
@@ -604,12 +712,14 @@ test('G02 Freizeitleistung mit festem Termin: kein_widerruf statt widerruf (nich
|
||||
})
|
||||
|
||||
test('G02 mit Widerrufsrecht, aber ohne Anschrift oder Telefon des Betriebs: Fehler statt unvollstaendiger Belehrung', () => {
|
||||
// Entwurf: nur so gibt es eine Einstellung ohne die Pflichtangaben (R41); die Mail prueft sie trotzdem selbst
|
||||
const entwurf = { entwurf: true }
|
||||
for (const felder of [{ adresse: '' }, { telefon: '' }]) {
|
||||
const ctx = ctxMit({ betrieb: betriebMit(felder) })
|
||||
const ctx = ctxMit({ betrieb: betriebMit(felder) }, undefined, entwurf)
|
||||
assert.throws(() => werteFuer('G02', ctx, buchung(), {}), TypeError, JSON.stringify(felder))
|
||||
}
|
||||
// ohne Widerrufsrecht braucht die Mail beides nicht
|
||||
const ctx = ctxMit({ betrieb: betriebMit({ adresse: '', telefon: '' }), recht: { gaesteSind: 'unternehmer', freizeit: false } })
|
||||
const ctx = ctxMit({ betrieb: betriebMit({ adresse: '', telefon: '' }), recht: { gaesteSind: 'unternehmer', freizeit: false } }, undefined, entwurf)
|
||||
assert.doesNotThrow(() => werteFuer('G02', ctx, buchung({ verbraucher: false }), {}))
|
||||
})
|
||||
|
||||
@@ -686,7 +796,8 @@ test('Impressum und Datenschutz im Fuss: Angaben des Betriebs, sonst dessen Webs
|
||||
assert.equal(nackt.impressum_url, config.publicBaseUrl)
|
||||
assert.equal(nackt.datenschutz_url, config.publicBaseUrl)
|
||||
|
||||
const leer = werteFuer('G03', ctxMit({ betrieb: betriebMit({ telefon: '', adresse: '' }) }), buchung(), {}).values
|
||||
// Entwurf (R41): nur so gibt es eine Einstellung ohne Telefon und Anschrift; die Mail setzt dann "–"
|
||||
const leer = werteFuer('G03', ctxMit({ betrieb: betriebMit({ telefon: '', adresse: '' }) }, undefined, { entwurf: true }), buchung(), {}).values
|
||||
assert.equal(leer.betrieb_telefon, '–')
|
||||
assert.equal(leer.betrieb_adresse, '–')
|
||||
assert.equal(mitAngaben.farbe, '#7c3aed')
|
||||
|
||||
Reference in new issue
Block a user