diff --git a/server/services/stripeBilling.js b/server/services/stripeBilling.js index aa752bf..656a073 100644 --- a/server/services/stripeBilling.js +++ b/server/services/stripeBilling.js @@ -456,7 +456,9 @@ function stripeCountry(value) { const raw = typeof value === 'string' ? value.normalize('NFC').trim() : '' if (!raw) return 'DE' if (/^[A-Za-z]{2}$/.test(raw)) return raw.toUpperCase() - return COUNTRY_CODES[raw.toLowerCase()] || null + // nur eigene Schluessel: 'constructor' oder '__proto__' duerfen nicht ueber die Prototypkette treffen + const key = raw.toLowerCase() + return Object.hasOwn(COUNTRY_CODES, key) ? COUNTRY_CODES[key] : null } /** @@ -464,19 +466,21 @@ function stripeCountry(value) { * Strasse eingetragen ist, die Anschrift. Mit Strasse gehen PLZ und Ort immer mit (fehlende als '': Stripe loescht dann * einen alten Wert, statt ihn neben der neuen Strasse stehen zu lassen). Land: siehe stripeCountry (unbekannt -> fehlt). * Leere Werte fehlen (undefined), damit Stripe sie nicht anfasst. Gemeinsam fuer das Anlegen und das Aktualisieren des - * Stripe-Kunden. + * Stripe-Kunden; forCreate: beim Anlegen gibt es nichts zu loeschen, leere PLZ/Ort fehlen dort ganz. */ -export function stripeCustomerFields(customer) { +export function stripeCustomerFields(customer, { forCreate = false } = {}) { const t = (v) => (typeof v === 'string' ? v.trim() : '') const country = stripeCountry(customer.country) + const postalCode = t(customer.postalCode) + const city = t(customer.city) return { email: t(customer.email) || undefined, name: t(customer.companyName) || t(customer.name) || undefined, address: t(customer.street) ? { line1: t(customer.street), - postal_code: t(customer.postalCode), - city: t(customer.city), + ...(postalCode || !forCreate ? { postal_code: postalCode } : {}), + ...(city || !forCreate ? { city } : {}), ...(country ? { country } : {}), } : undefined, @@ -495,7 +499,7 @@ export async function getOrCreateStripeCustomer(customer) { if (found.data[0]) return found.data[0].id const created = await stripe.customers.create({ - ...stripeCustomerFields(customer), + ...stripeCustomerFields(customer, { forCreate: true }), preferred_locales: ['de'], invoice_settings: { footer: config.stripe.invoiceFooter }, metadata: { webklarCustomerId: customer.$id, kundennummer: customer.code || '' }, diff --git a/test/financeStripeWrite.test.js b/test/financeStripeWrite.test.js index 865acad..12a3907 100644 --- a/test/financeStripeWrite.test.js +++ b/test/financeStripeWrite.test.js @@ -845,6 +845,8 @@ test('stripeCustomerFields (N3): Land - zweistelliger Code gross, deutsche Namen 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) + // Namen von Object.prototype sind keine Laender (kein Treffer ueber die Prototypkette) + for (const proto of ['constructor', 'Constructor', '__proto__', 'toString', 'hasOwnProperty', 'valueOf']) assert.ok(hasNoCountry(proto), proto) // ohne Strasse bleibt es bei keiner Anschrift assert.equal(stripeCustomerFields({ country: 'Österreich' }).address, undefined) }) @@ -858,6 +860,31 @@ test('stripeCustomerFields (N4): mit Strasse gehen PLZ und Ort immer mit, fehlen assert.equal(stripeCustomerFields({ postalCode: '10115', city: 'Berlin' }).address, undefined) }) +test('stripeCustomerFields (forCreate): beim Anlegen fehlen leere PLZ/Ort ganz, mit Werten bleibt alles gleich', () => { + assert.deepEqual(stripeCustomerFields({ street: 'Weg 1', postalCode: ' ', city: undefined }, { forCreate: true }).address, { line1: 'Weg 1', country: 'DE' }) + assert.deepEqual(stripeCustomerFields({ street: 'Weg 1', postalCode: '10115', city: '' }, { forCreate: true }).address, { line1: 'Weg 1', postal_code: '10115', country: 'DE' }) + assert.deepEqual( + stripeCustomerFields({ street: 'Weg 1', postalCode: '10115', city: 'Berlin', country: 'AT' }, { forCreate: true }).address, + stripeCustomerFields({ street: 'Weg 1', postalCode: '10115', city: 'Berlin', country: 'AT' }).address + ) + assert.equal(stripeCustomerFields({ postalCode: '10115' }, { forCreate: true }).address, undefined) +}) + +test('createProjectInvoice (forCreate): ein neuer Stripe-Kunde mit nur einer Strasse wird ohne leere Adressfelder angelegt, der Abgleich danach loescht per ""', async () => { + aw.seed('customers', { $id: 'newStr', email: 'ns@example.invalid', name: 'Neu Strasse', customerStatus: 'active', status: 'active', street: 'Neue Str. 5' }) + await createProjectInvoice({ customerId: 'newStr', items: [BASE_ITEMS[1]], createdBy: 'k' }) + const [created] = callsTo('POST', '/v1/customers') + assert.equal(created.body['address[line1]'], 'Neue Str. 5') + assert.ok(!('address[postal_code]' in created.body), 'keine leere PLZ beim Anlegen') + assert.ok(!('address[city]' in created.body), 'kein leerer Ort beim Anlegen') + assert.deepEqual(Object.keys(created.body).filter((k) => k.startsWith('address[')), ['address[line1]', 'address[country]']) + assert.ok(Object.entries(created.body).filter(([k]) => k.startsWith('address[')).every(([, v]) => v !== ''), 'leerer Adresswert im Anlege-Aufruf') + // der Abgleich direkt danach (Aktualisieren) behaelt das Loeschen per leerem Text + const [update] = callsTo('POST', /^\/v1\/customers\/cus_test_/) + assert.equal(update.body['address[postal_code]'], '') + assert.equal(update.body['address[city]'], '') +}) + 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()