refactor: remove obsolete member management API module
Some checks failed
Workflow / ʦ TypeScript (pull_request) Failing after 5m57s
Workflow / ⚫️ Test (pull_request) Has been skipped

This commit is contained in:
T. Zehetbauer
2026-04-03 14:08:31 +02:00
parent 124c6a632a
commit 5c5aaabae5
132 changed files with 10107 additions and 3442 deletions

View File

@@ -0,0 +1,68 @@
import { z } from 'zod';
export const CommunicationTypeEnum = z.enum([
'email',
'phone',
'letter',
'meeting',
'note',
'sms',
]);
export type CommunicationType = z.infer<typeof CommunicationTypeEnum>;
export const CommunicationDirectionEnum = z.enum([
'inbound',
'outbound',
'internal',
]);
export type CommunicationDirection = z.infer<typeof CommunicationDirectionEnum>;
export const CreateCommunicationSchema = z
.object({
memberId: z.string().uuid(),
accountId: z.string().uuid(),
type: CommunicationTypeEnum,
direction: CommunicationDirectionEnum.default('outbound'),
subject: z.string().max(500).optional(),
body: z.string().max(50000).optional(),
emailTo: z.string().email().optional().or(z.literal('')),
emailCc: z.string().max(1000).optional(),
emailMessageId: z.string().max(256).optional(),
attachmentPaths: z.array(z.string().max(512)).max(10).optional(),
})
.superRefine((data, ctx) => {
// Email type requires a recipient
if (
data.type === 'email' &&
(!data.emailTo || data.emailTo.trim() === '')
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'E-Mail-Empfänger ist für den Typ "E-Mail" erforderlich',
path: ['emailTo'],
});
}
});
export type CreateCommunicationInput = z.infer<
typeof CreateCommunicationSchema
>;
export const CommunicationListFiltersSchema = z.object({
memberId: z.string().uuid(),
accountId: z.string().uuid(),
type: CommunicationTypeEnum.optional(),
direction: CommunicationDirectionEnum.optional(),
search: z.string().max(256).optional(),
page: z.number().int().min(1).default(1),
pageSize: z.number().int().min(1).max(100).default(25),
});
export type CommunicationListFilters = z.infer<
typeof CommunicationListFiltersSchema
>;
export const DeleteCommunicationSchema = z.object({
communicationId: z.string().uuid(),
accountId: z.string().uuid(),
});

View File

