Öffnungszeiten: Ticket nachlegen lehnt eine Behalten-Zahlung ab (Stillstand an feature-admin)
bezahlteSitzung weist Sitzungen mit metadata.zweck behalten mit 409 ab. Sonst ginge die Sitzung über onFeaturePaid in bucheBehalten, das feature-admin:<projekt> nimmt, während die Team-Route sie schon hält: Stillstand und dauerhaft belegter Schlüssel. Test in test/zeitenTeamBehalten.test.js: 409 binnen Sekunden, Sperre frei, folgende Team-Aktion nicht blockiert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
019833d385
commit
811e054ca4
2 files changed
+136
No files matched your search
@@ -784,6 +784,12 @@ async function bezahlteSitzung(doc) {
|
||||
console.error(`[funktionen] Stripe-Sitzung ${doc.stripeSessionId} nicht abrufbar:`, err.message)
|
||||
throw httpError(503, 'Stripe konnte gerade nicht gefragt werden. Bitte später erneut versuchen.')
|
||||
}
|
||||
// Eine Behalten-Zahlung (Oeffnungszeiten live, zeiten/behalten.js) hat kein Einrichtungs-Ticket nachzulegen. Vor allem aber darf sie nie in
|
||||
// onFeaturePaid landen: dessen Behalten-Zweig (bucheBehalten) nimmt feature-admin:<projekt>, und die Team-Route haelt diese Sperre schon
|
||||
// (withLocks ist nicht wiederbetretbar => Stillstand, der den Schluessel dauerhaft blockiert).
|
||||
if (session?.metadata?.zweck === 'behalten') {
|
||||
throw httpError(409, 'Zu dieser Funktion gehört nur eine Behalten-Zahlung, es gibt kein Einrichtungs-Ticket nachzulegen.')
|
||||
}
|
||||
if (session?.payment_status !== 'paid') throw httpError(409, 'Die Zahlung ist noch nicht eingegangen.')
|
||||
return session
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { test, after, beforeEach, afterEach, mock } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { mailSink } from './support/mailSink.js'
|
||||
import { schemaKeys } from './support/schema.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
|
||||
|
||||
// Team-Aktion "Ticket nachlegen" auf Oeffnungszeiten live mit einer Behalten-Zahlung am Dokument (Merge mit TP2, Fix Runde 1): ticketNachlegen
|
||||
// holt die bezahlte Sitzung bei Stripe und fuettert sie dem Webhook-Weg. Eine Behalten-Sitzung (metadata.zweck 'behalten') ginge dort in
|
||||
// bucheBehalten, das `feature-admin:<projekt>` nimmt, waehrend die Team-Route diese Sperre schon haelt (nicht wiederbetretbar => Stillstand,
|
||||
// der den Schluessel dauerhaft blockiert). Sie wird darum mit 409 abgelehnt, ohne etwas zu veraendern.
|
||||
const aw = await startAppwriteMock({
|
||||
schema: {
|
||||
portalFeatures: schemaKeys('portalFeatures'),
|
||||
subscriptions: schemaKeys('subscriptions'),
|
||||
featureConsents: schemaKeys('featureConsents'),
|
||||
oeffnungszeiten: schemaKeys('oeffnungszeiten'),
|
||||
zeitenErinnerungen: schemaKeys('zeitenErinnerungen'),
|
||||
},
|
||||
datetimes: {
|
||||
...ZEITEN_DATETIMES,
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'k24At'],
|
||||
featureConsents: ['acceptedAt'],
|
||||
},
|
||||
defaultLimit: 25,
|
||||
})
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
|
||||
const { startTestApp, post } = await import('./support/testApp.js')
|
||||
const { seedFixtures } = await import('./support/fixtures.js')
|
||||
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { withLocks } = await import('../server/services/keyedLock.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
const { zeitenDeps } = await import('../server/services/zeiten/neubau.js')
|
||||
const { resetZeitenCacheForTests } = await import('../server/services/zeiten/cache.js')
|
||||
const { default: teamRoutes } = await import('../server/routes/team/index.js')
|
||||
|
||||
const app = await startTestApp({ '/api/team': teamRoutes })
|
||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
after(() => setMailTransportForTests(null))
|
||||
seedFixtures(aw)
|
||||
|
||||
const STAFF = { headers: { Authorization: 'Bearer jwt-staff' } }
|
||||
const doc = () => aw.docs('portalFeatures').get('f_p1_zeiten')
|
||||
const tickets = () => [...aw.docs('workorders').values()]
|
||||
const ticketAktion = () => post('/api/team/funktionen/c1/zeiten', { projectId: 'p1', action: 'ticket' }, STAFF)
|
||||
/** Antwortet die Anfrage nicht binnen 4 s, ist sie haengengeblieben (Stillstand statt 409). */
|
||||
const binnen4s = (promise) => Promise.race([promise, new Promise((resolve) => setTimeout(() => resolve('HANG'), 4000))])
|
||||
|
||||
let sink
|
||||
let rebuilds
|
||||
beforeEach(async () => {
|
||||
for (const col of ['portalFeatures', 'oeffnungszeiten', 'zeitenErinnerungen', 'workorders', 'websiteProjects']) {
|
||||
for (const id of [...aw.docs(col).keys()]) await deleteDocument(col, id)
|
||||
}
|
||||
resetZeitenCacheForTests()
|
||||
st.checkoutSessions.clear()
|
||||
aw.seed('customers', { $id: 'c1', appwriteUserId: 'u1', email: 'k@example.invalid', name: 'Kunde Eins', status: 'active', portalAccessEnabled: true })
|
||||
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Projekt p1', subdomain: 'sub-p1', repoFullName: 'JUSN/p1', previewMode: 'auto', previewDetected: 'static' })
|
||||
sink = mailSink()
|
||||
setMailTransportForTests(sink)
|
||||
rebuilds = []
|
||||
zeitenDeps.rebuild = async (p) => {
|
||||
rebuilds.push(p.$id)
|
||||
return { queued: true }
|
||||
}
|
||||
mock.method(console, 'error', () => {})
|
||||
mock.method(console, 'warn', () => {})
|
||||
mock.method(console, 'log', () => {})
|
||||
})
|
||||
afterEach(() => mock.restoreAll())
|
||||
|
||||
/** Zustand, den bucheBehalten hinterlaesst: setup ohne Ticket, stripeSessionId = bezahlte Behalten-Sitzung, unlockedBy kauf. */
|
||||
function behaltenDokument(sitzung = { payment_status: 'paid' }) {
|
||||
aw.seed('portalFeatures', {
|
||||
$id: 'f_p1_zeiten',
|
||||
customerId: 'c1',
|
||||
projectId: 'p1',
|
||||
featureKey: 'zeiten',
|
||||
state: 'setup',
|
||||
enabled: false,
|
||||
siteKey: 'abcdefghijklmnop',
|
||||
unlockedBy: 'kauf',
|
||||
unlockedByPurchase: true,
|
||||
purchaseStatus: 'paid',
|
||||
stripeSessionId: 'cs_beh',
|
||||
ticketId: '',
|
||||
einbau: 'angestossen',
|
||||
})
|
||||
st.checkoutSessions.set('cs_beh', {
|
||||
id: 'cs_beh',
|
||||
object: 'checkout.session',
|
||||
mode: 'payment',
|
||||
status: 'complete',
|
||||
invoice: null,
|
||||
metadata: { customerId: 'c1', projectId: 'p1', featureKey: 'zeiten', zweck: 'behalten' },
|
||||
...sitzung,
|
||||
})
|
||||
}
|
||||
|
||||
test('ticket auf Zeiten mit bezahlter Behalten-Sitzung: sofort 409, nichts verändert, die Sperre feature-admin bleibt frei', async () => {
|
||||
behaltenDokument()
|
||||
const vorher = { ...doc() }
|
||||
const res = await binnen4s(ticketAktion())
|
||||
assert.notEqual(res, 'HANG', 'die Anfrage darf nicht an feature-admin haengenbleiben')
|
||||
assert.equal(res.status, 409, JSON.stringify(res.body))
|
||||
assert.match(res.body.error, /Behalten-Zahlung/)
|
||||
assert.deepEqual(doc(), vorher, 'Dokument unveraendert')
|
||||
assert.equal(tickets().length, 0, 'kein Ticket')
|
||||
assert.deepEqual(rebuilds, [])
|
||||
assert.equal(sink.sent.length, 0, 'keine Mail')
|
||||
|
||||
// die Sperre ist frei: ein Probelauf und danach eine weitere Team-Aktion am selben Projekt kommen sofort durch
|
||||
assert.equal(await binnen4s(withLocks(['feature-admin:p1'], async () => 'frei')), 'frei')
|
||||
const aus = await binnen4s(post('/api/team/funktionen/c1/zeiten', { projectId: 'p1', action: 'disable' }, STAFF))
|
||||
assert.notEqual(aus, 'HANG', 'die folgende Team-Aktion darf nicht blockiert sein')
|
||||
assert.equal(aus.status, 200, JSON.stringify(aus.body))
|
||||
assert.equal(doc().state, 'disabled')
|
||||
})
|
||||
|
||||
test('ticket auf Zeiten mit noch nicht bezahlter Behalten-Sitzung: ebenfalls 409 und kein Stillstand', async () => {
|
||||
behaltenDokument({ payment_status: 'unpaid', status: 'open' })
|
||||
const res = await binnen4s(ticketAktion())
|
||||
assert.notEqual(res, 'HANG')
|
||||
assert.equal(res.status, 409, JSON.stringify(res.body))
|
||||
assert.equal(await binnen4s(withLocks(['feature-admin:p1'], async () => 'frei')), 'frei')
|
||||
})
|
||||
Reference in new issue
Block a user