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
163 lines
5.1 KiB
TypeScript
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 };
|
|
}
|