Schreiben erzeugen, ausgeben und ablegen

Die Anschreiben gab es bisher nur als Text zum Kopieren. Danach war
Handarbeit: in Word nachbauen, ausdrucken, abschicken - und noch einmal
von Hand als Dokument in die Akte legen. Der letzte Schritt wurde am
haeufigsten vergessen, und dann fehlt der Nachweis im Bericht.

Neu unter /schreiben/neu: bearbeiten, als Word-Datei herunterladen, in
die Akte legen, Wiedervorlage setzen - in einem Zug. Eine Seite fuer alle
Arten und alle drei Empfaengerwege, statt die Bearbeitung dreimal an
Vertrag, Vorgang und Freitext nachzubauen.

Der dritte Weg ist der eigentliche Zugewinn: Schreiben an eine Stelle,
zu der es noch keinen Datensatz gibt. Am Anfang einer Betreuung ist das
der Normalfall - man fragt ja an, um zu erfahren, ob dort ueberhaupt
etwas besteht. Aus der freien Anschrift laesst sich mit einem Klick ein
Kontakt anlegen.

Zwei Entscheidungen, die erklaert gehoeren:

Die Akte bekommt ein PDF, heruntergeladen wird .docx. Der
Objektspeicher nimmt nur PDF und Bilder an, geprueft an den Magic Bytes.
Eine .docx ist ein ZIP-Archiv; sie durchzulassen hiesse jedes ZIP
durchzulassen, und diese Pruefung ist eine bewusste Zusage der
Anwendung. Sachlich ist PDF fuer den Nachweis ohnehin richtiger: Es
aendert sich nicht mehr. Die Oberflaeche sagt dazu, dass eine spaeter in
Word geaenderte Fassung von der Akte abweicht.

Der Berichtssatz in berichte/docx.ts bleibt unangetastet. Er setzt einen
Bericht mit Kopfzeilen und Unterschriftenblock; ein Brief braucht
Absender, Anschriftenfeld, Datumszeile und Grussformel. Beides in eine
Funktion zu zwingen haette beide Ausgaben schlechter gemacht. Der
Umsetzungshinweis im Issue ("muss nur angebunden werden") war insoweit
falsch - von mir selbst geschrieben.

Die vorhandenen Erzeuger (Rueckkaufswert, Kuendigung, Sachstandsanfrage)
bleiben unveraendert; ihr Wortlaut ist getestet und erprobt.

Geprueft: zehn Einheitstests, darunter der Gleichschritt der
Wiedervorlagefristen ueber zwei Module hinweg. Fuenf End-to-End-Tests
auf beiden Geraeteprofilen: Entwurf, Vertragsbezug, Word-Download,
Ablage samt Wiedervorlage, Kontakt aus freier Anschrift.

