'use server'; import { z } from 'zod'; import { getDbClient } from '../dbClient'; import { UserSettings } from '../db-types'; import { withUser } from '@/app/lib/auth'; import { AuthenticatedUser } from '../types/next-auth'; import { unstable_noStore as noStore } from 'next/cache'; import { IntlTemplateFn } from '@/app/i18n'; import { getTranslations, getLocale } from "next-intl/server"; import { revalidatePath } from 'next/cache'; import { gotoHomeWithMessage } from './navigationActions'; import * as IBAN from 'iban'; export type State = { errors?: { ownerName?: string[]; ownerStreet?: string[]; ownerTown?: string[]; ownerIBAN?: string[]; currency?: string[]; show2dCodeInMonthlyStatement?: string[]; }; message?: string | null; success?: boolean; }; /** * Schema for validating user settings form fields */ const FormSchema = (t: IntlTemplateFn) => z.object({ ownerName: z.string().max(25).optional(), ownerStreet: z.string().max(25).optional(), ownerTown: z.string().max(27).optional(), ownerIBAN: z.string() .optional() .refine( (val) => { if (!val || val.trim() === '') return true; // Remove spaces and validate using iban.js library const cleaned = val.replace(/\s/g, '').toUpperCase(); return IBAN.isValid(cleaned); }, { message: t("owner-iban-invalid") } ), currency: z.string().optional(), show2dCodeInMonthlyStatement: z.boolean().optional().nullable(), }) .refine((data) => { if (data.show2dCodeInMonthlyStatement) { return !!data.ownerName && data.ownerName.trim().length > 0; } return true; }, { message: t("owner-name-required"), path: ["ownerName"], }) .refine((data) => { if (data.show2dCodeInMonthlyStatement) { return !!data.ownerStreet && data.ownerStreet.trim().length > 0; } return true; }, { message: t("owner-street-required"), path: ["ownerStreet"], }) .refine((data) => { if (data.show2dCodeInMonthlyStatement) { return !!data.ownerTown && data.ownerTown.trim().length > 0; } return true; }, { message: t("owner-town-required"), path: ["ownerTown"], }) .refine((data) => { if (data.show2dCodeInMonthlyStatement) { if (!data.ownerIBAN || data.ownerIBAN.trim().length === 0) { return false; } // Validate IBAN format when required const cleaned = data.ownerIBAN.replace(/\s/g, '').toUpperCase(); return IBAN.isValid(cleaned); } return true; }, { message: t("owner-iban-required"), path: ["ownerIBAN"], }) .refine((data) => { if (data.show2dCodeInMonthlyStatement) { return !!data.currency && data.currency.trim().length > 0; } return true; }, { message: t("currency-required"), path: ["currency"], }); /** * Get user settings */ export const getUserSettings = withUser(async (user: AuthenticatedUser) => { noStore(); const dbClient = await getDbClient(); const { id: userId } = user; const userSettings = await dbClient.collection("userSettings") .findOne({ userId }); return userSettings; }); /** * Get user settings by userId (without authentication) * Used for public/shared pages where we need to display owner's payment information */ export const getUserSettingsByUserId = async (userId: string): Promise => { noStore(); const dbClient = await getDbClient(); const userSettings = await dbClient.collection("userSettings") .findOne({ userId }); return userSettings; }; /** * Update user settings */ export const updateUserSettings = withUser(async (user: AuthenticatedUser, prevState: State, formData: FormData) => { noStore(); const t = await getTranslations("user-settings-form.validation"); const validatedFields = FormSchema(t).safeParse({ ownerName: formData.get('ownerName') || undefined, ownerStreet: formData.get('ownerStreet') || undefined, ownerTown: formData.get('ownerTown') || undefined, ownerIBAN: formData.get('ownerIBAN') || undefined, currency: formData.get('currency') || undefined, show2dCodeInMonthlyStatement: formData.get('generateTenantCode') === 'on', }); // If form validation fails, return errors early. Otherwise, continue... if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors, message: t("validation-failed"), success: false, }; } const { ownerName, ownerStreet, ownerTown, ownerIBAN, currency, show2dCodeInMonthlyStatement } = validatedFields.data; // Normalize IBAN: remove spaces and convert to uppercase const normalizedOwnerIBAN = ownerIBAN ? ownerIBAN.replace(/\s/g, '').toUpperCase() : null; // Update the user settings in MongoDB const dbClient = await getDbClient(); const { id: userId } = user; const userSettings: UserSettings = { userId, ownerName: ownerName || null, ownerStreet: ownerStreet || null, ownerTown: ownerTown || null, ownerIBAN: normalizedOwnerIBAN, currency: currency || null, show2dCodeInMonthlyStatement: show2dCodeInMonthlyStatement ?? false, }; await dbClient.collection("userSettings") .updateOne( { userId }, { $set: userSettings }, { upsert: true } ); revalidatePath('/settings'); // Get current locale and redirect to home with success message const locale = await getLocale(); await gotoHomeWithMessage(locale, 'userSettingsSaved'); // This return is needed for TypeScript, but won't be reached due to redirect return { message: null, errors: {}, success: true, }; });