/** * Merges mandate core fields with billing settings for SysAdmin mandate forms. * Billing is stored separately (BillingSettings); attributes API only exposes Mandate model fields. */ import type { AttributeDefinition } from '../components/FormGenerator/FormGeneratorForm'; import type { BillingSettings, BillingSettingsUpdate } from '../api/billingApi'; export const mandateBillingFieldNames = [ 'warningThresholdPercent', 'notifyOnWarning', 'notifyEmails', ] as const; export type MandateBillingFieldName = (typeof mandateBillingFieldNames)[number]; /** FormGenerator attribute definitions for mandate billing (appended after /api/attributes/Mandate). */ export function getMandateBillingFormAttributes(): AttributeDefinition[] { return [ { name: 'warningThresholdPercent', type: 'float', label: 'Warnschwelle (%)', description: 'Benachrichtigung, wenn das Guthaben unter diesem Prozentsatz fällt.', required: false, default: 10, editable: true, order: 102, }, { name: 'notifyOnWarning', type: 'checkbox', label: 'Bei Warnung benachrichtigen', required: false, default: true, editable: true, order: 102, }, { name: 'notifyEmails', type: 'textarea', label: 'Benachrichtigungs-E-Mails', description: 'Eine Adresse pro Zeile oder durch Komma/Semikolon getrennt.', required: false, default: '', editable: true, order: 103, minRows: 2, maxRows: 6, }, ]; } function _parseNotifyEmailsInput(val: unknown): string[] { if (Array.isArray(val)) { return val.map(String).map(s => s.trim()).filter(Boolean); } if (typeof val !== 'string') { return []; } return val .split(/[\n,;]+/) .map(s => s.trim()) .filter(Boolean); } /** Formatted multi-line invoice address for read-only display. */ export function formatMandateInvoiceAddress(data: Record): string | null { const lines: string[] = []; const company = data.invoiceCompanyName; const contact = data.invoiceContactName; const line1 = data.invoiceLine1; const line2 = data.invoiceLine2; const postal = data.invoicePostalCode; const city = data.invoiceCity; const state = data.invoiceState; const country = data.invoiceCountry; const vat = data.invoiceVatNumber; const email = data.invoiceEmail; if (typeof company === 'string' && company.trim()) lines.push(company.trim()); if (typeof contact === 'string' && contact.trim()) lines.push(`z. H. ${contact.trim()}`); if (typeof line1 === 'string' && line1.trim()) lines.push(line1.trim()); if (typeof line2 === 'string' && line2.trim()) lines.push(line2.trim()); const cityLine = [postal, city].filter((v) => typeof v === 'string' && v.trim()).join(' '); if (cityLine.trim()) { const withState = typeof state === 'string' && state.trim() ? `${cityLine.trim()} (${state.trim()})` : cityLine.trim(); lines.push(withState); } if (typeof country === 'string' && country.trim()) lines.push(country.trim()); if (typeof vat === 'string' && vat.trim()) lines.push(`UID: ${vat.trim()}`); if (typeof email === 'string' && email.trim()) lines.push(email.trim()); return lines.length > 0 ? lines.join('\n') : null; } export function isMandateBillingField(fieldName: string): fieldName is MandateBillingFieldName { return (mandateBillingFieldNames as readonly string[]).includes(fieldName); } export function mergeBillingIntoMandateFormData( mandate: Record, settings: BillingSettings | null ): Record { if (!settings) { return { ...mandate, warningThresholdPercent: 10, notifyOnWarning: true, notifyEmails: '', }; } return { ...mandate, warningThresholdPercent: Number(settings.warningThresholdPercent ?? 10), notifyOnWarning: settings.notifyOnWarning ?? true, notifyEmails: (settings.notifyEmails || []).join('\n'), }; } /** Mandate invoice address fields (read-only display + form update). */ export const MANDATE_INVOICE_FIELD_NAMES = [ 'invoiceCompanyName', 'invoiceContactName', 'invoiceEmail', 'invoiceLine1', 'invoiceLine2', 'invoicePostalCode', 'invoiceCity', 'invoiceState', 'invoiceCountry', 'invoiceVatNumber', ] as const; const _MANDATE_INVOICE_FIELDS = MANDATE_INVOICE_FIELD_NAMES; /** * Split form submit payload into mandate PUT body and billing POST body. * * Only fields that the user can actually edit are forwarded. Audit-only / * read-only fields (id, deletedAt, isSystem, ...) are intentionally dropped. * The structured ``invoice*`` address fields are round-tripped here so the * address entered in the form is persisted on Mandate; empty strings are * normalized to ``null`` so the backend stores nothing instead of "". */ export function splitMandateAndBillingFromForm( formData: Record ): { mandatePayload: Record; billingUpdate: BillingSettingsUpdate } { const mandatePayload: Record = {}; if ('name' in formData) mandatePayload.name = formData.name; if ('label' in formData) mandatePayload.label = formData.label; if ('enabled' in formData) mandatePayload.enabled = formData.enabled; if ('mfaRequired' in formData) mandatePayload.mfaRequired = formData.mfaRequired; for (const fieldName of _MANDATE_INVOICE_FIELDS) { if (!(fieldName in formData)) continue; const raw = formData[fieldName]; if (raw === null || raw === undefined) { mandatePayload[fieldName] = null; } else if (typeof raw === 'string') { const trimmed = raw.trim(); mandatePayload[fieldName] = trimmed.length === 0 ? null : trimmed; } else { mandatePayload[fieldName] = raw; } } const billingUpdate: BillingSettingsUpdate = {}; if ( 'warningThresholdPercent' in formData && formData.warningThresholdPercent !== undefined && formData.warningThresholdPercent !== '' ) { const n = Number(formData.warningThresholdPercent); if (!Number.isNaN(n)) billingUpdate.warningThresholdPercent = n; } if ('notifyOnWarning' in formData) { billingUpdate.notifyOnWarning = Boolean(formData.notifyOnWarning); } if ('notifyEmails' in formData) { billingUpdate.notifyEmails = _parseNotifyEmailsInput(formData.notifyEmails); } return { mandatePayload, billingUpdate }; }