From b45108c1dee025c7d8c50403e9b30b72e8f39d92 Mon Sep 17 00:00:00 2001 From: knso Date: Wed, 30 Sep 2026 17:58:44 +0000 Subject: [PATCH] Funktionen: Stripe-Sitzungs-ID bis 128 Zeichen, keine Rohmeldungen beim Bestellen Live-Fehler: cs_live-Sitzungs-IDs (~66 Zeichen) passten nicht in das Feld portalFeatures.stripeSessionId (64), die Bestellung brach nach dem Anlegen der Stripe-Sitzung ab und der Kunde sah die Appwrite-Meldung. - Setup-Skript: Feld 128 Zeichen, bestehendes Feld wird per ensureStringSize vergroessert - Fehler von Appwrite oder unerwartete Fehler: Log + verstaendlicher Hinweis Co-Authored-By: Claude Opus 5.5 --- scripts/setup-abo-collections.mjs | 22 +++++++++++++++++++++- server/routes/funktionen.js | 17 +++++++++++------ test/funktionenRoutes.test.js | 10 ++++++++++ test/support.test.js | 12 ++++++++++++ 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/scripts/setup-abo-collections.mjs b/scripts/setup-abo-collections.mjs index 3908ade..e4e4f82 100644 --- a/scripts/setup-abo-collections.mjs +++ b/scripts/setup-abo-collections.mjs @@ -63,6 +63,24 @@ async function waitForAttributes(collectionId, keys, maxSec = 60) { const col = (id) => `/databases/${databaseId}/collections/${id}` +/** + * Vergroessert ein bestehendes String-Attribut, falls es kleiner als size ist (ensureCollection + * ueberspringt vorhandene Attribute und prueft ihre Groesse nicht). Verkleinern tut es nie. + */ +async function ensureStringSize(collectionId, key, size) { + const attr = await aw(`${col(collectionId)}/attributes/${key}`) + if (attr.size >= size) { + console.log(`= ${collectionId}.${key} Groesse ${attr.size} (ok)`) + return + } + await aw(`${col(collectionId)}/attributes/string/${key}`, { + method: 'PATCH', + body: { required: Boolean(attr.required), default: attr.default ?? null, size }, + }) + await waitForAttributes(collectionId, [key]) + console.log(`+ ${collectionId}.${key} Groesse ${attr.size} -> ${size}`) +} + async function ensureCollection(id, name, attributes, indexes) { // Keine Dokument-Permissions: Zugriff ausschliesslich ueber den Server-API-Key await ensure(`Collection ${id}`, () => @@ -260,7 +278,7 @@ await ensureCollection( { type: 'string', key: 'state', size: 16 }, { type: 'string', key: 'siteKey', size: 16 }, { type: 'string', key: 'ticketId', size: 64 }, - { type: 'string', key: 'stripeSessionId', size: 64 }, + { type: 'string', key: 'stripeSessionId', size: 128 }, { type: 'string', key: 'stripeInvoiceId', size: 64 }, { type: 'string', key: 'invoiceUrl', size: 500 }, { type: 'datetime', key: 'orderedAt' }, @@ -279,6 +297,8 @@ await ensureCollection( { key: 'idx_session', type: 'key', attributes: ['stripeSessionId'] }, ] ) +// Live-Fehler 30.09.2026: cs_live-Sitzungs-IDs (~66 Zeichen) passten nicht in 64 +await ensureStringSize('portalFeatures', 'stripeSessionId', 128) // News-Pinnwand: Beitraege je Projekt await ensureCollection( diff --git a/server/routes/funktionen.js b/server/routes/funktionen.js index cc65bb7..421a63f 100644 --- a/server/routes/funktionen.js +++ b/server/routes/funktionen.js @@ -8,7 +8,8 @@ import { DISABLED_MESSAGE, assertOwnProject, backfillInvoiceLinks, - getFeatureDoc, listCustomerFeatures, + getFeatureDoc, + listCustomerFeatures, markOrdered, runActivation, } from '../services/siteFeatures.js' @@ -16,9 +17,13 @@ import { const router = Router() const DAY_MS = 24 * 60 * 60 * 1000 -function fail(res, err) { - const status = err.status >= 400 && err.status < 600 ? err.status : 500 - return res.status(status).json({ error: err.message || 'Interner Serverfehler' }) +// Eigene Fehler (httpError) tragen einen Text fuer den Kunden. Fehler von Appwrite (err.type) oder +// unerwartete Fehler werden nur geloggt; der Kunde sieht einen verstaendlichen Hinweis statt der Rohmeldung. +function fail(res, err, fallback) { + const own = err.status >= 400 && err.status < 500 && !err.type + if (own) return res.status(err.status).json({ error: err.message }) + console.error('[funktionen]', err.status || '', err.type || '', err.message) + return res.status(500).json({ error: fallback }) } // Zustaende der Funktionen je Projekt; geschlossene Einrichtungs-Tickets dieses Kunden werden vorher freigeschaltet @@ -34,7 +39,7 @@ router.get('/', requireSession, async (req, res) => { consentText: config.funktionen.consentText, }) } catch (err) { - fail(res, err) + fail(res, err, 'Die Funktionen konnten gerade nicht geladen werden. Bitte lade die Seite gleich noch einmal.') } }) @@ -101,7 +106,7 @@ router.post('/news/checkout', requireSession, async (req, res) => { }) res.json({ url: session.url }) } catch (err) { - fail(res, err) + fail(res, err, 'Die Bestellung konnte gerade nicht gestartet werden. Bitte versuch es später noch einmal oder schreib uns im Chat.') } }) diff --git a/test/funktionenRoutes.test.js b/test/funktionenRoutes.test.js index 8383882..96e7002 100644 --- a/test/funktionenRoutes.test.js +++ b/test/funktionenRoutes.test.js @@ -395,3 +395,13 @@ test('ticket_details_nennen_die_anleitung', () => { const doc = [...aw.docs('workorders').values()].find((t) => String(t.topic).startsWith('News-Pinnwand einbauen')) assert.ok(doc.details.includes('Anleitung: docs/funktionen/news-einbau.md (Repo Webklar-Kundenbereich)')) }) + +// Live-Fehler 30.09.2026: Appwrite lehnte die (zu lange) Sitzungs-ID ab, der Kunde sah die rohe Meldung +test('checkout_datenbankfehler_ohne_rohe_meldung', async () => { + aw.seed('websiteProjects', { $id: 'p-db', customerId: 'c1', projectName: 'Eisdiele' }) + aw.failNext({ collection: 'portalFeatures', method: 'PATCH', status: 400, message: 'Invalid document structure: Attribute "stripeSessionId" has invalid type.' }) + const res = await checkout('p-db') + assert.equal(res.status, 500, JSON.stringify(res.body)) + assert.doesNotMatch(res.body.error, /Invalid document structure|stripeSessionId/) + assert.match(res.body.error, /Bestellung konnte gerade nicht gestartet werden/) +}) diff --git a/test/support.test.js b/test/support.test.js index 02a5282..d0618a6 100644 --- a/test/support.test.js +++ b/test/support.test.js @@ -299,3 +299,15 @@ test('schema_funktionen_news', async () => { assert.ok(news.includes(k), `newsPosts.${k}`) } }) + +// Stripe-Checkout-IDs im Live-Modus sind ~66 Zeichen lang (cs_live_ + 58); 64 war zu knapp +test('schema_stripe_session_id_lang_genug', async () => { + const { readFileSync } = await import('node:fs') + const src = readFileSync(new URL('../scripts/setup-abo-collections.mjs', import.meta.url), 'utf8') + const block = src.slice(src.indexOf("'portalFeatures',")) + const m = block.match(/key: 'stripeSessionId', size: (\d+)/) + assert.ok(m, 'stripeSessionId im portalFeatures-Block') + assert.ok(Number(m[1]) >= 128, `stripeSessionId size ${m[1]} < 128`) + // bestehendes Feld in Produktion wird per Groessen-Update angehoben + assert.match(src, /ensureStringSize\('portalFeatures', 'stripeSessionId', 128\)/) +})