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:
1 parent
8d3eb73db7
commit
5c21023e28
3 files changed
+93
-2
No files matched your search
@@ -11,6 +11,10 @@ import {
|
||||
|
||||
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.get('/', async (_req, res) => {
|
||||
@@ -29,7 +33,7 @@ router.post('/', async (req, res) => {
|
||||
return res.status(201).json(result)
|
||||
} catch (err) {
|
||||
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)
|
||||
} catch (err) {
|
||||
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) })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
Query,
|
||||
} from './appwriteAdmin.js'
|
||||
import { forgetPasswordVersion } from './passwordVersion.js'
|
||||
import { KUNDENARTEN, kundenartGespeichert } from './kundenart.js'
|
||||
import { neueAnsichtModus } from './kundenansicht/schalter.js'
|
||||
|
||||
function readPortalPassword(customer) {
|
||||
if (customer?.portalPassword) return customer.portalPassword
|
||||
@@ -27,6 +29,17 @@ function notesWithoutPortalPassword(notes) {
|
||||
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 } = {}) {
|
||||
const storedPassword = portalPassword || readPortalPassword(customer)
|
||||
return {
|
||||
@@ -41,6 +54,7 @@ function sanitizeCustomer(customer, { portalPassword } = {}) {
|
||||
appwriteUserId: customer.appwriteUserId || '',
|
||||
customerStatus: customer.customerStatus || '',
|
||||
archived: Boolean(customer.archived),
|
||||
kundenart: kundenartGespeichert(customer),
|
||||
portalPassword: storedPassword || '',
|
||||
}
|
||||
}
|
||||
@@ -73,6 +87,7 @@ export async function createCustomerWithPortalAccess(payload) {
|
||||
email,
|
||||
phone = '',
|
||||
password,
|
||||
kundenart,
|
||||
} = payload || {}
|
||||
|
||||
if (!name?.trim()) {
|
||||
@@ -90,6 +105,9 @@ export async function createCustomerWithPortalAccess(payload) {
|
||||
error.status = 400
|
||||
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())
|
||||
if (existing) {
|
||||
@@ -117,6 +135,7 @@ export async function createCustomerWithPortalAccess(payload) {
|
||||
portalAccessEnabled: true,
|
||||
appwriteUserId: appwriteUser.$id,
|
||||
customerStatus: 'active',
|
||||
...(kundenartFehlt(kundenart) ? {} : { kundenart }),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
@@ -150,6 +169,7 @@ export async function updateCustomerWithPortalAccess(customerId, payload) {
|
||||
email,
|
||||
phone,
|
||||
password,
|
||||
kundenart,
|
||||
} = payload || {}
|
||||
|
||||
const nextEmail = (email ?? customer.email ?? '').trim()
|
||||
@@ -163,6 +183,8 @@ export async function updateCustomerWithPortalAccess(customerId, payload) {
|
||||
error.status = 400
|
||||
throw error
|
||||
}
|
||||
// Kundenart optional: nur wenn mitgeschickt, dann gueltig (V4)
|
||||
if (kundenart !== undefined && !KUNDENARTEN.includes(kundenart)) throw kundenartFehler()
|
||||
|
||||
const normalizedPassword =
|
||||
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() } : {}),
|
||||
email: nextEmail,
|
||||
...(phone !== undefined ? { phone: String(phone).trim() } : {}),
|
||||
...(kundenart !== undefined ? { kundenart } : {}),
|
||||
...(nextPortalPassword ? { portalPassword: nextPortalPassword } : {}),
|
||||
...(notesChanged ? { notes: cleanedNotes } : {}),
|
||||
// CRM-Stufe (lead/customer/lost) nicht ueberschreiben - nur setzen, wenn leer
|
||||
|
||||
@@ -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'])
|
||||
})
|
||||
Reference in new issue
Block a user