frontend_nyla/src/utils/mandateBillingFormMerge.ts
ValueOn AG 9b99020686 feat(billing): Nutzerhinweise bei leerem Budget + Mandats-Mail (402/SSE)
Gateway
- InsufficientBalanceException: billingModel, userAction (TOP_UP_SELF /
  CONTACT_MANDATE_ADMIN), DE/EN-Texte, toClientDict(), fromBalanceCheck()
- HTTP 402 + JSON detail für globale API-Fehlerbehandlung
- AI/Chatbot: vor Raise ggf. E-Mail an BillingSettings.notifyEmails
  (PREPAY_MANDATE, Throttle 1h/Mandat) via billingExhaustedNotify
- Agent-Loop & Workspace-Route: SSE-ERROR mit strukturiertem Billing-Payload
- datamodelBilling: notifyEmails-Doku für Pool-Alerts
frontend_nyla
- useWorkspace: SSE onError für INSUFFICIENT_BALANCE mit messageDe/En
  und Hinweis auf Billing-Pfad bei TOP_UP_SELF
2026-03-21 01:34:47 +01:00

163 lines
5.1 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 = [
'billingModel',
'defaultUserCredit',
'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: 'billingModel',
type: 'select',
label: 'Abrechnungsmodell',
description: 'Vorauszahlung auf Mandanten- oder Benutzerkonten.',
required: false,
default: 'PREPAY_MANDATE',
editable: true,
order: 100,
options: [
{ value: 'PREPAY_MANDATE', label: 'Vorauszahlung (Mandanten-Guthaben)' },
{ value: 'PREPAY_USER', label: 'Vorauszahlung pro Benutzer' },
],
},
{
name: 'defaultUserCredit',
type: 'float',
label: 'Startguthaben neuem Benutzer (CHF)',
description:
'Nur relevant bei PREPAY_USER (u. a. Root-Mandant). Sonst meist 0.',
required: false,
default: 0,
editable: true,
order: 101,
},
{
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: 103,
},
{
name: 'notifyEmails',
type: 'textarea',
label: 'Benachrichtigungs-E-Mails',
description: 'Eine Adresse pro Zeile oder durch Komma/Semikolon getrennt.',
required: false,
default: '',
editable: true,
order: 104,
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);
}
/** Build initial form values: mandate row + billing settings (notifyEmails as multi-line string). */
function _normalizeBillingModelUi(raw: string | undefined): BillingSettings['billingModel'] {
if (raw === 'PREPAY_USER') return 'PREPAY_USER';
return 'PREPAY_MANDATE';
}
export function mergeBillingIntoMandateFormData(
mandate: Record<string, unknown>,
settings: BillingSettings | null
): Record<string, unknown> {
if (!settings) {
return {
...mandate,
billingModel: 'PREPAY_MANDATE',
defaultUserCredit: 0,
warningThresholdPercent: 10,
notifyOnWarning: true,
notifyEmails: '',
};
}
return {
...mandate,
billingModel: _normalizeBillingModelUi(settings.billingModel),
defaultUserCredit: Number(settings.defaultUserCredit ?? 0),
warningThresholdPercent: Number(settings.warningThresholdPercent ?? 10),
notifyOnWarning: settings.notifyOnWarning ?? true,
notifyEmails: (settings.notifyEmails || []).join('\n'),
};
}
/** Split form submit payload into mandate PUT body and billing POST body. */
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;
const billingUpdate: BillingSettingsUpdate = {};
if ('billingModel' in formData && formData.billingModel !== undefined && formData.billingModel !== '') {
billingUpdate.billingModel = formData.billingModel as BillingSettingsUpdate['billingModel'];
}
{
const raw = formData.defaultUserCredit;
const n =
raw === undefined || raw === null || raw === ''
? 0
: Number(raw);
if (!Number.isNaN(n)) {
billingUpdate.defaultUserCredit = n;
}
}
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 };
}