Zustimmungsnachweis je Funktion (featureConsents) und Fristanker aus dem Klick
server/services/featureConsents.js speichert und liest den Nachweis zur Bestellung einer Funktion (nur anfuegen, sieben Exporte): speichereZustimmung (Texte und Fassungen als JSON, SHA-256 ueber die Texte, vollstaendig oder gar nicht), letzteZustimmung, zustimmungFuerSitzung (leere Sitzung fragt nicht ab), leseZustimmung, texteHash, zustimmungsfassungen und nachweisTexte (Haken, Belehrung, Knopf aus dem Register). paidFeatureOrders liefert featureKey und vertragAt (Klick der bezahlten Sitzung, sonst consentAt, sonst orderedAt; Lesefehler faellt mit Log auf orderedAt zurueck); der Widerruf einer Funktion rechnet die Frist aus vertragAt. Eine spaetere, unbezahlte Zustimmung verschiebt den Anker nicht. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d8e4d79891
commit
786df1230e
5 files changed
+627
-4
No files matched your search
+122
-1
@@ -16,6 +16,7 @@ const aw = await startAppwriteMock({
|
||||
contractEvents: schemaKeys('contractEvents'),
|
||||
hourCredits: schemaKeys('hourCredits'),
|
||||
portalFeatures: [...schemaKeys('portalFeatures'), 'siteKey', 'offSince', 'activatedAt', 'accentColor', 'removalTicketId'],
|
||||
featureConsents: schemaKeys('featureConsents'),
|
||||
},
|
||||
})
|
||||
const st = await startFakeStripe()
|
||||
@@ -531,7 +532,127 @@ test('Widerruf Funktion: nichts automatisch, K04 „wir prüfen“, T09 nennt Fu
|
||||
assert.ok(team.includes(fmtDate(withdrawalDeadline(orderedAt).endsAt)))
|
||||
assert.match(team, /RECHTZEITIG/)
|
||||
assert.doesNotMatch(team, /KEIN aktives Abo/)
|
||||
aw.docs('portalFeatures').delete('f_pf1_news')
|
||||
// Der Nachbau liefert aus docs() eine Kopie: geloescht wird ueber die API, sonst bliebe die Bestellung fuer die naechsten Tests
|
||||
await deleteDocument('portalFeatures', 'f_pf1_news')
|
||||
})
|
||||
|
||||
/** Zustimmungsnachweis zu einer Sitzung (featureConsents) fuer die Fristanker-Tests. */
|
||||
function seedZustimmung(id, { projectId = 'pa1', stripeSessionId, acceptedAt, featureKey = 'termine' }) {
|
||||
aw.seed('featureConsents', {
|
||||
$id: id,
|
||||
customerId: 'c1',
|
||||
projectId,
|
||||
featureKey,
|
||||
stripeSessionId,
|
||||
acceptedAt,
|
||||
fassungen: '{}',
|
||||
texte: '[]',
|
||||
textHash: '',
|
||||
})
|
||||
}
|
||||
|
||||
/** Bezahlte Funktion mit Zustimmung zur bezahlten Sitzung (vertragAt) und neuerer, unbezahlter Zustimmung derselben Funktion. */
|
||||
function seedBezahlteBestellung({ klickAt, orderedAt = daysAgo(1) }) {
|
||||
aw.seed('websiteProjects', { $id: 'pa1', customerId: 'c1', projectName: 'Studio Anker' })
|
||||
aw.seed('portalFeatures', {
|
||||
$id: 'f_pa1_termine',
|
||||
customerId: 'c1',
|
||||
projectId: 'pa1',
|
||||
featureKey: 'termine',
|
||||
state: 'setup',
|
||||
enabled: false,
|
||||
unlockedByPurchase: true,
|
||||
// durch eine Neubestellung ueberschrieben: spaeter als der Klick der bezahlten Sitzung
|
||||
orderedAt,
|
||||
stripeSessionId: 'cs_bezahlt',
|
||||
})
|
||||
seedZustimmung('fc_bezahlt', { stripeSessionId: 'cs_bezahlt', acceptedAt: klickAt })
|
||||
// spaetere, unbezahlte Zustimmung desselben Kunden und Projekts (andere Sitzung): verschiebt den Anker nicht
|
||||
seedZustimmung('fc_offen', { stripeSessionId: 'cs_offen', acceptedAt: daysAgo(0.5) })
|
||||
}
|
||||
|
||||
async function raeumeAnkerTest() {
|
||||
await deleteDocument('portalFeatures', 'f_pa1_termine')
|
||||
for (const id of ['fc_bezahlt', 'fc_offen']) if (aw.docs('featureConsents').has(id)) await deleteDocument('featureConsents', id)
|
||||
}
|
||||
|
||||
test('Widerruf Funktion: Fristanker ist die Zeit des Klicks der bezahlten Sitzung (rechtzeitig)', async () => {
|
||||
await resetSubH({ units: 0 })
|
||||
const klickAt = daysAgo(13)
|
||||
seedBezahlteBestellung({ klickAt })
|
||||
try {
|
||||
const res = await withdraw({ order: 'funktion' })
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
const team = t09().text
|
||||
// Frist aus dem Klick der bezahlten Sitzung, nicht aus orderedAt (vor 1 Tag) und nicht aus der spaeteren Zustimmung
|
||||
assert.ok(team.includes(fmtDate(withdrawalDeadline(klickAt).endsAt)), team)
|
||||
assert.ok(!team.includes(fmtDate(withdrawalDeadline(daysAgo(1)).endsAt)), 'nicht das Fristende aus orderedAt')
|
||||
assert.match(team, /RECHTZEITIG/)
|
||||
assert.doesNotMatch(team, /NACH Fristende/)
|
||||
assert.match(team, /Studio Anker: in Einrichtung/)
|
||||
} finally {
|
||||
await raeumeAnkerTest()
|
||||
}
|
||||
})
|
||||
|
||||
test('Widerruf Funktion: Fristanker aus dem Klick vor 20 Tagen ergibt NACH Fristende', async () => {
|
||||
await resetSubH({ units: 0 })
|
||||
const klickAt = daysAgo(20)
|
||||
seedBezahlteBestellung({ klickAt })
|
||||
try {
|
||||
const res = await withdraw({ order: 'funktion' })
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
const team = t09().text
|
||||
assert.ok(team.includes(fmtDate(withdrawalDeadline(klickAt).endsAt)), team)
|
||||
assert.match(team, /NACH Fristende/)
|
||||
assert.doesNotMatch(team, /RECHTZEITIG/)
|
||||
} finally {
|
||||
await raeumeAnkerTest()
|
||||
}
|
||||
})
|
||||
|
||||
test('Widerruf Funktion: ohne Nachweis zur Sitzung gilt consentAt, dann orderedAt', async () => {
|
||||
await resetSubH({ units: 0 })
|
||||
aw.seed('websiteProjects', { $id: 'pa1', customerId: 'c1', projectName: 'Studio Anker' })
|
||||
const base = { customerId: 'c1', projectId: 'pa1', featureKey: 'termine', state: 'setup', enabled: false, unlockedByPurchase: true, stripeSessionId: 'cs_ohne_nachweis' }
|
||||
try {
|
||||
// consentAt (Altbestand, News) vor orderedAt
|
||||
aw.seed('portalFeatures', { ...base, $id: 'f_pa1_termine', orderedAt: daysAgo(1), consentAt: daysAgo(20) })
|
||||
assert.equal((await withdraw({ order: 'funktion' })).status, 200)
|
||||
assert.ok(t09().text.includes(fmtDate(withdrawalDeadline(daysAgo(20)).endsAt)))
|
||||
assert.match(t09().text, /NACH Fristende/)
|
||||
|
||||
// weder Nachweis noch consentAt: orderedAt
|
||||
mails.sent.length = 0
|
||||
aw.seed('portalFeatures', { ...base, $id: 'f_pa1_termine', orderedAt: daysAgo(3), consentAt: null })
|
||||
assert.equal((await withdraw({ order: 'funktion' })).status, 200)
|
||||
assert.ok(t09().text.includes(fmtDate(withdrawalDeadline(daysAgo(3)).endsAt)))
|
||||
assert.match(t09().text, /RECHTZEITIG/)
|
||||
} finally {
|
||||
await raeumeAnkerTest()
|
||||
}
|
||||
})
|
||||
|
||||
test('Widerruf Funktion: Nachweis-Collection nicht lesbar fällt auf orderedAt zurück, Antwort bleibt 200', async () => {
|
||||
await resetSubH({ units: 0 })
|
||||
const orderedAt = daysAgo(2)
|
||||
seedBezahlteBestellung({ klickAt: daysAgo(20), orderedAt })
|
||||
const warn = console.warn
|
||||
const warnungen = []
|
||||
console.warn = (...args) => warnungen.push(args.join(' '))
|
||||
try {
|
||||
aw.failNext({ collection: 'featureConsents', method: 'GET', status: 500 })
|
||||
const res = await withdraw({ order: 'funktion' })
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
const team = t09().text
|
||||
assert.ok(team.includes(fmtDate(withdrawalDeadline(orderedAt).endsAt)), team)
|
||||
assert.match(team, /RECHTZEITIG/)
|
||||
assert.ok(warnungen.some((w) => w.includes('Zustimmungsnachweis')), 'Rueckfall wird geloggt')
|
||||
assert.ok(k04(), 'K04 geht trotzdem an den Kunden')
|
||||
} finally {
|
||||
console.warn = warn
|
||||
await raeumeAnkerTest()
|
||||
}
|
||||
})
|
||||
|
||||
test('Widerruf Abschluss in der Frist: auf Kündigung gesetzt; Altbestand ohne Ereignis zählt ab startedAt (F14)', async () => {
|
||||
|
||||
Reference in new issue
Block a user