Aufträge L5: Widerruf eines Projektauftrags auf der Widerrufsseite (nur Privatpersonen in der Frist, Nummer optional)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c72b252e63
commit
3beba876d6
2 files changed
+246
-13
No files matched your search
+61
-13
@@ -5,6 +5,8 @@ import { getSessionCustomerId, requireSession } from '../middleware/session.js'
|
||||
import { getCustomerByEmail, getDocument, updateDocument } from '../services/appwriteAdmin.js'
|
||||
import { eventIdFor, listContractEvents, openWithdrawals, recordContractEvent, updateContractEvent } from '../services/contractEvents.js'
|
||||
import { lateStartAt, withdrawalDeadline } from '../services/deadlines.js'
|
||||
import { findAuftragByNummer } from '../services/auftraege/auftragStore.js'
|
||||
import { widerrufeAuftrag } from '../services/auftraege/bestaetigung.js'
|
||||
import { getLedger } from '../services/hourLedger.js'
|
||||
import { sendAdminNotice, sendCustomerMail } from '../services/mailer.js'
|
||||
import { changeStorage, previewStorageChange, recordPaidStorage, storageTaskKind } from '../services/mailStorage.js'
|
||||
@@ -727,7 +729,8 @@ export function publicRateLimitState() {
|
||||
|
||||
// Auswahl im Formular: Vertrag (Kuendigung) und Bestellung (Widerruf), Standard jeweils der erste Wert
|
||||
const CONTRACTS = ['abo', 'zusatzspeicher']
|
||||
const ORDERS = ['abschluss', 'wechsel', 'zusatzspeicher', 'funktion', 'termine', 'shop', 'zeiten']
|
||||
const ORDERS = ['abschluss', 'wechsel', 'zusatzspeicher', 'funktion', 'termine', 'shop', 'zeiten', 'projekt']
|
||||
const AB_NUMMER = /^AB-\d{4}-\d{4}$/
|
||||
// Bestellung einer Funktion -> Funktionsschluessel im Register ('funktion' meint die News-Pinnwand; jede Funktion hat ihre eigene Frist)
|
||||
const FEATURE_OF_ORDER = { funktion: 'news', termine: 'termine', shop: 'shop', zeiten: 'zeiten' }
|
||||
// Bestellung -> Ereignistyp mit Widerrufsfrist (F03); beim Zusatzspeicher zaehlt nur ein Plus (openWithdrawals)
|
||||
@@ -735,7 +738,7 @@ const ORDER_EVENT_TYPE = { abschluss: 'abschluss', wechsel: 'wechsel', zusatzspe
|
||||
|
||||
/**
|
||||
* Erklaerung aus dem Formular: null ohne Name oder gueltige E-Mail; { error } bei einer unzulaessigen Paketzahl.
|
||||
* contract 'abo' | 'zusatzspeicher', order 'abschluss' | 'wechsel' | 'zusatzspeicher' | 'funktion' | 'termine' | 'shop' | 'zeiten'; packages (nur beim
|
||||
* contract 'abo' | 'zusatzspeicher', order 'abschluss' | 'wechsel' | 'zusatzspeicher' | 'funktion' | 'termine' | 'shop' | 'zeiten' | 'projekt'; packages (nur beim
|
||||
* Zusatzspeicher) ganze Zahl von 1 bis maxUnits, null heisst alle gebuchten (Rechtsrunde 28.09.2026).
|
||||
*/
|
||||
function validDeclaration(body) {
|
||||
@@ -764,6 +767,7 @@ function validDeclaration(body) {
|
||||
contract,
|
||||
order,
|
||||
packages,
|
||||
abNummer: String(body?.abNummer || '').trim().toUpperCase().slice(0, 20),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -804,7 +808,8 @@ function contractLabel(contract, sub) {
|
||||
}
|
||||
|
||||
/** Beschriftung der widerrufenen Bestellung fuer K04 (an den Kunden) und T09 (intern). */
|
||||
function orderLabel(order, sub, { customer = false } = {}) {
|
||||
function orderLabel(order, sub, { customer = false, abNummer = '' } = {}) {
|
||||
if (order === 'projekt') return AB_NUMMER.test(abNummer) ? `Projektauftrag (Auftragsbestätigung ${abNummer})` : 'Projektauftrag (Auftragsbestätigung)'
|
||||
if (order === 'wechsel') return 'Wechsel ins Wartungs-Abo'
|
||||
if (order === 'zusatzspeicher') return 'E-Mail-Zusatzspeicher'
|
||||
if (order === 'funktion') return 'Funktion (z. B. News-Pinnwand)'
|
||||
@@ -1147,6 +1152,37 @@ async function withdrawFeature(d, receivedAt, featureKey) {
|
||||
return { customer, orders, deadline, lines, inTime: Boolean(deadline) && receivedAt <= deadline }
|
||||
}
|
||||
|
||||
/** Projektauftrag (Aufträge und Rechnungen, Spec 2026-10-06 Abschnitt 2): Nummer und E-Mail-Adresse müssen zur gesendeten AB passen. Ohne gültige Nummer wird nichts nachgeschlagen. Wirft bei Datenbankfehlern. */
|
||||
async function withdrawProjekt(d, receivedAt) {
|
||||
const auftrag = AB_NUMMER.test(d.abNummer) ? await findAuftragByNummer(d.abNummer) : null
|
||||
const adressen = [auftrag?.gesendetAn, auftrag?.kunde?.email].map((adresse) => String(adresse || '').trim().toLowerCase())
|
||||
if (!auftrag || !adressen.includes(d.email.toLowerCase())) return { auftrag: null, ergebnis: 'ungeprueft', fristEnde: null }
|
||||
return widerrufeAuftrag(auftrag, { jetzt: receivedAt })
|
||||
}
|
||||
|
||||
const PROJEKT_EINGANG = { widerrufen: 'RECHTZEITIG', nach_frist: 'NACH Fristende, vor einer Ablehnung bitte prüfen', bereits_widerrufen: 'bereits widerrufen' }
|
||||
|
||||
/** Werte für die Zeilen „Ergebnis“, „Widerrufsfrist bis“ und „Eingang“ im Block projekt von T09 (nur das Team sieht sie). */
|
||||
function projektWerte({ ergebnis, auftrag, fristEnde }, abNummer) {
|
||||
const ergebnisText = {
|
||||
widerrufen: () => `Auftragsbestätigung auf „widerrufen“ gesetzt${auftrag.fruehbeginn ? '; Frühbeginn war verlangt: Wertersatz prüfen (§ 357a Abs. 2 BGB)' : ''}`,
|
||||
bereits_widerrufen: () => 'war schon widerrufen, nichts geändert',
|
||||
kein_widerrufsrecht: () => 'Unternehmen: kein gesetzliches Widerrufsrecht, nichts geändert',
|
||||
nach_frist: () => 'nichts geändert',
|
||||
nicht_widerrufbar: () => `Status „${auftrag.status}“, nichts geändert, bitte prüfen`,
|
||||
fehler: () => 'ungeprüft: Prüfung fehlgeschlagen (Datenbank), bitte von Hand prüfen',
|
||||
}[ergebnis]
|
||||
return {
|
||||
ereignisse: ergebnisText
|
||||
? ergebnisText()
|
||||
: AB_NUMMER.test(abNummer)
|
||||
? `ungeprüft: keine Auftragsbestätigung zu Nummer „${abNummer}“ und dieser E-Mail-Adresse gefunden, nichts geändert`
|
||||
: 'ungeprüft: keine gültige AB-Nummer angegeben, nichts geändert, bitte von Hand zuordnen',
|
||||
fristende: ergebnis === 'kein_widerrufsrecht' ? 'entfällt (Unternehmen)' : fristEnde ? `${fmtDate(fristEnde)}, 24 Uhr` : 'keine Frist gefunden',
|
||||
rechtzeitig: PROJEKT_EINGANG[ergebnis] ?? 'ungeprüft',
|
||||
}
|
||||
}
|
||||
|
||||
/** Zeile je widerrufenem Ereignis fuer T09: ID, Inhalt (Speicher: Menge vorher -> nachher), Bestelldatum. */
|
||||
function describeWithdrawn(events) {
|
||||
return events
|
||||
@@ -1168,10 +1204,14 @@ router.post('/public/withdraw', async (req, res) => {
|
||||
// Funktion (News, Terminplaner oder Webshop): kein Abo, keine Vertragsereignisse; nur Nachschlagen fuer das Team (withdrawFeature)
|
||||
const featureKey = FEATURE_OF_ORDER[d.order] ?? null
|
||||
const feature = featureKey !== null
|
||||
const projekt = d.order === 'projekt'
|
||||
let pr = { auftrag: null, ergebnis: 'ungeprueft', fristEnde: null }
|
||||
const funktion = feature ? featureConfig(featureKey) : null
|
||||
let fn = { customer: null, orders: [], deadline: null, lines: '', inTime: false }
|
||||
try {
|
||||
if (feature) {
|
||||
if (projekt) {
|
||||
pr = await withdrawProjekt(d, receivedAt)
|
||||
} else if (feature) {
|
||||
fn = await withdrawFeature(d, receivedAt, featureKey)
|
||||
result = { ...result, deadline: fn.deadline, inTime: fn.inTime, late: !fn.inTime && Boolean(fn.deadline) }
|
||||
} else {
|
||||
@@ -1183,8 +1223,11 @@ router.post('/public/withdraw', async (req, res) => {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[abo] Widerruf nicht verarbeitbar:', err.message)
|
||||
if (projekt) pr = { auftrag: null, ergebnis: 'fehler', fristEnde: null }
|
||||
}
|
||||
const { inTime } = result
|
||||
const projektAngaben = projekt ? projektWerte(pr, d.abNummer) : null
|
||||
const produktText = projekt ? 'Projektauftrag' : feature ? funktion.label.name : productLabelOf(sub)
|
||||
// Fuer T09: warum sich die Frist nicht pruefen liess
|
||||
const pruefhinweis = result.unreadable ? 'Ereignisse nicht lesbar – bitte von Hand prüfen' : result.startMissing ? 'Abo-Beginn fehlt' : ''
|
||||
|
||||
@@ -1192,8 +1235,8 @@ router.post('/public/withdraw', async (req, res) => {
|
||||
template: 'K04',
|
||||
values: {
|
||||
name: d.name,
|
||||
bestellung: orderLabel(d.order, sub, { customer: true }),
|
||||
produkt: feature ? funktion.label.name : productLabelOf(sub),
|
||||
bestellung: orderLabel(d.order, sub, { customer: true, abNummer: d.abNummer }),
|
||||
produkt: produktText,
|
||||
eingang_zeitpunkt: fmtDateTime(receivedAt),
|
||||
email: d.email,
|
||||
kundennummer: d.customerNumber,
|
||||
@@ -1204,7 +1247,9 @@ router.post('/public/withdraw', async (req, res) => {
|
||||
wechsel: inTime && d.order === 'wechsel',
|
||||
zusatzspeicher: inTime && d.order === 'zusatzspeicher',
|
||||
funktion: feature,
|
||||
pruefen: !inTime && !feature,
|
||||
pruefen: !inTime && !feature && !projekt,
|
||||
projekt,
|
||||
projekt_unternehmen: projekt && pr.ergebnis === 'kein_widerrufsrecht',
|
||||
},
|
||||
}).catch((e) => console.error('[abo] Widerrufsbestaetigung fehlgeschlagen:', e.message))
|
||||
|
||||
@@ -1215,13 +1260,15 @@ router.post('/public/withdraw', async (req, res) => {
|
||||
email: d.email,
|
||||
name: d.name,
|
||||
kundennummer: d.customerNumber,
|
||||
bestellung: orderLabel(d.order, sub),
|
||||
produkt: feature ? funktion.label.name : productLabelOf(sub),
|
||||
bestellung: orderLabel(d.order, sub, { abNummer: d.abNummer }),
|
||||
produkt: produktText,
|
||||
preis: feature ? funktion.priceLabel : '',
|
||||
stripe_abo_id: sub?.stripeSubscriptionId || '',
|
||||
abo_beginn: fmtDate(sub?.startedAt),
|
||||
fristende: result.deadline ? `${fmtDate(result.deadline)}, 24 Uhr` : 'keine Frist gefunden',
|
||||
rechtzeitig: feature
|
||||
fristende: projekt ? projektAngaben.fristende : result.deadline ? `${fmtDate(result.deadline)}, 24 Uhr` : 'keine Frist gefunden',
|
||||
rechtzeitig: projekt
|
||||
? projektAngaben.rechtzeitig
|
||||
: feature
|
||||
? !fn.customer
|
||||
? 'ungeprüft (kein Kunde zu dieser E-Mail-Adresse gefunden)'
|
||||
: !fn.orders.length
|
||||
@@ -1242,14 +1289,14 @@ router.post('/public/withdraw', async (req, res) => {
|
||||
: result.deadline
|
||||
? 'ungeprüft (keine offene Frist: Bestellung gescheitert)'
|
||||
: 'ungeprüft (keine Bestellung dieser Art gefunden)',
|
||||
ereignisse: feature ? fn.lines : describeWithdrawn(result.withdrawn),
|
||||
ereignisse: projekt ? projektAngaben.ereignisse : feature ? fn.lines : describeWithdrawn(result.withdrawn),
|
||||
pruefhinweis,
|
||||
},
|
||||
blocks: {
|
||||
kundennummer: Boolean(d.customerNumber),
|
||||
abo_gefunden: Boolean(sub),
|
||||
auf_kuendigung_gesetzt: result.canceled,
|
||||
kein_abo_gefunden: !sub && !feature,
|
||||
kein_abo_gefunden: !sub && !feature && !projekt,
|
||||
kuendigung_fehlgeschlagen: result.cancelFailed,
|
||||
abschluss: inTime && d.order === 'abschluss',
|
||||
wechsel: inTime && d.order === 'wechsel',
|
||||
@@ -1261,6 +1308,7 @@ router.post('/public/withdraw', async (req, res) => {
|
||||
abschluss_verspaetet: Boolean(sub) && d.order === 'abschluss' && result.late && !result.alreadyWithdrawn,
|
||||
nicht_pruefbar: Boolean(sub) && Boolean(pruefhinweis),
|
||||
bereits_widerrufen: Boolean(sub) && result.alreadyWithdrawn,
|
||||
projekt,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user