Rechnungen: baueRechnung für freie Rechnung und Festbetrag, Testhilfe
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fd189b085d
commit
b78ca971af
3 files changed
+312
No files matched your search
@@ -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.')
|
||||
}
|
||||
Reference in new issue
Block a user