Buchungskalender: origin/main (Aufträge) zusammenführen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
fd971309a8
145 files changed
+42590
-129
No files matched your search
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Auftragsbestätigungen (Admin), Mount: /api/admin/finanzen/auftraege (vor dem Finanzen-Router, Übersicht V5 „L3“).
|
||||
* GET / (?kunde=), POST /, GET|PATCH|DELETE /:id, GET /:id/pdf, POST /:id/projekt, /:id/senden, /:id/erinnern,
|
||||
* /:id/neue-fassung, /:id/stornieren. Nur dünne HTTP-Schicht: Kunde laden, Schalter prüfen, Dienst aufrufen, Fehler über
|
||||
* sendeFehler. Ist der Schalter für den Kunden aus, antworten alle Routen 404 mit SCHALTER_AUS_MELDUNG.
|
||||
*/
|
||||
import { Router } from 'express'
|
||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
import { auftraegeAktiv } from '../../services/auftraege/schalter.js'
|
||||
import { SCHALTER_AUS_MELDUNG } from '../../services/auftraege/konstanten.js'
|
||||
import { AuftragError, sendeFehler } from '../../services/auftraege/fehler.js'
|
||||
import { auftragFuerTeam } from '../../services/auftraege/auftragSicht.js'
|
||||
import { listAuftraege } from '../../services/auftraege/auftragStore.js'
|
||||
import {
|
||||
aendereEntwurf, erinnereAuftrag, ladeAuftrag, ladeGespeichertesPdf, ladeKunde, legeEntwurfAn, loescheEntwurf, neueFassung,
|
||||
ordneProjektZu, sendeAuftrag, storniereAuftrag, vorschauPdf,
|
||||
} from '../../services/auftraege/auftraege.js'
|
||||
|
||||
const router = Router()
|
||||
router.use(requireAppwriteStaff)
|
||||
|
||||
async function ladeKundeMitSchalter(kundenId) {
|
||||
const kunde = await ladeKunde(kundenId)
|
||||
if (!auftraegeAktiv(kunde)) throw new AuftragError(404, SCHALTER_AUS_MELDUNG)
|
||||
return kunde
|
||||
}
|
||||
|
||||
async function ladeMitSchalter(id) {
|
||||
const auftrag = await ladeAuftrag(id)
|
||||
await ladeKundeMitSchalter(auftrag.kundenId)
|
||||
return auftrag
|
||||
}
|
||||
|
||||
const vonAdmin = (req) => req.appwriteUser?.email
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const kundenId = req.query.kunde
|
||||
if (typeof kundenId !== 'string' || kundenId === '') return res.status(400).json({ error: 'Kunde fehlt.' })
|
||||
await ladeKundeMitSchalter(kundenId)
|
||||
return res.json({ auftraege: (await listAuftraege({ kundenId })).map(auftragFuerTeam) })
|
||||
} catch (err) {
|
||||
return sendeFehler(res, 'Auftragsbestätigungen laden', err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const kundenId = req.body?.kundenId
|
||||
if (typeof kundenId === 'string' && kundenId !== '') await ladeKundeMitSchalter(kundenId)
|
||||
return res.json({ auftrag: auftragFuerTeam(await legeEntwurfAn(req.body, { von: vonAdmin(req) })) })
|
||||
} catch (err) {
|
||||
return sendeFehler(res, 'Auftragsbestätigung anlegen', err)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
return res.json({ auftrag: auftragFuerTeam(await ladeMitSchalter(req.params.id)) })
|
||||
} catch (err) {
|
||||
return sendeFehler(res, 'Auftragsbestätigung laden', err)
|
||||
}
|
||||
})
|
||||
|
||||
router.patch('/:id', async (req, res) => {
|
||||
try {
|
||||
await ladeMitSchalter(req.params.id)
|
||||
return res.json({ auftrag: auftragFuerTeam(await aendereEntwurf(req.params.id, req.body ?? {})) })
|
||||
} catch (err) {
|
||||
return sendeFehler(res, 'Auftragsbestätigung ändern', err)
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
await ladeMitSchalter(req.params.id)
|
||||
await loescheEntwurf(req.params.id)
|
||||
return res.json({ ok: true })
|
||||
} catch (err) {
|
||||
return sendeFehler(res, 'Auftragsbestätigung löschen', err)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/:id/pdf', async (req, res) => {
|
||||
try {
|
||||
const auftrag = await ladeMitSchalter(req.params.id)
|
||||
const entwurf = auftrag.status === 'entwurf'
|
||||
const buffer = entwurf ? await vorschauPdf(auftrag.id) : await ladeGespeichertesPdf(auftrag)
|
||||
const name = entwurf ? 'Auftragsbestaetigung-Entwurf.pdf' : /^AB-\d{4}-\d{4}$/.test(auftrag.nummer) ? `${auftrag.nummer}.pdf` : 'Auftragsbestaetigung.pdf'
|
||||
res.set({ 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="${name}"`, 'Cache-Control': 'no-store' })
|
||||
return res.send(buffer)
|
||||
} catch (err) {
|
||||
return sendeFehler(res, 'Auftragsbestätigung-PDF laden', err)
|
||||
}
|
||||
})
|
||||
|
||||
/** Antwort der Aktionen, die eine Mail auslösen: { auftrag, mailFehler }. */
|
||||
const mitMail = ({ auftrag, mailFehler }) => ({ auftrag: auftragFuerTeam(auftrag), mailFehler })
|
||||
|
||||
router.post('/:id/senden', async (req, res) => {
|
||||
try {
|
||||
await ladeMitSchalter(req.params.id)
|
||||
return res.json(mitMail(await sendeAuftrag(req.params.id, { von: vonAdmin(req) })))
|
||||
} catch (err) {
|
||||
return sendeFehler(res, 'Auftragsbestätigung senden', err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:id/erinnern', async (req, res) => {
|
||||
try {
|
||||
await ladeMitSchalter(req.params.id)
|
||||
return res.json(mitMail(await erinnereAuftrag(req.params.id)))
|
||||
} catch (err) {
|
||||
return sendeFehler(res, 'Auftragsbestätigung erinnern', err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:id/neue-fassung', async (req, res) => {
|
||||
try {
|
||||
await ladeMitSchalter(req.params.id)
|
||||
return res.json({ auftrag: auftragFuerTeam(await neueFassung(req.params.id, { von: vonAdmin(req) })) })
|
||||
} catch (err) {
|
||||
return sendeFehler(res, 'Neue Fassung anlegen', err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:id/stornieren', async (req, res) => {
|
||||
try {
|
||||
await ladeMitSchalter(req.params.id)
|
||||
return res.json({ auftrag: auftragFuerTeam(await storniereAuftrag(req.params.id)) })
|
||||
} catch (err) {
|
||||
return sendeFehler(res, 'Auftragsbestätigung stornieren', err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:id/projekt', async (req, res) => {
|
||||
try {
|
||||
await ladeMitSchalter(req.params.id)
|
||||
return res.json({ auftrag: auftragFuerTeam(await ordneProjektZu(req.params.id, req.body?.projektId)) })
|
||||
} catch (err) {
|
||||
return sendeFehler(res, 'Projekt zuordnen', err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Router } from 'express'
|
||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
import { withLocks } from '../../services/keyedLock.js'
|
||||
import { AuftragError } from '../../services/auftraege/fehler.js'
|
||||
import { EnableBankingError } from '../../services/finance/enableBanking.js'
|
||||
import { completeBankAuth, startBankAuth, syncBank } from '../../services/finance/bankSync.js'
|
||||
import { getConnection, listTransactions, publicConnection, saveConnection } from '../../services/finance/bankStore.js'
|
||||
@@ -21,13 +22,14 @@ import {
|
||||
createProjectInvoice,
|
||||
deleteDraft,
|
||||
listInvoices,
|
||||
rechnungsVorschau,
|
||||
sendInvoiceNow,
|
||||
voidInvoice,
|
||||
} from '../../services/finance/stripeFinance.js'
|
||||
|
||||
/**
|
||||
* Finanzen im WOMS (nur Admins): Übersicht, Bankstatus, Abruf, Neuverbindung, Buchungen und deren Zuordnung,
|
||||
* Rechnungen (lesen, anlegen, senden, stornieren, Entwurf löschen), Einstellungen und Fixkosten. Die Handler sind klein
|
||||
* Rechnungen (lesen, anlegen, Vorschau, senden, stornieren, Entwurf löschen), Einstellungen und Fixkosten. Die Handler sind klein
|
||||
* und rufen die Dienste in server/services/finance auf.
|
||||
*/
|
||||
const router = Router()
|
||||
@@ -148,7 +150,7 @@ const isStripeError = (err) => typeof err?.type === 'string' && err.type.startsW
|
||||
* Fehler einen status tragen: nur die beiden eigenen Fehlerarten werden weitergegeben.
|
||||
*/
|
||||
function sendFinanceError(res, what, err) {
|
||||
if (err instanceof AssignError || err instanceof InvoiceError) {
|
||||
if (err instanceof AssignError || err instanceof InvoiceError || err instanceof AuftragError) {
|
||||
// Ein 502 an einer bestehenden Rechnung (nicht gesendet, nicht lesbar) hat seine Ursache bei Stripe: nur Art, Status und Code ins Protokoll
|
||||
if (err.status === 502 && err.cause) logFailure(what, err.cause)
|
||||
const body = { error: err.message }
|
||||
@@ -186,7 +188,6 @@ router.delete('/buchungen/:id/zuordnung', async (req, res) => {
|
||||
// --- Rechnungen (Stripe) ----------------------------------------------------------------------------------------
|
||||
|
||||
const INVOICE_FILTERS = new Set(['alle', 'offen', 'ueberfaellig', 'bezahlt', 'entwurf'])
|
||||
const DEFAULT_DAYS_UNTIL_DUE = 14
|
||||
const createdOf = (invoice) => invoice.createdDate ?? ''
|
||||
|
||||
router.get('/rechnungen', async (req, res) => {
|
||||
@@ -208,12 +209,24 @@ router.get('/rechnungen', async (req, res) => {
|
||||
})
|
||||
|
||||
router.post('/rechnungen', async (req, res) => {
|
||||
const { customerId, items, daysUntilDue, memo, senden, idempotencyKey } = req.body || {}
|
||||
const {
|
||||
customerId, art, projektId, auftragId, items, arbeitsblattIds, anzahlung, zeilentexte, leistungszeitraum,
|
||||
daysUntilDue, memo, senden, idempotencyKey,
|
||||
} = req.body || {}
|
||||
try {
|
||||
// Die Typen der Felder prueft baueRechnung (400); die Route reicht sie unveraendert durch
|
||||
const rechnung = await createProjectInvoice({
|
||||
customerId,
|
||||
art,
|
||||
projektId,
|
||||
auftragId,
|
||||
items,
|
||||
daysUntilDue: daysUntilDue ?? DEFAULT_DAYS_UNTIL_DUE,
|
||||
arbeitsblattIds,
|
||||
anzahlung,
|
||||
zeilentexte,
|
||||
leistungszeitraum,
|
||||
// Ohne Angabe entscheidet createProjectInvoice (AB-Zahlungsziel bei Anzahlung und Schluss, sonst 14 Tage)
|
||||
daysUntilDue: daysUntilDue ?? undefined,
|
||||
memo,
|
||||
// Nur ein echtes Ja sendet sofort: alles andere bleibt ein Entwurf
|
||||
senden: senden === true || senden === 'true',
|
||||
@@ -227,6 +240,16 @@ router.post('/rechnungen', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Vorschau: dieselben Pruefungen wie das Anlegen, aber ohne Schreibzugriff
|
||||
router.post('/rechnungen/vorschau', async (req, res) => {
|
||||
const { customerId, art, projektId, auftragId, items, arbeitsblattIds, anzahlung, zeilentexte, leistungszeitraum } = req.body || {}
|
||||
try {
|
||||
return res.json(await rechnungsVorschau({ customerId, art, projektId, auftragId, items, arbeitsblattIds, anzahlung, zeilentexte, leistungszeitraum }))
|
||||
} catch (err) {
|
||||
return sendFinanceError(res, 'Rechnungsvorschau', err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/rechnungen/:id/senden', async (req, res) => {
|
||||
try {
|
||||
return res.json({ rechnung: await sendInvoiceNow(req.params.id) })
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Projektkonten (Admin). Mount: /api/admin/finanzen/projektkonten (vor dem Finanzen-Router).
|
||||
* GET /?kunde=<id> Projektkonten eines Kunden samt Zeile "Ohne Projekt"
|
||||
* GET /:projektId Konto, ABs, Rechnungen und offene Arbeitsblaetter eines Projekts
|
||||
* Nur lesen (Strang L1).
|
||||
*/
|
||||
import { Router } from 'express'
|
||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
import { sendeFehler } from '../../services/auftraege/fehler.js'
|
||||
import { projektkontenFuerKunde, projektkontoDetail } from '../../services/auftraege/projektkonto.js'
|
||||
|
||||
const router = Router()
|
||||
router.use(requireAppwriteStaff)
|
||||
|
||||
// Appwrite-UID: erst pruefen, dann an Appwrite geben
|
||||
const KENNUNG = /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const kunde = req.query.kunde
|
||||
if (typeof kunde !== 'string' || !KENNUNG.test(kunde)) return res.status(400).json({ error: 'Kunde fehlt oder ist ungültig.' })
|
||||
try {
|
||||
res.json(await projektkontenFuerKunde(kunde))
|
||||
} catch (err) {
|
||||
sendeFehler(res, 'Projektkonten laden', err)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/:projektId', async (req, res) => {
|
||||
const { projektId } = req.params
|
||||
if (!KENNUNG.test(projektId)) return res.status(404).json({ error: 'Projekt nicht gefunden.' })
|
||||
try {
|
||||
res.json(await projektkontoDetail(projektId))
|
||||
} catch (err) {
|
||||
sendeFehler(res, 'Projektkonto laden', err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
Reference in new issue
Block a user