Refs #4
This commit is contained in:
Florian Wilde
2026-08-23 10:14:59 +02:00
parent de21f1c320
commit c21d03abf6
19 changed files with 2186 additions and 396 deletions
+100
View File
@@ -0,0 +1,100 @@
import { describe, expect, it } from 'vitest'
import {
SCHREIBEN_ARTEN,
SCHREIBEN_EIGENSCHAFTEN,
dateinameFuerSchreiben,
istSchreibenArt,
} from '@/lib/schreiben/arten'
import { RUECKKAUFSWERT_WIEDERVORLAGE_TAGE } from '@/lib/vertraege/rueckkaufswert'
import { NACHFASSEN_AB_TAGEN } from '@/lib/vorgaenge/sachstandsanfrage'
/**
* Schreiben erzeugen — die Teile ohne Datenbank.
*
* Der Schwerpunkt liegt auf zwei Dingen, die sonst still auseinanderlaufen:
* den Wiedervorlagefristen (sie stehen an zwei Stellen) und dem Dateinamen
* (er landet im Objektspeicher und im Download).
*/
describe('Arten von Schreiben', () => {
it('erkennt bekannte Arten und weist alles andere ab', () => {
expect(istSchreibenArt('RUECKKAUFSWERT')).toBe(true)
expect(istSchreibenArt('FREI')).toBe(true)
expect(istSchreibenArt('etwas-anderes')).toBe(false)
expect(istSchreibenArt(null)).toBe(false)
expect(istSchreibenArt(undefined)).toBe(false)
})
it('beschreibt jede Art vollständig', () => {
for (const art of SCHREIBEN_ARTEN) {
const eigenschaft = SCHREIBEN_EIGENSCHAFTEN[art]
expect(eigenschaft.beschriftung.length, art).toBeGreaterThan(3)
// Wo es eine Frist gibt, muss auch dastehen, warum sie so lang ist.
if (eigenschaft.wiedervorlageTage !== null) {
expect(eigenschaft.fristBegruendung, art).toBeTruthy()
}
}
})
it('schlägt für ein freies Schreiben KEINE Frist vor', () => {
// Bei einem freien Schreiben weiß niemand, ob und wann eine Antwort
// kommt. Eine erfundene Frist wäre schlechter als keine.
expect(SCHREIBEN_EIGENSCHAFTEN.FREI.wiedervorlageTage).toBeNull()
})
})
describe('Wiedervorlagefristen bleiben mit den Modulen im Gleichschritt', () => {
it('nimmt beim Rückkaufswert dieselbe Zahl wie das Vertragsmodul', () => {
// Sie steht dort für die Anzeige am Vertrag, hier für die erzeugte
// Frist. Laufen sie auseinander, mahnt die Anwendung zu zwei
// verschiedenen Terminen — und man glaubt keinem mehr.
expect(SCHREIBEN_EIGENSCHAFTEN.RUECKKAUFSWERT.wiedervorlageTage).toBe(
RUECKKAUFSWERT_WIEDERVORLAGE_TAGE,
)
})
it('nimmt bei der Sachstandsanfrage dieselbe Zahl wie das Vorgangsmodul', () => {
expect(SCHREIBEN_EIGENSCHAFTEN.SACHSTAND.wiedervorlageTage).toBe(NACHFASSEN_AB_TAGEN)
})
})
describe('Dateiname', () => {
const tag = new Date('2026-08-23T00:00:00.000Z')
it('beginnt mit dem Datum, damit die Dateien sortierbar bleiben', () => {
const name = dateinameFuerSchreiben('KUENDIGUNG', 'Allianz', tag, 'docx')
expect(name.startsWith('2026-08-23_')).toBe(true)
expect(name.endsWith('.docx')).toBe(true)
})
it('nennt Art und Empfänger', () => {
const name = dateinameFuerSchreiben('KUENDIGUNG', 'Allianz', tag, 'pdf')
expect(name).toContain('Kuendigung')
expect(name).toContain('Allianz')
expect(name.endsWith('.pdf')).toBe(true)
})
it('löst Umlaute auf und wirft Sonderzeichen heraus', () => {
// Der Name geht in den Objektspeicher und über einen HTTP-Kopf. Beides
// verträgt keine Umlaute und keine Anführungszeichen.
const name = dateinameFuerSchreiben('FREI', 'Müller & Co. "Süd"', tag, 'docx')
// Nur der Namensteil — der Punkt vor der Endung gehört dazu.
const ohneEndung = name.replace(/\.docx$/, '')
expect(name).not.toMatch(/[^\x20-\x7E]/)
expect(ohneEndung).not.toMatch(/["&.]/)
expect(name).toContain('Mueller')
expect(name).toContain('Sued')
})
it('kommt ohne Empfänger aus', () => {
const name = dateinameFuerSchreiben('FREI', null, tag, 'docx')
expect(name).toBe('2026-08-23_Freies-Schreiben.docx')
})
it('kürzt einen sehr langen Empfänger, statt den Namen ausufern zu lassen', () => {
const lang = 'Sehr Lange Versicherungsgesellschaft auf Gegenseitigkeit in Oberfranken'
const name = dateinameFuerSchreiben('FREI', lang, tag, 'pdf')
expect(name.length).toBeLessThan(90)
})
})