Files
opencrm/backend/src/controllers/customerReferral.controller.ts
T
duffyduckandClaude Opus 4.8 81cd284cc4 Referrals: 4 neue Beziehungen + Bearbeiten-Stift pro Eintrag
Beziehungs-Dropdown erweitert um Schwiegertochter/Schwiegersohn,
Schwiegermutter/Schwiegervater, Oma/Opa, Uroma/Uropa (Whitelist
front- und backend synchron).

Bearbeiten-Stift pro Zeile (vor der Muelltonne): Beziehung und/oder
Gegen-Kunde aenderbar. Neuer PUT /:customerId/referrals/:referralId
mit Whitelist-Pruefung, Doppel-Werber-409 (Self-Ausschluss),
Portal-Block und UPDATE-Auditeintrag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 10:57:04 +02:00

189 lines
6.8 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 updateReferral(req: AuthRequest, res: Response): Promise<void> {
try {
if (blockPortal(req, res)) return;
const customerId = parseInt(req.params.customerId);
const referralId = parseInt(req.params.referralId);
if (Number.isNaN(customerId) || Number.isNaN(referralId)) {
res.status(400).json({ success: false, error: 'Ungültige 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;
}
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 updated = await referralService.updateReferral({
id: referralId,
recruiterId,
recruitedId,
relationship: (relationship ?? '').toString(),
});
await logChange({
req,
action: 'UPDATE',
resourceType: 'CustomerReferral',
resourceId: updated.id.toString(),
label: `Werbe-Beziehung geändert: Kunde #${recruiterId} hat Kunde #${recruitedId} geworben (${updated.relationship})`,
customerId: recruitedId,
});
res.json({ success: true, data: updated } 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 Ändern 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);
}
}