202 lines
6.5 KiB
TypeScript
202 lines
6.5 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
|
||
|
|
* - Generating a sequential MUSA-YYYY-XXXX member ID
|
||
|
|
* - 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;
|
||
|
|
/** Whether to generate a MUSA-YYYY-XXXX member ID. Defaults to true. */
|
||
|
|
generateMemberId?: boolean;
|
||
|
|
}
|
||
|
|
): Promise<CreateMemberResult> {
|
||
|
|
const generateMemberId = options?.generateMemberId ?? true;
|
||
|
|
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.' };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate member ID if requested
|
||
|
|
let memberId: string | undefined;
|
||
|
|
if (generateMemberId) {
|
||
|
|
const year = new Date().getFullYear();
|
||
|
|
const { count } = await supabase
|
||
|
|
.from('members')
|
||
|
|
.select('*', { count: 'exact', head: true });
|
||
|
|
|
||
|
|
const memberNumber = String((count || 0) + 1).padStart(4, '0');
|
||
|
|
memberId = `MUSA-${year}-${memberNumber}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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
|
||
|
|
};
|
||
|
|
|
||
|
|
if (memberId) {
|
||
|
|
insertPayload.member_id = memberId;
|
||
|
|
}
|
||
|
|
|
||
|
|
const { error: memberError } = await supabase.from('members').insert(insertPayload);
|
||
|
|
|
||
|
|
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 };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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' };
|
||
|
|
}
|
||
|
|
}
|