Zusatzspeicher: Plus sofort anteilig, Minus zum Monatsende, Aufgaben für konsoleH

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Fable 5.1 committed 2026-09-28 10:48:29 +00:00
1 parent 887e1e54a0
commit 379cc26dcd
11 files changed
+1531 -27

No files matched your search

+47 -1
View File
@@ -11,7 +11,7 @@ const aw = await startAppwriteMock({
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 { grantCreditForInvoice, loadInvoice, invoiceLines } = 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))
@@ -116,3 +116,49 @@ test('Altbestand: Abo ohne bekannten Preis bekommt weiter seine Stunde, auch bei
assert.equal(aw.docs('hourCredits').get('in_6').minutes, 60)
assert.equal(aw.docs('subscriptions').get('sub_alt').productKey, 'wartung')
})
// F18 / Hinweis aus dem Review von Plan 1: invoices.retrieve bettet hoechstens 10 Zeilen ein. Anteilige
// Zeilen stehen vor den Abo-Zeilen; mehrere Speicheraenderungen in einem Monat schieben die Wartungs-Zeile
// hinaus. Der Nachbau bettet wie Stripe 10 Zeilen ein (has_more), der Rest kommt ueber /lines.
test('Rechnung mit 11 Zeilen: die Wartungs-Zeile als elfte wird nachgeladen → 60 Minuten', async () => {
st.prices.set('price_s', price('price_s', 'webklar_mailspeicher_5gb_monatlich_100', 100))
const proration = (amount) => ({
amount,
pricing: { type: 'price_details', price_details: { price: 'price_s' } },
parent: { type: 'subscription_item_details', subscription_item_details: { proration: true } },
})
const prorations = Array.from({ length: 10 }, (_, i) => proration(10 + i))
st.invoices.set('in_11', inv('in_11', 'sub_w', [...prorations, regular('price_w', 5000)]))
const before = st.calls.length
const r = await grantCreditForInvoice(payload('in_11', 'sub_w', 5000))
assert.equal(r.granted, true)
assert.equal(aw.docs('hourCredits').get('in_11').minutes, 60)
const calls = st.calls.slice(before)
assert.equal(calls.filter((c) => c.method === 'GET' && c.path === '/v1/invoices/in_11').length, 1)
assert.ok(calls.some((c) => c.method === 'GET' && c.path === '/v1/invoices/in_11/lines'), 'weitere Zeilen ueber /lines geladen')
})
test('invoiceLines: eingebettete Zeilen ohne has_more, sonst alle Seiten; loadInvoice liefert alle Zeilen', async () => {
const embedded = { id: 'in_egal', lines: { data: [{ amount: 1 }, { amount: 2 }], has_more: false } }
const before = st.calls.length
assert.deepEqual(await invoiceLines(embedded), [{ amount: 1 }, { amount: 2 }])
assert.equal(st.calls.length, before, 'kein Stripe-Aufruf ohne has_more')
const loaded = await loadInvoice('in_11')
assert.equal(loaded.lines.data.length, 11)
assert.equal(loaded.lines.has_more, false)
assert.equal(loaded.lines.data[10].amount, 5000)
// Vorschau-Rechnungen haben keine ID: has_more laesst sich dann nicht nachladen, die eingebetteten zaehlen
assert.deepEqual(await invoiceLines({ lines: { data: [{ amount: 3 }], has_more: true } }), [{ amount: 3 }])
})
// F18: der Webhook laedt die Rechnung einmal (loadInvoice) und reicht sie als fresh weiter - kein zweiter Abruf
test('Schon frisch geladene Rechnung (fresh: true) wird nicht noch einmal geladen', async () => {
st.invoices.set('in_7', inv('in_7', 'sub_w', [regular('price_w', 5000)]))
const fresh = await loadInvoice('in_7')
const before = st.calls.length
const r = await grantCreditForInvoice(fresh, { fresh: true })
assert.equal(r.granted, true)
assert.equal(aw.docs('hourCredits').get('in_7').minutes, 60)
assert.equal(aw.docs('hourCredits').get('in_7').note, 'Rechnung WK-in_7')
assert.deepEqual(st.calls.slice(before).filter((c) => c.path.startsWith('/v1/invoices')), [])
})