Abo-API: Nachweis-ID nach Appwrite-Regeln, Webhook bei dauerhaften Fehlern, Tests

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Fable 5.1 committed 2026-09-28 10:19:05 +00:00
1 parent 7df0b72019
commit 887e1e54a0
10 files changed
+409 -107

No files matched your search

+19 -6
View File
@@ -5,7 +5,7 @@ import { Router } from 'express'
import { config } from '../config.js'
import { getSessionCustomerId, requireSession } from '../middleware/session.js'
import { getCustomerByEmail, getDocument, updateDocument } from '../services/appwriteAdmin.js'
import { listContractEvents, openWithdrawals, recordContractEvent } from '../services/contractEvents.js'
import { eventIdFor, listContractEvents, openWithdrawals, recordContractEvent } from '../services/contractEvents.js'
import { lateStartAt, withdrawalDeadline } from '../services/deadlines.js'
import { getLedger } from '../services/hourLedger.js'
import { sendAdminNotice, sendCustomerMail } from '../services/mailer.js'
@@ -324,11 +324,12 @@ async function sendPortalCancellationConfirmation(sub, createdUnix) {
* Nachweis des Neuabschlusses als Vertragsereignis. Der Vertrag kommt mit dem Abschluss bei Stripe
* zustande, die Widerrufsfrist zaehlt ab dann (details.contractAt = startedAt, F04); die Zustimmung
* (Fassung, Zeitpunkt, IP, Browser) kommt aus den Abo-Metadaten, die der Spiegel traegt. Feste
* Dokument-ID je Abo: eine Webhook-Wiederholung legt kein zweites Ereignis an (F05).
* Dokument-ID je Abo (eventIdFor, 36 Zeichen): eine Webhook-Wiederholung legt kein zweites
* Ereignis an (F05).
*/
async function recordAbschluss(sub, session) {
return recordContractEvent({
id: `abschluss-${sub.$id}`,
id: eventIdFor('abschluss', sub.$id),
customerId: sub.customerId,
subscriptionId: sub.$id,
type: 'abschluss',
@@ -357,8 +358,20 @@ async function handleStripeEvent(event) {
case 'checkout.session.completed': {
if (obj.mode !== 'subscription' || !obj.subscription) return
const sub = await syncSubscription(obj.subscription)
// Ein Fehler hier (etwa Collection fehlt) gibt 500 -> Stripe wiederholt, das Ereignis wird nachgeholt
if (sub) await recordAbschluss(sub, obj)
// Nachweis: voruebergehend nicht speicherbar (Appwrite nicht erreichbar, 5xx) -> 500, Stripe
// wiederholt und der Nachweis kommt nach. Dauerhaft nicht speicherbar (4xx, etwa Collection
// oder Attribut fehlt) -> Angebot, T02 und K01 laufen trotzdem, T02 sagt es dem Team; eine
// Wiederholung wuerde nichts aendern, der Kunde soll seine Bestaetigung nicht warten lassen.
let proofProblem = false
if (sub) {
try {
await recordAbschluss(sub, obj)
} catch (err) {
if (!err.status || err.status >= 500) throw err
console.error(`[abo] Nachweis des Abschlusses ${obj.subscription} nicht gespeichert (${err.status}):`, err.message)
proofProblem = true
}
}
const offerProblem = await activateOfferFromCheckout(obj).then(
() => false,
(e) => {
@@ -375,7 +388,7 @@ async function handleStripeEvent(event) {
stripe_abo_id: obj.subscription,
angebot_text: obj.metadata?.offerText || '',
},
blocks: { angebot: Boolean(obj.metadata?.offerText), angebot_fehler: offerProblem },
blocks: { angebot: Boolean(obj.metadata?.offerText), angebot_fehler: offerProblem, nachweis_fehlt: proofProblem },
})
await sendContractConfirmation(sub, obj.customer_details?.name).catch((e) =>
console.error('[abo] Vertragsbestaetigung fehlgeschlagen:', e.message)