Vertrag: Kunden-/Vertragsnummer bei Vertriebsplattform

Viele Vertriebsplattformen vergeben eigene Nummern, die nicht mit
denen des Endanbieters identisch sind. Zwei neue optionale Felder
unter "Anbieter & Tarif".

- Schema: Contract.customerNumberAtSalesPlatform +
  contractNumberAtSalesPlatform, Migration mit IF NOT EXISTS.
- ContractForm: zwei neue Inputs direkt unter den entsprechenden
  Provider-Feldern.
- ContractDetail: eigene Zeilen mit CopyButton.
- Audit-Log-Mapping + Renewal-Copy + XSS-Strip-Whitelist mitgezogen.
- Bonus: contractNumberAtProvider war im Renewal-Copy und Audit-
  Label-Mapping fehlend – mitkorrigiert.
This commit is contained in:
2026-06-03 18:13:17 +02:00
parent 101369c205
commit fcc3b04725
9 changed files with 59 additions and 0 deletions
@@ -0,0 +1,7 @@
-- Vertrieb-/Provider-Trennung: viele Plattformen vergeben eigene Kunden-/
-- Vertragsnummern, die nicht mit denen beim Endanbieter identisch sind.
-- Zwei neue optionale Felder unter "Anbieter & Tarif".
ALTER TABLE `Contract`
ADD COLUMN IF NOT EXISTS `customerNumberAtSalesPlatform` VARCHAR(191) NULL,
ADD COLUMN IF NOT EXISTS `contractNumberAtSalesPlatform` VARCHAR(191) NULL;
+2
View File
@@ -687,6 +687,8 @@ model Contract {
tariffName String? tariffName String?
customerNumberAtProvider String? customerNumberAtProvider String?
contractNumberAtProvider String? // Vertragsnummer beim Anbieter contractNumberAtProvider String? // Vertragsnummer beim Anbieter
customerNumberAtSalesPlatform String? // Kundennummer bei der Vertriebsplattform
contractNumberAtSalesPlatform String? // Vertragsnummer bei der Vertriebsplattform
priceFirst12Months String? // Preis erste 12 Monate priceFirst12Months String? // Preis erste 12 Monate
priceFrom13Months String? // Preis ab 13. Monat priceFrom13Months String? // Preis ab 13. Monat
priceAfter24Months String? // Preis nach 24 Monaten priceAfter24Months String? // Preis nach 24 Monaten
@@ -203,6 +203,9 @@ export async function updateContract(req: AuthRequest, res: Response): Promise<v
const fieldLabels: Record<string, string> = { const fieldLabels: Record<string, string> = {
status: 'Status', startDate: 'Vertragsbeginn', endDate: 'Vertragsende', status: 'Status', startDate: 'Vertragsbeginn', endDate: 'Vertragsende',
portalUsername: 'Portal-Benutzername', customerNumberAtProvider: 'Kundennummer beim Anbieter', portalUsername: 'Portal-Benutzername', customerNumberAtProvider: 'Kundennummer beim Anbieter',
contractNumberAtProvider: 'Vertragsnummer beim Anbieter',
customerNumberAtSalesPlatform: 'Kundennummer bei Vertriebsplattform',
contractNumberAtSalesPlatform: 'Vertragsnummer bei Vertriebsplattform',
providerId: 'Anbieter', tariffId: 'Tarif', cancellationPeriodId: 'Kündigungsfrist', providerId: 'Anbieter', tariffId: 'Tarif', cancellationPeriodId: 'Kündigungsfrist',
contractDurationId: 'Vertragslaufzeit', platformId: 'Vertriebsplattform', contractDurationId: 'Vertragslaufzeit', platformId: 'Vertriebsplattform',
cancellationDate: 'Kündigungsdatum', cancellationSentDate: 'Kündigung gesendet am', cancellationDate: 'Kündigungsdatum', cancellationSentDate: 'Kündigung gesendet am',
+5
View File
@@ -203,6 +203,8 @@ interface ContractCreateData {
providerName?: string; providerName?: string;
tariffName?: string; tariffName?: string;
customerNumberAtProvider?: string; customerNumberAtProvider?: string;
customerNumberAtSalesPlatform?: string;
contractNumberAtSalesPlatform?: string;
priceFirst12Months?: string; priceFirst12Months?: string;
priceFrom13Months?: string; priceFrom13Months?: string;
priceAfter24Months?: string; priceAfter24Months?: string;
@@ -896,6 +898,9 @@ export async function createRenewalContract(previousContractId: number) {
providerName: previousContract.providerName, providerName: previousContract.providerName,
tariffName: previousContract.tariffName, tariffName: previousContract.tariffName,
customerNumberAtProvider: previousContract.customerNumberAtProvider, customerNumberAtProvider: previousContract.customerNumberAtProvider,
contractNumberAtProvider: previousContract.contractNumberAtProvider,
customerNumberAtSalesPlatform: previousContract.customerNumberAtSalesPlatform,
contractNumberAtSalesPlatform: previousContract.contractNumberAtSalesPlatform,
portalUsername: previousContract.portalUsername, portalUsername: previousContract.portalUsername,
portalPasswordEncrypted: previousContract.portalPasswordEncrypted, portalPasswordEncrypted: previousContract.portalPasswordEncrypted,
commission: previousContract.commission, commission: previousContract.commission,
+2
View File
@@ -75,6 +75,8 @@ const CONTRACT_DISPLAY_STRING_FIELDS = [
'tariffName', 'tariffName',
'customerNumberAtProvider', 'customerNumberAtProvider',
'contractNumberAtProvider', 'contractNumberAtProvider',
'customerNumberAtSalesPlatform',
'contractNumberAtSalesPlatform',
'portalUsername', 'portalUsername',
'previousProviderName', 'previousProviderName',
'previousCustomerNumber', 'previousCustomerNumber',
+14
View File
@@ -97,6 +97,20 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt ## ✅ Erledigt
- [x] **🆕 Vertrag: Kunden-/Vertragsnummer bei Vertriebsplattform**
- Zwei neue optionale Felder
`Contract.customerNumberAtSalesPlatform` +
`contractNumberAtSalesPlatform`, Migration
`20260603150000_contract_sales_platform_numbers` mit
`IF NOT EXISTS`.
- Im ContractForm direkt unter „Kundennummer/Vertragsnummer beim
Anbieter" angeordnet. ContractDetail zeigt sie als eigene Zeilen
mit Copy-Button. Audit-Log-Mapping + Renewal-Copy + XSS-Strip
(CONTRACT_DISPLAY_STRING_FIELDS) mitgezogen.
- Bonus: das fehlende `contractNumberAtProvider` im Renewal-Copy
und Audit-Label-Mapping ist gleich mit drin wurde bisher
nicht in VVL-Folgeverträge kopiert.
- [x] **🆕 Email-Links öffnen im neuen Tab** - [x] **🆕 Email-Links öffnen im neuen Tab**
- In `EmailDetail` nach der DOMPurify-Sanitize jedes `<a>`-Element - In `EmailDetail` nach der DOMPurify-Sanitize jedes `<a>`-Element
auf `target="_blank"` + `rel="noopener noreferrer"` gesetzt. Letzteres auf `target="_blank"` + `rel="noopener noreferrer"` gesetzt. Letzteres
@@ -2080,6 +2080,24 @@ export default function ContractDetail() {
</dd> </dd>
</div> </div>
)} )}
{c.customerNumberAtSalesPlatform && (
<div>
<dt className="text-sm text-gray-500">Kundennr. Vertriebsplattform</dt>
<dd className="font-mono flex items-center gap-1">
{c.customerNumberAtSalesPlatform}
<CopyButton value={c.customerNumberAtSalesPlatform} />
</dd>
</div>
)}
{c.contractNumberAtSalesPlatform && (
<div>
<dt className="text-sm text-gray-500">Vertragsnr. Vertriebsplattform</dt>
<dd className="font-mono flex items-center gap-1">
{c.contractNumberAtSalesPlatform}
<CopyButton value={c.contractNumberAtSalesPlatform} />
</dd>
</div>
)}
{c.salesPlatform && ( {c.salesPlatform && (
<div> <div>
<dt className="text-sm text-gray-500">Vertriebsplattform</dt> <dt className="text-sm text-gray-500">Vertriebsplattform</dt>
@@ -302,6 +302,8 @@ export default function ContractForm() {
tariffName: c.tariffName || '', tariffName: c.tariffName || '',
customerNumberAtProvider: c.customerNumberAtProvider || '', customerNumberAtProvider: c.customerNumberAtProvider || '',
contractNumberAtProvider: c.contractNumberAtProvider || '', contractNumberAtProvider: c.contractNumberAtProvider || '',
customerNumberAtSalesPlatform: c.customerNumberAtSalesPlatform || '',
contractNumberAtSalesPlatform: c.contractNumberAtSalesPlatform || '',
priceFirst12Months: c.priceFirst12Months || '', priceFirst12Months: c.priceFirst12Months || '',
priceFrom13Months: c.priceFrom13Months || '', priceFrom13Months: c.priceFrom13Months || '',
priceAfter24Months: c.priceAfter24Months || '', priceAfter24Months: c.priceAfter24Months || '',
@@ -556,6 +558,8 @@ export default function ContractForm() {
tariffName: emptyToNull(data.tariffName), tariffName: emptyToNull(data.tariffName),
customerNumberAtProvider: emptyToNull(data.customerNumberAtProvider), customerNumberAtProvider: emptyToNull(data.customerNumberAtProvider),
contractNumberAtProvider: emptyToNull(data.contractNumberAtProvider), contractNumberAtProvider: emptyToNull(data.contractNumberAtProvider),
customerNumberAtSalesPlatform: emptyToNull(data.customerNumberAtSalesPlatform),
contractNumberAtSalesPlatform: emptyToNull(data.contractNumberAtSalesPlatform),
priceFirst12Months: emptyToNull(data.priceFirst12Months), priceFirst12Months: emptyToNull(data.priceFirst12Months),
priceFrom13Months: emptyToNull(data.priceFrom13Months), priceFrom13Months: emptyToNull(data.priceFrom13Months),
priceAfter24Months: emptyToNull(data.priceAfter24Months), priceAfter24Months: emptyToNull(data.priceAfter24Months),
@@ -952,6 +956,8 @@ export default function ContractForm() {
/> />
<Input label="Kundennummer beim Anbieter" {...register('customerNumberAtProvider')} /> <Input label="Kundennummer beim Anbieter" {...register('customerNumberAtProvider')} />
<Input label="Vertragsnummer beim Anbieter" {...register('contractNumberAtProvider')} /> <Input label="Vertragsnummer beim Anbieter" {...register('contractNumberAtProvider')} />
<Input label="Kundennummer bei Vertriebsplattform" {...register('customerNumberAtSalesPlatform')} />
<Input label="Vertragsnummer bei Vertriebsplattform" {...register('contractNumberAtSalesPlatform')} />
<Input label="Provision (€)" type="number" step="0.01" {...register('commission')} /> <Input label="Provision (€)" type="number" step="0.01" {...register('commission')} />
<Input label="Preis erste 12 Monate" {...register('priceFirst12Months')} placeholder="z.B. 29,99 €/Monat" /> <Input label="Preis erste 12 Monate" {...register('priceFirst12Months')} placeholder="z.B. 29,99 €/Monat" />
<Input label="Preis ab 13. Monat" {...register('priceFrom13Months')} placeholder="z.B. 39,99 €/Monat" /> <Input label="Preis ab 13. Monat" {...register('priceFrom13Months')} placeholder="z.B. 39,99 €/Monat" />
+2
View File
@@ -454,6 +454,8 @@ export interface Contract {
tariffName?: string; tariffName?: string;
customerNumberAtProvider?: string; customerNumberAtProvider?: string;
contractNumberAtProvider?: string; contractNumberAtProvider?: string;
customerNumberAtSalesPlatform?: string;
contractNumberAtSalesPlatform?: string;
priceFirst12Months?: string; priceFirst12Months?: string;
priceFrom13Months?: string; priceFrom13Months?: string;
priceAfter24Months?: string; priceAfter24Months?: string;