WIP: Zahlungsrouten (Test folgt auf dem Server)
This commit is contained in:
@@ -0,0 +1,456 @@
|
||||
/* =====================================================================
|
||||
routes/webdesign-zahlungen.js — Zahlungen im Kundenportal
|
||||
|
||||
Das PayPal-Modul (lib/webdesign-paypal.js) und die Tabellen standen
|
||||
seit dem 22.08.2026. Was fehlte, waren die Wege dorthin.
|
||||
|
||||
DREI DINGE, DIE HIER ANDERS SIND ALS BEI EINEM ÜBLICHEN BEZAHLKNOPF
|
||||
|
||||
1. DIE ZUSTIMMUNG IST TEIL DER ZAHLUNG, nicht ein Häkchen daneben.
|
||||
|
||||
Die 30-%-Anzahlung wird fällig, BEVOR die 14-tägige Widerrufsfrist
|
||||
abgelaufen ist. Damit die Arbeit trotzdem sofort beginnen darf,
|
||||
verlangt § 356 Abs. 4 BGB zweierlei vom Verbraucher: die
|
||||
ausdrückliche Zustimmung zum vorzeitigen Beginn UND die Bestätigung,
|
||||
dass er dadurch sein Widerrufsrecht verliert.
|
||||
|
||||
Die Beweislast für beides liegt beim Unternehmer. Ohne Nachweis
|
||||
bleibt das Widerrufsrecht bestehen — und die bereits geleistete
|
||||
Arbeit wäre im Streit schwer durchsetzbar.
|
||||
|
||||
Deshalb wird die Zustimmung nicht nur als Wahrheitswert gespeichert,
|
||||
sondern mit dem WORTLAUT, den der Kunde gesehen hat, und mit
|
||||
Zeitstempel. Im Streit zählt nicht "hat zugestimmt", sondern WOZU.
|
||||
|
||||
Und weil das nur Verbraucher betrifft: Bei einem Geschäftskunden
|
||||
wird gar nicht erst danach gefragt. Ein Unternehmer hat kein
|
||||
Widerrufsrecht, ihn eine Verzichtserklärung unterschreiben zu
|
||||
lassen wäre sinnlos und würde nur Misstrauen wecken.
|
||||
|
||||
2. NICHT EINGERICHTET IST EIN ZUSTAND, KEIN FEHLER.
|
||||
|
||||
Solange die PayPal-Zugangsdaten fehlen, sagt die Seite das
|
||||
freundlich und nennt den Weg per Überweisung. Ein Knopf, der beim
|
||||
Klick eine technische Fehlermeldung wirft, ist schlechter als
|
||||
keiner — er sieht nach einer kaputten Seite aus, und niemand
|
||||
bezahlt gern auf einer kaputten Seite.
|
||||
|
||||
3. DER WEBHOOK IST DIE WAHRHEIT, NICHT DIE RÜCKKEHR DES BROWSERS.
|
||||
|
||||
Der Kunde kann das Fenster schliessen, bevor er zurückgeleitet
|
||||
wird. Die Zahlung ist dann trotzdem erfolgt. Deshalb schreibt der
|
||||
Webhook den endgültigen Stand — der Rückkehrweg ist nur der
|
||||
schnellere von beiden, nicht der verlässlichere.
|
||||
===================================================================== */
|
||||
|
||||
import { db } from "../db.js";
|
||||
import { json } from "../lib/http.js";
|
||||
import { requireSession } from "../lib/auth.js";
|
||||
import { hasPermission } from "../lib/permissions.js";
|
||||
import { ausweisPruefen } from "../lib/webdesign-ausweis.js";
|
||||
import { brauchtKunde } from "./webdesign-portal.js";
|
||||
import { zustimmungSpeichern } from "./webdesign-widerruf.js";
|
||||
import * as paypal from "../lib/webdesign-paypal.js";
|
||||
import {
|
||||
jetzt, neueId, naechsteNummer, text, verlauf, centZuText,
|
||||
besucherIp, spracheOderDe,
|
||||
} from "../lib/webdesign-helfer.js";
|
||||
|
||||
/* Der Wortlaut, dem der Verbraucher zustimmt. Er steht HIER und nicht
|
||||
im Browser: Was gespeichert wird, muss das sein, was der Server
|
||||
kennt — sonst könnte man ihm einen beliebigen Text unterschieben.
|
||||
Die Fassung wandert mit in die Datenbank, damit später nachvollziehbar
|
||||
bleibt, welche Formulierung galt. */
|
||||
export const SOFORTBEGINN_FASSUNG = "2026-08-23";
|
||||
export const SOFORTBEGINN_TEXT = {
|
||||
de: "Ich verlange ausdrücklich, dass Dogfather Webdesign vor Ablauf der "
|
||||
+ "Widerrufsfrist mit der Arbeit beginnt. Mir ist bekannt, dass ich mein "
|
||||
+ "Widerrufsrecht verliere, sobald der Vertrag vollständig erfüllt ist.",
|
||||
"de-CH": "Ich verlang usdrücklich, dass Dogfather Webdesign vor Ablauf vo dr "
|
||||
+ "Widerrüefsfrist mit dr Arbet aafangt. Mir isch bekannt, dass ich mis "
|
||||
+ "Widerrüefsrächt verlüre, sobald dr Vertrag vollständig erfüllt isch.",
|
||||
en: "I expressly request that Dogfather Web Design begin work before the "
|
||||
+ "withdrawal period expires. I am aware that I lose my right of withdrawal "
|
||||
+ "once the contract has been fully performed.",
|
||||
fr: "Je demande expressément que Dogfather Web Design commence le travail avant "
|
||||
+ "l'expiration du délai de rétractation. Je sais que je perds mon droit de "
|
||||
+ "rétractation dès que le contrat est intégralement exécuté.",
|
||||
pt: "Solicito expressamente que a Dogfather Web Design comece o trabalho antes "
|
||||
+ "do fim do prazo de rescisão. Sei que perco o meu direito de rescisão assim "
|
||||
+ "que o contrato estiver integralmente cumprido.",
|
||||
};
|
||||
|
||||
async function darf(req, recht) {
|
||||
const ausweis = ausweisPruefen(req);
|
||||
if (ausweis) return { erlaubt: true, sitzung: { actor: ausweis.rolle, isOwner: true } };
|
||||
const sitzung = await requireSession(req);
|
||||
if (!sitzung) return { erlaubt: false, grund: 401, sitzung: null };
|
||||
if (sitzung.isOwner) return { erlaubt: true, sitzung };
|
||||
const ok = hasPermission(sitzung.permissions, sitzung.overrides, recht);
|
||||
return { erlaubt: ok, grund: ok ? null : 403, sitzung };
|
||||
}
|
||||
|
||||
const BASIS = () => process.env.SITE_BASIS_URL || "https://dogfather-universe.com";
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
ZUSTAND ABFRAGEN — braucht der Kunde überhaupt einen Bezahlknopf?
|
||||
GET /webdesign/portal/zahlung/:id
|
||||
--------------------------------------------------------------------- */
|
||||
export function zahlungAnsehen(req, res) {
|
||||
const kunde = brauchtKunde(req, res);
|
||||
if (!kunde) return;
|
||||
|
||||
const z = db
|
||||
.prepare(`SELECT * FROM wd_zahlungen WHERE id = ? AND kunde_id = ?`)
|
||||
.get(String(req.params.id || ""), kunde.id);
|
||||
if (!z) return json(res, { ok: false, error: "Zahlung nicht gefunden." }, 404);
|
||||
|
||||
const sprache = spracheOderDe(kunde.sprache);
|
||||
/* Nur Verbraucher bekommen die Zustimmungsfrage, und auch nur, solange
|
||||
sie noch nicht erteilt wurde. */
|
||||
const brauchtZustimmung = z.kundenart !== "unternehmen"
|
||||
&& z.art === "anzahlung"
|
||||
&& !z.widerruf_zugestimmt;
|
||||
|
||||
return json(res, {
|
||||
ok: true,
|
||||
zahlung: {
|
||||
id: z.id, nummer: z.nummer, art: z.art, zweck: z.zweck_text,
|
||||
betragText: centZuText(z.betrag_cent, z.waehrung),
|
||||
status: z.status,
|
||||
},
|
||||
eingerichtet: paypal.istEingerichtet("einmal"),
|
||||
brauchtZustimmung,
|
||||
zustimmungstext: brauchtZustimmung
|
||||
? (SOFORTBEGINN_TEXT[sprache] || SOFORTBEGINN_TEXT.de)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
ZAHLUNG STARTEN
|
||||
POST /webdesign/portal/zahlung/:id/start
|
||||
--------------------------------------------------------------------- */
|
||||
export async function zahlungStarten(req, res) {
|
||||
const kunde = brauchtKunde(req, res);
|
||||
if (!kunde) return;
|
||||
|
||||
const z = db
|
||||
.prepare(`SELECT * FROM wd_zahlungen WHERE id = ? AND kunde_id = ?`)
|
||||
.get(String(req.params.id || ""), kunde.id);
|
||||
if (!z) return json(res, { ok: false, error: "Zahlung nicht gefunden." }, 404);
|
||||
|
||||
if (z.status === "bezahlt") {
|
||||
return json(res, { ok: false, error: "Diese Zahlung ist bereits beglichen." }, 409);
|
||||
}
|
||||
|
||||
/* Ohne Zugangsdaten NICHT scheitern, sondern ehrlich sein. */
|
||||
if (!paypal.istEingerichtet("einmal")) {
|
||||
return json(res, {
|
||||
ok: false,
|
||||
nichtEingerichtet: true,
|
||||
error: "Die Zahlung über PayPal ist noch nicht freigeschaltet. "
|
||||
+ "Schreib kurz über das Postfach — dann bekommst du die Bankverbindung.",
|
||||
}, 503);
|
||||
}
|
||||
|
||||
const sprache = spracheOderDe(kunde.sprache);
|
||||
|
||||
/* ---- § 356 Abs. 4 BGB: Zustimmung zum vorzeitigen Leistungsbeginn ---- */
|
||||
const brauchtZustimmung = z.kundenart !== "unternehmen"
|
||||
&& z.art === "anzahlung"
|
||||
&& !z.widerruf_zugestimmt;
|
||||
|
||||
if (brauchtZustimmung) {
|
||||
/* === true, nicht bloss "wahrheitsähnlich". Bei einer Erklärung, mit
|
||||
der jemand ein Recht aufgibt, darf eine "1" oder ein "ja" nicht
|
||||
genügen — die Absicht muss eindeutig übermittelt worden sein. */
|
||||
if (req.body?.sofortbeginn !== true) {
|
||||
return json(res, {
|
||||
ok: false,
|
||||
brauchtZustimmung: true,
|
||||
zustimmungstext: SOFORTBEGINN_TEXT[sprache] || SOFORTBEGINN_TEXT.de,
|
||||
error: "Bitte bestätige zuerst, dass sofort begonnen werden darf.",
|
||||
}, 400);
|
||||
}
|
||||
|
||||
const wortlaut = SOFORTBEGINN_TEXT[sprache] || SOFORTBEGINN_TEXT.de;
|
||||
const nun = jetzt();
|
||||
db.prepare(
|
||||
`UPDATE wd_zahlungen SET widerruf_zugestimmt = 1, widerruf_zugestimmt_am = ?,
|
||||
aktualisiert_am = ? WHERE id = ?`
|
||||
).run(nun, nun, z.id);
|
||||
|
||||
/* Zusätzlich als eigenständiger Nachweis — die Zahlungszeile kann
|
||||
sich ändern, der Nachweis darf das nicht. */
|
||||
zustimmungSpeichern({
|
||||
kundeId: kunde.id,
|
||||
projektId: z.projekt_id,
|
||||
art: "sofortbeginn",
|
||||
wortlaut,
|
||||
fassung: SOFORTBEGINN_FASSUNG,
|
||||
sprache,
|
||||
ip: besucherIp(req),
|
||||
});
|
||||
verlauf("zahlung", z.id, "sofortbeginn_zugestimmt", "kunde:" + kunde.id, { fassung: SOFORTBEGINN_FASSUNG });
|
||||
}
|
||||
|
||||
try {
|
||||
const bestellung = await paypal.bestellungAnlegen({
|
||||
zahlungId: z.id,
|
||||
nummer: z.nummer,
|
||||
betragCent: z.betrag_cent,
|
||||
waehrung: z.waehrung || "EUR",
|
||||
zweck: z.zweck_text,
|
||||
rueckkehrUrl: `${BASIS()}/webdesign/portal.html?zahlung=${encodeURIComponent(z.id)}&zurueck=1`,
|
||||
abbruchUrl: `${BASIS()}/webdesign/portal.html?zahlung=${encodeURIComponent(z.id)}&abbruch=1`,
|
||||
sprache,
|
||||
});
|
||||
|
||||
db.prepare(
|
||||
`UPDATE wd_zahlungen SET status = 'freigegeben', paypal_order_id = ?,
|
||||
freigegeben_am = ?, aktualisiert_am = ? WHERE id = ?`
|
||||
).run(bestellung.orderId, jetzt(), jetzt(), z.id);
|
||||
|
||||
verlauf("zahlung", z.id, "paypal_gestartet", "kunde:" + kunde.id, { orderId: bestellung.orderId });
|
||||
return json(res, { ok: true, weiterUrl: bestellung.approveUrl, orderId: bestellung.orderId });
|
||||
} catch (fehler) {
|
||||
db.prepare(`UPDATE wd_zahlungen SET fehler_text = ?, aktualisiert_am = ? WHERE id = ?`)
|
||||
.run(String(fehler.message || fehler).slice(0, 500), jetzt(), z.id);
|
||||
verlauf("zahlung", z.id, "paypal_fehler", "kunde:" + kunde.id, null);
|
||||
return json(res, {
|
||||
ok: false,
|
||||
error: "Die Zahlung konnte nicht gestartet werden. Bitte versuch es gleich noch einmal "
|
||||
+ "oder schreib kurz über das Postfach.",
|
||||
}, 502);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
ZAHLUNG ABSCHLIESSEN (Rückkehr aus PayPal)
|
||||
POST /webdesign/portal/zahlung/:id/abschluss
|
||||
|
||||
Der schnellere von zwei Wegen. Der verlässliche ist der Webhook.
|
||||
--------------------------------------------------------------------- */
|
||||
export async function zahlungAbschliessen(req, res) {
|
||||
const kunde = brauchtKunde(req, res);
|
||||
if (!kunde) return;
|
||||
|
||||
const z = db
|
||||
.prepare(`SELECT * FROM wd_zahlungen WHERE id = ? AND kunde_id = ?`)
|
||||
.get(String(req.params.id || ""), kunde.id);
|
||||
if (!z) return json(res, { ok: false, error: "Zahlung nicht gefunden." }, 404);
|
||||
|
||||
/* Schon durch — etwa weil der Webhook schneller war. Das ist kein
|
||||
Fehler, sondern der Normalfall bei guter Verbindung. */
|
||||
if (z.status === "bezahlt") {
|
||||
return json(res, { ok: true, schonBezahlt: true, status: "bezahlt" });
|
||||
}
|
||||
if (!z.paypal_order_id) {
|
||||
return json(res, { ok: false, error: "Für diese Zahlung wurde noch nichts gestartet." }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const einzug = await paypal.bestellungEinziehen(z.paypal_order_id);
|
||||
if (einzug.status !== "COMPLETED") {
|
||||
return json(res, { ok: false, error: "Die Zahlung wurde von PayPal noch nicht abgeschlossen." }, 409);
|
||||
}
|
||||
zahlungAlsBezahltVermerken(z.id, einzug, "kunde:" + kunde.id);
|
||||
return json(res, { ok: true, status: "bezahlt" });
|
||||
} catch (fehler) {
|
||||
/* Wichtig: NICHT als Fehler des Kunden darstellen. Das Geld kann
|
||||
trotzdem geflossen sein — der Webhook klärt es. */
|
||||
verlauf("zahlung", z.id, "einzug_fehler", "kunde:" + kunde.id, null);
|
||||
return json(res, {
|
||||
ok: false,
|
||||
error: "Wir konnten den Abschluss gerade nicht bestätigen. Falls die Zahlung bei PayPal "
|
||||
+ "durchgegangen ist, wird sie hier gleich als bezahlt angezeigt — du musst nichts weiter tun.",
|
||||
}, 502);
|
||||
}
|
||||
}
|
||||
|
||||
/* Gemeinsame Stelle für "bezahlt". Wird vom Rückkehrweg UND vom Webhook
|
||||
benutzt, damit beide dieselben Felder setzen. Zwei getrennte
|
||||
Fassungen würden über kurz oder lang auseinanderlaufen. */
|
||||
function zahlungAlsBezahltVermerken(zahlungId, einzug, wer) {
|
||||
const nun = jetzt();
|
||||
db.prepare(
|
||||
`UPDATE wd_zahlungen SET status = 'bezahlt', paypal_capture_id = ?,
|
||||
netto_cent = COALESCE(?, netto_cent), gebuehr_cent = COALESCE(?, gebuehr_cent),
|
||||
bezahlt_am = COALESCE(bezahlt_am, ?), aktualisiert_am = ? WHERE id = ?`
|
||||
).run(
|
||||
einzug.captureId || null,
|
||||
einzug.nettoCent ?? null,
|
||||
einzug.gebuehrCent ?? null,
|
||||
nun, nun, zahlungId
|
||||
);
|
||||
|
||||
/* Anzahlung/Rest am Projekt mitführen, damit die Verwaltung den Stand
|
||||
sieht, ohne die Zahlungen einzeln durchzugehen. */
|
||||
const z = db.prepare(`SELECT art, projekt_id FROM wd_zahlungen WHERE id = ?`).get(zahlungId);
|
||||
if (z?.projekt_id && (z.art === "anzahlung" || z.art === "rest")) {
|
||||
const feld = z.art === "anzahlung" ? "anzahlung_bezahlt" : "rest_bezahlt";
|
||||
db.prepare(`UPDATE wd_projekte SET ${feld} = 1, aktualisiert_am = ? WHERE id = ?`)
|
||||
.run(nun, z.projekt_id);
|
||||
}
|
||||
|
||||
verlauf("zahlung", zahlungId, "bezahlt", wer, { captureId: einzug.captureId || null });
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
WEBHOOK — PayPal ruft hier an
|
||||
POST /webdesign/paypal-webhook
|
||||
|
||||
Öffentlich erreichbar (PayPal hat keine Sitzung), aber durch die
|
||||
Signaturprüfung geschützt. Ohne gültige Signatur wird nichts
|
||||
verarbeitet — sonst könnte jeder eine Zahlung als bezahlt melden.
|
||||
--------------------------------------------------------------------- */
|
||||
export async function paypalWebhook(req, res) {
|
||||
const roh = req.rawBody || JSON.stringify(req.body || {});
|
||||
let ereignis = req.body || {};
|
||||
|
||||
let echt = false;
|
||||
try {
|
||||
echt = await paypal.webhookEchtheitPruefen(req.headers, roh);
|
||||
} catch (e) {
|
||||
echt = false;
|
||||
}
|
||||
if (!echt) {
|
||||
verlauf("paypal", "webhook", "abgewiesen", "paypal", { typ: ereignis.event_type || null });
|
||||
return json(res, { ok: false }, 400);
|
||||
}
|
||||
|
||||
const id = paypal.ereignisId(ereignis);
|
||||
/* Doppelte Zustellung ist bei PayPal normal — dieselbe Meldung kommt
|
||||
bei Zweifeln erneut. Ohne diese Merkliste würde eine Zahlung
|
||||
mehrfach verbucht. */
|
||||
const schonDa = db.prepare(`SELECT 1 FROM wd_paypal_ereignisse WHERE event_id = ?`).get(id);
|
||||
if (schonDa) return json(res, { ok: true, doppelt: true });
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO wd_paypal_ereignisse (event_id, typ, zahlung_id, signatur_ok, verarbeitet, rohdaten, empfangen_am)
|
||||
VALUES (?,?,?,1,0,?,?)`
|
||||
).run(
|
||||
id, ereignis.event_type || "?",
|
||||
(ereignis.resource || {}).custom_id || null,
|
||||
JSON.stringify(ereignis).slice(0, 20000), jetzt()
|
||||
);
|
||||
|
||||
const typ = ereignis.event_type;
|
||||
const kern = ereignis.resource || {};
|
||||
|
||||
if (typ === "PAYMENT.CAPTURE.COMPLETED") {
|
||||
const zahlungId = kern.custom_id || null;
|
||||
if (zahlungId) {
|
||||
const aufschl = kern.seller_receivable_breakdown || {};
|
||||
zahlungAlsBezahltVermerken(zahlungId, {
|
||||
captureId: kern.id || null,
|
||||
nettoCent: aufschl.net_amount ? paypal.paypalZuCent(aufschl.net_amount.value) : null,
|
||||
gebuehrCent: aufschl.paypal_fee ? paypal.paypalZuCent(aufschl.paypal_fee.value) : null,
|
||||
}, "paypal:webhook");
|
||||
}
|
||||
} else if (typ === "PAYMENT.CAPTURE.DENIED" || typ === "PAYMENT.CAPTURE.REFUNDED") {
|
||||
const zahlungId = kern.custom_id || null;
|
||||
if (zahlungId) {
|
||||
db.prepare(`UPDATE wd_zahlungen SET status = ?, aktualisiert_am = ? WHERE id = ?`)
|
||||
.run(typ.endsWith("REFUNDED") ? "erstattet" : "fehlge", jetzt(), zahlungId);
|
||||
verlauf("zahlung", zahlungId, typ.toLowerCase(), "paypal:webhook", null);
|
||||
}
|
||||
}
|
||||
|
||||
/* Erst NACH der Auswertung als verarbeitet markieren. Bricht der
|
||||
Server dazwischen ab, steht die Meldung als empfangen-aber-offen in
|
||||
der Tabelle und faellt beim Nachsehen auf -- statt spurlos als
|
||||
"schon behandelt" zu gelten. */
|
||||
db.prepare(`UPDATE wd_paypal_ereignisse SET verarbeitet = 1 WHERE event_id = ?`).run(id);
|
||||
return json(res, { ok: true });
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
VERWALTUNG: Zahlung anlegen
|
||||
POST /webdesign/admin/zahlungen
|
||||
--------------------------------------------------------------------- */
|
||||
export async function zahlungAnlegen(req, res) {
|
||||
const p = await darf(req, "WD_ZAHLUNGEN_VIEW");
|
||||
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
||||
|
||||
const projektId = text(req.body?.projektId, 60);
|
||||
const projekt = db
|
||||
.prepare(`SELECT p.*, k.id AS kid FROM wd_projekte p JOIN wd_kunden k ON k.id = p.kunde_id WHERE p.id = ?`)
|
||||
.get(projektId);
|
||||
if (!projekt) return json(res, { ok: false, error: "Projekt nicht gefunden." }, 404);
|
||||
|
||||
const art = ["anzahlung", "rest", "aenderung", "sonstige"].includes(req.body?.art)
|
||||
? req.body.art : "sonstige";
|
||||
|
||||
/* Betrag: bei der Anzahlung aus dem Projektpreis BERECHNEN, sonst
|
||||
eingeben. Ein getippter Anzahlungsbetrag ist eine Fehlerquelle, die
|
||||
erst beim Geldeingang auffällt. */
|
||||
let betragCent = null;
|
||||
if (art === "anzahlung" && projekt.anzahlung_cent) betragCent = projekt.anzahlung_cent;
|
||||
else if (art === "rest" && projekt.preis_cent != null && projekt.anzahlung_cent != null) {
|
||||
betragCent = projekt.preis_cent - projekt.anzahlung_cent;
|
||||
} else {
|
||||
const euro = Number(req.body?.betragEuro);
|
||||
if (Number.isFinite(euro) && euro > 0) betragCent = Math.round(euro * 100);
|
||||
}
|
||||
if (!betragCent || betragCent <= 0) {
|
||||
return json(res, { ok: false, error: "Bitte einen Betrag angeben." }, 400);
|
||||
}
|
||||
|
||||
const id = neueId();
|
||||
const nummer = naechsteNummer("wd_zahlungen", "Z");
|
||||
db.prepare(
|
||||
`INSERT INTO wd_zahlungen
|
||||
(id, nummer, kunde_id, projekt_id, art, zweck_text, betrag_cent, waehrung,
|
||||
status, kundenart, erstellt_am)
|
||||
VALUES (?,?,?,?,?,?,?,?, 'offen', ?, ?)`
|
||||
).run(
|
||||
id, nummer, projekt.kunde_id, projekt.id, art,
|
||||
text(req.body?.zweck, 200) || standardZweck(art, projekt.titel),
|
||||
betragCent, projekt.waehrung || "EUR",
|
||||
req.body?.kundenart === "unternehmen" ? "unternehmen" : "verbraucher",
|
||||
jetzt()
|
||||
);
|
||||
|
||||
verlauf("zahlung", id, "angelegt", "team:" + (p.sitzung?.actor || "?"), { nummer, betragCent });
|
||||
return json(res, { ok: true, id, nummer, betragText: centZuText(betragCent, projekt.waehrung) });
|
||||
}
|
||||
|
||||
function standardZweck(art, titel) {
|
||||
const t = titel || "Projekt";
|
||||
if (art === "anzahlung") return `Anzahlung 30 % — ${t}`;
|
||||
if (art === "rest") return `Restbetrag — ${t}`;
|
||||
if (art === "aenderung") return `Zusatzleistung — ${t}`;
|
||||
return t;
|
||||
}
|
||||
|
||||
/* GET /webdesign/admin/zahlungen — Übersicht samt Einrichtungsstand */
|
||||
export async function zahlungenListe(req, res) {
|
||||
const p = await darf(req, "WD_ZAHLUNGEN_VIEW");
|
||||
if (!p.erlaubt) return json(res, { ok: false, error: "Kein Zugriff." }, p.grund);
|
||||
|
||||
const zeilen = db
|
||||
.prepare(
|
||||
`SELECT z.*, k.name AS kunde_name, pr.titel AS projekt_titel
|
||||
FROM wd_zahlungen z
|
||||
LEFT JOIN wd_kunden k ON k.id = z.kunde_id
|
||||
LEFT JOIN wd_projekte pr ON pr.id = z.projekt_id
|
||||
ORDER BY z.erstellt_am DESC LIMIT 300`
|
||||
)
|
||||
.all()
|
||||
.map((z) => ({ ...z, betragText: centZuText(z.betrag_cent, z.waehrung) }));
|
||||
|
||||
return json(res, {
|
||||
ok: true,
|
||||
zahlungen: zeilen,
|
||||
/* Damit die Verwaltung sagen kann, WAS noch fehlt, statt nur
|
||||
"nicht eingerichtet". */
|
||||
einrichtung: {
|
||||
einmal: paypal.istEingerichtet("einmal"),
|
||||
abo: paypal.istEingerichtet("abo"),
|
||||
fehlt: paypal.fehlendeEinstellungen("abo"),
|
||||
live: paypal.istLive(),
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user