ui-nyla/src/utils/mandateBillingFormMerge.ts

186 lines
6.3 KiB
TypeScript

/**
* 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, unknown>): 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<string, unknown>,
settings: BillingSettings | null
): Record<string, unknown> {
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<string, unknown>
): { mandatePayload: Record<string, unknown>; billingUpdate: BillingSettingsUpdate } {
const mandatePayload: Record<string, unknown> = {};
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 };
}