Kundenakte: Tab "Geworben / angeworben" (Kundenempfehlungen)
Neuer Tab vor "Datenschutz", nur Mitarbeiter/Admin (nicht Portal), ohne Consent-Pflicht. Zwei Abschnitte: 1. "<Kunde> wurde an Board geholt durch:" – max. 1 Werber (DB-Unique auf recruitedId). 2. "<Kunde> hat folgende Kunden an Board geholt:" – beliebig viele. Jede Zeile: Kunde per Lupe-Such-Modal (breite Suche über Name/ Kundennr./Firma/E-Mail/Telefon) + Beziehungs-Dropdown. Löschen + Externtab-Link zur Kundenakte pro Zeile. Bidirektional aus EINEM Datensatz: "A geworben durch B" erscheint automatisch bei B unter "hat geworben"; von beiden Akten hinzufügbar/löschbar. Backend: neues Model CustomerReferral (recruiter/recruited FKs, recruitedId @unique, relationship) + Migration. Beziehungs-Whitelist serverseitig; Self-Werbung + Doppel-Werber (409) abgefangen. Portal-Token wird explizit geblockt (Defense-in-Depth, nicht nur UI-Ausblendung). CREATE/DELETE auditiert. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import { Response } from 'express';
|
||||
import { ApiResponse, AuthRequest } from '../types/index.js';
|
||||
import { logChange } from '../services/audit.service.js';
|
||||
import * as referralService from '../services/customerReferral.service.js';
|
||||
|
||||
// Der ganze Bereich ist rein für Mitarbeiter/Admins – Portal-Kunden dürfen
|
||||
// Werbe-Beziehungen weder sehen noch ändern. Defense-in-Depth: nicht nur die
|
||||
// UI blendet den Tab aus, der Endpoint blockt Portal-Token explizit.
|
||||
function blockPortal(req: AuthRequest, res: Response): boolean {
|
||||
if (req.user?.isCustomerPortal) {
|
||||
res.status(403).json({ success: false, error: 'Kein Zugriff' } as ApiResponse);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function getReferrals(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
if (blockPortal(req, res)) return;
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
if (Number.isNaN(customerId)) {
|
||||
res.status(400).json({ success: false, error: 'Ungültige Kunden-ID' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
const data = await referralService.getReferralsForCustomer(customerId);
|
||||
res.json({ success: true, data } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden der Werbe-Beziehungen' } as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchCustomers(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
if (blockPortal(req, res)) return;
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
if (Number.isNaN(customerId)) {
|
||||
res.status(400).json({ success: false, error: 'Ungültige Kunden-ID' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
const search = req.query.search;
|
||||
if (!search || typeof search !== 'string' || search.trim().length < 2) {
|
||||
res.json({ success: true, data: [] } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
const customers = await referralService.searchCustomersForReferral(search.trim(), customerId);
|
||||
res.json({ success: true, data: customers } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: 'Fehler bei der Suche' } as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createReferral(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
if (blockPortal(req, res)) return;
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
if (Number.isNaN(customerId)) {
|
||||
res.status(400).json({ success: false, error: 'Ungültige Kunden-ID' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
const { direction, otherCustomerId, relationship } = req.body ?? {};
|
||||
const otherId = parseInt(otherCustomerId);
|
||||
if (Number.isNaN(otherId)) {
|
||||
res.status(400).json({ success: false, error: 'Kein Kunde ausgewählt' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// direction bestimmt, wer Werber und wer Geworbener ist:
|
||||
// 'recruitedBy' = dieser Kunde wurde vom anderen geworben (Abschnitt 1)
|
||||
// 'recruited' = dieser Kunde hat den anderen geworben (Abschnitt 2)
|
||||
let recruiterId: number;
|
||||
let recruitedId: number;
|
||||
if (direction === 'recruitedBy') {
|
||||
recruiterId = otherId;
|
||||
recruitedId = customerId;
|
||||
} else if (direction === 'recruited') {
|
||||
recruiterId = customerId;
|
||||
recruitedId = otherId;
|
||||
} else {
|
||||
res.status(400).json({ success: false, error: 'Ungültige Richtung' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const created = await referralService.createReferral({
|
||||
recruiterId,
|
||||
recruitedId,
|
||||
relationship: (relationship ?? '').toString(),
|
||||
createdBy: req.user?.email,
|
||||
});
|
||||
|
||||
await logChange({
|
||||
req,
|
||||
action: 'CREATE',
|
||||
resourceType: 'CustomerReferral',
|
||||
resourceId: created.id.toString(),
|
||||
label: `Werbe-Beziehung angelegt: Kunde #${recruiterId} hat Kunde #${recruitedId} geworben (${created.relationship})`,
|
||||
customerId: recruitedId,
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, data: created } as ApiResponse);
|
||||
} catch (error) {
|
||||
const status = (error as referralService.ReferralError)?.status ?? 500;
|
||||
res.status(status).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Anlegen der Werbe-Beziehung',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteReferral(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
if (blockPortal(req, res)) return;
|
||||
const referralId = parseInt(req.params.referralId);
|
||||
if (Number.isNaN(referralId)) {
|
||||
res.status(400).json({ success: false, error: 'Ungültige ID' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
const ok = await referralService.deleteReferral(referralId);
|
||||
if (!ok) {
|
||||
res.status(404).json({ success: false, error: 'Eintrag nicht gefunden' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
await logChange({
|
||||
req,
|
||||
action: 'DELETE',
|
||||
resourceType: 'CustomerReferral',
|
||||
resourceId: referralId.toString(),
|
||||
label: `Werbe-Beziehung #${referralId} entfernt`,
|
||||
});
|
||||
res.json({ success: true } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Entfernen der Werbe-Beziehung' } as ApiResponse);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user