Funktionen: Freischalten nach Abnahme und Ticket nachlegen (Team)
Neue Team-Aktionen abnahme (Freischalten ohne Ticket-Schliessung, K16 einmal, unter feature-activate:<id> mit Haken freischaltHindernis) und ticket (Ticket nachlegen: Zahlung bei Stripe pruefen und Webhook-Weg, geloeschtes Ticket ersetzen, vorhandenes unberuehrt). Ticketanlage aus markPaid in legeTicketAn herausgeloest. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8fe14c2fe6
commit
f830698dc3
3 files changed
+699
-12
No files matched your search
@@ -21,9 +21,17 @@ const sf = await import('../server/services/siteFeatures.js')
|
||||
const { loeschTag } = await import('../server/services/termine/frist.js')
|
||||
const { deleteCustomerWithPortalAccess } = await import('../server/services/customerAdmin.js')
|
||||
const { aboMeldung } = await import('../server/services/featureOrder.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
const { mailSink } = await import('./support/mailSink.js')
|
||||
|
||||
const app = await startTestApp({ '/api/funktionen': funktionenRoutes, '/api/team': teamRoutes })
|
||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
// Mails (K16 der Abnahme, K15/T16 des nachgelegten Tickets) landen in einer Senke
|
||||
const mails = mailSink()
|
||||
setMailTransportForTests(mails)
|
||||
after(() => {
|
||||
setMailTransportForTests(null)
|
||||
return Promise.all([app.close(), aw.close(), st.close()])
|
||||
})
|
||||
seedFixtures(aw)
|
||||
aw.seed('employees', { $id: 'e_staff2', userId: 'u_staff2', displayName: 'Andrej', email: 'andrej@webklar.com' })
|
||||
aw.seed('websiteProjects', { $id: 'pt1', customerId: 'c1', projectName: 'Salon' })
|
||||
@@ -546,3 +554,488 @@ test('adminActivate (Dienst): bezahlter Terminplaner ohne Abo => Fehler 409, bei
|
||||
const pilot = legeKunde({ over: { unlockedByPurchase: false } })
|
||||
assert.equal((await sf.adminActivate(pilot.customerId, pilot.projectId, 'termine', { by: 'test' })).state, 'active')
|
||||
})
|
||||
|
||||
// ------------------------------------------ Team-Aktionen Freischalten (Abnahme) und Ticket nachlegen (D7) ---
|
||||
// abnahme: Terminplaner in setup freischalten, ohne dass das Einrichtungs-Ticket geschlossen wird (Ticket und Kauf bleiben, K16 einmal).
|
||||
// ticket: ein fehlendes Einrichtungs-Ticket nachlegen - (a) Ticket nie angelegt: Zahlung bei Stripe pruefen und den Weg des Webhooks gehen
|
||||
// (Ticket, K15, T16 genau einmal), (b) Ticket geloescht: neues Ticket, keine Mails, (c) Ticket da: nichts.
|
||||
const K16_BETREFF = { termine: 'Dein Terminplaner ist bereit', news: 'Deine News-Pinnwand ist bereit' }
|
||||
const K15_BETREFF = 'Deine Bestellung: Terminplaner'
|
||||
const T16_BETREFF = 'Neue Funktion bestellt: Terminplaner'
|
||||
const NUR_SETUP = 'Freischalten (Abnahme) gibt es nur für Funktionen in Einrichtung.'
|
||||
const NUR_SETUP_TICKET = 'Ein Ticket nachlegen gibt es nur für Funktionen in Einrichtung.'
|
||||
const OHNE_ABO = 'Der Kunde hat kein laufendes Wartungs-Abo. Freischalten ist erst mit laufendem Abo möglich.'
|
||||
const OHNE_EINSTELLUNG = 'Es sind noch keine Einstellungen gespeichert.'
|
||||
const ABO_UNKLAR = 'Das Abo konnte gerade nicht geprüft werden.'
|
||||
const NICHT_BEZAHLT = 'Die Zahlung ist noch nicht eingegangen.'
|
||||
const KEINE_AKTION = 'Diese Aktion gibt es für diese Funktion nicht.'
|
||||
const mailsAn = (email, subject) => mails.sent.filter((m) => m.subject === subject && m.to.some((t) => t.address === email))
|
||||
const seit = (n) => mails.sent.slice(n)
|
||||
|
||||
let d7Nr = 0
|
||||
/**
|
||||
* Kunde mit Projekt, Funktionsdokument (Zustand state), Wartungs-Abo und gespeicherter Einstellung.
|
||||
* ticket: 'Open' | 'Closed' (Ticket da) | 'geloescht' (ticketId gesetzt, Ticket fehlt) | null (nie angelegt).
|
||||
* session: null | 'paid' | 'unpaid' legt die Stripe-Sitzung samt Rechnung im Nachbau an und traegt sie am Dokument ein.
|
||||
*/
|
||||
function legeEinrichtung({ key = 'termine', abo = 'wartung', einstellung = true, ticket = 'Open', session = null, state = 'setup', over = {} } = {}) {
|
||||
d7Nr += 1
|
||||
const n = d7Nr
|
||||
const customerId = `cd${n}`
|
||||
const projectId = `cdp${n}`
|
||||
const docId = `f_${projectId}_${key}`
|
||||
const ticketId = `t_cd${n}`
|
||||
const sessionId = session ? `cs_cd${n}` : ''
|
||||
const email = `cd${n}@example.invalid`
|
||||
aw.seed('customers', { $id: customerId, name: `Salon ${n}`, email, status: 'active' })
|
||||
aw.seed('websiteProjects', { $id: projectId, customerId, projectName: `Salon ${n}` })
|
||||
if (abo) aw.seed('subscriptions', subscriptionDoc(`sub_cd${n}`, { ...(abo === 'wartung' ? wartungDoc : hostingDoc), customerId }))
|
||||
if (ticket === 'Open' || ticket === 'Closed') {
|
||||
aw.seed('workorders', { $id: ticketId, woid: 7000 + n, status: ticket, topic: `Terminplaner einbauen – Salon ${n}`, details: 'Original-Ticket' })
|
||||
}
|
||||
if (session) {
|
||||
st.invoices.set(`in_cd${n}`, { id: `in_cd${n}`, object: 'invoice', hosted_invoice_url: `https://invoice.stripe.test/in_cd${n}` })
|
||||
st.checkoutSessions.set(sessionId, {
|
||||
id: sessionId, object: 'checkout.session', mode: 'payment', status: 'complete', payment_status: session, invoice: `in_cd${n}`,
|
||||
metadata: { customerId, projectId, featureKey: key },
|
||||
})
|
||||
}
|
||||
aw.seed('portalFeatures', {
|
||||
$id: docId, customerId, projectId, featureKey: key, siteKey: 'abcdefghijklmnop', state, enabled: state === 'active',
|
||||
unlockedByPurchase: true, purchaseStatus: 'paid', orderedAt: '2026-10-01T10:00:00.000Z', stripeSessionId: sessionId,
|
||||
...(ticket ? { ticketId } : {}), adminNote: '', ...over,
|
||||
})
|
||||
if (einstellung) {
|
||||
aw.seed('termineConfig', {
|
||||
$id: docId, customerId, projectId, einstellung: JSON.stringify({ betrieb: { name: `Salon ${n}` } }), version: 1, updatedAt: '2026-10-03T08:00:00.000Z', updatedBy: 'test',
|
||||
})
|
||||
}
|
||||
return { customerId, projectId, docId, ticketId, sessionId, email }
|
||||
}
|
||||
const abnahme = (k, key = 'termine', opts) => team(k.customerId, key, { projectId: k.projectId, action: 'abnahme' }, opts)
|
||||
const nachlegen = (k, key = 'termine', opts) => team(k.customerId, key, { projectId: k.projectId, action: 'ticket' }, opts)
|
||||
const ticketDoc = (id) => aw.docs('workorders').get(id)
|
||||
const dienst = (k, fn, key = 'termine') => sf[fn]({ customerId: k.customerId, projectId: k.projectId, key, by: 'Andrej', adminNote: `x Andrej: ${fn}` })
|
||||
|
||||
// ----------------------------------------------------------------------------- abnahme ---
|
||||
test('abnahme auf setup mit Einstellung und Abo: active, Ticket (Open) und Kauf bleiben, adminNote, K16 genau einmal', async () => {
|
||||
const k = legeEinrichtung()
|
||||
const res = await abnahme(k)
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.deepEqual(res.body.projects.map((p) => p.projectId), [k.projectId])
|
||||
const doc = docById(k.docId)
|
||||
assert.deepEqual([doc.state, doc.enabled], ['active', true])
|
||||
assert.ok(Math.abs(Date.parse(doc.activatedAt) - Date.now()) < 5000, 'activatedAt gesetzt')
|
||||
assert.equal(featuresOf(res, k.projectId).termine.state, 'active')
|
||||
assert.equal(featuresOf(res, k.projectId).termine.activatedAt, doc.activatedAt)
|
||||
assert.equal(doc.ticketId, k.ticketId, 'ticketId bleibt')
|
||||
assert.equal(ticketDoc(k.ticketId).status, 'Open', 'das Ticket wird nicht geschlossen')
|
||||
assert.equal(doc.unlockedByPurchase, true)
|
||||
assert.equal(doc.purchaseStatus, 'paid')
|
||||
assert.match(doc.adminNote, /^\d{4}-\d\d-\d\dT[\d:.]+Z Andrej: abnahme$/)
|
||||
const k16 = mailsAn(k.email, K16_BETREFF.termine)
|
||||
assert.equal(k16.length, 1, 'K16 genau einmal')
|
||||
assert.match(k16[0].text, /dashboard\.html#termine/)
|
||||
})
|
||||
|
||||
test('abnahme: zweiter Klick => 200 ohne zweite Mail und ohne Schreibzugriff; Dienst meldet activated true, dann false', async () => {
|
||||
const k = legeEinrichtung()
|
||||
assert.equal((await abnahme(k)).status, 200)
|
||||
const vorher = { ...docById(k.docId) }
|
||||
const nochmal = await abnahme(k)
|
||||
assert.equal(nochmal.status, 200)
|
||||
assert.equal(featuresOf(nochmal, k.projectId).termine.state, 'active')
|
||||
assert.deepEqual(docById(k.docId), vorher, 'nichts geschrieben: activatedAt und Notiz bleiben')
|
||||
assert.equal(mailsAn(k.email, K16_BETREFF.termine).length, 1)
|
||||
|
||||
const k2 = legeEinrichtung()
|
||||
const erster = await dienst(k2, 'freischaltenNachAbnahme')
|
||||
assert.equal(erster.activated, true)
|
||||
assert.equal(erster.doc.state, 'active')
|
||||
const zweiter = await dienst(k2, 'freischaltenNachAbnahme')
|
||||
assert.equal(zweiter.activated, false)
|
||||
assert.equal(zweiter.doc.state, 'active')
|
||||
assert.equal(mailsAn(k2.email, K16_BETREFF.termine).length, 1)
|
||||
})
|
||||
|
||||
test('abnahme: zwei parallele Klicks => eine Mail, ein Zustandswechsel', async () => {
|
||||
const k = legeEinrichtung()
|
||||
const [a, b] = await Promise.all([abnahme(k), abnahme(k)])
|
||||
assert.deepEqual([a.status, b.status], [200, 200])
|
||||
assert.equal(docById(k.docId).state, 'active')
|
||||
assert.equal(mailsAn(k.email, K16_BETREFF.termine).length, 1)
|
||||
// auch ohne die Sperre der Route: zwei gleichzeitige Dienstaufrufe aktivieren nur einmal
|
||||
const k2 = legeEinrichtung()
|
||||
const [x, y] = await Promise.all([dienst(k2, 'freischaltenNachAbnahme'), dienst(k2, 'freischaltenNachAbnahme')])
|
||||
assert.deepEqual([x.activated, y.activated].sort(), [false, true])
|
||||
assert.equal(mailsAn(k2.email, K16_BETREFF.termine).length, 1)
|
||||
})
|
||||
|
||||
test('abnahme und der Freischalt-Job (runActivation) bei geschlossenem Ticket gleichzeitig => eine Mail insgesamt, in beiden Reihenfolgen', async () => {
|
||||
for (const abnahmeZuerst of [true, false]) {
|
||||
const k = legeEinrichtung({ ticket: 'Closed' })
|
||||
const start = [() => abnahme(k), () => sf.runActivation({ customerId: k.customerId })]
|
||||
if (!abnahmeZuerst) start.reverse()
|
||||
await Promise.all(start.map((f) => f()))
|
||||
assert.equal(docById(k.docId).state, 'active', `abnahmeZuerst ${abnahmeZuerst}`)
|
||||
assert.equal(mailsAn(k.email, K16_BETREFF.termine).length, 1, `abnahmeZuerst ${abnahmeZuerst}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('abnahme: bereits aktive Funktion (Pilot, Kulanz) => 200 ohne Mail und ohne Schreibzugriff', async () => {
|
||||
const k = legeEinrichtung({ state: 'active', ticket: null, over: { unlockedByPurchase: false, purchaseStatus: '', activatedAt: '2026-10-02T10:00:00.000Z' } })
|
||||
const vorher = { ...docById(k.docId) }
|
||||
const res = await abnahme(k)
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.deepEqual(docById(k.docId), vorher)
|
||||
assert.equal(mailsAn(k.email, K16_BETREFF.termine).length, 0)
|
||||
})
|
||||
|
||||
test('abnahme: off, ordered und disabled => 409 mit Text, nichts geschrieben, keine Mail', async () => {
|
||||
const faelle = [
|
||||
legeEinrichtung({ state: 'off', ticket: null, over: { unlockedByPurchase: false, purchaseStatus: '' } }),
|
||||
legeEinrichtung({ state: 'ordered', ticket: null, over: { purchaseStatus: 'ordered', unlockedByPurchase: false } }),
|
||||
legeEinrichtung({ state: 'disabled', over: { disabledReason: 'team', disabledAt: '2026-10-01T10:00:00.000Z' } }),
|
||||
]
|
||||
for (const k of faelle) {
|
||||
const vorher = { ...docById(k.docId) }
|
||||
const res = await abnahme(k)
|
||||
assert.equal(res.status, 409, vorher.state)
|
||||
assert.equal(res.body.error, NUR_SETUP, vorher.state)
|
||||
assert.deepEqual(docById(k.docId), vorher, vorher.state)
|
||||
assert.equal(mailsAn(k.email, K16_BETREFF.termine).length, 0, vorher.state)
|
||||
}
|
||||
})
|
||||
|
||||
test('abnahme ohne laufendes Wartungs-Abo (kein Abo, nur Hosting, beendet) => 409, setup bleibt, keine Mail', async () => {
|
||||
const faelle = [
|
||||
legeEinrichtung({ abo: null }),
|
||||
legeEinrichtung({ abo: 'hosting' }),
|
||||
legeEinrichtung({ abo: 'wartung' }),
|
||||
]
|
||||
// beendetes Wartungs-Abo: das dritte Abo auf canceled stellen
|
||||
aw.seed('subscriptions', subscriptionDoc(`sub_cd${d7Nr}`, { ...wartungDoc, customerId: faelle[2].customerId, status: 'canceled', endedAt: '2026-10-01T10:00:00.000Z' }))
|
||||
for (const k of faelle) {
|
||||
const vorher = { ...docById(k.docId) }
|
||||
const res = await abnahme(k)
|
||||
assert.equal(res.status, 409, k.customerId)
|
||||
assert.equal(res.body.error, OHNE_ABO)
|
||||
assert.deepEqual(docById(k.docId), vorher, 'setup bleibt, nichts geschrieben')
|
||||
assert.equal(mailsAn(k.email, K16_BETREFF.termine).length, 0)
|
||||
}
|
||||
})
|
||||
|
||||
test('abnahme: eine vorgemerkte Kuendigung und past_due laufen noch => 200', async () => {
|
||||
for (const aboOver of [{ cancelAtPeriodEnd: true }, { status: 'past_due' }]) {
|
||||
const k = legeEinrichtung()
|
||||
aw.seed('subscriptions', subscriptionDoc(`sub_cd${d7Nr}`, { ...wartungDoc, customerId: k.customerId, ...aboOver }))
|
||||
assert.equal((await abnahme(k)).status, 200, JSON.stringify(aboOver))
|
||||
assert.equal(docById(k.docId).state, 'active')
|
||||
}
|
||||
})
|
||||
|
||||
test('abnahme ohne gespeicherte Einstellung => 409, setup bleibt', async () => {
|
||||
const k = legeEinrichtung({ einstellung: false })
|
||||
const vorher = { ...docById(k.docId) }
|
||||
const res = await abnahme(k)
|
||||
assert.equal(res.status, 409)
|
||||
assert.equal(res.body.error, OHNE_EINSTELLUNG)
|
||||
assert.deepEqual(docById(k.docId), vorher)
|
||||
assert.equal(mailsAn(k.email, K16_BETREFF.termine).length, 0)
|
||||
})
|
||||
|
||||
test('abnahme: Abo nicht lesbar => 503 mit Text (geloggt), setup bleibt; danach geht es', async (t) => {
|
||||
const log = t.mock.method(console, 'error', () => {})
|
||||
const k = legeEinrichtung()
|
||||
const vorher = { ...docById(k.docId) }
|
||||
aw.failNext({ collection: 'subscriptions', method: 'GET', status: 500, message: 'Nachbau: Abo kaputt' })
|
||||
const res = await abnahme(k)
|
||||
assert.equal(res.status, 503)
|
||||
assert.equal(res.body.error, ABO_UNKLAR)
|
||||
assert.deepEqual(docById(k.docId), vorher, 'nie freischalten, wenn etwas nicht lesbar war')
|
||||
assert.equal(mailsAn(k.email, K16_BETREFF.termine).length, 0)
|
||||
assert.ok(log.mock.calls.some((c) => /nicht prüfbar/.test(c.arguments.join(' '))), 'der Fehler steht im Log')
|
||||
assert.equal((await abnahme(k)).status, 200)
|
||||
assert.equal(docById(k.docId).state, 'active')
|
||||
})
|
||||
|
||||
test('abnahme: Projekt eines anderen Kunden => 403, Kundensitzung statt JWT => 401/403, ohne Dokument => 409 und nichts angelegt', async () => {
|
||||
const k = legeEinrichtung()
|
||||
const vorher = { ...docById(k.docId) }
|
||||
assert.equal((await team('c1', 'termine', { projectId: 'pt3', action: 'abnahme' })).status, 403, 'pt3 gehoert c2')
|
||||
assert.equal((await team(k.customerId, 'termine', { projectId: 'pt3', action: 'abnahme' })).status, 403)
|
||||
assert.equal((await team('c1', 'termine', { projectId: 'gibtsnicht', action: 'abnahme' })).status, 403)
|
||||
const alsKunde = await abnahme(k, 'termine', { cookie: customerCookie(k.customerId) })
|
||||
assert.ok(alsKunde.status === 401 || alsKunde.status === 403, `Status ${alsKunde.status}`)
|
||||
assert.deepEqual(docById(k.docId), vorher)
|
||||
const docs = aw.docs('portalFeatures').size
|
||||
aw.seed('websiteProjects', { $id: 'pt_d7_leer', customerId: 'c1', projectName: 'Ohne Dokument D7' })
|
||||
const ohneDoc = await team('c1', 'termine', { projectId: 'pt_d7_leer', action: 'abnahme' })
|
||||
assert.equal(ohneDoc.status, 409)
|
||||
assert.equal(ohneDoc.body.error, 'Für dieses Projekt gibt es noch keinen Terminplaner')
|
||||
assert.equal(aw.docs('portalFeatures').size, docs, 'kein Dokument entstanden')
|
||||
assert.equal(mails.sent.filter((m) => m.subject === K16_BETREFF.termine && m.to.some((to) => to.address === 'k@example.invalid')).length, 0)
|
||||
})
|
||||
|
||||
test('abnahme bei News funktioniert ohne Abo-Pruefung (es gibt keinen Haken freischaltbar): active, K16 News, Abo nicht gelesen', async () => {
|
||||
const k = legeEinrichtung({ key: 'news', abo: null, einstellung: false })
|
||||
const abfragen = aw.listCalls('subscriptions').length
|
||||
const res = await abnahme(k, 'news')
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.equal(featuresOf(res, k.projectId).news.state, 'active')
|
||||
assert.equal(docById(k.docId).state, 'active')
|
||||
assert.equal(docById(k.docId).ticketId, k.ticketId)
|
||||
assert.equal(aw.listCalls('subscriptions').length, abfragen, 'keine Abo-Abfrage')
|
||||
assert.equal(mailsAn(k.email, K16_BETREFF.news).length, 1)
|
||||
})
|
||||
|
||||
test('abnahme und ticket bei analyse und unbekannten Schluesseln => 404, nichts angelegt', async () => {
|
||||
const k = legeEinrichtung()
|
||||
const docs = aw.docs('portalFeatures').size
|
||||
for (const action of ['abnahme', 'ticket']) {
|
||||
for (const key of ['analyse', 'kalender']) {
|
||||
const res = await team(k.customerId, key, { projectId: k.projectId, action })
|
||||
assert.equal(res.status, 404, `${key} ${action}`)
|
||||
}
|
||||
}
|
||||
assert.equal(aw.docs('portalFeatures').size, docs)
|
||||
})
|
||||
|
||||
test('abnahme und ticket brauchen ticketFreischaltung im Register, sonst 409 ohne Schreibzugriff', async () => {
|
||||
const k = legeEinrichtung({ key: 'news' })
|
||||
const vorher = { ...docById(k.docId) }
|
||||
const alt = config.features.news.ticketFreischaltung
|
||||
config.features.news.ticketFreischaltung = false
|
||||
try {
|
||||
for (const action of ['abnahme', 'ticket']) {
|
||||
const res = await team(k.customerId, 'news', { projectId: k.projectId, action })
|
||||
assert.equal(res.status, 409, action)
|
||||
assert.equal(res.body.error, KEINE_AKTION, action)
|
||||
}
|
||||
} finally {
|
||||
config.features.news.ticketFreischaltung = alt
|
||||
}
|
||||
assert.deepEqual(docById(k.docId), vorher)
|
||||
assert.equal(mailsAn(k.email, K16_BETREFF.news).length, 0)
|
||||
// activate und disable gehoeren nicht dazu und gehen weiter
|
||||
assert.equal((await team(k.customerId, 'news', { projectId: k.projectId, action: 'disable' })).status, 200)
|
||||
})
|
||||
|
||||
test('abnahme: scheitert der Versand von K16, ist die Funktion trotzdem freigeschaltet (Fehler im Log)', async (t) => {
|
||||
const log = t.mock.method(console, 'error', () => {})
|
||||
const k = legeEinrichtung()
|
||||
setMailTransportForTests({ sendMail: async () => { throw new Error('SMTP weg') } })
|
||||
t.after(() => setMailTransportForTests(mails))
|
||||
const res = await abnahme(k)
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.equal(docById(k.docId).state, 'active')
|
||||
assert.ok(log.mock.calls.some((c) => /K16 an Kunde .* fehlgeschlagen/.test(c.arguments.join(' '))))
|
||||
})
|
||||
|
||||
// ----------------------------------------------------------------------------- ticket ---
|
||||
test('ticket (a): setup ohne ticketId, Sitzung bezahlt => Ticket mit Kurzfassung, K15 und T16 je einmal; zweiter Klick und Webhook-Wiederholung aendern nichts', async () => {
|
||||
const k = legeEinrichtung({ ticket: null, session: 'paid' })
|
||||
const tickets = workorders()
|
||||
const mailsVorher = mails.sent.length
|
||||
const res = await nachlegen(k)
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
const doc = docById(k.docId)
|
||||
assert.ok(doc.ticketId, 'ticketId gesetzt')
|
||||
assert.equal(workorders(), tickets + 1)
|
||||
const ticket = ticketDoc(doc.ticketId)
|
||||
assert.match(ticket.topic, /^Terminplaner einbauen – Salon \d+$/)
|
||||
assert.ok(ticket.details.includes('Konfigurationsstand: Version 1'), ticket.details)
|
||||
assert.equal(ticket.status, 'Open')
|
||||
assert.deepEqual([doc.state, doc.unlockedByPurchase], ['setup', true])
|
||||
assert.match(doc.adminNote, /Andrej: ticket/)
|
||||
assert.equal(featuresOf(res, k.projectId).termine.ticketMissing, false)
|
||||
// K15 an den Kunden, T16 ans Team, je einmal
|
||||
assert.equal(mailsAn(k.email, K15_BETREFF).length, 1)
|
||||
const neu = seit(mailsVorher)
|
||||
assert.equal(neu.filter((m) => m.subject === T16_BETREFF).length, 1)
|
||||
assert.match(neu.find((m) => m.subject === T16_BETREFF).text, new RegExp(String(ticket.woid)))
|
||||
assert.equal(neu.length, 2, 'sonst nichts')
|
||||
|
||||
// zweiter Klick: Ticket vorhanden => nichts Neues
|
||||
const nochmal = await nachlegen(k)
|
||||
assert.equal(nochmal.status, 200)
|
||||
assert.equal(workorders(), tickets + 1)
|
||||
assert.equal(docById(k.docId).ticketId, doc.ticketId)
|
||||
assert.equal(seit(mailsVorher).length, 2, 'keine weiteren Mails')
|
||||
|
||||
// ein folgender Webhook mit derselben Sitzung (Stripe-Retry) sendet und legt nichts doppelt an
|
||||
await sf.handleFeatureCheckoutEvent({ type: 'checkout.session.completed', data: { object: st.checkoutSessions.get(k.sessionId) } })
|
||||
assert.equal(workorders(), tickets + 1)
|
||||
assert.equal(seit(mailsVorher).length, 2, 'Webhook-Wiederholung: keine weiteren Mails')
|
||||
assert.equal(docById(k.docId).ticketId, doc.ticketId)
|
||||
})
|
||||
|
||||
test('ticket (a), Dienst: created true beim Anlegen, false danach; zwei gleichzeitige Aufrufe legen ein Ticket und senden K15/T16 einmal', async () => {
|
||||
const k = legeEinrichtung({ ticket: null, session: 'paid' })
|
||||
const tickets = workorders()
|
||||
const mailsVorher = mails.sent.length
|
||||
const [a, b] = await Promise.all([dienst(k, 'ticketNachlegen'), dienst(k, 'ticketNachlegen')])
|
||||
assert.deepEqual([a.created, b.created].sort(), [false, true])
|
||||
assert.equal(a.doc.ticketId, b.doc.ticketId)
|
||||
assert.equal(workorders(), tickets + 1)
|
||||
assert.equal(seit(mailsVorher).length, 2, 'K15 und T16 je einmal')
|
||||
const spaeter = await dienst(k, 'ticketNachlegen')
|
||||
assert.equal(spaeter.created, false)
|
||||
assert.equal(workorders(), tickets + 1)
|
||||
})
|
||||
|
||||
test('ticket (a): zwei parallele Klicks => ein Ticket, K15 und T16 je einmal', async () => {
|
||||
const k = legeEinrichtung({ ticket: null, session: 'paid' })
|
||||
const tickets = workorders()
|
||||
const mailsVorher = mails.sent.length
|
||||
const [a, b] = await Promise.all([nachlegen(k), nachlegen(k)])
|
||||
assert.deepEqual([a.status, b.status], [200, 200])
|
||||
assert.equal(workorders(), tickets + 1)
|
||||
assert.equal(seit(mailsVorher).length, 2)
|
||||
assert.equal(mailsAn(k.email, K15_BETREFF).length, 1)
|
||||
})
|
||||
|
||||
test('ticket (a): Sitzung nicht bezahlt => 409, kein Ticket, keine Mail; Stripe-Ausfall => 503, danach geht es', async (t) => {
|
||||
const log = t.mock.method(console, 'error', () => {})
|
||||
const unbezahlt = legeEinrichtung({ ticket: null, session: 'unpaid' })
|
||||
const tickets = workorders()
|
||||
const mailsVorher = mails.sent.length
|
||||
const vorher = { ...docById(unbezahlt.docId) }
|
||||
const res = await nachlegen(unbezahlt)
|
||||
assert.equal(res.status, 409)
|
||||
assert.equal(res.body.error, NICHT_BEZAHLT)
|
||||
assert.deepEqual(docById(unbezahlt.docId), vorher)
|
||||
assert.equal(workorders(), tickets)
|
||||
|
||||
const k = legeEinrichtung({ ticket: null, session: 'paid' })
|
||||
const vorherK = { ...docById(k.docId) }
|
||||
st.failNext({ method: 'GET', path: /^\/v1\/checkout\/sessions\//, status: 400, message: 'Nachbau: Stripe kaputt' })
|
||||
const aus = await nachlegen(k)
|
||||
assert.equal(aus.status, 503)
|
||||
assert.equal(typeof aus.body.error, 'string')
|
||||
assert.ok(!JSON.stringify(aus.body).includes('Nachbau'), 'nichts Technisches nach aussen')
|
||||
assert.deepEqual(docById(k.docId), vorherK)
|
||||
assert.equal(workorders(), tickets)
|
||||
assert.equal(seit(mailsVorher).length, 0)
|
||||
assert.ok(log.mock.calls.some((c) => /Nachbau: Stripe kaputt/.test(c.arguments.join(' '))), 'Ursache im Log')
|
||||
|
||||
assert.equal((await nachlegen(k)).status, 200, 'Stripe wieder da')
|
||||
assert.equal(workorders(), tickets + 1)
|
||||
assert.equal(seit(mailsVorher).length, 2)
|
||||
})
|
||||
|
||||
test('ticket (a): ohne vermerkte Sitzung => 409, Stripe wird nicht gefragt', async () => {
|
||||
const k = legeEinrichtung({ ticket: null })
|
||||
const aufrufe = st.calls.length
|
||||
const res = await nachlegen(k)
|
||||
assert.equal(res.status, 409)
|
||||
assert.equal(res.body.error, 'Zu dieser Funktion ist keine Zahlung vermerkt.')
|
||||
assert.equal(st.calls.length, aufrufe)
|
||||
})
|
||||
|
||||
test('ticket (b): Ticket geloescht => neues Ticket mit neuer ticketId, keine Mail, Stripe nicht gefragt', async () => {
|
||||
const k = legeEinrichtung({ ticket: 'geloescht', session: 'paid' })
|
||||
const tickets = workorders()
|
||||
const mailsVorher = mails.sent.length
|
||||
const aufrufe = st.calls.length
|
||||
assert.equal(ticketDoc(k.ticketId), undefined, 'Ausgangslage: Ticket fehlt')
|
||||
assert.equal(featuresOf({ body: await get(`/api/team/funktionen/${k.customerId}`, STAFF) }, k.projectId).termine.ticketMissing, true)
|
||||
const res = await nachlegen(k)
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
const doc = docById(k.docId)
|
||||
assert.ok(doc.ticketId && doc.ticketId !== k.ticketId, 'neue ticketId')
|
||||
assert.equal(workorders(), tickets + 1)
|
||||
const ticket = ticketDoc(doc.ticketId)
|
||||
assert.match(ticket.topic, /^Terminplaner einbauen – Salon \d+$/)
|
||||
assert.ok(ticket.details.includes('Konfigurationsstand: Version 1'))
|
||||
assert.equal(ticket.status, 'Open')
|
||||
assert.equal(doc.state, 'setup')
|
||||
assert.match(doc.adminNote, /Andrej: ticket/)
|
||||
assert.equal(featuresOf(res, k.projectId).termine.ticketMissing, false)
|
||||
assert.equal(seit(mailsVorher).length, 0, 'keine Mails: K15/T16 gingen beim ersten Mal raus')
|
||||
assert.equal(st.calls.length, aufrufe, 'Stripe nicht gefragt')
|
||||
})
|
||||
|
||||
test('ticket (b): zwei parallele Klicks und zwei gleichzeitige Dienstaufrufe => genau ein neues Ticket', async () => {
|
||||
const k = legeEinrichtung({ ticket: 'geloescht', session: 'paid' })
|
||||
const tickets = workorders()
|
||||
const [a, b] = await Promise.all([nachlegen(k), nachlegen(k)])
|
||||
assert.deepEqual([a.status, b.status], [200, 200])
|
||||
assert.equal(workorders(), tickets + 1)
|
||||
|
||||
const k2 = legeEinrichtung({ ticket: 'geloescht', session: 'paid' })
|
||||
const tickets2 = workorders()
|
||||
const [x, y] = await Promise.all([dienst(k2, 'ticketNachlegen'), dienst(k2, 'ticketNachlegen')])
|
||||
assert.deepEqual([x.created, y.created].sort(), [false, true])
|
||||
assert.equal(workorders(), tickets2 + 1)
|
||||
assert.equal(x.doc.ticketId, y.doc.ticketId)
|
||||
})
|
||||
|
||||
test('ticket (c): Ticket vorhanden (offen oder geschlossen) => no-op, created false, nichts geschrieben, Stripe nicht gefragt', async () => {
|
||||
for (const ticket of ['Open', 'Closed']) {
|
||||
const k = legeEinrichtung({ ticket, session: 'paid' })
|
||||
const tickets = workorders()
|
||||
const mailsVorher = mails.sent.length
|
||||
const aufrufe = st.calls.length
|
||||
const vorher = { ...docById(k.docId) }
|
||||
const res = await nachlegen(k)
|
||||
assert.equal(res.status, 200, ticket)
|
||||
assert.deepEqual(docById(k.docId), vorher, ticket)
|
||||
assert.equal(workorders(), tickets, ticket)
|
||||
assert.equal(seit(mailsVorher).length, 0, ticket)
|
||||
assert.equal(st.calls.length, aufrufe, ticket)
|
||||
const dienstErgebnis = await dienst(k, 'ticketNachlegen')
|
||||
assert.equal(dienstErgebnis.created, false, ticket)
|
||||
assert.equal(dienstErgebnis.doc.ticketId, k.ticketId, ticket)
|
||||
}
|
||||
})
|
||||
|
||||
test('ticket: nur in setup - off, ordered, active und disabled => 409; ohne Dokument => 409', async () => {
|
||||
const faelle = [
|
||||
legeEinrichtung({ state: 'off', ticket: null, over: { unlockedByPurchase: false, purchaseStatus: '' } }),
|
||||
legeEinrichtung({ state: 'ordered', ticket: null, session: 'paid', over: { purchaseStatus: 'ordered', unlockedByPurchase: false } }),
|
||||
legeEinrichtung({ state: 'active', ticket: 'geloescht', session: 'paid' }),
|
||||
legeEinrichtung({ state: 'disabled', ticket: null, session: 'paid', over: { disabledReason: 'team', disabledAt: '2026-10-01T10:00:00.000Z' } }),
|
||||
]
|
||||
const tickets = workorders()
|
||||
const mailsVorher = mails.sent.length
|
||||
for (const k of faelle) {
|
||||
const vorher = { ...docById(k.docId) }
|
||||
const res = await nachlegen(k)
|
||||
assert.equal(res.status, 409, vorher.state)
|
||||
assert.equal(res.body.error, NUR_SETUP_TICKET, vorher.state)
|
||||
assert.deepEqual(docById(k.docId), vorher, vorher.state)
|
||||
}
|
||||
assert.equal(workorders(), tickets)
|
||||
assert.equal(seit(mailsVorher).length, 0)
|
||||
aw.seed('websiteProjects', { $id: 'pt_d7_leer2', customerId: 'c1', projectName: 'Ohne Dokument D7b' })
|
||||
const docs = aw.docs('portalFeatures').size
|
||||
const ohneDoc = await team('c1', 'termine', { projectId: 'pt_d7_leer2', action: 'ticket' })
|
||||
assert.equal(ohneDoc.status, 409)
|
||||
assert.equal(ohneDoc.body.error, 'Für dieses Projekt gibt es noch keinen Terminplaner')
|
||||
assert.equal(aw.docs('portalFeatures').size, docs)
|
||||
})
|
||||
|
||||
test('ticket: fremdes Projekt => 403, Kundensitzung statt JWT => 401/403, nichts geschrieben', async () => {
|
||||
const k = legeEinrichtung({ ticket: 'geloescht', session: 'paid' })
|
||||
const vorher = { ...docById(k.docId) }
|
||||
const tickets = workorders()
|
||||
assert.equal((await team('c1', 'termine', { projectId: 'pt3', action: 'ticket' })).status, 403)
|
||||
const alsKunde = await nachlegen(k, 'termine', { cookie: customerCookie(k.customerId) })
|
||||
assert.ok(alsKunde.status === 401 || alsKunde.status === 403, `Status ${alsKunde.status}`)
|
||||
assert.deepEqual(docById(k.docId), vorher)
|
||||
assert.equal(workorders(), tickets)
|
||||
})
|
||||
|
||||
test('ticket bei News (a): derselbe Weg des Webhooks, K15 und T16 der News-Pinnwand', async () => {
|
||||
const k = legeEinrichtung({ key: 'news', abo: null, einstellung: false, ticket: null, session: 'paid' })
|
||||
const mailsVorher = mails.sent.length
|
||||
const res = await nachlegen(k, 'news')
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
const doc = docById(k.docId)
|
||||
assert.ok(doc.ticketId)
|
||||
assert.equal(doc.state, 'setup')
|
||||
const neu = seit(mailsVorher)
|
||||
assert.equal(neu.filter((m) => m.subject === 'Neue Funktion bestellt: News-Pinnwand').length, 1)
|
||||
assert.equal(neu.filter((m) => m.to.some((to) => to.address === k.email)).length, 1, 'K15 an den Kunden')
|
||||
})
|
||||
Reference in new issue
Block a user