/** * 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.') }