383 lines
19 KiB
JavaScript
383 lines
19 KiB
JavaScript
/**
|
|
* 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, listAllDocuments, Query } from '../appwriteAdmin.js'
|
|
import { getStripe } from '../stripeBilling.js'
|
|
import { withLocks } from '../keyedLock.js'
|
|
import { AuftragError } from './fehler.js'
|
|
import {
|
|
RECHNUNGS_ARTEN, AB_RECHNUNGS_ARTEN, LIMITS, VEREINBARTE_STATUS, STUNDENSATZ_STANDARD_CENTS, SCHALTER_AUS_MELDUNG, sperreAuftrag,
|
|
} from './konstanten.js'
|
|
import {
|
|
festePositionen, zusatzPositionen, pruefePositionen, kuerzen, auftragDatum, aufwandPositionen, leistungszeitraumText,
|
|
anzahlungPosition, schlussPositionen, SCHLUSS_GRENZEN,
|
|
} from './positionen.js'
|
|
import { auftraegeAktiv } from './schalter.js'
|
|
import { getAuftrag, listAuftraege, updateAuftrag } from './auftragStore.js'
|
|
import { listProjektRechnungen, updateProjektRechnung } from './projektRechnungenStore.js'
|
|
import {
|
|
ladeArbeitsblaetter, ladeArbeitsblaetterNachIds, abrechenbar, abgerechneteArbeitsblaetter, offeneArbeitsblaetter, zeitquelleFuerProjekt,
|
|
} from './arbeitsblaetter.js'
|
|
import { berlinDate, formatDe } from '../finance/dates.js'
|
|
import { euroDe } from '../finance/format.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). Mit weich = true liefert jeder
|
|
* Fehler stattdessen null (nur Lesefehler von Appwrite werden weitergereicht).
|
|
*/
|
|
async function ladeProjekt(kundenId, projektId, { weich = false } = {}) {
|
|
const fehler = (status, meldung) => {
|
|
if (weich) return null
|
|
throw new AuftragError(status, meldung)
|
|
}
|
|
if (typeof projektId !== 'string' || !projektId) return fehler(400, 'Für diese Rechnungsart ist ein Projekt nötig.')
|
|
if (!KENNUNG.test(projektId)) return fehler(404, 'Projekt nicht gefunden.')
|
|
let projekt
|
|
try {
|
|
projekt = await getDocument(config.collections.websiteProjects, projektId)
|
|
} catch (err) {
|
|
if (err.status === 404) return fehler(404, 'Projekt nicht gefunden.')
|
|
throw err
|
|
}
|
|
if (projekt.customerId !== kundenId) return fehler(404, 'Projekt nicht gefunden.')
|
|
return { id: projekt.$id, name: projekt.projectName || projekt.subdomain || 'Projekt', ticketId: projekt.ticketId || '' }
|
|
}
|
|
|
|
/** AB des Kunden; fremde und unbekannte sind nicht zu unterscheiden (404). */
|
|
async function ladeAuftrag(kundenId, auftragId) {
|
|
if (typeof auftragId !== 'string' || !KENNUNG.test(auftragId)) throw new AuftragError(404, 'Auftrag nicht gefunden.')
|
|
const auftrag = await getAuftrag(auftragId)
|
|
if (!auftrag || auftrag.kundenId !== kundenId) throw new AuftragError(404, 'Auftrag nicht gefunden.')
|
|
return auftrag
|
|
}
|
|
|
|
/**
|
|
* Buchung nach einem Stripe-Erfolg: scheitert sie, wird nur protokolliert (eine festgeschriebene Rechnung scheitert nicht an
|
|
* Appwrite). Fehler mit Status 404 sind still. Wirft nie.
|
|
*/
|
|
export async function buche(was, fn) {
|
|
try {
|
|
await fn()
|
|
} catch (err) {
|
|
if (err?.status === 404) return
|
|
console.error(`[finanzen] ${was} fehlgeschlagen:`, err?.type || err?.name || 'Error', err?.status ?? err?.statusCode ?? '', err?.code ?? '')
|
|
}
|
|
}
|
|
|
|
/** Schlussrechnung finalisiert: die AB geht von versendet oder bestaetigt auf abgerechnet. Unter der Sperre der AB. */
|
|
export async function setzeAbAbgerechnet(auftragId) {
|
|
await withLocks(sperreAuftrag(auftragId), async () => {
|
|
const ab = await getAuftrag(auftragId)
|
|
if (ab && (ab.status === 'versendet' || ab.status === 'bestaetigt')) await updateAuftrag(ab.id, { status: 'abgerechnet' })
|
|
})
|
|
}
|
|
|
|
/** Schlussrechnung weg (storniert, geloescht): die AB geht von abgerechnet zurueck. Unter der Sperre der AB. */
|
|
export async function setzeAbZurueck(auftragId) {
|
|
await withLocks(sperreAuftrag(auftragId), async () => {
|
|
const ab = await getAuftrag(auftragId)
|
|
if (ab && ab.status === 'abgerechnet') await updateAuftrag(ab.id, { status: ab.bestaetigtAm ? 'bestaetigt' : 'versendet' })
|
|
})
|
|
}
|
|
|
|
/** Projektdatensatz inaktiv setzen (Blaetter wieder frei); bei einer Schlussrechnung geht die AB zurueck. */
|
|
export async function gibProjektRechnungFrei(invoiceId) {
|
|
const doc = await updateProjektRechnung(invoiceId, { aktiv: false })
|
|
if (doc.art === 'schluss' && doc.auftragId) await setzeAbZurueck(doc.auftragId)
|
|
return doc
|
|
}
|
|
|
|
const fehltBeiStripe = (err) => err?.statusCode === 404 || err?.code === 'resource_missing'
|
|
|
|
/**
|
|
* Selbstheilung: belegt ist Map<blattId, { invoiceId, rechnungsnummer }>. Je Rechnung bei Stripe nachsehen: storniert oder
|
|
* geloescht -> Datensatz inaktiv, die Belegung gilt nicht; sonst gilt sie (mit der Nummer von Stripe, fehlende Nummer wird
|
|
* im Datensatz nachgetragen). Rueckgabe: die noch geltenden Belegungen.
|
|
*/
|
|
async function pruefeBelegungen(belegt) {
|
|
const urteile = new Map()
|
|
for (const { invoiceId } of belegt.values()) {
|
|
if (urteile.has(invoiceId)) continue
|
|
let roh
|
|
try {
|
|
roh = await getStripe().invoices.retrieve(invoiceId)
|
|
} catch (err) {
|
|
if (!fehltBeiStripe(err)) throw err
|
|
roh = null
|
|
}
|
|
if (!roh || roh.status === 'void') {
|
|
await buche('Rechnung freigeben', () => gibProjektRechnungFrei(invoiceId))
|
|
urteile.set(invoiceId, null)
|
|
continue
|
|
}
|
|
const nummer = roh.status === 'draft' ? '' : roh.number || ''
|
|
urteile.set(invoiceId, nummer)
|
|
}
|
|
const gueltig = new Map()
|
|
for (const [blattId, belegung] of belegt) {
|
|
const nummer = urteile.get(belegung.invoiceId)
|
|
if (nummer === null) continue
|
|
if (nummer && nummer !== belegung.rechnungsnummer && !gueltig.has(`nachgetragen:${belegung.invoiceId}`)) {
|
|
gueltig.set(`nachgetragen:${belegung.invoiceId}`, true)
|
|
await buche('Rechnungsnummer nachtragen', () => updateProjektRechnung(belegung.invoiceId, { rechnungsnummer: nummer }))
|
|
}
|
|
gueltig.set(blattId, { invoiceId: belegung.invoiceId, rechnungsnummer: nummer })
|
|
}
|
|
for (const schluessel of [...gueltig.keys()]) if (schluessel.startsWith('nachgetragen:')) gueltig.delete(schluessel)
|
|
return gueltig
|
|
}
|
|
|
|
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)
|
|
if (AB_RECHNUNGS_ARTEN.includes(art) && !auftraegeAktiv(kunde)) throw new AuftragError(400, SCHALTER_AUS_MELDUNG)
|
|
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) })
|
|
}
|
|
if (art === 'aufwand') return baueAufwand(basis, eingabe)
|
|
return baueAbRechnung(basis, eingabe)
|
|
}
|
|
|
|
const MAX_BLAETTER = 400
|
|
const KEIN_BLATT = 'Arbeitsblatt nicht gefunden.'
|
|
|
|
function pruefeZeilentexte(wert) {
|
|
if (wert === undefined || wert === null) return {}
|
|
if (typeof wert !== 'object' || Array.isArray(wert) || Object.values(wert).some((text) => typeof text !== 'string')) {
|
|
throw new AuftragError(400, 'Die Zeilentexte müssen ein Objekt mit Texten sein.')
|
|
}
|
|
return wert
|
|
}
|
|
|
|
/** Auftrag der Aufwandsrechnung: ausdruecklich genannt (muss passen) oder die juengste gueltige Aufwands-AB des Projekts. */
|
|
async function ladeAufwandAuftrag(kunde, projekt, auftragId) {
|
|
if (auftragId !== undefined && auftragId !== null && auftragId !== '') {
|
|
if (!auftraegeAktiv(kunde)) throw new AuftragError(400, SCHALTER_AUS_MELDUNG)
|
|
const auftrag = await ladeAuftrag(kunde.$id, auftragId)
|
|
if (auftrag.art !== 'aufwand') throw new AuftragError(400, 'Eine Aufwandsrechnung passt nur zu einer Aufwands-Auftragsbestätigung.')
|
|
if (!VEREINBARTE_STATUS.includes(auftrag.status)) throw new AuftragError(400, 'Die Auftragsbestätigung ist nicht mehr gültig.')
|
|
if (auftrag.projektId && auftrag.projektId !== projekt.id) throw new AuftragError(400, 'Der Auftrag gehört nicht zu diesem Projekt.')
|
|
return auftrag
|
|
}
|
|
if (!auftraegeAktiv(kunde)) return null
|
|
const liste = await listAuftraege({ kundenId: kunde.$id, projektId: projekt.id })
|
|
return liste.find((ab) => ab.art === 'aufwand' && VEREINBARTE_STATUS.includes(ab.status)) ?? null
|
|
}
|
|
|
|
/** Ausdrueckliche Auswahl der Blaetter (Pruefung, Zugehoerigkeit zum Kunden, Doppelabrechnung mit Selbstheilung). */
|
|
async function waehleBlaetter(kunde, ids, eigeneTickets) {
|
|
if (!Array.isArray(ids) || ids.some((id) => typeof id !== 'string')) {
|
|
throw new AuftragError(400, 'Die Arbeitsblätter müssen als Liste von Kennungen übergeben werden.')
|
|
}
|
|
const eindeutig = [...new Set(ids)]
|
|
if (!eindeutig.length) throw new AuftragError(400, 'Keine Arbeitsblätter ausgewählt.')
|
|
if (eindeutig.length > MAX_BLAETTER) throw new AuftragError(400, `Zu viele Arbeitsblätter ausgewählt (höchstens ${MAX_BLAETTER}).`)
|
|
if (eindeutig.some((id) => !KENNUNG.test(id))) throw new AuftragError(400, KEIN_BLATT)
|
|
const blaetter = await ladeArbeitsblaetterNachIds(eindeutig)
|
|
if (blaetter.length !== eindeutig.length || blaetter.some((b) => !eigeneTickets.has(b.ticketId))) throw new AuftragError(400, KEIN_BLATT)
|
|
if (blaetter.some((b) => !abrechenbar(b))) {
|
|
throw new AuftragError(400, 'Mindestens ein Arbeitsblatt ist nicht abrechenbar (Abo-Zeit, Kommentar oder ohne Zeit).')
|
|
}
|
|
const belegtAlle = await abgerechneteArbeitsblaetter(kunde.$id)
|
|
const belegt = new Map(eindeutig.filter((id) => belegtAlle.has(id)).map((id) => [id, belegtAlle.get(id)]))
|
|
if (belegt.size) {
|
|
const gueltig = await pruefeBelegungen(belegt)
|
|
for (const id of eindeutig) {
|
|
const b = gueltig.get(id)
|
|
if (b) throw new AuftragError(409, `Arbeitsblatt bereits abgerechnet (${b.rechnungsnummer ? `Rechnung ${b.rechnungsnummer}` : 'Entwurf'}).`)
|
|
}
|
|
}
|
|
return blaetter
|
|
}
|
|
|
|
/** Standardauswahl: alle offenen Blaetter der Zeitquelle des Projekts; ist nichts offen, vorher Selbstheilung. */
|
|
async function standardBlaetter(kunde, projekt, eigeneTickets) {
|
|
const abs = auftraegeAktiv(kunde) ? await listAuftraege({ kundenId: kunde.$id, projektId: projekt.id }) : []
|
|
const zeitquelle = zeitquelleFuerProjekt(projekt, abs).filter((id) => eigeneTickets.has(id))
|
|
const lade = () => (zeitquelle.length ? offeneArbeitsblaetter(zeitquelle, kunde.$id) : [])
|
|
let offene = await lade()
|
|
if (!offene.length && zeitquelle.length) {
|
|
const belegtAlle = await abgerechneteArbeitsblaetter(kunde.$id)
|
|
const belegt = new Map()
|
|
for (const blatt of await ladeArbeitsblaetter(zeitquelle)) {
|
|
if (abrechenbar(blatt) && belegtAlle.has(blatt.id)) belegt.set(blatt.id, belegtAlle.get(blatt.id))
|
|
}
|
|
if (belegt.size) {
|
|
const gueltig = await pruefeBelegungen(belegt)
|
|
if (gueltig.size < belegt.size) offene = await lade()
|
|
}
|
|
}
|
|
if (!offene.length) throw new AuftragError(409, 'Keine offenen Arbeitsblätter zum Abrechnen.')
|
|
if (offene.length > MAX_BLAETTER) throw new AuftragError(400, `Zu viele Arbeitsblätter ausgewählt (höchstens ${MAX_BLAETTER}).`)
|
|
return offene
|
|
}
|
|
|
|
async function baueAufwand(basis, eingabe) {
|
|
const { kunde } = basis
|
|
const projekt = await ladeProjekt(kunde.$id, eingabe.projektId)
|
|
const auftrag = await ladeAufwandAuftrag(kunde, projekt, eingabe.auftragId)
|
|
const satzCents = auftrag?.stundensatzCents ?? STUNDENSATZ_STANDARD_CENTS
|
|
const zusatz = zusatzPositionen(eingabe.items)
|
|
const zeilentexte = pruefeZeilentexte(eingabe.zeilentexte)
|
|
const eigeneTickets = new Set(
|
|
(await listAllDocuments(config.collections.workorders, [Query.equal('customerId', kunde.$id)])).map((d) => d.$id)
|
|
)
|
|
const ausdruecklich = eingabe.arbeitsblattIds !== undefined && eingabe.arbeitsblattIds !== null
|
|
const blaetter = ausdruecklich ? await waehleBlaetter(kunde, eingabe.arbeitsblattIds, eigeneTickets) : await standardBlaetter(kunde, projekt, eigeneTickets)
|
|
const { positionen: tage, von, bis } = aufwandPositionen(blaetter, { satzCents, zeilentexte, maxZeilen: LIMITS.maxPositionen - zusatz.length })
|
|
return schliesseAb({
|
|
...basis,
|
|
projekt,
|
|
auftrag,
|
|
positionen: [...tage, ...zusatz],
|
|
leistungszeitraum: basis.leistungszeitraum || leistungszeitraumText(von, bis),
|
|
arbeitsblattIds: blaetter.map((b) => b.id),
|
|
})
|
|
}
|
|
|
|
const FINALISIERT = ['open', 'paid', 'uncollectible']
|
|
|
|
/**
|
|
* Fruehere Projektrechnungen einer AB (aktive Datensaetze) mit dem Stand bei Stripe. Selbstheilung: eine im Dashboard
|
|
* stornierte oder geloeschte Rechnung wird freigegeben (bei einer Schlussrechnung geht die AB zurueck) und uebersprungen.
|
|
*/
|
|
async function ladeFruehere(auftragId) {
|
|
const ergebnis = []
|
|
for (const doc of await listProjektRechnungen({ auftragId })) {
|
|
let roh
|
|
try {
|
|
roh = await getStripe().invoices.retrieve(doc.invoiceId)
|
|
} catch (err) {
|
|
if (!fehltBeiStripe(err)) throw err
|
|
roh = null
|
|
}
|
|
if (!roh || roh.status === 'void') {
|
|
await buche('Rechnung freigeben', () => gibProjektRechnungFrei(doc.invoiceId))
|
|
continue
|
|
}
|
|
const nummer = roh.number || ''
|
|
if (FINALISIERT.includes(roh.status) && nummer && !doc.rechnungsnummer) {
|
|
await buche('Rechnungsnummer nachtragen', () => updateProjektRechnung(doc.invoiceId, { rechnungsnummer: nummer }))
|
|
}
|
|
ergebnis.push({
|
|
invoiceId: doc.invoiceId,
|
|
art: doc.art,
|
|
status: roh.status,
|
|
rechnungsnummer: nummer,
|
|
datum: berlinDate(new Date((roh.status_transitions?.finalized_at ?? roh.created) * 1000)),
|
|
betragCents: roh.total ?? 0,
|
|
})
|
|
}
|
|
return ergebnis
|
|
}
|
|
|
|
/** Anzahlung und Schlussrechnung zu einer Festpreis-AB. */
|
|
async function baueAbRechnung(basis, eingabe) {
|
|
const { kunde, art } = basis
|
|
const { auftragId } = eingabe
|
|
if (typeof auftragId !== 'string' || !auftragId) throw new AuftragError(400, 'Anzahlung und Schlussrechnung brauchen eine Auftragsbestätigung.')
|
|
let auftrag = await ladeAuftrag(kunde.$id, auftragId)
|
|
if (auftrag.art !== 'festpreis') throw new AuftragError(400, 'Anzahlung und Schlussrechnung gehen nur mit einer Festpreis-Auftragsbestätigung.')
|
|
if (auftrag.status === 'abgerechnet') {
|
|
await ladeFruehere(auftrag.id) // gibt eine im Dashboard stornierte Schlussrechnung frei und setzt die AB zurueck
|
|
auftrag = await ladeAuftrag(kunde.$id, auftragId)
|
|
}
|
|
if (!['versendet', 'bestaetigt'].includes(auftrag.status)) throw new AuftragError(400, 'Die Auftragsbestätigung muss versendet oder bestätigt sein.')
|
|
|
|
let projekt = null
|
|
if (typeof eingabe.projektId === 'string' && eingabe.projektId) {
|
|
projekt = await ladeProjekt(kunde.$id, eingabe.projektId)
|
|
if (auftrag.projektId && auftrag.projektId !== projekt.id) throw new AuftragError(400, 'Der Auftrag gehört nicht zu diesem Projekt.')
|
|
} else if (auftrag.projektId) {
|
|
projekt = await ladeProjekt(kunde.$id, auftrag.projektId, { weich: true })
|
|
}
|
|
|
|
const fruehere = await ladeFruehere(auftrag.id)
|
|
const entwurf = fruehere.find((f) => f.status === 'draft' && (art === 'schluss' || f.art === 'schluss'))
|
|
if (entwurf) {
|
|
throw new AuftragError(409, art === 'schluss'
|
|
? `Erst den Entwurf über ${euroDe(entwurf.betragCents)} senden oder löschen.`
|
|
: 'Zu dieser Auftragsbestätigung gibt es schon einen Entwurf der Schlussrechnung. Erst den Entwurf senden oder löschen.')
|
|
}
|
|
const finalisierte = fruehere.filter((f) => FINALISIERT.includes(f.status))
|
|
let positionen
|
|
if (art === 'anzahlung') {
|
|
const angaben = eingabe.anzahlung ?? {}
|
|
if (typeof angaben !== 'object' || Array.isArray(angaben)) throw new AuftragError(400, 'Die Angaben zur Anzahlung müssen ein Objekt sein.')
|
|
positionen = [anzahlungPosition(auftrag, angaben, finalisierte.reduce((s, f) => s + f.betragCents, 0))]
|
|
} else {
|
|
positionen = schlussPositionen(
|
|
auftrag,
|
|
finalisierte.map(({ invoiceId, rechnungsnummer, datum, betragCents, art: fArt }) => ({ invoiceId, rechnungsnummer, datum, betragCents, art: fArt })),
|
|
eingabe.items
|
|
)
|
|
}
|
|
return schliesseAb({ ...basis, projekt, auftrag, positionen })
|
|
}
|