Rechnungen: baueRechnung für freie Rechnung und Festbetrag, Testhilfe

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-07 14:13:52 +00:00
1 parent fd189b085d
commit b78ca971af
3 files changed
+312

No files matched your search

+98
View File
@@ -0,0 +1,98 @@
/**
* Baut aus der Eingabe einer Projektrechnung den RechnungsEntwurf (Positionen, Zusatzfelder, Metadaten, Projektdatensatz).
* Lesezugriffe: Kunde, Projekt, AB, Arbeitsblaetter und fruehere Rechnungen; geschrieben wird hier nichts (ausser der
* Selbstheilung veralteter Projektdatensaetze, siehe unten). Importiert nichts aus stripeFinance.js (Importzyklus).
*/
import { config } from '../../config.js'
import { getDocument } from '../appwriteAdmin.js'
import { AuftragError } from './fehler.js'
import { RECHNUNGS_ARTEN, LIMITS } from './konstanten.js'
import { festePositionen, pruefePositionen, kuerzen, auftragDatum, SCHLUSS_GRENZEN } from './positionen.js'
import { formatDe } from '../finance/dates.js'
// Appwrite-Dokument-ID
const KENNUNG = /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/
/** Kunde aus Appwrite; nur feste Kunden (nicht lead/lost) bekommen Rechnungen. */
export async function ladeFestenKunden(customerId) {
if (typeof customerId !== 'string' || !customerId || !KENNUNG.test(customerId)) throw new AuftragError(400, 'Kunde nicht gefunden.')
let kunde
try {
kunde = await getDocument(config.collections.customers, customerId)
} catch (err) {
if (err.status === 404) throw new AuftragError(400, 'Kunde nicht gefunden.')
throw err
}
const stufe = String(kunde.customerStatus || '').trim().toLowerCase()
if (stufe === 'lead' || stufe === 'lost') throw new AuftragError(400, 'Rechnungen nur für feste Kunden.')
return kunde
}
/** Projekt des Kunden; fremde und unbekannte Projekte sind nicht zu unterscheiden (404). */
async function ladeProjekt(kundenId, projektId) {
if (typeof projektId !== 'string' || !projektId) throw new AuftragError(400, 'Für diese Rechnungsart ist ein Projekt nötig.')
if (!KENNUNG.test(projektId)) throw new AuftragError(404, 'Projekt nicht gefunden.')
let projekt
try {
projekt = await getDocument(config.collections.websiteProjects, projektId)
} catch (err) {
if (err.status === 404) throw new AuftragError(404, 'Projekt nicht gefunden.')
throw err
}
if (projekt.customerId !== kundenId) throw new AuftragError(404, 'Projekt nicht gefunden.')
return { id: projekt.$id, name: projekt.projectName || projekt.subdomain || 'Projekt', ticketId: projekt.ticketId || '' }
}
function pruefeLeistungszeitraum(wert) {
if (wert === undefined || wert === null) return ''
if (typeof wert !== 'string' || wert.trim().length > LIMITS.maxLeistungszeitraum) {
throw new AuftragError(400, `Der Leistungszeitraum muss ein Text mit höchstens ${LIMITS.maxLeistungszeitraum} Zeichen sein.`)
}
return wert.trim()
}
/** Gemeinsamer Abschluss aller Arten: Grenzen, Zusatzfelder, Metadaten, Projektdatensatz. */
function schliesseAb({ kunde, art, projekt, auftrag, positionen, leistungszeitraum, arbeitsblattIds = [], createdBy }) {
const summeCents = pruefePositionen(positionen, { max: art === 'schluss' ? SCHLUSS_GRENZEN.zeilen : LIMITS.maxPositionen })
const projektId = projekt?.id ?? auftrag?.projektId ?? ''
const projektName = kuerzen(projekt?.name ?? auftrag?.titel ?? '', 140)
const customFields = []
if (art !== 'frei' && projektName) customFields.push({ name: 'Projekt', value: projektName })
if (auftrag) customFields.push({ name: 'Auftrag', value: `${auftrag.nummer} vom ${formatDe(auftragDatum(auftrag))}` })
if (leistungszeitraum) customFields.push({ name: 'Leistungszeitraum', value: leistungszeitraum })
const metadata = { webklarCustomerId: kunde.$id, quelle: 'woms', erstelltVon: String(createdBy || '').slice(0, 500), art }
if (projektId) metadata.projektId = projektId
if (projektName) metadata.projektName = projektName
if (auftrag) {
metadata.auftragId = auftrag.id
if (auftrag.nummer) metadata.auftragNummer = auftrag.nummer
}
const projektRechnung = art === 'frei' ? null : {
kundenId: kunde.$id,
projektId,
auftragId: auftrag?.id ?? '',
art,
betragCents: summeCents,
arbeitsblattIds: art === 'aufwand' ? [...arbeitsblattIds].sort() : [],
rechnungsnummer: '',
aktiv: true,
erstelltVon: String(createdBy || '').slice(0, 255),
erstelltAm: new Date().toISOString(),
}
return { kunde, art, projekt: projekt ?? null, auftrag: auftrag ?? null, positionen, summeCents, leistungszeitraum, customFields, metadata, projektRechnung }
}
/** Baut den RechnungsEntwurf einer Rechnung der Art frei, festbetrag, aufwand, anzahlung oder schluss. */
export async function baueRechnung(eingabe) {
const art = eingabe.art ?? 'frei'
if (typeof art !== 'string' || !RECHNUNGS_ARTEN.includes(art)) throw new AuftragError(400, 'Unbekannte Rechnungsart.')
const kunde = await ladeFestenKunden(eingabe.customerId)
const leistungszeitraum = pruefeLeistungszeitraum(eingabe.leistungszeitraum)
const basis = { kunde, art, leistungszeitraum, createdBy: eingabe.createdBy }
if (art === 'frei') return schliesseAb({ ...basis, positionen: festePositionen(eingabe.items) })
if (art === 'festbetrag') {
const projekt = await ladeProjekt(kunde.$id, eingabe.projektId)
return schliesseAb({ ...basis, projekt, positionen: festePositionen(eingabe.items) })
}
throw new AuftragError(501, 'Rechnungsart noch nicht umgesetzt.')
}