Files
Webklar-Kundenbereich/test/hourCredits.test.js
T
ClaudeandClaude Opus 5.5 51fc2434e5 Stundenkonto: Arbeitszeit nur für bezahlte Wartungs-Monate, nicht für andere Abo-Rechnungen
Die Rechnung wird frisch geladen; eine Stunde bringt nur eine reguläre
(nicht anteilige) Wartungs-Zeile über 0 €. Preis-IDs werden einmal bei
Stripe nachgeschlagen und zwischengespeichert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:33:33 +00:00

101 lines
4.4 KiB
JavaScript

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'
const aw = await startAppwriteMock({
schema: { subscriptions: schemaKeys('subscriptions'), hourCredits: schemaKeys('hourCredits') },
})
const st = await startFakeStripe()
after(() => Promise.all([aw.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { grantCreditForInvoice } = await import('../server/services/stripeBilling.js')
const price = (id, lookup_key, unit_amount) => ({ id, object: 'price', lookup_key, unit_amount, active: true })
st.prices.set('price_w', price('price_w', 'webklar_wartung_monatlich_5000', 5000))
st.prices.set('price_h', price('price_h', 'webklar_hosting_monatlich_1500', 1500))
const sub = (id, priceObj) => ({
id,
object: 'subscription',
status: 'active',
customer: 'cus_1',
start_date: 1790000000,
current_period_end: 1792600000,
metadata: { webklarCustomerId: 'c1', email: 'k@example.invalid', termsVersion: '2026-09-25-v3' },
items: { object: 'list', data: [{ id: `si_${id}`, object: 'subscription_item', quantity: 1, price: priceObj }] },
})
st.subscriptions.set('sub_w', sub('sub_w', st.prices.get('price_w')))
st.subscriptions.set('sub_h', sub('sub_h', st.prices.get('price_h')))
// Frisch geladene Rechnung in der Form der SDK-API-Version (2025-10-29.clover)
const inv = (id, subscription, lines) => ({
id,
object: 'invoice',
number: `WK-${id}`,
created: 1790000000,
status_transitions: { paid_at: 1790000100 },
parent: { type: 'subscription_details', subscription_details: { subscription } },
lines: { object: 'list', data: lines },
})
const regular = (priceId, amount) => ({
amount,
pricing: { type: 'price_details', price_details: { price: priceId } },
parent: { type: 'subscription_item_details', subscription_item_details: { proration: false } },
})
// Webhook-Nutzlast (Auszug), wie sie heute ankommt
const payload = (id, subscription, subtotal) => ({
id,
subtotal,
created: 1790000000,
status_transitions: { paid_at: 1790000100 },
parent: { subscription_details: { subscription } },
})
test('Wartungs-Rechnung: 60 Minuten, genau einmal', async () => {
st.invoices.set('in_1', inv('in_1', 'sub_w', [regular('price_w', 5000)]))
const r = await grantCreditForInvoice(payload('in_1', 'sub_w', 5000))
assert.equal(r.granted, true)
const credit = aw.docs('hourCredits').get('in_1')
assert.equal(credit.minutes, 60)
assert.equal(credit.customerId, 'c1')
assert.equal(credit.grantedAt, new Date(1790000100 * 1000).toISOString())
assert.deepEqual(await grantCreditForInvoice(payload('in_1', 'sub_w', 5000)), { skipped: 'bereits gutgeschrieben' })
})
test('Hosting-Rechnung: keine Stunde', async () => {
st.invoices.set('in_2', inv('in_2', 'sub_h', [regular('price_h', 1500)]))
assert.deepEqual(await grantCreditForInvoice(payload('in_2', 'sub_h', 1500)), { skipped: 'keine Wartungs-Leistung' })
assert.equal(aw.docs('hourCredits').has('in_2'), false)
})
test('Frisch geladene Rechnung zählt, nicht die Webhook-Nutzlast', async () => {
// Nutzlast in alter Form (ohne pricing/parent) behauptet Wartung, frisch geladen ist es Hosting
st.invoices.set('in_3', inv('in_3', 'sub_h', [regular('price_h', 1500)]))
const oldPayload = {
id: 'in_3',
subscription: 'sub_h',
subtotal: 5000,
created: 1790000000,
status_transitions: { paid_at: 1790000100 },
lines: { data: [{ amount: 5000, price: { id: 'price_w' }, proration: false }] },
}
assert.deepEqual(await grantCreditForInvoice(oldPayload), { skipped: 'keine Wartungs-Leistung' })
assert.equal(aw.docs('hourCredits').has('in_3'), false)
})
test('Preis-Zuordnung wird zwischengespeichert', async () => {
st.invoices.set('in_4', inv('in_4', 'sub_w', [regular('price_w', 5000)]))
await grantCreditForInvoice(payload('in_4', 'sub_w', 5000))
assert.equal(aw.docs('hourCredits').get('in_4').minutes, 60)
assert.equal(st.calls.filter((c) => c.path === '/v1/prices/price_w').length, 1)
})
test('Unbekannte Preis-ID (404): keine Stunde, kein Absturz', async () => {
st.invoices.set('in_5', inv('in_5', 'sub_w', [regular('price_weg', 5000)]))
assert.deepEqual(await grantCreditForInvoice(payload('in_5', 'sub_w', 5000)), { skipped: 'keine Wartungs-Leistung' })
assert.equal(aw.docs('hourCredits').has('in_5'), false)
})