Kundenansicht neu P7: Zusatzspeicher-Plus in Fassung 6 mit Wortlaut-Nachweis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
528ca0875e
commit
da2fe83d47
2 files changed
+153
-2
No files matched your search
+29
-2
@@ -409,8 +409,9 @@ router.post('/storage', requireSession, async (req, res) => {
|
|||||||
const { units, acceptTerms, termsVersion } = req.body || {}
|
const { units, acceptTerms, termsVersion } = req.body || {}
|
||||||
let consent = null
|
let consent = null
|
||||||
if (acceptTerms === true) {
|
if (acceptTerms === true) {
|
||||||
if (termsVersion !== config.abo.termsVersion) {
|
const fassung = fassungDerAnfrage(req, termsVersion)
|
||||||
return res.status(409).json({ error: 'Die Bedingungen wurden aktualisiert. Bitte Seite neu laden.' })
|
if (fassung === null) {
|
||||||
|
return res.status(409).json({ error: neueAnsicht(req) ? FEHLER_BEDINGUNGEN : 'Die Bedingungen wurden aktualisiert. Bitte Seite neu laden.' })
|
||||||
}
|
}
|
||||||
consent = {
|
consent = {
|
||||||
termsVersion,
|
termsVersion,
|
||||||
@@ -418,6 +419,32 @@ router.post('/storage', requireSession, async (req, res) => {
|
|||||||
consentIp: req.ip,
|
consentIp: req.ip,
|
||||||
consentUserAgent: req.get('user-agent') || '',
|
consentUserAgent: req.get('user-agent') || '',
|
||||||
}
|
}
|
||||||
|
// Fassung 6: Der gezeigte Wortlaut samt Kundenart (aus dem Kundendokument) wird gespeichert, bevor Stripe etwas aendert. Eine ungueltige
|
||||||
|
// Menge ueberlaesst die Ablehnung wie bisher changeStorage (400, ohne Nachweis)
|
||||||
|
const mengeOk = Number.isInteger(units) && units >= 1 && units <= config.mailStorage.maxUnits
|
||||||
|
if (fassung === 'neu' && mengeOk) {
|
||||||
|
let kundenart
|
||||||
|
try {
|
||||||
|
kundenart = kundenartWirksam(await getDocument(config.collections.customers, customerId))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[abo] Kunde für den Zusatzspeicher nicht lesbar:', err.message)
|
||||||
|
return res.status(500).json({ error: FEHLER_NACHWEIS })
|
||||||
|
}
|
||||||
|
await speichereNachweisFuerRoute({
|
||||||
|
customerId,
|
||||||
|
art: 'speicher',
|
||||||
|
bezug: `units=${units}`,
|
||||||
|
texte: nachweisTexte('speicher', {
|
||||||
|
kundenart,
|
||||||
|
fassung: termsVersion,
|
||||||
|
bestellung: bestellZeile('speicher', { units, gb: units * config.mailStorage.gbPerUnit, monatCents: units * config.mailStorage.amountCents }),
|
||||||
|
}),
|
||||||
|
fassungen: { zustimmung: termsVersion },
|
||||||
|
kundenart,
|
||||||
|
req,
|
||||||
|
at: consent.consentAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const { subscription, change } = await changeStorage(customerId, units, consent)
|
const { subscription, change } = await changeStorage(customerId, units, consent)
|
||||||
const events = await listContractEvents(customerId).catch((e) => {
|
const events = await listContractEvents(customerId).catch((e) => {
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { test, after } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||||
|
import { startFakeStripe } from './support/fakeStripe.js'
|
||||||
|
import { useTestEnv } from './support/testEnv.js'
|
||||||
|
import { schemaKeys } from './support/schema.js'
|
||||||
|
import { mailSink } from './support/mailSink.js'
|
||||||
|
|
||||||
|
// Zusatzspeicher-Plus in Fassung 6: Nachweis mit Kundenart aus dem Dokument vor der Änderung, das Minus bleibt ohne Nachweis
|
||||||
|
const aw = await startAppwriteMock({
|
||||||
|
schema: {
|
||||||
|
subscriptions: schemaKeys('subscriptions'),
|
||||||
|
contractEvents: schemaKeys('contractEvents'),
|
||||||
|
hourCredits: schemaKeys('hourCredits'),
|
||||||
|
featureConsents: schemaKeys('featureConsents'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const st = await startFakeStripe()
|
||||||
|
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||||
|
Object.assign(process.env, { KUNDENANSICHT_NEU: 'pilot', KUNDENANSICHT_NEU_PILOT_EMAILS: 'K@example.invalid, k3@example.invalid' })
|
||||||
|
const { config } = await import('../server/config.js')
|
||||||
|
const { startTestApp, post } = await import('./support/testApp.js')
|
||||||
|
const { hostingDoc, customerCookie, customers, kundenansichtCookie, seedFixtures, stripeSubscription } = await import('./support/fixtures.js')
|
||||||
|
const { default: aboRoutes } = await import('../server/routes/abo.js')
|
||||||
|
const { listContractEvents } = await import('../server/services/contractEvents.js')
|
||||||
|
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||||
|
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||||
|
const { FEHLER_BEDINGUNGEN } = await import('../server/services/bestelltexte.js')
|
||||||
|
|
||||||
|
const app = await startTestApp({ '/api/abo': aboRoutes })
|
||||||
|
const mails = mailSink()
|
||||||
|
setMailTransportForTests(mails)
|
||||||
|
after(() => {
|
||||||
|
setMailTransportForTests(null)
|
||||||
|
return Promise.all([app.close(), aw.close(), st.close()])
|
||||||
|
})
|
||||||
|
seedFixtures(aw)
|
||||||
|
|
||||||
|
const price = (id, lookup_key, unit_amount) => ({ id, object: 'price', active: true, lookup_key, unit_amount, recurring: { interval: 'month' } })
|
||||||
|
st.prices.set('price_mailStorage', price('price_mailStorage', config.mailStorage.lookupKey, config.mailStorage.amountCents))
|
||||||
|
st.prices.set('price_hosting', price('price_hosting', config.products.hosting.lookupKey, config.products.hosting.amountCents))
|
||||||
|
st.prices.set('price_wartung', price('price_wartung', config.products.wartung.lookupKey, config.products.wartung.amountCents))
|
||||||
|
|
||||||
|
const setzeKundenart = (id, wert) => aw.seed('customers', { ...customers[id], ...(wert === undefined ? {} : { kundenart: wert }) })
|
||||||
|
async function clearEvents(customerId) {
|
||||||
|
for (const e of await listContractEvents(customerId)) await deleteDocument(config.collections.contractEvents, e.$id)
|
||||||
|
}
|
||||||
|
async function resetSubH({ units = 0 } = {}) {
|
||||||
|
await clearEvents('c1')
|
||||||
|
mails.sent.length = 0
|
||||||
|
aw.seed('subscriptions', { ...hostingDoc, storageUnits: units, storagePaidUnits: 0, storagePaidPeriodEnd: null })
|
||||||
|
st.subscriptions.set('sub_h', stripeSubscription({ id: 'sub_h', customerId: 'c1', productKey: 'hosting', storageUnits: units }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const NEU = () => config.abo.termsVersionNeu
|
||||||
|
const zeilen = () => [...aw.docs('featureConsents').values()].filter((d) => d.featureKey === 'speicher')
|
||||||
|
const stripeAenderungen = (von) => st.calls.slice(von).filter((c) => c.method === 'POST' && c.path === '/v1/subscriptions/sub_h')
|
||||||
|
|
||||||
|
test('Plus in Fassung 6: Nachweis mit Kundenart aus dem Dokument, Ereignis mit der neuen Fassung', async () => {
|
||||||
|
setzeKundenart('c1', 'unternehmen')
|
||||||
|
await resetSubH({ units: 0 })
|
||||||
|
const n0 = zeilen().length
|
||||||
|
const res = await post('/api/abo/storage', { units: 2, acceptTerms: true, termsVersion: NEU(), kundenart: 'privat' }, { cookie: customerCookie() })
|
||||||
|
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||||
|
const [z] = zeilen().slice(n0)
|
||||||
|
assert.deepEqual([z.kundenart, JSON.parse(z.fassungen).bezug, z.projectId], ['unternehmen', 'units=2', ''])
|
||||||
|
assert.deepEqual(JSON.parse(z.texte).map((t) => t.id), ['bestellung', 'linkzeile', 'knopf', 'kundenart'])
|
||||||
|
assert.equal(JSON.parse(z.texte)[0].text, 'Zusatzspeicher: 2 Pakete (+10 GB), 2,00 € pro Monat (gemäß § 19 UStG ohne Umsatzsteuer), monatlich kündbar')
|
||||||
|
assert.equal((await listContractEvents('c1'))[0].termsVersion, NEU())
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Privat und leer: Nachweis mit privat, nie mit dem Wert aus dem Body', async () => {
|
||||||
|
for (const art of ['privat', undefined, 'firma']) {
|
||||||
|
setzeKundenart('c1', art)
|
||||||
|
await resetSubH({ units: 0 })
|
||||||
|
const res = await post('/api/abo/storage', { units: 1, acceptTerms: true, termsVersion: NEU(), kundenart: 'unternehmen' }, { cookie: customerCookie() })
|
||||||
|
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||||
|
assert.equal(zeilen().at(-1).kundenart, 'privat', String(art))
|
||||||
|
assert.match(JSON.parse(zeilen().at(-1).texte)[0].text, /^Zusatzspeicher: 1 Paket \(\+5 GB\), 1,00 € pro Monat/)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Minus, Fassung 5, falsche Fassung, ungültige Menge und Nachweisfehler', async () => {
|
||||||
|
setzeKundenart('c1', 'unternehmen')
|
||||||
|
await resetSubH({ units: 2 })
|
||||||
|
const n0 = zeilen().length
|
||||||
|
assert.equal((await post('/api/abo/storage', { units: 1 }, { cookie: customerCookie() })).status, 200)
|
||||||
|
assert.equal(zeilen().length, n0) // Minus: kein Nachweis
|
||||||
|
await resetSubH({ units: 0 })
|
||||||
|
assert.equal((await post('/api/abo/storage', { units: 1, acceptTerms: true, termsVersion: config.abo.termsVersion }, { cookie: customerCookie() })).status, 200)
|
||||||
|
assert.equal(zeilen().length, n0) // Fassung 5: alter Weg ohne Nachweis
|
||||||
|
const fremd = await post('/api/abo/storage', { units: 2, acceptTerms: true, termsVersion: NEU() }, { cookie: customerCookie('c2') })
|
||||||
|
assert.equal(fremd.status, 409)
|
||||||
|
const v3 = await post('/api/abo/storage', { units: 2, acceptTerms: true, termsVersion: '2026-09-25-v3' }, { cookie: customerCookie() })
|
||||||
|
assert.deepEqual([v3.status, v3.body.error], [409, FEHLER_BEDINGUNGEN])
|
||||||
|
for (const units of [11, -1, 1.5, '2', undefined, 0]) {
|
||||||
|
const r = await post('/api/abo/storage', { units, acceptTerms: true, termsVersion: NEU() }, { cookie: customerCookie() })
|
||||||
|
assert.ok(r.status === 400 || r.status === 200, `${String(units)}: ${r.status}`)
|
||||||
|
}
|
||||||
|
assert.equal(zeilen().length, n0, 'ungültige Mengen und 0 schreiben keinen Nachweis')
|
||||||
|
await resetSubH({ units: 0 })
|
||||||
|
aw.failNext({ collection: config.collections.featureConsents, method: 'POST' })
|
||||||
|
const vor = st.calls.length
|
||||||
|
const f = await post('/api/abo/storage', { units: 2, acceptTerms: true, termsVersion: NEU() }, { cookie: customerCookie() })
|
||||||
|
assert.deepEqual([f.status, stripeAenderungen(vor).length], [500, 0])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Mitarbeiter-Kundenansicht ändert nichts und schreibt nichts', async () => {
|
||||||
|
await resetSubH({ units: 0 })
|
||||||
|
const n0 = zeilen().length
|
||||||
|
const vor = st.calls.length
|
||||||
|
const r = await post('/api/abo/storage', { units: 2, acceptTerms: true, termsVersion: NEU() }, { cookie: kundenansichtCookie({ customerId: 'c1' }) })
|
||||||
|
assert.deepEqual([r.status, zeilen().length, stripeAenderungen(vor).length], [403, n0, 0])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Kunde nicht lesbar: 500 statt Rückfall, nichts geändert', async () => {
|
||||||
|
setzeKundenart('c1', 'unternehmen')
|
||||||
|
await resetSubH({ units: 0 })
|
||||||
|
const n0 = zeilen().length
|
||||||
|
const vor = st.calls.length
|
||||||
|
aw.failNext({ collection: config.collections.customers, method: 'GET', status: 500 })
|
||||||
|
const r = await post('/api/abo/storage', { units: 2, acceptTerms: true, termsVersion: NEU() }, { cookie: customerCookie() })
|
||||||
|
assert.deepEqual([r.status, zeilen().length, stripeAenderungen(vor).length], [500, n0, 0])
|
||||||
|
})
|
||||||
Reference in new issue
Block a user