News: Zitat § 356 Abs. 5 Nr. 2, Wertersatz-Satz und § 19 UStG statt brutto
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b20d0464c4
commit
3073fffcf5
10 files changed
+277
-26
No files matched your search
@@ -145,7 +145,7 @@ test('News-Mails bleiben wörtlich: K15 mit Rechnung', async () => {
|
||||
'',
|
||||
'Funktion: News-Pinnwand – Einrichtung',
|
||||
'Projekt: Bäckerei',
|
||||
'Einmalig bezahlt: 30,00 € (brutto, einmalig; die Nutzung ist kostenlos, es läuft kein Abo)',
|
||||
'Einmalig bezahlt: 30,00 € (einmalig, ohne Umsatzsteuer gemäß § 19 UStG; die Nutzung ist kostenlos, es läuft kein Abo)',
|
||||
'Einrichtung: meist innerhalb von 1–2 Wochen',
|
||||
'',
|
||||
'Wie geht es weiter?',
|
||||
@@ -180,7 +180,7 @@ test('News-Mails bleiben wörtlich: K15 ohne Rechnung', async () => {
|
||||
'',
|
||||
'Funktion: News-Pinnwand – Einrichtung',
|
||||
'Projekt: Café',
|
||||
'Einmalig bezahlt: 30,00 € (brutto, einmalig; die Nutzung ist kostenlos, es läuft kein Abo)',
|
||||
'Einmalig bezahlt: 30,00 € (einmalig, ohne Umsatzsteuer gemäß § 19 UStG; die Nutzung ist kostenlos, es läuft kein Abo)',
|
||||
'Einrichtung: meist innerhalb von 1–2 Wochen',
|
||||
'',
|
||||
'Wie geht es weiter?',
|
||||
|
||||
@@ -15,10 +15,11 @@ const reg = await import('../server/services/featureRegistry.js')
|
||||
const sf = await import('../server/services/siteFeatures.js')
|
||||
const { createFeatureTicket } = await import('../server/services/ticketAdmin.js')
|
||||
|
||||
// Wortlaut-Pin des News-Satzes und der Fassung (der einzige Literal-Pin auf Texte, die Block F2 ändert)
|
||||
// Wortlaut-Pin des News-Satzes und der Fassung (der einzige Literal-Pin auf Texte, die Block F2 ändert); die Fassung ist der
|
||||
// Tag <F> = config.termine.avvVersion, beim Anheben von <F> ändert sich auch dieser Pin
|
||||
const NEWS_SATZ =
|
||||
'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht erlischt, sobald der Einbau vollständig erbracht ist (§ 356 Abs. 4 BGB).'
|
||||
const NEWS_FASSUNG = '2026-09-30'
|
||||
'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung des Einbaus erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).'
|
||||
const NEWS_FASSUNG = '2026-10-05'
|
||||
|
||||
const SITE_KEY = 'abcdefghijklmnop'
|
||||
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { test, after } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
import { schemaKeys } from './support/schema.js'
|
||||
|
||||
// Rechtstext-Korrekturen der News-Pinnwand (Task F2): Zitat § 356 Abs. 5 Nr. 2, Wertersatz-Satz, § 19 UStG statt "brutto".
|
||||
// Die Wortlaute sind hier als Literale eingefroren (Wortlaut-Pin); die Fassung folgt config.termine.avvVersion.
|
||||
const aw = await startAppwriteMock({
|
||||
schema: { portalFeatures: schemaKeys('portalFeatures'), subscriptions: schemaKeys('subscriptions'), featureConsents: schemaKeys('featureConsents') },
|
||||
})
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { startTestApp, get, post } = await import('./support/testApp.js')
|
||||
const { customerCookie, seedFixtures } = await import('./support/fixtures.js')
|
||||
const { default: funktionenRoutes } = await import('../server/routes/funktionen.js')
|
||||
const sf = await import('../server/services/siteFeatures.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
const { mailSink } = await import('./support/mailSink.js')
|
||||
|
||||
const app = await startTestApp({ '/api/funktionen': funktionenRoutes })
|
||||
const mails = mailSink()
|
||||
setMailTransportForTests(mails)
|
||||
after(() => {
|
||||
setMailTransportForTests(null)
|
||||
return Promise.all([app.close(), aw.close(), st.close()])
|
||||
})
|
||||
seedFixtures(aw)
|
||||
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei', liveDomain: 'baeckerei.de' })
|
||||
aw.seed('websiteProjects', { $id: 'p2', customerId: 'c1', projectName: 'Café' })
|
||||
const cookie = customerCookie('c1')
|
||||
|
||||
const root = (rel) => new URL(`../${rel}`, import.meta.url)
|
||||
const lesen = (rel) => readFileSync(root(rel), 'utf8')
|
||||
|
||||
const NEWS_SATZ =
|
||||
'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung des Einbaus erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).'
|
||||
const ALTER_SATZ =
|
||||
'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht erlischt, sobald der Einbau vollständig erbracht ist (§ 356 Abs. 4 BGB).'
|
||||
const USTHINWEIS = 'ohne Umsatzsteuer gemäß § 19 UStG'
|
||||
|
||||
test('news_zustimmung_wortlaut: Register = Alias = exakter Text mit Abs. 5 Nr. 2 und § 357a Abs. 2, Fassung = <F>', () => {
|
||||
const news = config.features.news
|
||||
assert.deepEqual(news.consent.items, [{ id: 'beginn', haken: true, text: NEWS_SATZ }])
|
||||
assert.equal(config.funktionen.consentText, NEWS_SATZ)
|
||||
assert.equal(news.consent.items[0].text, config.funktionen.consentText)
|
||||
assert.ok(NEWS_SATZ.includes('Abs. 5 Nr. 2') && NEWS_SATZ.includes('§ 357a Abs. 2'))
|
||||
assert.ok(!NEWS_SATZ.includes('Abs. 4'))
|
||||
assert.equal(news.consent.knopf, 'Zahlungspflichtig bestellen · 30 €')
|
||||
// die Fassung folgt der Konstante <F> (zur Laufzeit), auch die der Leistungsbeschreibung, deren Stand sich mit dem Text ändert
|
||||
assert.equal(news.consent.version, config.termine.avvVersion)
|
||||
assert.equal(news.leistungsbeschreibung.version, config.termine.avvVersion)
|
||||
assert.ok(news.consent.version > '2026-10-02')
|
||||
const alt = config.termine.avvVersion
|
||||
config.termine.avvVersion = '2099-01-02'
|
||||
try {
|
||||
assert.equal(news.consent.version, '2099-01-02')
|
||||
assert.equal(news.leistungsbeschreibung.version, '2099-01-02')
|
||||
} finally {
|
||||
config.termine.avvVersion = alt
|
||||
}
|
||||
assert.equal(news.leistungsbeschreibung.url, '/vertragsunterlagen/funktionen-leistungsbeschreibung.html')
|
||||
assert.equal(news.mails.preisHinweis, `einmalig, ${USTHINWEIS}; die Nutzung ist kostenlos, es läuft kein Abo`)
|
||||
})
|
||||
|
||||
test('waechter_zitat_und_preistext: kein altes Zitat, keine Mehrwertsteuer-Angabe, kein "brutto" in den Kundentexten', () => {
|
||||
const dateien = [
|
||||
'server/config.js',
|
||||
'public/vertragsunterlagen/funktionen-leistungsbeschreibung.html',
|
||||
'public/funktionen.js',
|
||||
'server/mail-templates/K15 Bestellung Funktion.html',
|
||||
'server/services/stripeBilling.js',
|
||||
]
|
||||
for (const datei of dateien) {
|
||||
const text = lesen(datei)
|
||||
assert.doesNotMatch(text, /§ 356 Abs\. 4/, `${datei}: altes Zitat`)
|
||||
assert.doesNotMatch(text, /inklusive (Mehr|Umsatz)steuer/i, `${datei}: inklusive Steuer`)
|
||||
assert.doesNotMatch(text, /inklusive Mehrwertsteuer/i, `${datei}: inklusive Mehrwertsteuer`)
|
||||
assert.doesNotMatch(text, /\bbrutto\b/i, `${datei}: brutto`)
|
||||
}
|
||||
const lb = lesen('public/vertragsunterlagen/funktionen-leistungsbeschreibung.html')
|
||||
assert.equal(lb.split(USTHINWEIS).length - 1, 2, 'LB nennt den Hinweis in Kopf-Tabelle und Abschnitt 2')
|
||||
assert.ok(lb.includes('<td>30,00 € einmalig (ohne Umsatzsteuer gemäß § 19 UStG), keine laufenden Kosten</td>'))
|
||||
assert.ok(lb.includes('Der Einbau kostet 30,00 € einmalig (ohne Umsatzsteuer gemäß § 19 UStG).'))
|
||||
assert.ok(lb.includes('erlischt nach § 356 Abs. 5 Nr. 2 BGB'))
|
||||
assert.ok(lb.includes('(§ 357a Abs. 2 BGB)'))
|
||||
assert.ok(lb.includes('<!-- ENTWURF, Freigabe Nutzer ausstehend -->'), 'die Freigabe bleibt Sache des Inhabers')
|
||||
// der Stand der LB ist der Tag der Fassung <F>
|
||||
const [j, m, t] = config.termine.avvVersion.split('-')
|
||||
assert.ok(lb.includes(`<p class="meta">Stand ${t}.${m}.${j}</p>`), 'Stand der LB = config.termine.avvVersion')
|
||||
})
|
||||
|
||||
test('bestellseite_hinweis: GET /api/funktionen liefert ustHinweis', async () => {
|
||||
const body = await get('/api/funktionen', { cookie })
|
||||
assert.equal(body.ustHinweis, config.ustHinweis)
|
||||
assert.equal(config.ustHinweis, USTHINWEIS)
|
||||
assert.equal(body.consentText, NEWS_SATZ)
|
||||
assert.equal(body.features.news.consent.items[0].text, NEWS_SATZ)
|
||||
assert.equal(body.features.news.consent.version, config.termine.avvVersion)
|
||||
})
|
||||
|
||||
// ---- Client: public/funktionen.js wirklich laden (absolute Importe auf die Dateien umleiten) und die Buchungsseite rendern
|
||||
const dir = mkdtempSync(join(tmpdir(), 'fn-recht-'))
|
||||
const fileUrl = (name) => pathToFileURL(new URL(`../public/${name}`, import.meta.url).pathname).href
|
||||
const clientQuelle = lesen('public/funktionen.js')
|
||||
.replace("'/analyse-view.js'", `'${fileUrl('analyse-view.js')}'`)
|
||||
.replace("'/geraete.js'", `'${fileUrl('geraete.js')}'`)
|
||||
.replace("'./vertraege-link.js'", `'${fileUrl('vertraege-link.js')}'`)
|
||||
const clientDatei = join(dir, 'funktionen.mjs')
|
||||
writeFileSync(clientDatei, clientQuelle)
|
||||
const { initNews } = await import(pathToFileURL(clientDatei).href)
|
||||
|
||||
function fakeEl() {
|
||||
const handlers = {}
|
||||
return { innerHTML: '', textContent: '', disabled: false, checked: false, value: '', dataset: {}, classList: { add() {}, remove() {}, toggle() {} }, handlers, addEventListener(ev, fn) { handlers[ev] = fn }, querySelector: () => null, querySelectorAll: () => [] }
|
||||
}
|
||||
async function renderBuchung(antwort) {
|
||||
const echtesFetch = globalThis.fetch
|
||||
globalThis.window = { location: { hash: '#news', pathname: '/dashboard.html', search: '', href: '' }, history: { replaceState() {} } }
|
||||
globalThis.fetch = async () => ({ ok: true, status: 200, json: async () => antwort })
|
||||
const els = {}
|
||||
const stage = { innerHTML: '', els, querySelector: (sel) => (els[sel] ||= fakeEl()), querySelectorAll: () => [] }
|
||||
const rootEl = fakeEl()
|
||||
rootEl.querySelector = (sel) => (sel === '#news-stage' ? stage : null)
|
||||
try {
|
||||
await initNews(rootEl, { projectId: 'p1' })
|
||||
return stage
|
||||
} finally {
|
||||
delete globalThis.window
|
||||
globalThis.fetch = echtesFetch
|
||||
}
|
||||
}
|
||||
const frei = { projectId: 'p1', projectName: 'Muster', features: { news: { state: 'off' } } }
|
||||
|
||||
test('bestellseite_hinweis (Client): Preis mit Hinweis aus ustHinweis, per textContent; Rückfall = config.ustHinweis; kein "inklusive Mehrwertsteuer"', async () => {
|
||||
const stage = await renderBuchung({ projects: [frei], consentText: NEWS_SATZ, ustHinweis: 'Testhinweis § 19' })
|
||||
assert.match(stage.innerHTML, /<strong>30 €<\/strong>/)
|
||||
assert.doesNotMatch(stage.innerHTML, /Mehrwertsteuer|inklusive|brutto/i)
|
||||
assert.doesNotMatch(stage.innerHTML, /Testhinweis/, 'der Hinweis steht nie im HTML-Text, nur als textContent')
|
||||
assert.equal(stage.els['#news-ust-hinweis'].textContent, 'Testhinweis § 19')
|
||||
assert.match(stage.innerHTML, /einmalig, <span id="news-ust-hinweis"><\/span>/)
|
||||
|
||||
// ein Hinweis mit Markup bleibt Text
|
||||
const bose = await renderBuchung({ projects: [frei], ustHinweis: '<img src=x onerror=1>' })
|
||||
assert.doesNotMatch(bose.innerHTML, /<img/)
|
||||
assert.equal(bose.els['#news-ust-hinweis'].textContent, '<img src=x onerror=1>')
|
||||
|
||||
// fehlt der Hinweis in der Antwort, gilt der Rückfall im Client: derselbe Wortlaut wie config.ustHinweis
|
||||
const rueck = await renderBuchung({ projects: [frei], consentText: NEWS_SATZ })
|
||||
assert.equal(rueck.els['#news-ust-hinweis'].textContent, config.ustHinweis)
|
||||
for (const leer of ['', ' ', 5, null]) {
|
||||
const r = await renderBuchung({ projects: [frei], ustHinweis: leer })
|
||||
assert.equal(r.els['#news-ust-hinweis'].textContent, config.ustHinweis, String(leer))
|
||||
}
|
||||
})
|
||||
|
||||
// ---- K15 (Bestellbestätigung) der News
|
||||
const paidEvent = (sessionId, projectId) => ({
|
||||
id: `evt_${sessionId}`,
|
||||
object: 'event',
|
||||
type: 'checkout.session.completed',
|
||||
data: { object: { id: sessionId, object: 'checkout.session', mode: 'payment', payment_status: 'paid', metadata: { customerId: 'c1', projectId, featureKey: 'news' } } },
|
||||
})
|
||||
|
||||
test('k15_news: Preiszeile mit § 19 UStG statt "brutto"', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'p3', customerId: 'c1', projectName: 'Bistro' })
|
||||
await sf.markOrdered({ customerId: 'c1', projectId: 'p3', key: 'news', sessionId: 'cs_k15', consent: {} })
|
||||
const vor = mails.sent.length
|
||||
await sf.handleFeatureCheckoutEvent(paidEvent('cs_k15', 'p3'))
|
||||
const k15 = mails.sent.slice(vor).filter((m) => m.subject.startsWith('Deine Bestellung'))
|
||||
assert.equal(k15.length, 1)
|
||||
assert.ok(k15[0].text.includes(`Einmalig bezahlt: 30,00 € (einmalig, ${USTHINWEIS}; die Nutzung ist kostenlos, es läuft kein Abo)`), k15[0].text)
|
||||
assert.doesNotMatch(k15[0].text, /brutto/i)
|
||||
assert.doesNotMatch(k15[0].html, /brutto/i)
|
||||
assert.ok(k15[0].html.includes(USTHINWEIS))
|
||||
})
|
||||
|
||||
// ---- bestehende Bestellungen behalten Text und Fassung; neue speichern die Register-Werte
|
||||
test('alte_bestellung_unveraendert: altes Dokument bleibt, neue Bestellung speichert Register-Text und Fassung', async () => {
|
||||
aw.seed('portalFeatures', {
|
||||
$id: 'f_p2_news', // feste ID je (Projekt, Funktion)
|
||||
enabled: true,
|
||||
unlockedByPurchase: true,
|
||||
customerId: 'c1',
|
||||
projectId: 'p2',
|
||||
featureKey: 'news',
|
||||
state: 'active',
|
||||
purchaseStatus: 'paid',
|
||||
consentText: ALTER_SATZ,
|
||||
consentAt: '2026-09-30T10:00:00.000Z',
|
||||
orderedAt: '2026-09-30T10:00:00.000Z',
|
||||
})
|
||||
aw.seed('featureConsents', {
|
||||
$id: 'fc_alt',
|
||||
customerId: 'c1',
|
||||
projectId: 'p2',
|
||||
featureKey: 'news',
|
||||
stripeSessionId: 'cs_alt',
|
||||
acceptedAt: '2026-09-30T10:00:00.000Z',
|
||||
fassungen: JSON.stringify({ zustimmung: '2026-09-30', leistungsbeschreibung: '2026-09-30' }),
|
||||
texte: JSON.stringify([{ id: 'beginn', text: ALTER_SATZ, haken: true }]),
|
||||
})
|
||||
const alt = await sf.getFeatureDoc('c1', 'p2', 'news')
|
||||
assert.equal(alt.consentText, ALTER_SATZ)
|
||||
const liste = await get('/api/funktionen', { cookie })
|
||||
assert.equal(liste.projects.find((p) => p.projectId === 'p2').features.news.state, 'active')
|
||||
|
||||
const res = await post('/api/funktionen/news/checkout', { projectId: 'p1', consent: true }, { cookie })
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
const neu = await sf.getFeatureDoc('c1', 'p1', 'news')
|
||||
assert.equal(neu.consentText, NEWS_SATZ)
|
||||
const nachweise = [...aw.docs('featureConsents').values()].filter((d) => d.projectId === 'p1')
|
||||
assert.equal(nachweise.length, 1)
|
||||
assert.deepEqual(JSON.parse(nachweise[0].fassungen), { zustimmung: config.termine.avvVersion, leistungsbeschreibung: config.termine.avvVersion })
|
||||
const texte = JSON.parse(nachweise[0].texte)
|
||||
assert.equal(texte[0].text, NEWS_SATZ)
|
||||
|
||||
// das alte Dokument und sein Nachweis sind unverändert
|
||||
assert.equal((await sf.getFeatureDoc('c1', 'p2', 'news')).consentText, ALTER_SATZ)
|
||||
assert.deepEqual(JSON.parse(aw.docs('featureConsents').get('fc_alt').fassungen), { zustimmung: '2026-09-30', leistungsbeschreibung: '2026-09-30' })
|
||||
assert.equal(JSON.parse(aw.docs('featureConsents').get('fc_alt').texte)[0].text, ALTER_SATZ)
|
||||
})
|
||||
@@ -113,7 +113,9 @@ test('News-Buchungsseite: Preis, Knopf und Zustimmungstext aus features.news, se
|
||||
features: { news: register },
|
||||
})
|
||||
assert.match(stage.innerHTML, /<strong>31 €<\/strong>/)
|
||||
assert.match(stage.innerHTML, /inklusive Mehrwertsteuer/, 'der Wortlaut bleibt bis Block F')
|
||||
assert.doesNotMatch(stage.innerHTML, /Mehrwertsteuer/, 'der Preishinweis kommt aus ustHinweis (Block F)')
|
||||
assert.match(stage.innerHTML, /einmalig, <span id="news-ust-hinweis"><\/span>/)
|
||||
assert.equal(stage.els['#news-ust-hinweis'].textContent, 'ohne Umsatzsteuer gemäß § 19 UStG', 'Rückfall ohne ustHinweis in der Antwort')
|
||||
assert.match(stage.innerHTML, /id="news-order" disabled>Jetzt kaufen · 31 €</)
|
||||
assert.equal(stage.els['#news-consent-text'].textContent, 'Registertext der Zustimmung')
|
||||
assert.equal(knopfAktiv, true)
|
||||
|
||||
@@ -156,9 +156,9 @@ test('Register: leistungsbeschreibung und avv des Terminplaners folgen der Konst
|
||||
assert.deepEqual(t.leistungsbeschreibung, merke)
|
||||
})
|
||||
|
||||
test('News-Rechtsdokumente bleiben unberührt', () => {
|
||||
test('News: Leistungsbeschreibung folgt der Fassung <F> (Adresse unverändert, kein AVV, kein Getter am Eintrag)', () => {
|
||||
const n = featureConfig('news')
|
||||
assert.deepEqual(n.leistungsbeschreibung, { version: '2026-09-30', url: '/vertragsunterlagen/funktionen-leistungsbeschreibung.html' })
|
||||
assert.deepEqual(n.leistungsbeschreibung, { version: config.termine.avvVersion, url: '/vertragsunterlagen/funktionen-leistungsbeschreibung.html' })
|
||||
assert.equal(n.avv, null)
|
||||
assert.deepEqual(analyse.legalPaths(ALT), legalPaths('analyse', ALT))
|
||||
assert.equal(Object.getOwnPropertyDescriptor(n, 'leistungsbeschreibung').get, undefined)
|
||||
|
||||
@@ -482,7 +482,8 @@ test('legal_datenschutz_news: Abschnitt 6a und Leistungsbeschreibung (Entwurf)',
|
||||
const text = strip(lb)
|
||||
assert.ok(text.includes('Auf Ihren Wunsch schalten wir die Pinnwand ab') && !text.includes('Endet die Zusammenarbeit mit uns, schalten wir'))
|
||||
assert.ok(text.includes('abgeschlossen') && !text.includes('dauert erfahrungsgemäß'))
|
||||
for (const s of ['30,00 €', '§ 356 Abs. 4 BGB', '1–2 Wochen', 'keine laufenden Kosten']) assert.ok(text.includes(s), s)
|
||||
for (const s of ['30,00 €', '§ 356 Abs. 5 Nr. 2 BGB', '§ 357a Abs. 2 BGB', '1–2 Wochen', 'keine laufenden Kosten']) assert.ok(text.includes(s), s)
|
||||
assert.ok(!text.includes('§ 356 Abs. 4') && !/inklusive Umsatzsteuer/.test(text), 'altes Zitat und alter Preistext sind weg')
|
||||
|
||||
const muster = read('docs/funktionen/website-datenschutz-absatz.md')
|
||||
// 14 Tage Protokolle nur, solange Abschnitt 3 der WEBklar-Erklaerung dieselbe Frist nennt
|
||||
|
||||
Reference in new issue
Block a user