monacousa-portal/src/lib/server/registration.ts

194 lines
6.3 KiB
TypeScript

/**
* Shared registration helpers used by both the signup and join pages.
* Consolidates member creation and welcome email logic.
*/
import { supabaseAdmin } from '$lib/server/supabase';
import { sendTemplatedEmail } from '$lib/server/email';
import type { SupabaseClient } from '@supabase/supabase-js';
// ────────────────────────────────────────────────────────────────
// Types
// ────────────────────────────────────────────────────────────────
export interface RegistrationData {
userId: string;
email: string;
firstName: string;
lastName: string;
phone?: string;
dateOfBirth?: string;
address?: string;
nationality?: string[];
}
export interface CreateMemberResult {
success: boolean;
error?: string;
memberId?: string;
}
// ────────────────────────────────────────────────────────────────
// Member Creation
// ────────────────────────────────────────────────────────────────
/**
* Create a member record in the database after auth user creation.
*
* Handles:
* - Looking up the default/pending membership status and type
* - Member ID auto-generated by database trigger (atomic sequence)
* - Inserting the member record
*
* @param data Core registration data.
* @param supabase The Supabase client to use for DB operations.
* @param options Additional options for how the member is created.
*/
export async function createMemberRecord(
data: RegistrationData,
supabase: SupabaseClient,
options?: {
/** Look up status by name instead of is_default. Defaults to undefined (uses is_default). */
statusName?: string;
/** @deprecated Member ID now auto-generated by database trigger. */
generateMemberId?: boolean;
}
): Promise<CreateMemberResult> {
const statusName = options?.statusName;
// Look up the membership status
let statusQuery;
if (statusName) {
statusQuery = supabase
.from('membership_statuses')
.select('id')
.eq('name', statusName)
.single();
} else {
statusQuery = supabase
.from('membership_statuses')
.select('id')
.eq('is_default', true)
.single();
}
const { data: statusData, error: statusError } = await statusQuery;
if (statusError || !statusData?.id) {
console.error('No membership status found:', statusError);
return { success: false, error: 'System configuration error. Please contact support.' };
}
// Look up the default membership type
const { data: typeData, error: typeError } = await supabase
.from('membership_types')
.select('id')
.eq('is_default', true)
.single();
if (typeError || !typeData?.id) {
console.error('No default membership type found:', typeError);
return { success: false, error: 'System configuration error. Please contact support.' };
}
// Member ID will be auto-generated by database trigger (generate_member_id)
// See migration 018_atomic_member_id_generation.sql
// Create the member profile
const insertPayload: Record<string, unknown> = {
id: data.userId,
first_name: data.firstName,
last_name: data.lastName,
email: data.email,
phone: data.phone || null,
date_of_birth: data.dateOfBirth || null,
address: data.address || null,
nationality: data.nationality || [],
role: 'member',
membership_status_id: statusData.id,
membership_type_id: typeData.id
};
// member_id is auto-generated by trigger, no need to set it
const { error: memberError, data: insertedMember } = await supabase
.from('members')
.insert(insertPayload)
.select('member_id')
.single();
if (memberError) {
console.error('Failed to create member profile:', memberError);
return { success: false, error: 'Failed to create member profile. Please try again or contact support.' };
}
return { success: true, memberId: insertedMember?.member_id };
}
/**
* Clean up auth user on registration failure.
* Uses supabaseAdmin to ensure we can always delete the user.
*/
export async function cleanupAuthUser(userId: string): Promise<void> {
try {
await supabaseAdmin.auth.admin.deleteUser(userId);
} catch (deleteError) {
console.error('Failed to clean up auth user:', deleteError);
}
}
// ────────────────────────────────────────────────────────────────
// Welcome Email
// ────────────────────────────────────────────────────────────────
/**
* Send the onboarding welcome email with payment instructions.
*
* @param member Basic member info for the email template.
* @param paymentSettings Payment account details from app_settings.
* @param duesAmount The annual dues amount.
* @param paymentDeadline The payment deadline date.
*/
export async function sendWelcomeEmail(
member: {
id: string;
first_name: string;
email: string;
member_id?: string;
},
paymentSettings: Record<string, string>,
duesAmount: number,
paymentDeadline: Date
): Promise<{ success: boolean; error?: string }> {
try {
const result = await sendTemplatedEmail(
'onboarding_welcome',
member.email,
{
first_name: member.first_name,
member_id: member.member_id || 'N/A',
amount: `\u20AC${duesAmount}`,
payment_deadline: paymentDeadline.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
}),
account_holder: paymentSettings.account_holder || 'Monaco USA',
bank_name: paymentSettings.bank_name || 'Credit Foncier de Monaco',
iban: paymentSettings.iban || 'Contact for details'
},
{
recipientId: member.id,
recipientName: member.first_name,
sentBy: 'system'
}
);
return result;
} catch (emailError) {
console.error('Failed to send welcome email:', emailError);
return { success: false, error: 'Failed to send welcome email' };
}
}