Kundenansicht P5: Kundenart beim Anlegen Pflicht (ab Schalter pilot), beim Ändern optional

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 15:49:25 +00:00
1 parent 8d3eb73db7
commit 5c21023e28
3 files changed
+93 -2

No files matched your search

+6 -2
View File
@@ -11,6 +11,10 @@ import {
const router = Router() const router = Router()
// Eigene Fehlercodes (klein, mit Unterstrich, z. B. kundenart_fehlt) gehen mit in die Antwort; Appwrite-Fehler tragen eine
// Zahl und Systemfehler Grossbuchstaben (ECONNREFUSED): die Antworten von heute bleiben dafuer unveraendert
const codeVon = (err) => (typeof err.code === 'string' && /^[a-z][a-z_]*$/.test(err.code) ? { code: err.code } : {})
router.use(requireAppwriteStaff) router.use(requireAppwriteStaff)
router.get('/', async (_req, res) => { router.get('/', async (_req, res) => {
@@ -29,7 +33,7 @@ router.post('/', async (req, res) => {
return res.status(201).json(result) return res.status(201).json(result)
} catch (err) { } catch (err) {
const status = err.status || 500 const status = err.status || 500
return res.status(status).json({ error: err.message || 'Kunde konnte nicht angelegt werden' }) return res.status(status).json({ error: err.message || 'Kunde konnte nicht angelegt werden', ...codeVon(err) })
} }
}) })
@@ -39,7 +43,7 @@ router.patch('/:customerId', async (req, res) => {
return res.json(result) return res.json(result)
} catch (err) { } catch (err) {
const status = err.status || 500 const status = err.status || 500
return res.status(status).json({ error: err.message || 'Kunde konnte nicht aktualisiert werden' }) return res.status(status).json({ error: err.message || 'Kunde konnte nicht aktualisiert werden', ...codeVon(err) })
} }
}) })
+23
View File
@@ -14,6 +14,8 @@ import {
Query, Query,
} from './appwriteAdmin.js' } from './appwriteAdmin.js'
import { forgetPasswordVersion } from './passwordVersion.js' import { forgetPasswordVersion } from './passwordVersion.js'
import { KUNDENARTEN, kundenartGespeichert } from './kundenart.js'
import { neueAnsichtModus } from './kundenansicht/schalter.js'
function readPortalPassword(customer) { function readPortalPassword(customer) {
if (customer?.portalPassword) return customer.portalPassword if (customer?.portalPassword) return customer.portalPassword
@@ -27,6 +29,17 @@ function notesWithoutPortalPassword(notes) {
return (notes || '').replace(/^__portalPassword__:[^\n]*\n?/, '').trim() return (notes || '').replace(/^__portalPassword__:[^\n]*\n?/, '').trim()
} }
/** Fehler fuer eine fehlende oder ungueltige Kundenart (400 kundenart_fehlt); die Route reicht `code` durch. */
function kundenartFehler() {
const error = new Error('Kundenart ist erforderlich (unternehmen oder privat).')
error.status = 400
error.code = 'kundenart_fehlt'
return error
}
/** Als "nicht mitgeschickt" gelten undefined, null und ''. */
const kundenartFehlt = (wert) => wert === undefined || wert === null || wert === ''
function sanitizeCustomer(customer, { portalPassword } = {}) { function sanitizeCustomer(customer, { portalPassword } = {}) {
const storedPassword = portalPassword || readPortalPassword(customer) const storedPassword = portalPassword || readPortalPassword(customer)
return { return {
@@ -41,6 +54,7 @@ function sanitizeCustomer(customer, { portalPassword } = {}) {
appwriteUserId: customer.appwriteUserId || '', appwriteUserId: customer.appwriteUserId || '',
customerStatus: customer.customerStatus || '', customerStatus: customer.customerStatus || '',
archived: Boolean(customer.archived), archived: Boolean(customer.archived),
kundenart: kundenartGespeichert(customer),
portalPassword: storedPassword || '', portalPassword: storedPassword || '',
} }
} }
@@ -73,6 +87,7 @@ export async function createCustomerWithPortalAccess(payload) {
email, email,
phone = '', phone = '',
password, password,
kundenart,
} = payload || {} } = payload || {}
if (!name?.trim()) { if (!name?.trim()) {
@@ -90,6 +105,9 @@ export async function createCustomerWithPortalAccess(payload) {
error.status = 400 error.status = 400
throw error throw error
} }
// Kundenart (V4): Pflicht nur ab Schalter pilot (bei off legt das WOMS Kunden wie heute ohne Kundenart an);
// ein mitgeschickter, ungueltiger Wert ist in jedem Modus ein Fehler. Vor jedem Schreibzugriff.
if (kundenartFehlt(kundenart) ? neueAnsichtModus() !== 'off' : !KUNDENARTEN.includes(kundenart)) throw kundenartFehler()
const existing = await getCustomerByEmail(email.trim()) const existing = await getCustomerByEmail(email.trim())
if (existing) { if (existing) {
@@ -117,6 +135,7 @@ export async function createCustomerWithPortalAccess(payload) {
portalAccessEnabled: true, portalAccessEnabled: true,
appwriteUserId: appwriteUser.$id, appwriteUserId: appwriteUser.$id,
customerStatus: 'active', customerStatus: 'active',
...(kundenartFehlt(kundenart) ? {} : { kundenart }),
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
}) })
@@ -150,6 +169,7 @@ export async function updateCustomerWithPortalAccess(customerId, payload) {
email, email,
phone, phone,
password, password,
kundenart,
} = payload || {} } = payload || {}
const nextEmail = (email ?? customer.email ?? '').trim() const nextEmail = (email ?? customer.email ?? '').trim()
@@ -163,6 +183,8 @@ export async function updateCustomerWithPortalAccess(customerId, payload) {
error.status = 400 error.status = 400
throw error throw error
} }
// Kundenart optional: nur wenn mitgeschickt, dann gueltig (V4)
if (kundenart !== undefined && !KUNDENARTEN.includes(kundenart)) throw kundenartFehler()
const normalizedPassword = const normalizedPassword =
typeof password === 'string' && password.trim().length > 0 ? password : undefined typeof password === 'string' && password.trim().length > 0 ? password : undefined
@@ -216,6 +238,7 @@ export async function updateCustomerWithPortalAccess(customerId, payload) {
...(location !== undefined ? { location: String(location).trim() } : {}), ...(location !== undefined ? { location: String(location).trim() } : {}),
email: nextEmail, email: nextEmail,
...(phone !== undefined ? { phone: String(phone).trim() } : {}), ...(phone !== undefined ? { phone: String(phone).trim() } : {}),
...(kundenart !== undefined ? { kundenart } : {}),
...(nextPortalPassword ? { portalPassword: nextPortalPassword } : {}), ...(nextPortalPassword ? { portalPassword: nextPortalPassword } : {}),
...(notesChanged ? { notes: cleanedNotes } : {}), ...(notesChanged ? { notes: cleanedNotes } : {}),
// CRM-Stufe (lead/customer/lost) nicht ueberschreiben - nur setzen, wenn leer // CRM-Stufe (lead/customer/lost) nicht ueberschreiben - nur setzen, wenn leer
+64
View File
@@ -0,0 +1,64 @@
import { test, after, beforeEach } 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'
// Kundenart in der Kundenverwaltung (V4): Pflicht beim Anlegen nur ab Schalter pilot, beim Aendern optional
const aw = await startAppwriteMock()
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { startTestApp, post, send } = await import('./support/testApp.js')
const { default: customersRouter } = await import('../server/routes/admin/customers.js')
const app = await startTestApp({ '/api/admin/customers': customersRouter })
const urspruenglich = config.kundenansichtNeu.mode
after(() => {
config.kundenansichtNeu.mode = urspruenglich
return Promise.all([app.close(), aw.close(), st.close()])
})
const modus = (m) => { config.kundenansichtNeu.mode = m }
beforeEach(() => modus('pilot'))
const admin = { headers: { Authorization: 'Bearer jwt-admin' } }
const neu = (extra = {}) => ({ name: 'Bäckerei Müller', email: 'mueller@example.invalid', password: 'geheim-1234', ...extra })
test('pilot und on: Anlegen ohne oder mit ungültiger Kundenart: 400 kundenart_fehlt, nichts geschrieben', async () => {
for (const mode of ['pilot', 'on']) {
modus(mode)
for (const kundenart of [undefined, '', 'Unternehmen', 'firma', 5, null]) {
const res = await post('/api/admin/customers', neu({ kundenart }), admin)
assert.deepEqual([res.status, res.body], [400, { error: 'Kundenart ist erforderlich (unternehmen oder privat).', code: 'kundenart_fehlt' }], `${mode} ${String(kundenart)}`)
}
}
assert.equal(aw.docs('customers').size, 0)
})
test('Anlegen mit unternehmen oder privat; Antwort und Dokument tragen die Kundenart', async () => {
const a = await post('/api/admin/customers', neu({ kundenart: 'unternehmen' }), admin)
assert.deepEqual([a.status, a.body.customer.kundenart], [201, 'unternehmen'])
assert.equal(aw.docs('customers').get(a.body.customer.$id).kundenart, 'unternehmen')
const b = await post('/api/admin/customers', neu({ email: 'frau@example.invalid', kundenart: 'privat' }), admin)
assert.equal(b.body.customer.kundenart, 'privat')
})
test('Ändern: ohne Feld bleibt die Kundenart, gültig ändert sie, ungültig ist 400 ohne Änderung', async () => {
const id = [...aw.docs('customers').keys()][0]
assert.equal((await send('PATCH', `/api/admin/customers/${id}`, { body: { name: 'Neu' }, ...admin })).body.customer.kundenart, 'unternehmen')
assert.equal((await send('PATCH', `/api/admin/customers/${id}`, { body: { kundenart: 'privat' }, ...admin })).body.customer.kundenart, 'privat')
const bad = await send('PATCH', `/api/admin/customers/${id}`, { body: { kundenart: 'x' }, ...admin })
assert.deepEqual([bad.status, bad.body.code], [400, 'kundenart_fehlt'])
assert.equal(aw.docs('customers').get(id).kundenart, 'privat')
})
test('off (Schalter aus): Anlegen ohne Kundenart wie heute, kein Feld im Dokument; ungültig mitgeschickt bleibt 400', async () => {
// als letzter Test der Datei: legt Kunden an, die die Tests davor nicht sehen sollen
modus('off')
const a = await post('/api/admin/customers', neu({ email: 'off1@example.invalid' }), admin)
assert.deepEqual([a.status, a.body.customer.kundenart], [201, ''])
assert.equal(Object.hasOwn(aw.docs('customers').get(a.body.customer.$id), 'kundenart'), false)
assert.equal((await post('/api/admin/customers', neu({ email: 'off2@example.invalid', kundenart: '' }), admin)).status, 201)
assert.equal((await post('/api/admin/customers', neu({ email: 'off3@example.invalid', kundenart: 'privat' }), admin)).status, 201)
const bad = await post('/api/admin/customers', neu({ email: 'off4@example.invalid', kundenart: 'firma' }), admin)
assert.deepEqual([bad.status, bad.body.code], [400, 'kundenart_fehlt'])
})