Files
opencrm/backend/src/controllers/customerReferral.controller.ts
T
duffyduckandClaude Opus 4.7 cda6d2814e 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>
2026-07-27 17:50:15 +02:00

134 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}