Aufträge: Speicher der Projektrechnungen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a3db7f310a
commit
9cd5ea3038
2 files changed
+264
No files matched your search
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Speicher der Projektrechnungen (Collection projektRechnungen, Übersicht V1/V3/V4). Je Stripe-Rechnung, die zu einem
|
||||
* Projekt, einer AB oder einer Aufwandsabrechnung gehört, ein Dokument; Dokument-ID = Stripe-Rechnungs-ID. Aktiv
|
||||
* heißt: die Rechnung zählt (ein Storno oder gelöschter Entwurf setzt aktiv auf false und gibt die Arbeitsblätter
|
||||
* frei). Programmfehler (ungültige Kennungen, Typen, Filter) sind TypeError, AuftragError nur für 404/409/400.
|
||||
*/
|
||||
import { config } from '../../config.js'
|
||||
import { createDocument, getDocument, listAllDocuments, Query, updateDocument } from '../appwriteAdmin.js'
|
||||
import { AuftragError } from './fehler.js'
|
||||
import { AUFTRAEGE_COLLECTIONS } from './schema.js'
|
||||
|
||||
// Wie STRIPE_INVOICE_ID im Bestand (/^in_\w+$/), höchstens 36 Zeichen (Grenze der Appwrite-Dokument-ID); der
|
||||
// Stripe-Nachbau vergibt in_test_<n>
|
||||
const RECHNUNGS_ID = /^in_\w{1,33}$/
|
||||
const ARTEN = ['festbetrag', 'aufwand', 'anzahlung', 'schluss'] // frei hat nie ein Dokument
|
||||
const JSON_GROESSE = AUFTRAEGE_COLLECTIONS.projektRechnungen.attributes.find((a) => a.key === 'arbeitsblattIdsJson').size
|
||||
|
||||
const istText = (wert) => typeof wert === 'string'
|
||||
const istNichtLeer = (wert) => istText(wert) && wert !== ''
|
||||
|
||||
function docToRechnung(doc) {
|
||||
let ids = []
|
||||
try {
|
||||
const roh = JSON.parse(doc.arbeitsblattIdsJson)
|
||||
if (Array.isArray(roh)) ids = roh.filter(istNichtLeer)
|
||||
} catch {
|
||||
// kaputtes oder leeres JSON: keine Arbeitsblätter
|
||||
}
|
||||
const text = (wert) => (istText(wert) ? wert : '')
|
||||
const zeit = new Date(doc.erstelltAm || doc.$createdAt)
|
||||
return {
|
||||
invoiceId: doc.$id,
|
||||
kundenId: text(doc.kundenId),
|
||||
projektId: text(doc.projektId),
|
||||
auftragId: text(doc.auftragId),
|
||||
art: text(doc.art),
|
||||
betragCents: Number.isSafeInteger(doc.betragCents) ? doc.betragCents : 0,
|
||||
arbeitsblattIds: ids,
|
||||
rechnungsnummer: text(doc.rechnungsnummer),
|
||||
aktiv: doc.aktiv === true,
|
||||
erstelltVon: text(doc.erstelltVon),
|
||||
erstelltAm: Number.isNaN(zeit.getTime()) ? null : zeit.toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
/** Prüft die genannten Felder (jede Verletzung ist ein TypeError) und liefert das Dokument dazu. */
|
||||
function feldDoc(daten, { vollstaendig }) {
|
||||
const doc = {}
|
||||
const hat = (key) => Object.hasOwn(daten, key) && daten[key] !== undefined
|
||||
const text = (key, { pflicht = false } = {}) => {
|
||||
if (!hat(key)) return
|
||||
if (pflicht ? !istNichtLeer(daten[key]) : !istText(daten[key])) throw new TypeError(`Projektrechnung.${key} ist ungültig.`)
|
||||
doc[key] = daten[key]
|
||||
}
|
||||
text('kundenId', { pflicht: true })
|
||||
text('projektId')
|
||||
text('auftragId')
|
||||
text('rechnungsnummer')
|
||||
text('erstelltVon')
|
||||
if (hat('art')) {
|
||||
if (!ARTEN.includes(daten.art)) throw new TypeError('Projektrechnung.art ist ungültig.')
|
||||
doc.art = daten.art
|
||||
}
|
||||
if (hat('betragCents')) {
|
||||
if (!Number.isSafeInteger(daten.betragCents) || daten.betragCents < 0) throw new TypeError('Projektrechnung.betragCents ist ungültig.')
|
||||
doc.betragCents = daten.betragCents
|
||||
}
|
||||
if (hat('arbeitsblattIds')) {
|
||||
if (!Array.isArray(daten.arbeitsblattIds) || !daten.arbeitsblattIds.every(istNichtLeer)) throw new TypeError('Projektrechnung.arbeitsblattIds ist ungültig.')
|
||||
const json = JSON.stringify(daten.arbeitsblattIds)
|
||||
if (json.length > JSON_GROESSE) throw new AuftragError(400, 'Zu viele Arbeitsblätter in einer Rechnung.')
|
||||
doc.arbeitsblattIdsJson = json
|
||||
}
|
||||
if (hat('aktiv')) {
|
||||
if (typeof daten.aktiv !== 'boolean') throw new TypeError('Projektrechnung.aktiv ist ungültig.')
|
||||
doc.aktiv = daten.aktiv
|
||||
}
|
||||
if (vollstaendig && (!Object.hasOwn(doc, 'kundenId') || !Object.hasOwn(doc, 'art') || !Object.hasOwn(doc, 'betragCents'))) {
|
||||
throw new TypeError('Projektrechnung braucht kundenId, art und betragCents.')
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
export async function getProjektRechnung(invoiceId) {
|
||||
if (typeof invoiceId !== 'string' || !RECHNUNGS_ID.test(invoiceId)) return null
|
||||
try {
|
||||
return docToRechnung(await getDocument(config.collections.projektRechnungen, invoiceId))
|
||||
} catch (err) {
|
||||
if (err?.status === 404) return null
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function createProjektRechnung(invoiceId, daten) {
|
||||
if (typeof invoiceId !== 'string' || !RECHNUNGS_ID.test(invoiceId)) throw new TypeError('Ungültige Rechnungs-ID.')
|
||||
const doc = {
|
||||
projektId: '', auftragId: '', rechnungsnummer: '', erstelltVon: '', arbeitsblattIds: [], aktiv: true,
|
||||
...Object.fromEntries(Object.entries(daten).filter(([, wert]) => wert !== undefined)),
|
||||
}
|
||||
const felder = { ...feldDoc(doc, { vollstaendig: true }), invoiceId, erstelltAm: new Date().toISOString() }
|
||||
try {
|
||||
return docToRechnung(await createDocument(config.collections.projektRechnungen, felder, invoiceId))
|
||||
} catch (err) {
|
||||
if (err?.status === 409) throw new AuftragError(409, 'Zu dieser Rechnung gibt es schon einen Eintrag.')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateProjektRechnung(invoiceId, patch) {
|
||||
const nichtGefunden = () => new AuftragError(404, 'Projektrechnung nicht gefunden.')
|
||||
if (typeof invoiceId !== 'string' || !RECHNUNGS_ID.test(invoiceId)) throw nichtGefunden()
|
||||
const doc = feldDoc(patch, { vollstaendig: false })
|
||||
try {
|
||||
return docToRechnung(await updateDocument(config.collections.projektRechnungen, invoiceId, doc))
|
||||
} catch (err) {
|
||||
if (err?.status === 404) throw nichtGefunden()
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/** Nach erstelltAm aufsteigend. Ein angegebener, aber leerer oder ungültiger Filter ist ein TypeError, nie „alle“. */
|
||||
export async function listProjektRechnungen({ kundenId, projektId, auftragId, nurAktiv = true } = {}) {
|
||||
const abfragen = []
|
||||
for (const [key, wert] of Object.entries({ kundenId, projektId, auftragId })) {
|
||||
if (wert === undefined) continue
|
||||
if (!istNichtLeer(wert)) throw new TypeError(`Filter ${key} muss ein nicht leerer Text sein.`)
|
||||
abfragen.push(Query.equal(key, wert))
|
||||
}
|
||||
if (typeof nurAktiv !== 'boolean') throw new TypeError('Filter nurAktiv muss ein Wahrheitswert sein.')
|
||||
if (nurAktiv) abfragen.push(Query.equal('aktiv', true))
|
||||
const docs = await listAllDocuments(config.collections.projektRechnungen, abfragen)
|
||||
return docs
|
||||
.map(docToRechnung)
|
||||
.sort((a, b) => (a.erstelltAm || '').localeCompare(b.erstelltAm || '') || a.invoiceId.localeCompare(b.invoiceId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Hängt alle Dokumente einer AB (auch stornierte) an die neue AB. Bei einem Fehler bleibt ein Teilstand, ein
|
||||
* erneuter Aufruf setzt fort. Liefert die Anzahl umgehängter Dokumente.
|
||||
*/
|
||||
export async function umhaengen(auftragIdAlt, auftragIdNeu) {
|
||||
if (!istNichtLeer(auftragIdAlt) || !istNichtLeer(auftragIdNeu)) throw new TypeError('umhaengen: beide AB-IDs müssen nicht leerer Text sein.')
|
||||
if (auftragIdAlt === auftragIdNeu) return 0
|
||||
const alte = await listProjektRechnungen({ auftragId: auftragIdAlt, nurAktiv: false })
|
||||
for (const rechnung of alte) {
|
||||
await updateDocument(config.collections.projektRechnungen, rechnung.invoiceId, { auftragId: auftragIdNeu })
|
||||
}
|
||||
return alte.length
|
||||
}
|
||||
Reference in new issue
Block a user