Buchungskalender: Bestellung auf Anfrage, Freischaltung und Ticket
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
827ee54875
commit
9d01d38a45
7 files changed
+799
-15
No files matched your search
@@ -1,10 +1,87 @@
|
||||
import { Router } from 'express'
|
||||
import { config } from '../../config.js'
|
||||
import { Query, countDocuments, listDocuments } from '../../services/appwriteAdmin.js'
|
||||
import { anfrageAnsichtTeam, entscheideAnfrage } from '../../services/unterkunft/anfrageFunktion.js'
|
||||
import { ladeEinstellungen, listeUnterkuenfte } from '../../services/unterkunft/speicher.js'
|
||||
import { listCustomerFeatureDocs } from '../../services/siteFeatures.js'
|
||||
|
||||
/**
|
||||
* Buchungskalender im Ticketsystem: GET /api/team/unterkunft/:customerId (Stand je Projekt) und
|
||||
* POST /api/team/unterkunft/:customerId/anfrage (Entscheidung ueber die Anfrage der Funktion).
|
||||
* Eingehaengt in routes/team/index.js unter /unterkunft, dort hinter requireStaffJwt. Geruest (Task 5): Task 11 fuellt es.
|
||||
* Buchungskalender im Ticketsystem (Kunden-Reiter "Funktionen"): GET /api/team/unterkunft/:customerId (Stand je Projekt) und
|
||||
* POST /api/team/unterkunft/:customerId/anfrage (Entscheidung ueber die Anfrage der Funktion). Huelle wie routes/team/shop.js.
|
||||
* Eingehaengt in routes/team/index.js unter /unterkunft, dort hinter requireStaffJwt.
|
||||
*/
|
||||
const router = Router()
|
||||
|
||||
const sendError = (res, err) => {
|
||||
const status = err.status >= 400 && err.status < 600 ? err.status : 500
|
||||
if (status >= 500) console.error('[team-unterkunft]', err.message)
|
||||
return res.status(status).json({ error: status >= 500 ? 'Interner Serverfehler' : err.message, ...(err.body?.code ? { code: err.body.code } : {}) })
|
||||
}
|
||||
const route = (fn) => (req, res) => fn(req, res).catch((err) => sendError(res, err))
|
||||
const person = (req) => ({ name: req.session.adminName || req.session.name || '', email: req.session.email || '' })
|
||||
|
||||
/** Zaehler oder null, wenn die Abfrage scheitert (die Uebersicht bleibt lesbar). */
|
||||
async function zaehle(collection, queries, was) {
|
||||
try {
|
||||
return await countDocuments(config.collections[collection], queries)
|
||||
} catch (err) {
|
||||
console.warn(`[team-unterkunft] ${was} nicht lesbar: ${err.message}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const stripeZustand = (einst) => (!einst?.stripeAccountId ? 'keins' : einst.stripeStatus?.charges_enabled === true ? 'aktiv' : 'onboarding')
|
||||
|
||||
async function projektStand(project, feature) {
|
||||
let einst = null
|
||||
let unterkuenfte = 0
|
||||
if (feature) {
|
||||
try {
|
||||
einst = await ladeEinstellungen(feature.$id)
|
||||
} catch (err) {
|
||||
console.warn(`[team-unterkunft] Einstellungen ${feature.$id} nicht lesbar: ${err.message}`)
|
||||
}
|
||||
try {
|
||||
unterkuenfte = (await listeUnterkuenfte(feature.$id)).length
|
||||
} catch (err) {
|
||||
console.warn(`[team-unterkunft] Unterkuenfte ${feature.$id} nicht lesbar: ${err.message}`)
|
||||
unterkuenfte = null
|
||||
}
|
||||
}
|
||||
const offeneAnfragen = feature ? await zaehle('ukAnfragen', [Query.equal('featureId', feature.$id), Query.equal('status', 'neu')], 'Anfragenzahl') : 0
|
||||
return {
|
||||
projectId: project.$id,
|
||||
name: project.projectName || '',
|
||||
state: feature?.state || 'off',
|
||||
anfrage: anfrageAnsichtTeam(einst),
|
||||
stripe: { status: stripeZustand(einst) },
|
||||
unterkuenfte,
|
||||
offeneAnfragen,
|
||||
}
|
||||
}
|
||||
|
||||
// Uebersicht je Projekt des Kunden; archivierte Projekte nur, wenn dort ein Dokument der Funktion existiert
|
||||
router.get('/:customerId', route(async (req, res) => {
|
||||
const customerId = req.params.customerId
|
||||
const [docs, projects] = await Promise.all([
|
||||
listCustomerFeatureDocs(customerId),
|
||||
listDocuments(config.collections.websiteProjects, [Query.equal('customerId', customerId), Query.orderDesc('$createdAt'), Query.limit(1000)]),
|
||||
])
|
||||
const ukDocs = new Map(docs.filter((d) => d.featureKey === 'unterkunft').map((d) => [d.projectId, d]))
|
||||
const sichtbar = projects.filter((p) => !p.archived || ukDocs.has(p.$id))
|
||||
res.json({ projects: await Promise.all(sichtbar.map((p) => projektStand(p, ukDocs.get(p.$id)))) })
|
||||
}))
|
||||
|
||||
router.post('/:customerId/anfrage', route(async (req, res) => {
|
||||
const { projectId, entscheidung, begruendung } = req.body || {}
|
||||
const ergebnis = await entscheideAnfrage({
|
||||
customerId: req.params.customerId,
|
||||
projectId,
|
||||
entscheidung,
|
||||
begruendung,
|
||||
person: person(req),
|
||||
})
|
||||
res.json({ ok: true, anfrage: ergebnis.anfrage, ...(ergebnis.mailFehler ? { mailFehler: true } : {}) })
|
||||
}))
|
||||
|
||||
export default router
|
||||
@@ -10,6 +10,7 @@ import { kontoAnlegen, kontoStatus, onboardingLink, statusAusKonto } from '../se
|
||||
import * as ablauf from '../services/unterkunft/ablauf.js'
|
||||
import { istFrei } from '../services/unterkunft/belegung.js'
|
||||
import { istTag, naechte } from '../services/unterkunft/datum.js'
|
||||
import { anfrageAnsicht as funktionAnfrageAnsicht, stelleAnfrage, zieheZurueck } from '../services/unterkunft/anfrageFunktion.js'
|
||||
import { UkFehler } from '../services/unterkunft/fehler.js'
|
||||
import { ladeFuerProjekt } from '../services/unterkunft/kontext.js'
|
||||
import { berechnePreis, preiseVollstaendig, summiere } from '../services/unterkunft/preis.js'
|
||||
@@ -22,7 +23,7 @@ import * as vw from '../services/unterkunft/verwaltung.js'
|
||||
* Antwortformen: ui-erwartungen.md). Jede Route: Sitzung, eigenes Projekt (403), Funktion vorhanden und nicht off (409 nicht_aktiv).
|
||||
* Schreibende Routen zusaetzlich: nur der Kunde selbst (nicht Kundenansicht/"Ansehen als"), Zustand setup|active, Schreibgrenze
|
||||
* (300 je Kunde und Stunde). Alle IDs aus dem Pfad werden vor jedem Zugriff geprueft und gehoeren zum Projekt (sonst 404).
|
||||
* Fehler: { fehler: code, felder? }; Texte von Appwrite oder Stripe verlassen den Server nie. Die Anfrage-Funktion (/anfrage-funktion) folgt in Task 11.
|
||||
* Fehler: { fehler: code, felder? }; Texte von Appwrite oder Stripe verlassen den Server nie. Die Anfrage der Funktion (/anfrage-funktion, nur solange sie off ist) hat eine eigene Grenze (20 je Kunde und Stunde).
|
||||
*/
|
||||
const router = Router()
|
||||
// Jede Route hier braucht eine Sitzung; Kunden-Pruefung je Route (nurKunde)
|
||||
@@ -33,6 +34,7 @@ router.use((_req, res, next) => {
|
||||
})
|
||||
|
||||
const MAX_WRITES_PER_HOUR = 300
|
||||
const MAX_ANFRAGE_WRITES_PER_HOUR = 20
|
||||
const MELDUNG = {
|
||||
kundenansicht: 'In der Kundenansicht kann das nicht geändert werden.',
|
||||
zuViele: 'Zu viele Änderungen in kurzer Zeit. Bitte versuch es in einer Stunde wieder.',
|
||||
@@ -42,9 +44,12 @@ const MELDUNG = {
|
||||
}
|
||||
|
||||
const schreibLimiter = createWriteLimiter({ max: MAX_WRITES_PER_HOUR })
|
||||
const anfrageLimiter = createWriteLimiter({ max: MAX_ANFRAGE_WRITES_PER_HOUR })
|
||||
const limitAnfrage = anfrageLimiter.middleware((req) => getSessionCustomerId(req), MELDUNG.zuViele)
|
||||
const limitWrites = schreibLimiter.middleware((req) => getSessionCustomerId(req), MELDUNG.zuViele)
|
||||
export function resetUnterkunftRoutesForTests() {
|
||||
schreibLimiter.reset()
|
||||
anfrageLimiter.reset()
|
||||
}
|
||||
|
||||
const nurKunde = (req, res, next) => {
|
||||
@@ -227,7 +232,12 @@ router.get('/status', route(async (req, res) => {
|
||||
const doc = await getFeatureDoc(customerId, projectId, 'unterkunft')
|
||||
const state = doc?.state ?? 'off'
|
||||
const antwort = { state, stripe: { status: 'keins', vorhandenesShopKonto: false }, onlineZahlung: false, unterkuenfte: 0, rechnungenVorhanden: false }
|
||||
if (!doc || state === 'off') return res.json(antwort)
|
||||
if (state === 'off') {
|
||||
// Anfrage der Funktion: offen | abgelehnt | angenommen (zurueckgezogen und keine zaehlen als keine)
|
||||
const ansicht = doc ? funktionAnfrageAnsicht(await sp.ladeEinstellungen(doc.$id)) : null
|
||||
antwort.anfrage = ansicht && ansicht.status !== 'zurueckgezogen' ? ansicht : null
|
||||
return res.json(antwort)
|
||||
}
|
||||
const [einst, unterkuenfte, rechnungen, shop] = await Promise.all([
|
||||
sp.ladeEinstellungen(doc.$id),
|
||||
sp.listeUnterkuenfte(doc.$id),
|
||||
@@ -244,6 +254,37 @@ router.get('/status', route(async (req, res) => {
|
||||
res.json(antwort)
|
||||
}))
|
||||
|
||||
// ---- Anfrage der Funktion (nur solange sie off ist; kein mitProjekt)
|
||||
|
||||
/** Fehler des Anfrage-Dienstes (status, Text, body.code, fehler je Feld) in { fehler: code, felder? }. */
|
||||
function anfrageFehler(err) {
|
||||
if (err instanceof UkFehler || err?.status === 403 || err?.fachlich || !(err?.status >= 400 && err.status < 500)) return err
|
||||
if (err.fehler) return new UkFehler(400, 'ungueltig', err.fehler)
|
||||
return new UkFehler(err.status, err.body?.code || (err.status === 400 ? 'ungueltig' : 'anfrage'), err.status === 400 ? { projectId: err.message } : {})
|
||||
}
|
||||
const anfrageKunde = (req) => ({ name: req.session.name || '', email: req.session.email || '' })
|
||||
|
||||
router.post('/anfrage-funktion', nurKunde, limitAnfrage, route(async (req, res) => {
|
||||
const projectId = projectIdOf(req)
|
||||
if (!projectId) throw ungueltig({ projectId: 'Bitte wähle ein Projekt.' })
|
||||
try {
|
||||
const anfrage = await stelleAnfrage({ customerId: getSessionCustomerId(req), projectId, eingabe: body(req), person: anfrageKunde(req) })
|
||||
res.json({ anfrage })
|
||||
} catch (err) {
|
||||
throw anfrageFehler(err)
|
||||
}
|
||||
}))
|
||||
|
||||
router.post('/anfrage-funktion/zurueckziehen', nurKunde, limitAnfrage, route(async (req, res) => {
|
||||
const projectId = projectIdOf(req)
|
||||
if (!projectId) throw ungueltig({ projectId: 'Bitte wähle ein Projekt.' })
|
||||
try {
|
||||
res.json({ anfrage: await zieheZurueck({ customerId: getSessionCustomerId(req), projectId, person: anfrageKunde(req) }) })
|
||||
} catch (err) {
|
||||
throw anfrageFehler(err)
|
||||
}
|
||||
}))
|
||||
|
||||
// ---- Einstellungen
|
||||
|
||||
async function einstellungenAntwort(ctx, einst) {
|
||||
|
||||
Reference in new issue
Block a user