Shop D2: Kundenrouten Stripe-Konto (einrichten, Link, aktualisieren) mit Sperre gegen Doppelklick
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
98d1e6613d
commit
38bd56fe8b
2 files changed
+319
-2
No files matched your search
+74
-2
@@ -8,6 +8,7 @@ import { isKundenansicht, logIfKundenansicht } from '../services/kundenansicht.j
|
||||
import { createWriteLimiter } from '../services/writeLimiter.js'
|
||||
import { anfrageAnsicht, ensureShopConfig, leseShopConfig, stelleAnfrage, zieheZurueck } from '../services/shop/anfrage.js'
|
||||
import { shopFuerKunde } from '../services/shop/schalter.js'
|
||||
import { kontoAnlegen, kontoStatus, onboardingLink, statusAusKonto } from '../services/shop/connect.js'
|
||||
import { uploadSlot } from './chat.js'
|
||||
import { istVollstaendig, speichereEinstellungen } from '../services/shop/einstellungen.js'
|
||||
import { bereitschaft, istGeoeffnet, ladeShop, leereCache } from '../services/shop/kontext.js'
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
|
||||
/**
|
||||
* Kunden-Schnittstelle des Webshops: /api/shop (Spezifikation 2026-10-05-webshop-design.md). Stand: Anfrage (B1) und
|
||||
* Verwaltung (C7): Status, Einstellungen, Artikel, Rechtstexte, Oeffnen, Bilder. Stripe (D2) und Bestellungen (E) kommen dazu.
|
||||
* Verwaltung (C7): Status, Einstellungen, Artikel, Rechtstexte, Oeffnen, Bilder. Stripe-Konto (D2) und Bestellungen (E) kommen dazu.
|
||||
*
|
||||
* Jede Route: Session, projectId (Query oder Body), eigenes Projekt. Die Verwaltungs-Routen laufen ueber mitShop: Schalter
|
||||
* (409 nicht verfuegbar), Funktion im Zustand setup|active (lesend auch disabled), shopConfig vorhanden (setup|active legt es an).
|
||||
@@ -50,11 +51,14 @@ const MELDUNG = {
|
||||
zuVieleBilder: 'Zu viele Bilder in kurzer Zeit. Bitte versuch es später wieder.',
|
||||
bildFehlt: 'Bild nicht gefunden.',
|
||||
eingabe: 'Die Eingabe fehlt oder ist unlesbar.',
|
||||
stripeKundenansicht: 'In der Kundenansicht kann das Stripe-Konto nicht eingerichtet werden.',
|
||||
stripeFern: 'Stripe ist gerade nicht erreichbar. Bitte versuch es gleich noch einmal.',
|
||||
keinKonto: 'Es ist noch kein Stripe-Konto angelegt.',
|
||||
}
|
||||
|
||||
/** Fehler des Kunden (4xx/503 ohne Appwrite-Typ) wie sie sind, alles andere 500 ohne Rohtext. */
|
||||
function fail(res, err) {
|
||||
const own = ((err?.status >= 400 && err.status < 500) || err?.status === 503) && !err.type
|
||||
const own = ((err?.status >= 400 && err.status < 500) || err?.status === 502 || err?.status === 503) && !err.type
|
||||
if (!own) {
|
||||
console.error(`[shop] ${err?.name ?? 'Fehler'} ${err?.status ?? ''} ${String(err?.message ?? '').slice(0, 200)}`.trim())
|
||||
return res.status(500).json({ error: 'Interner Serverfehler' })
|
||||
@@ -449,6 +453,74 @@ router.post('/oeffnen', requireSession, nurKunde, limitWrites, route(async (req,
|
||||
res.json({ schalter: req.body.offen, geoeffnet: await istGeoeffnet({ doc, shop: neu, project: null }, { produkte }), bereitschaft: await bereitschaft({ doc, shop: neu, project: null }, { produkte }) })
|
||||
}))
|
||||
|
||||
// ---- Stripe-Konto (D2)
|
||||
|
||||
/** Nur der Kunde selbst richtet sein Stripe-Konto ein (weder Kundenansicht noch "Ansehen als"). */
|
||||
const nurKundeStripe = (req, res, next) => {
|
||||
if (isKundenansicht(req.session) || req.session?.viewAs) return res.status(403).json({ error: MELDUNG.stripeKundenansicht })
|
||||
return next()
|
||||
}
|
||||
|
||||
/** Stripe-Aufruf: jeder Fehler von Stripe wird zu 502 mit festem Text (Stripe-Texte verlassen den Server nie, geloggt wird nur Typ und Code). */
|
||||
async function ueberStripe(fn) {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (err) {
|
||||
if (err?.status >= 400 && err.status < 500 && !err.type) throw err
|
||||
console.error(`[shop] Stripe ${String(err?.type ?? err?.name ?? 'Fehler').slice(0, 60)} ${String(err?.code ?? '').slice(0, 60)}`.trim())
|
||||
throw stop(502, MELDUNG.stripeFern)
|
||||
}
|
||||
}
|
||||
|
||||
const speichereStripe = async (shop, felder) => {
|
||||
await updateDocument(config.collections.shopConfig, shop.id, felder)
|
||||
leereCache(shop.siteKey)
|
||||
}
|
||||
|
||||
const stripeRueckUrl = (projectId, was) => `${config.publicBaseUrl}/dashboard.html#webshop?stripe=${was}&projekt=${encodeURIComponent(projectId)}`
|
||||
const onboardingFuer = (accountId, projectId) =>
|
||||
ueberStripe(() => onboardingLink(accountId, { returnUrl: stripeRueckUrl(projectId, 'zurueck'), refreshUrl: stripeRueckUrl(projectId, 'neu') }))
|
||||
|
||||
// Konto anlegen (einmalig, unter Sperre) und Onboarding-Link liefern
|
||||
router.post('/stripe/einrichten', requireSession, nurKundeStripe, limitWrites, route(async (req, res) => {
|
||||
const { shop, customerId, projectId, project } = await mitShop(req, { schreiben: true, zustaende: ['setup', 'active'] })
|
||||
const accountId = await withLocks([`shop-stripe:${shop.id}`], async () => {
|
||||
const frisch = await frischerShop(shop.id)
|
||||
if (frisch.stripeAccountId) return frisch.stripeAccountId
|
||||
const customer = await getDocument(config.collections.customers, customerId).catch(() => null)
|
||||
const neu = await ueberStripe(() => kontoAnlegen({ customer: customer || { $id: customerId, email: req.session?.email }, project }))
|
||||
let status
|
||||
try {
|
||||
status = await ueberStripe(() => kontoStatus(neu))
|
||||
} catch {
|
||||
status = statusAusKonto({})
|
||||
}
|
||||
await speichereStripe(shop, { stripeAccountId: neu, stripeStatus: JSON.stringify(status) })
|
||||
return neu
|
||||
})
|
||||
res.json({ url: await onboardingFuer(accountId, projectId) })
|
||||
}))
|
||||
|
||||
// Neuer Onboarding-Link (der alte laeuft nach wenigen Minuten ab)
|
||||
router.post('/stripe/link', requireSession, nurKundeStripe, limitWrites, route(async (req, res) => {
|
||||
const { shop, projectId } = await mitShop(req, { schreiben: true, zustaende: ['setup', 'active'] })
|
||||
if (!shop.stripeAccountId) throw stop(409, MELDUNG.keinKonto, 'kein_konto')
|
||||
res.json({ url: await onboardingFuer(shop.stripeAccountId, projectId) })
|
||||
}))
|
||||
|
||||
// Status bei Stripe neu lesen und speichern
|
||||
router.post('/stripe/aktualisieren', requireSession, nurKundeStripe, limitWrites, route(async (req, res) => {
|
||||
const { shop } = await mitShop(req, { schreiben: true, zustaende: ['setup', 'active'] })
|
||||
if (!shop.stripeAccountId) throw stop(409, MELDUNG.keinKonto, 'kein_konto')
|
||||
const status = await ueberStripe(() => kontoStatus(shop.stripeAccountId))
|
||||
const alt = shop.stripeStatus
|
||||
const zahlungsarten = Array.isArray(shop.einstellungen?.zahlungsarten) ? shop.einstellungen.zahlungsarten : ['card']
|
||||
const behalteHinweis = typeof alt?.hinweis === 'string' && alt.hinweis !== '' && status.charges_enabled === true && zahlungsarten.some((z) => z === 'klarna' || z === 'sepa')
|
||||
const neu = behalteHinweis ? { ...status, hinweis: alt.hinweis } : status
|
||||
await speichereStripe(shop, { stripeStatus: JSON.stringify(neu) })
|
||||
res.json({ stripe: stripeAnsicht({ ...shop, stripeStatus: neu }) })
|
||||
}))
|
||||
|
||||
// ---- Bilder
|
||||
|
||||
// Bild als rohe Bytes (application/octet-stream), hoechstens 8 MB; die Pruefung des Shops laeuft vor dem Einlesen
|
||||
|
||||
Reference in new issue
Block a user