Shop Review B I-1: Eingangsmail G15 bei SEPA, G09 nimmt die Bestellung ausdruecklich an

G15 "Wir haben Ihre Bestellung erhalten" geht einmal raus, sobald Stripe die Sitzung
abgeschlossen, die Zahlung aber noch nicht gemeldet hat (meldeEingang, Marker mailsEingangAt,
aus dem Webhook und aus pruefeOffene). G09 sagt ausdruecklich, dass der Kaufvertrag mit der
Mail geschlossen ist, trennt Bestell- und Zahlungsdatum und nennt die Telefonnummer des
Anbieters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 09:44:48 +00:00
1 parent 1b7aa9ebe3
commit 77fb8c8f44
11 files changed
+238 -27

No files matched your search

+45 -1
View File
@@ -604,6 +604,46 @@ export async function schliesseAb({ accountId, session, now = new Date() }) {
})
}
/**
* Eingang der Bestellung (G15): Stripe meldet die Sitzung abgeschlossen, die Zahlung steht aber noch aus (SEPA-Lastschrift,
* payment_status unpaid / no_payment_required). Die Bestellung bleibt offen, der Kaeufer bekommt einmal eine Eingangsmail ohne
* Vertragsschluss (der Vertrag kommt mit G09 nach Zahlungseingang zustande). Marker `mailsEingangAt` im Bestell-JSON, Versuche in
* mailsVersuche.eingang (hoechstens MAIL_VERSUCHE_MAX). Aufrufer: Webhook (checkout.session.completed) und pruefeOffene.
* Nie ein Wurf wegen der Mail; falsches Konto, fremde Sitzung oder eine nicht mehr offene Bestellung bleiben wirkungslos.
* @param {{ accountId: string, session: object, now?: Date }} p
* @returns {Promise<{ gesendet: boolean }>}
*/
export async function meldeEingang({ accountId, session, now = new Date() }) {
const orderId = session?.metadata?.orderId
if (!istObjekt(session) || session.metadata?.wkShop !== '1' || typeof orderId !== 'string' || !orderId) return { gesendet: false }
if (session.payment_status !== 'unpaid' && session.payment_status !== 'no_payment_required') return { gesendet: false }
const an = kurz(session.customer_details?.email, 320)
if (!an) return { gesendet: false }
const jetzt = new Date(now)
return withLocks([`shop-order:${orderId}`], async () => {
const order = await bestellungLesen(orderId)
if (!order || order.status !== 'offen') return { gesendet: false }
const shop = await shopDokument(order.shopId)
if (!shop || typeof accountId !== 'string' || shop.stripeAccountId !== accountId) return { gesendet: false }
const daten = order.daten
const versuche = istObjekt(daten.mailsVersuche) ? daten.mailsVersuche : {}
if (daten.mailsEingangAt || (versuche.eingang ?? 0) >= MAIL_VERSUCHE_MAX) return { gesendet: false }
let ok = false
try {
leereCache(shop.siteKey)
const ctx = await shopPerSiteKey(shop.siteKey, { now: jetzt })
if (!ctx) throw new Error('Shop-Kontext nicht gefunden')
ok = (await anKaeufer('G15', ctx, order, { an }, jetzt)).ok
} catch (err) {
console.warn(`[shop] G15 zur Bestellung ${orderId.slice(0, 36)} nicht eingereiht: ${err?.name}: ${String(err?.message).slice(0, 120)}`)
}
const neu = ok ? { ...daten, mailsEingangAt: jetzt.toISOString() } : { ...daten, mailsVersuche: { ...versuche, eingang: (versuche.eingang ?? 0) + 1 } }
await updateDocument(col(), orderId, { bestellung: JSON.stringify(neu) })
return { gesendet: ok }
})
}
/**
* Verwirft eine offene Bestellung (Status storniert, Bestand wird frei). Nur aus offen; sonst keine Wirkung.
* @param {string} orderId
@@ -692,7 +732,11 @@ export async function pruefeOffene(now = new Date()) {
if (session.payment_status === 'paid') {
const erg = await schliesseAb({ accountId: shop.stripeAccountId, session, now })
if (erg.neu) zaehler.bezahlt += 1
} else zaehler.wartend += 1 // SEPA unterwegs: async_payment_succeeded/failed entscheidet
} else {
// SEPA unterwegs: async_payment_succeeded/failed entscheidet; bis dahin hat der Kaeufer die Eingangsmail (G15) zu bekommen
zaehler.wartend += 1
await meldeEingang({ accountId: shop.stripeAccountId, session, now })
}
continue
}
if (session?.status === 'open') {