@@ -17,66 +17,140 @@ export const SepaMandateStatusEnum = z.enum([
'expired',
]);
export const CreateMemberSchema = z.object({
accountId: z.string().uuid(),
memberNumber: z.string().optional(),
firstName: z.string().min(1).max(128),
lastName: z.string().min(1).max(128),
dateOfBirth: z.string().optional(),
gender: z.enum(['male', 'female', 'diverse']).optional(),
title: z.string().max(32).optional(),
email: z.string().email().optional().or(z.literal('')),
phone: z.string().max(32).optional(),
mobile: z.string().max(32).optional(),
street: z.string().max(256).optional(),
houseNumber: z.string().max(16).optional(),
postalCode: z.string().max(10).optional(),
city: z.string().max(128).optional(),
country: z.string().max(2).default('DE'),
status: MembershipStatusEnum.default('active'),
entryDate: z.string().default(() => new Date().toISOString().split('T')[0]!),
duesCategoryId: z.string().uuid().optional(),
iban: z.string().max(34).optional(),
bic: z.string().max(11).optional(),
accountHolder: z.string().max(128).optional(),
gdprConsent: z.boolean().default(false),
notes: z.string().optional(),
// New optional fields
salutation: z.string().optional(),
street2: z.string().optional(),
phone2: z.string().optional(),
fax: z.string().optional(),
birthplace: z.string().optional(),
birthCountry: z.string().default('DE'),
isHonorary: z.boolean().default(false),
isFoundingMember: z.boolean().default(false),
isYouth: z.boolean().default(false),
isRetiree: z.boolean().default(false),
isProbationary: z.boolean().default(false),
isTransferred: z.boolean().default(false),
exitDate: z.string().optional(),
exitReason: z.string().optional(),
guardianName: z.string().optional(),
guardianPhone: z.string().optional(),
guardianEmail: z.string().optional(),
duesYear: z.number().int().optional(),
duesPaid: z.boolean().default(false),
additionalFees: z.number().default(0),
exemptionType: z.string().optional(),
exemptionReason: z.string().optional(),
exemptionAmount: z.number().optional(),
gdprNewsletter: z.boolean().default(false),
gdprInternet: z.boolean().default(false),
gdprPrint: z.boolean().default(false),
gdprBirthdayInfo: z.boolean().default(false),
sepaMandateReference: z.string().optional(),
});
// --- Shared validators ---
/** IBAN validation with mod-97 checksum (ISO 13616) */
export function validateIban(iban: string): boolean {
const cleaned = iban.replace(/\s/g, '').toUpperCase();
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{4,30}$/.test(cleaned)) return false;
const rearranged = cleaned.slice(4) + cleaned.slice(0, 4);
let numStr = '';
for (const char of rearranged) {
const code = char.charCodeAt(0);
numStr += code >= 65 && code <= 90 ? (code - 55).toString() : char;
}
let remainder = 0;
for (const digit of numStr) {
remainder = (remainder * 10 + parseInt(digit, 10)) % 97;
}
return remainder === 1;
}
const ibanSchema = z
.string()
.max(34)
.optional()
.refine((v) => !v || v.trim() === '' || validateIban(v), {
message: 'Ungültige IBAN (Prüfsumme fehlerhaft)',
});
const dateNotFutureSchema = (fieldName: string) =>
z
.string()
.optional()
.refine((v) => !v || new Date(v) <= new Date(), {
message: `${fieldName} darf nicht in der Zukunft liegen`,
});
// --- Main schemas ---
export const CreateMemberSchema = z
.object({
accountId: z.string().uuid(),
memberNumber: z.string().optional(),
firstName: z.string().min(1).max(128),
lastName: z.string().min(1).max(128),
dateOfBirth: dateNotFutureSchema('Geburtsdatum'),
gender: z.enum(['male', 'female', 'diverse']).optional(),
title: z.string().max(32).optional(),
email: z.string().email().optional().or(z.literal('')),
phone: z.string().max(32).optional(),
mobile: z.string().max(32).optional(),
street: z.string().max(256).optional(),
houseNumber: z.string().max(16).optional(),
postalCode: z.string().max(10).optional(),
city: z.string().max(128).optional(),
country: z.string().max(2).default('DE'),
status: MembershipStatusEnum.default('active'),
entryDate: z
.string()
.default(() => new Date().toISOString().split('T')[0]!),
duesCategoryId: z.string().uuid().optional(),
iban: ibanSchema,
bic: z.string().max(11).optional(),
accountHolder: z.string().max(128).optional(),
gdprConsent: z.boolean().default(false),
notes: z.string().optional(),
salutation: z.string().optional(),
street2: z.string().optional(),
phone2: z.string().optional(),
fax: z.string().optional(),
birthplace: z.string().optional(),
birthCountry: z.string().default('DE'),
isHonorary: z.boolean().default(false),
isFoundingMember: z.boolean().default(false),
isYouth: z.boolean().default(false),
isRetiree: z.boolean().default(false),
isProbationary: z.boolean().default(false),
isTransferred: z.boolean().default(false),
exitDate: z.string().optional(),
exitReason: z.string().optional(),
guardianName: z.string().optional(),
guardianPhone: z.string().optional(),
guardianEmail: z.string().optional(),
duesYear: z.number().int().optional(),
duesPaid: z.boolean().default(false),
additionalFees: z.number().default(0),
exemptionType: z.string().optional(),
exemptionReason: z.string().optional(),
exemptionAmount: z.number().optional(),
gdprNewsletter: z.boolean().default(false),
gdprInternet: z.boolean().default(false),
gdprPrint: z.boolean().default(false),
gdprBirthdayInfo: z.boolean().default(false),
sepaMandateReference: z.string().optional(),
})
.superRefine((data, ctx) => {
// Cross-field: exit_date must be after entry_date
if (data.exitDate && data.entryDate && data.exitDate < data.entryDate) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Austrittsdatum muss nach dem Eintrittsdatum liegen',
path: ['exitDate'],
});
}
// Cross-field: entry_date must be after date_of_birth
if (
data.dateOfBirth &&
data.entryDate &&
data.entryDate < data.dateOfBirth
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Eintrittsdatum muss nach dem Geburtsdatum liegen',
path: ['entryDate'],
});
}
// Cross-field: youth members should have guardian info
if (data.isYouth && !data.guardianName) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Jugendmitglieder benötigen einen Erziehungsberechtigten',
path: ['guardianName'],
});
}
});
export type CreateMemberInput = z.infer<typeof CreateMemberSchema>;
export const UpdateMemberSchema = CreateMemberSchema.partial().extend({
memberId: z.string().uuid(),
isArchived: z.boolean().optional(),
version: z.number().int().optional(),
});
export type UpdateMemberInput = z.infer<typeof UpdateMemberSchema>;
@@ -128,7 +202,13 @@ export const CreateSepaMandateSchema = z.object({
memberId: z.string().uuid(),
accountId: z.string().uuid(),
mandateReference: z.string().min(1),
iban: z.string().min(15).max(34),
iban: z
.string()
.min(15)
.max(34)
.refine((v) => validateIban(v), {
message: 'Ungültige IBAN (Prüfsumme fehlerhaft)',
}),
bic: z.string().optional(),
accountHolder: z.string().min(1),
mandateDate: z.string(),
@@ -149,7 +229,14 @@ export type UpdateDuesCategoryInput = z.infer<typeof UpdateDuesCategorySchema>;
export const UpdateMandateSchema = z.object({
mandateId: z.string().uuid(),
iban: z.string().min(15).max(34).optional(),
iban: z
.string()
.min(15)
.max(34)
.refine((v) => validateIban(v), {
message: 'Ungültige IBAN (Prüfsumme fehlerhaft)',
})
.optional(),
bic: z.string().optional(),
accountHolder: z.string().optional(),
sequence: z.enum(['FRST', 'RCUR', 'FNAL', 'OOFF']).optional(),
@@ -191,6 +278,7 @@ export const MemberSearchFiltersSchema = z.object({
search: z.string().optional(),
status: z.array(MembershipStatusEnum).optional(),
departmentIds: z.array(z.string().uuid()).optional(),
tagIds: z.array(z.string().uuid()).optional(),
duesCategoryId: z.string().uuid().optional(),
flags: z
.array(

View File

@@ -0,0 +1,76 @@
import { z } from 'zod';
export const TriggerEventEnum = z.enum([
'application.submitted',
'application.approved',
'application.rejected',
'member.created',
'member.status_changed',
'member.birthday',
'member.anniversary',
'dues.unpaid',
'mandate.revoked',
]);
export const NotificationChannelEnum = z.enum(['in_app', 'email', 'both']);
export const RecipientTypeEnum = z.enum([
'admin',
'member',
'specific_user',
'role_holder',
]);
export const CreateNotificationRuleSchema = z.object({
accountId: z.string().uuid(),
triggerEvent: TriggerEventEnum,
channel: NotificationChannelEnum.default('in_app'),
recipientType: RecipientTypeEnum,
recipientConfig: z.record(z.string(), z.unknown()).default({}),
subjectTemplate: z.string().max(256).optional(),
messageTemplate: z.string().min(1).max(2000),
isActive: z.boolean().default(true),
});
export type CreateNotificationRuleInput = z.infer<
typeof CreateNotificationRuleSchema
>;
export const UpdateNotificationRuleSchema = z.object({
ruleId: z.string().uuid(),
triggerEvent: TriggerEventEnum.optional(),
channel: NotificationChannelEnum.optional(),
recipientType: RecipientTypeEnum.optional(),
recipientConfig: z.record(z.string(), z.unknown()).optional(),
subjectTemplate: z.string().max(256).optional(),
messageTemplate: z.string().min(1).max(2000).optional(),
isActive: z.boolean().optional(),
});
export const DeleteNotificationRuleSchema = z.object({
ruleId: z.string().uuid(),
});
export const ListNotificationRulesSchema = z.object({
accountId: z.string().uuid(),
});
// Scheduled jobs
export const JobTypeEnum = z.enum([
'birthday_notification',
'anniversary_notification',
'dues_reminder',
'data_quality_check',
'gdpr_retention_check',
]);
export const ConfigureScheduledJobSchema = z.object({
accountId: z.string().uuid(),
jobType: JobTypeEnum,
isEnabled: z.boolean(),
config: z.record(z.string(), z.unknown()).default({}),
});
export const ListScheduledJobsSchema = z.object({
accountId: z.string().uuid(),
});

View File

@@ -0,0 +1,41 @@
import { z } from 'zod';
const hexColorRegex = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
export const CreateTagSchema = z.object({
accountId: z.string().uuid(),
name: z.string().min(1).max(64),
color: z
.string()
.regex(hexColorRegex, 'Ungültiger Hex-Farbcode')
.default('#6B7280'),
description: z.string().max(256).optional(),
});
export type CreateTagInput = z.infer<typeof CreateTagSchema>;
export const UpdateTagSchema = z.object({
tagId: z.string().uuid(),
name: z.string().min(1).max(64).optional(),
color: z.string().regex(hexColorRegex, 'Ungültiger Hex-Farbcode').optional(),
description: z.string().max(256).optional(),
sortOrder: z.number().int().min(0).optional(),
});
export type UpdateTagInput = z.infer<typeof UpdateTagSchema>;
export const DeleteTagSchema = z.object({
tagId: z.string().uuid(),
});
export const AssignTagSchema = z.object({
memberId: z.string().uuid(),
tagId: z.string().uuid(),
});
export const BulkAssignTagSchema = z.object({
memberIds: z.array(z.string().uuid()).min(1),
tagId: z.string().uuid(),
});
export const ListTagsSchema = z.object({
accountId: z.string().uuid(),
});