Finanzen: Nachtrag vor Livegang (Kundenabgleich beim Senden, Adress-Sperre, Land und Anschrift sauber an Stripe)
- sendInvoiceNow: Entwurf mit webklarCustomerId laedt den WOMS-Kunden und bringt den Stripe-Kunden vor dem
Finalisieren auf die aktuellen Daten (Fehler: 502, nichts finalisiert); ohne Kennung kein Abgleich
- Adress-Sperre (§ 14 Abs. 4 Nr. 1 UStG): Senden ueber 250 € ohne Strasse/PLZ/Ort -> 409 vor jedem Stripe-Schreibzugriff
(Anlegen mit senden=true und Entwurf senden; Text und Grenze wie im WOMS)
- stripeCustomerFields: Land aus deutschen Namen (unbekannt -> weglassen), PLZ/Ort mit Strasse immer mitsenden ('' loescht)
- Stripe-Nachbau: Kunden-Update fuehrt die Anschrift feldweise zusammen, '' loescht ein Feld
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
73811e0300
commit
f0b57ebe41
5 files changed
+377
-26
No files matched your search
@@ -987,13 +987,60 @@ test('POST /rechnungen/:id/senden: bezahlt -> 409, Abo-Rechnung -> 409, unbekann
|
||||
})
|
||||
|
||||
test('POST /rechnungen/:id/senden: Kunde ohne E-Mail-Adresse -> 400', async () => {
|
||||
putInvoice({ id: 'in_nomail', status: 'draft', number: null, customer_email: null, customer: 'cus_ohne' })
|
||||
// Entwurf ohne webklarCustomerId (z. B. aus dem Stripe-Dashboard): es gibt keine WOMS-E-Mail, die Stripe nachgetragen werden koennte
|
||||
putInvoice({ id: 'in_nomail', status: 'draft', number: null, customer_email: null, customer: 'cus_ohne', metadata: {} })
|
||||
st.customers.set('cus_ohne', { id: 'cus_ohne', object: 'customer', name: 'Ohne', email: null, metadata: {} })
|
||||
const res = await senden('in_nomail')
|
||||
assert.equal(res.status, 400)
|
||||
assert.equal(res.body.error, 'Für den Versand fehlt die E-Mail-Adresse des Kunden.')
|
||||
})
|
||||
|
||||
const ADDRESS_INCOMPLETE = 'Adresse des Kunden unvollständig (Straße, PLZ, Ort). Für den Versand bitte zuerst ergänzen.'
|
||||
const BIG_ITEMS = [{ description: 'Großauftrag', quantity: 1, unitAmountCents: 30_000 }]
|
||||
|
||||
test('POST /rechnungen: senden bei unvollstaendiger Anschrift und Summe ueber 250 € -> 409 mit fester Meldung, nichts bei Stripe; Entwurf geht', async () => {
|
||||
// c1 hat in den Testdaten keine Anschrift
|
||||
const blocked = await postInvoice({ customerId: 'c1', items: BIG_ITEMS, senden: true })
|
||||
assert.equal(blocked.status, 409)
|
||||
assert.deepEqual(blocked.body, { error: ADDRESS_INCOMPLETE })
|
||||
assert.equal(st.calls.length, 0)
|
||||
const draft = await postInvoice({ customerId: 'c1', items: BIG_ITEMS })
|
||||
assert.equal(draft.status, 200, JSON.stringify(draft.body))
|
||||
assert.equal(draft.body.rechnung.status, 'entwurf')
|
||||
})
|
||||
|
||||
test('POST /rechnungen/:id/senden: Entwurf ueber 250 € ohne Anschrift -> 409; mit ergaenzter Anschrift wird der Stripe-Kunde vor dem Finalisieren abgeglichen', async () => {
|
||||
const draft = (await postInvoice({ customerId: 'c1', items: BIG_ITEMS })).body.rechnung.id
|
||||
st.calls.length = 0
|
||||
const blocked = await senden(draft)
|
||||
assert.equal(blocked.status, 409)
|
||||
assert.deepEqual(blocked.body, { error: ADDRESS_INCOMPLETE })
|
||||
assert.equal(st.calls.filter((c) => c.method !== 'GET').length, 0)
|
||||
|
||||
aw.seed('customers', { ...customers.c1, street: 'Hauptstr. 1', postalCode: '10115', city: 'Berlin' })
|
||||
st.calls.length = 0
|
||||
const sent = await senden(draft)
|
||||
assert.equal(sent.status, 200, JSON.stringify(sent.body))
|
||||
assert.equal(sent.body.rechnung.status, 'offen')
|
||||
assert.deepEqual(
|
||||
st.calls.filter((c) => c.method !== 'GET').map((c) => c.path.replace(draft, ':id')),
|
||||
['/v1/customers/cus_c1', '/v1/invoices/:id/finalize', '/v1/invoices/:id/send']
|
||||
)
|
||||
assert.equal(st.customers.get('cus_c1').address.line1, 'Hauptstr. 1')
|
||||
aw.seed('customers', customers.c1)
|
||||
})
|
||||
|
||||
test('POST /rechnungen/:id/senden: scheitert der Abgleich des Stripe-Kunden -> 502 mit fester Meldung, nicht finalisiert', async () => {
|
||||
const draft = (await postInvoice({ customerId: 'c1', items: ITEMS })).body.rechnung.id
|
||||
st.calls.length = 0
|
||||
st.failNext({ method: 'POST', path: '/v1/customers/cus_c1', status: 400, message: 'geheime Stripe-Meldung' })
|
||||
const res = await senden(draft)
|
||||
assert.equal(res.status, 502)
|
||||
assert.deepEqual(res.body, { error: 'Kundendaten konnten nicht an Stripe übertragen werden.' })
|
||||
assert.equal(callsTo('POST', /\/(finalize|send)$/).length, 0)
|
||||
assert.equal(st.invoices.get(draft).status, 'draft')
|
||||
})
|
||||
|
||||
test('POST /rechnungen/:id/stornieren: offene Rechnung -> storniert; Entwurf und bezahlte -> 409', async () => {
|
||||
putInvoice({ id: 'in_open' })
|
||||
putInvoice({ id: 'in_draft', status: 'draft', number: null })
|
||||
|
||||
@@ -12,7 +12,7 @@ useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { seedFixtures } = await import('./support/fixtures.js')
|
||||
const { berlinDate } = await import('../server/services/finance/dates.js')
|
||||
const { getStripe } = await import('../server/services/stripeBilling.js')
|
||||
const { getStripe, stripeCustomerFields } = await import('../server/services/stripeBilling.js')
|
||||
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { invoiceFooter, createProjectInvoice, sendInvoiceNow, voidInvoice, deleteDraft, listInvoices, invalidateFinanceCache, resetInvoiceCreateGuardForTests } = await import(
|
||||
'../server/services/finance/stripeFinance.js'
|
||||
@@ -29,10 +29,16 @@ aw.seed('customers', {
|
||||
$id: 'addr1', email: 'neu@example.invalid', name: 'Anna Adresse', companyName: 'Adresse GmbH', customerStatus: 'active', status: 'active',
|
||||
street: ' Hauptstr. 1 ', postalCode: '10115', city: 'Berlin', country: 'Deutschland',
|
||||
})
|
||||
aw.seed('customers', { $id: 'noAddr', email: 'ohne@example.invalid', name: 'Ohne Adresse', customerStatus: 'active', status: 'active' })
|
||||
aw.seed('customers', { $id: 'noPlz', email: 'plz@example.invalid', name: 'Ohne PLZ', customerStatus: 'active', status: 'active', street: 'Hauptstr. 2', postalCode: ' ', city: 'Berlin' })
|
||||
aw.seed('customers', { $id: 'noCity', email: 'ort@example.invalid', name: 'Ohne Ort', customerStatus: 'active', status: 'active', street: 'Hauptstr. 3', postalCode: '10115', city: '' })
|
||||
aw.seed('customers', { $id: 'noStreet', email: 'str@example.invalid', name: 'Ohne Strasse', customerStatus: 'active', status: 'active', street: ' ', postalCode: '10115', city: 'Berlin' })
|
||||
aw.seed('customers', { $id: 'noMail', name: 'Ohne Mail', customerStatus: 'active', status: 'active' })
|
||||
aw.seed('customers', { $id: 'blankMail', email: ' ', name: 'Leere Mail', customerStatus: 'active', status: 'active' })
|
||||
|
||||
const NO_MAIL = 'Für den Versand fehlt die E-Mail-Adresse des Kunden.'
|
||||
const ADDRESS_INCOMPLETE = 'Adresse des Kunden unvollständig (Straße, PLZ, Ort). Für den Versand bitte zuerst ergänzen.'
|
||||
const SYNC_FAILED = 'Kundendaten konnten nicht an Stripe übertragen werden.'
|
||||
const NOT_SENDABLE = 'Diese Rechnung kann nicht mehr gesendet werden.'
|
||||
const PAYMENT_NOTE = 'Bitte geben Sie bei Überweisung die Rechnungsnummer als Verwendungszweck an.'
|
||||
const BASE_ITEMS = [
|
||||
@@ -353,9 +359,10 @@ test('sendInvoiceNow: Entwurf wird finalisiert (ohne Auto-Advance) und gesendet'
|
||||
const draft = await createProjectInvoice({ customerId: 'c1', items: BASE_ITEMS, createdBy: 'k' })
|
||||
st.calls.length = 0
|
||||
const sent = await sendInvoiceNow(draft.id)
|
||||
// zuerst der Stripe-Kunde (N1), dann finalisieren und senden
|
||||
assert.deepEqual(
|
||||
writes().map((c) => c.path.replace(draft.id, ':id')),
|
||||
['/v1/invoices/:id/finalize', '/v1/invoices/:id/send']
|
||||
['/v1/customers/cus_c1', '/v1/invoices/:id/finalize', '/v1/invoices/:id/send']
|
||||
)
|
||||
assert.equal(callsTo('POST', /\/finalize$/)[0].body.auto_advance, 'false')
|
||||
assert.equal(sent.status, 'offen')
|
||||
@@ -680,3 +687,216 @@ test('createProjectInvoice: Rechnung nach dem Senden nicht lesbar -> 502 mit inv
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
// ---------- Nachtrag N1: Kundendaten vor dem Finalisieren eines Entwurfs ----------
|
||||
|
||||
test('sendInvoiceNow (N1): ein Entwurf bringt den Stripe-Kunden vor dem Finalisieren auf die aktuellen WOMS-Daten', async () => {
|
||||
const draft = await createProjectInvoice({ customerId: 'addr1', items: [BASE_ITEMS[1]], createdBy: 'k' })
|
||||
const stripeCustomerId = st.invoices.get(draft.id).customer
|
||||
// zwischen Entwurf und Senden hat sich beim Stripe-Kunden (oder im WOMS) etwas geaendert: Stripe traegt wieder alte Daten
|
||||
st.customers.set(stripeCustomerId, {
|
||||
...st.customers.get(stripeCustomerId), name: 'Anna Alt', email: 'alt@example.invalid',
|
||||
address: { line1: 'Alte Str. 9', postal_code: '80331', city: 'München', country: 'DE' },
|
||||
})
|
||||
st.calls.length = 0
|
||||
const sent = await sendInvoiceNow(draft.id)
|
||||
|
||||
assert.equal(sent.status, 'offen')
|
||||
assert.deepEqual(
|
||||
writes().map((c) => c.path.replace(draft.id, ':id')),
|
||||
[`/v1/customers/${stripeCustomerId}`, '/v1/invoices/:id/finalize', '/v1/invoices/:id/send']
|
||||
)
|
||||
assert.deepEqual(callsTo('POST', `/v1/customers/${stripeCustomerId}`)[0].body, {
|
||||
email: 'neu@example.invalid', name: 'Adresse GmbH',
|
||||
'address[line1]': 'Hauptstr. 1', 'address[postal_code]': '10115', 'address[city]': 'Berlin', 'address[country]': 'DE',
|
||||
})
|
||||
const stored = st.customers.get(stripeCustomerId)
|
||||
assert.equal(stored.email, 'neu@example.invalid')
|
||||
assert.equal(stored.name, 'Adresse GmbH')
|
||||
assert.deepEqual(stored.address, { line1: 'Hauptstr. 1', postal_code: '10115', city: 'Berlin', country: 'DE' })
|
||||
// die Rechnung wurde mit den aktuellen Daten festgeschrieben
|
||||
assert.equal(sent.customerEmail, 'neu@example.invalid')
|
||||
assert.equal(sent.customerName, 'Adresse GmbH')
|
||||
})
|
||||
|
||||
test('sendInvoiceNow (N1): scheitert die Uebertragung, wird nicht finalisiert (502, feste Meldung), ein neuer Versuch geht', async () => {
|
||||
const draft = await createProjectInvoice({ customerId: 'addr1', items: [BASE_ITEMS[1]], createdBy: 'k' })
|
||||
const stripeCustomerId = st.invoices.get(draft.id).customer
|
||||
st.calls.length = 0
|
||||
st.failNext({ method: 'POST', path: `/v1/customers/${stripeCustomerId}`, status: 400, message: 'geheime Stripe-Meldung' })
|
||||
await assert.rejects(sendInvoiceNow(draft.id), (err) => {
|
||||
assert.equal(err.name, 'InvoiceError')
|
||||
assert.equal(err.status, 502)
|
||||
assert.equal(err.message, SYNC_FAILED)
|
||||
assert.equal(err.invoiceId, undefined)
|
||||
assert.ok(err.cause, 'Ursache bleibt fuers Protokoll')
|
||||
return true
|
||||
})
|
||||
assert.equal(callsTo('POST', /\/(finalize|send)$/).length, 0)
|
||||
assert.equal(st.invoices.get(draft.id).status, 'draft')
|
||||
const sent = await sendInvoiceNow(draft.id)
|
||||
assert.equal(sent.status, 'offen')
|
||||
})
|
||||
|
||||
test('sendInvoiceNow (N1): Entwurf ohne webklarCustomerId (z. B. aus dem Stripe-Dashboard) -> keine Uebertragung, Senden wie bisher', async () => {
|
||||
st.customers.set('cus_dash', { id: 'cus_dash', object: 'customer', name: 'Dashboard', email: 'dash@example.invalid', metadata: {} })
|
||||
st.invoices.set('in_dash', projectInvoice({ id: 'in_dash', customer: 'cus_dash', metadata: {}, total: 90_000 }))
|
||||
const sent = await sendInvoiceNow('in_dash')
|
||||
assert.equal(sent.status, 'offen')
|
||||
assert.deepEqual(writes().map((c) => c.path), ['/v1/invoices/in_dash/finalize', '/v1/invoices/in_dash/send'])
|
||||
assert.equal(callsTo('POST', /^\/v1\/customers/).length, 0)
|
||||
})
|
||||
|
||||
test('sendInvoiceNow (N1): fehlt die E-Mail nur beim Stripe-Kunden, traegt der Abgleich sie aus dem WOMS nach und der Entwurf wird gesendet', async () => {
|
||||
st.customers.set('cus_nomail2', { id: 'cus_nomail2', object: 'customer', name: 'Ohne Mail', email: null, metadata: { webklarCustomerId: 'addr1' } })
|
||||
st.invoices.set('in_nomail2', projectInvoice({ id: 'in_nomail2', customer: 'cus_nomail2', metadata: { webklarCustomerId: 'addr1' } }))
|
||||
const sent = await sendInvoiceNow('in_nomail2')
|
||||
assert.equal(sent.status, 'offen')
|
||||
assert.equal(st.customers.get('cus_nomail2').email, 'neu@example.invalid')
|
||||
assert.deepEqual(writes().map((c) => c.path), ['/v1/customers/cus_nomail2', '/v1/invoices/in_nomail2/finalize', '/v1/invoices/in_nomail2/send'])
|
||||
})
|
||||
|
||||
test('sendInvoiceNow (N1): offene Rechnung wird nur erneut gesendet, ohne Kundenabgleich und ohne Adresspruefung', async () => {
|
||||
st.customers.set('cus_noaddr', { id: 'cus_noaddr', object: 'customer', name: 'Ohne Adresse', email: 'ohne@example.invalid', metadata: { webklarCustomerId: 'noAddr' } })
|
||||
st.invoices.set('in_open_big', projectInvoice({ id: 'in_open_big', customer: 'cus_noaddr', status: 'open', number: 'WK-0009', total: 90_000, metadata: { webklarCustomerId: 'noAddr' } }))
|
||||
const sent = await sendInvoiceNow('in_open_big')
|
||||
assert.equal(sent.status, 'offen')
|
||||
assert.deepEqual(writes().map((c) => c.path), ['/v1/invoices/in_open_big/send'])
|
||||
})
|
||||
|
||||
test('sendInvoiceNow (N1): Entwurf eines unbekannten oder nicht festen Kunden -> 400 vor jedem Schreibzugriff', async () => {
|
||||
st.customers.set('cus_gone', { id: 'cus_gone', object: 'customer', name: 'Weg', email: 'weg@example.invalid', metadata: { webklarCustomerId: 'gibtEsNicht' } })
|
||||
st.invoices.set('in_gone', projectInvoice({ id: 'in_gone', customer: 'cus_gone', metadata: { webklarCustomerId: 'gibtEsNicht' } }))
|
||||
await rejectsWith(sendInvoiceNow('in_gone'), 400, 'Kunde nicht gefunden.')
|
||||
st.customers.set('cus_lead', { id: 'cus_lead', object: 'customer', name: 'Lead', email: 'lead@example.invalid', metadata: { webklarCustomerId: 'lead1' } })
|
||||
st.invoices.set('in_lead', projectInvoice({ id: 'in_lead', customer: 'cus_lead', metadata: { webklarCustomerId: 'lead1' } }))
|
||||
await rejectsWith(sendInvoiceNow('in_lead'), 400, 'Rechnungen nur für feste Kunden.')
|
||||
assert.equal(writes().length, 0)
|
||||
})
|
||||
|
||||
// ---------- Nachtrag N2: Adress-Sperre beim Senden (§ 14 Abs. 4 Nr. 1 UStG, Kleinbetragsrechnung bis 250 €) ----------
|
||||
|
||||
const bigItems = (cents) => [{ description: 'Großauftrag', quantity: 1, unitAmountCents: cents }]
|
||||
|
||||
test('createProjectInvoice (N2): senden=true ohne vollstaendige Anschrift und Summe ueber 250 € -> 409 vor jedem Stripe-Aufruf', async () => {
|
||||
for (const customerId of ['noAddr', 'noPlz', 'noCity', 'noStreet']) {
|
||||
await rejectsWith(createProjectInvoice({ customerId, items: bigItems(25_001), senden: true, createdBy: 'k' }), 409, ADDRESS_INCOMPLETE)
|
||||
}
|
||||
// mehrere Positionen: die Summe zaehlt, nicht die einzelne Position
|
||||
await rejectsWith(
|
||||
createProjectInvoice({ customerId: 'noAddr', items: [{ description: 'A', quantity: 2, unitAmountCents: 12_501 }], senden: true, createdBy: 'k' }),
|
||||
409,
|
||||
ADDRESS_INCOMPLETE
|
||||
)
|
||||
assert.equal(st.calls.length, 0, 'weder Lesen noch Schreiben bei Stripe')
|
||||
assert.equal(st.customers.size, 1)
|
||||
})
|
||||
|
||||
test('createProjectInvoice (N2): Kleinbetragsrechnung bis genau 250,00 € darf auch ohne Anschrift gesendet werden', async () => {
|
||||
const result = await createProjectInvoice({ customerId: 'noAddr', items: bigItems(25_000), senden: true, createdBy: 'k' })
|
||||
assert.equal(result.status, 'offen')
|
||||
assert.equal(result.totalCents, 25_000)
|
||||
})
|
||||
|
||||
test('createProjectInvoice (N2): als Entwurf geht jede Summe ohne Anschrift, mit vollstaendiger Anschrift darf auch gesendet werden', async () => {
|
||||
const draft = await createProjectInvoice({ customerId: 'noAddr', items: bigItems(90_000), createdBy: 'k' })
|
||||
assert.equal(draft.status, 'entwurf')
|
||||
const sent = await createProjectInvoice({ customerId: 'addr1', items: bigItems(90_000), senden: true, createdBy: 'k' })
|
||||
assert.equal(sent.status, 'offen')
|
||||
})
|
||||
|
||||
test('createProjectInvoice (N2): nach der Sperre ist der Schutz vor doppeltem Anlegen frei, mit ergaenzter Anschrift geht derselbe Aufruf', async () => {
|
||||
aw.seed('customers', { $id: 'later1', email: 'later@example.invalid', name: 'Spaeter', customerStatus: 'active', status: 'active' })
|
||||
const input = { customerId: 'later1', items: bigItems(30_000), senden: true, createdBy: 'k' }
|
||||
await rejectsWith(createProjectInvoice(input), 409, ADDRESS_INCOMPLETE)
|
||||
aw.seed('customers', { $id: 'later1', email: 'later@example.invalid', name: 'Spaeter', customerStatus: 'active', status: 'active', street: 'Weg 1', postalCode: '10115', city: 'Berlin' })
|
||||
const result = await createProjectInvoice(input)
|
||||
assert.equal(result.status, 'offen')
|
||||
})
|
||||
|
||||
test('sendInvoiceNow (N2): Entwurf ohne vollstaendige Anschrift und Summe ueber 250 € -> 409 vor jedem Schreibzugriff, bis 250,00 € geht es', async () => {
|
||||
const big = await createProjectInvoice({ customerId: 'noAddr', items: bigItems(25_001), createdBy: 'k' })
|
||||
const small = await createProjectInvoice({ customerId: 'noAddr', items: bigItems(25_000), createdBy: 'k' })
|
||||
st.calls.length = 0
|
||||
await rejectsWith(sendInvoiceNow(big.id), 409, ADDRESS_INCOMPLETE)
|
||||
assert.equal(writes().length, 0, 'kein Kundenabgleich, kein Finalisieren')
|
||||
assert.equal(st.invoices.get(big.id).status, 'draft')
|
||||
assert.equal((await sendInvoiceNow(small.id)).status, 'offen')
|
||||
})
|
||||
|
||||
test('sendInvoiceNow (N2): Entwurf mit vollstaendiger Anschrift darf gesendet werden, auch ueber 250 €', async () => {
|
||||
const draft = await createProjectInvoice({ customerId: 'addr1', items: bigItems(90_000), createdBy: 'k' })
|
||||
assert.equal((await sendInvoiceNow(draft.id)).status, 'offen')
|
||||
})
|
||||
|
||||
// ---------- Nachtrag N3/N4: Stripe-Kundenfelder ----------
|
||||
|
||||
test('stripeCustomerFields (N3): Land - zweistelliger Code gross, deutsche Namen ohne Gross-/Kleinschreibung, leer = DE, sonst weglassen', () => {
|
||||
const country = (value) => stripeCustomerFields({ street: 'Str. 1', postalCode: '1', city: 'X', country: value }).address.country
|
||||
const hasNoCountry = (value) => !('country' in stripeCustomerFields({ street: 'Str. 1', postalCode: '1', city: 'X', country: value }).address)
|
||||
for (const code of ['de', 'DE', ' at ', 'Ch', 'fr']) assert.equal(country(code), code.trim().toUpperCase())
|
||||
const names = {
|
||||
Deutschland: 'DE', deutschland: 'DE', DEUTSCHLAND: 'DE', Österreich: 'AT', österreich: 'AT', ÖSTERREICH: 'AT', Oesterreich: 'AT',
|
||||
Schweiz: 'CH', Niederlande: 'NL', Belgien: 'BE', Frankreich: 'FR', Italien: 'IT', Spanien: 'ES', Polen: 'PL', Dänemark: 'DK',
|
||||
Luxemburg: 'LU', Liechtenstein: 'LI', ' schweiz ': 'CH', [`O\u0308sterreich`]: 'AT',
|
||||
}
|
||||
for (const [name, code] of Object.entries(names)) assert.equal(country(name), code, name)
|
||||
// leer oder nicht gesetzt: wie bisher DE
|
||||
for (const empty of ['', ' ', undefined, null]) assert.equal(country(empty), 'DE', JSON.stringify(empty))
|
||||
// unbekannt: kein Land (Stripe behaelt seinen Wert)
|
||||
for (const unknown of ['Atlantis', 'Germany', 'D', 'DEU', 'Deutschland (DE)']) assert.ok(hasNoCountry(unknown), unknown)
|
||||
// ohne Strasse bleibt es bei keiner Anschrift
|
||||
assert.equal(stripeCustomerFields({ country: 'Österreich' }).address, undefined)
|
||||
})
|
||||
|
||||
test('stripeCustomerFields (N4): mit Strasse gehen PLZ und Ort immer mit, fehlende als leerer Text (loescht alte Werte)', () => {
|
||||
assert.deepEqual(stripeCustomerFields({ street: ' Weg 1 ', postalCode: ' ', city: undefined }).address, { line1: 'Weg 1', postal_code: '', city: '', country: 'DE' })
|
||||
assert.deepEqual(stripeCustomerFields({ street: 'Weg 1', postalCode: '10115', city: 'Berlin', country: 'AT' }).address, {
|
||||
line1: 'Weg 1', postal_code: '10115', city: 'Berlin', country: 'AT',
|
||||
})
|
||||
// ohne Strasse keine Anschrift, auch nicht mit PLZ/Ort
|
||||
assert.equal(stripeCustomerFields({ postalCode: '10115', city: 'Berlin' }).address, undefined)
|
||||
})
|
||||
|
||||
test('Stripe-Nachbau (N4): Kunden-Update fuehrt die Anschrift zusammen wie Stripe, ein leerer Text loescht das Feld', async () => {
|
||||
st.customers.set('cus_m', { id: 'cus_m', object: 'customer', name: 'M', email: 'm@example.invalid', address: { line1: 'Alt 1', postal_code: '80331', city: 'München', country: 'DE' }, metadata: {} })
|
||||
const stripe = getStripe()
|
||||
// nur ein Feld senden: die anderen bleiben
|
||||
await stripe.customers.update('cus_m', { address: { line1: 'Neu 2' } })
|
||||
assert.deepEqual(st.customers.get('cus_m').address, { line1: 'Neu 2', postal_code: '80331', city: 'München', country: 'DE' })
|
||||
// leerer Text loescht genau dieses Feld (Stripe liefert dann null)
|
||||
await stripe.customers.update('cus_m', { address: { postal_code: '', city: '' } })
|
||||
assert.deepEqual(st.customers.get('cus_m').address, { line1: 'Neu 2', postal_code: null, city: null, country: 'DE' })
|
||||
// ohne address im Aufruf bleibt die Anschrift unberuehrt
|
||||
await stripe.customers.update('cus_m', { name: 'M2' })
|
||||
assert.deepEqual(st.customers.get('cus_m').address, { line1: 'Neu 2', postal_code: null, city: null, country: 'DE' })
|
||||
// Kunde ohne Anschrift: das Update legt sie an
|
||||
st.customers.set('cus_n', { id: 'cus_n', object: 'customer', name: 'N', email: null, address: null, metadata: {} })
|
||||
await stripe.customers.update('cus_n', { address: { line1: 'Erst 1', postal_code: '', city: 'Köln' } })
|
||||
assert.deepEqual(st.customers.get('cus_n').address, { line1: 'Erst 1', postal_code: null, city: 'Köln' })
|
||||
})
|
||||
|
||||
test('createProjectInvoice (N4): alte PLZ und alter Ort beim Stripe-Kunden werden geloescht, wenn der WOMS-Kunde nur eine Strasse hat', async () => {
|
||||
aw.seed('customers', { $id: 'strOnly', email: 's@example.invalid', name: 'Nur Strasse', customerStatus: 'active', status: 'active', street: 'Neue Str. 5' })
|
||||
st.customers.set('cus_strOnly', {
|
||||
id: 'cus_strOnly', object: 'customer', name: 'Alt', email: 'alt@example.invalid',
|
||||
address: { line1: 'Alte Str. 9', postal_code: '80331', city: 'München', country: 'DE' }, metadata: { webklarCustomerId: 'strOnly' },
|
||||
})
|
||||
await createProjectInvoice({ customerId: 'strOnly', items: [BASE_ITEMS[1]], createdBy: 'k' })
|
||||
const [update] = callsTo('POST', '/v1/customers/cus_strOnly')
|
||||
assert.equal(update.body['address[postal_code]'], '')
|
||||
assert.equal(update.body['address[city]'], '')
|
||||
assert.deepEqual(st.customers.get('cus_strOnly').address, { line1: 'Neue Str. 5', postal_code: null, city: null, country: 'DE' })
|
||||
})
|
||||
|
||||
test('createProjectInvoice (N3): unbekanntes Land ueberschreibt das Land beim Stripe-Kunden nicht', async () => {
|
||||
aw.seed('customers', { $id: 'atlantis', email: 'a@example.invalid', name: 'Atlantis', customerStatus: 'active', status: 'active', street: 'Weg 1', postalCode: '1', city: 'Z', country: 'Atlantis' })
|
||||
st.customers.set('cus_atlantis', {
|
||||
id: 'cus_atlantis', object: 'customer', name: 'Atlantis', email: 'a@example.invalid',
|
||||
address: { line1: 'Weg 1', postal_code: '1', city: 'Z', country: 'CH' }, metadata: { webklarCustomerId: 'atlantis' },
|
||||
})
|
||||
await createProjectInvoice({ customerId: 'atlantis', items: [BASE_ITEMS[1]], createdBy: 'k' })
|
||||
const [update] = callsTo('POST', '/v1/customers/cus_atlantis')
|
||||
assert.ok(!('address[country]' in update.body))
|
||||
assert.equal(st.customers.get('cus_atlantis').address.country, 'CH')
|
||||
})
|
||||
@@ -28,7 +28,8 @@ import http from 'node:http'
|
||||
* Rechnung muss nicht in st.customers stehen (dann bleiben Name und E-Mail leer). Traegt ein Aufruf die Kopfzeile
|
||||
* Idempotency-Key, steht sie als idempotencyKey in calls; delayNext verzoegert eine Antwort (Zeitgrenzen testen).
|
||||
* Kunden: GET /v1/customers/search (Metadaten), POST /v1/customers (anlegen) und POST /v1/customers/:id (aendern:
|
||||
* email, name, address, metadata; unbekannte ID -> 404).
|
||||
* email, name, address, metadata; unbekannte ID -> 404). Wie Stripe fuehrt ein Update die Anschrift feldweise zusammen
|
||||
* (nur gesendete Felder aendern sich) und ein leerer Text loescht das Feld (gespeichert als null).
|
||||
*/
|
||||
|
||||
// Parameter, die Stripe bei ausstehenden Aktualisierungen annimmt (docs.stripe.com/billing/subscriptions/
|
||||
@@ -153,6 +154,16 @@ export async function startFakeStripe() {
|
||||
return root
|
||||
}
|
||||
|
||||
// Anschrift eines Kunden: ein Hash wird feldweise in die bestehende Anschrift geschrieben, '' loescht das Feld (null);
|
||||
// ein leerer Text statt eines Hashes loescht die ganze Anschrift (wie Stripe)
|
||||
const mergeAddress = (current, incoming) => {
|
||||
if (incoming === undefined) return current ?? null
|
||||
if (typeof incoming !== 'object' || incoming === null) return incoming === '' ? null : (current ?? null)
|
||||
const merged = { ...(current || {}) }
|
||||
for (const [field, value] of Object.entries(incoming)) merged[field] = value === '' ? null : value
|
||||
return merged
|
||||
}
|
||||
|
||||
const route = (method, path, query, body) => {
|
||||
let m
|
||||
if (method === 'GET' && (m = path.match(/^\/v1\/subscriptions\/([^/]+)$/))) {
|
||||
@@ -513,7 +524,7 @@ export async function startFakeStripe() {
|
||||
object: 'customer',
|
||||
email: data.email ?? null,
|
||||
name: data.name ?? null,
|
||||
address: data.address ?? null,
|
||||
address: mergeAddress(null, data.address),
|
||||
invoice_settings: data.invoice_settings || {},
|
||||
metadata: data.metadata || {},
|
||||
}
|
||||
@@ -531,7 +542,7 @@ export async function startFakeStripe() {
|
||||
...customer,
|
||||
...(data.email !== undefined ? { email: data.email || null } : {}),
|
||||
...(data.name !== undefined ? { name: data.name || null } : {}),
|
||||
...(data.address !== undefined ? { address: data.address } : {}),
|
||||
...(data.address !== undefined ? { address: mergeAddress(customer.address, data.address) } : {}),
|
||||
metadata,
|
||||
}
|
||||
customers.set(customer.id, updated)
|
||||
|
||||
Reference in new issue
Block a user