import type { UseCase } from '@core/shared/application/UseCase'; import type { Logger } from '@core/shared/domain/Logger'; import { Result } from '@core/shared/domain/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import { Company } from '../../domain/entities/Company'; import { User } from '../../domain/entities/User'; import { AuthRepository } from '../../domain/repositories/AuthRepository'; import { CompanyRepository } from '../../domain/repositories/CompanyRepository'; import { PasswordHashingService } from '../../domain/services/PasswordHashingService'; import { EmailAddress } from '../../domain/value-objects/EmailAddress'; import { UserId } from '../../domain/value-objects/UserId'; export type SignupSponsorInput = { email: string; password: string; displayName: string; companyName: string; }; export type SignupSponsorResult = { user: User; company: Company; }; export type SignupSponsorErrorCode = 'USER_ALREADY_EXISTS' | 'WEAK_PASSWORD' | 'INVALID_DISPLAY_NAME' | 'REPOSITORY_ERROR'; export type SignupSponsorApplicationError = ApplicationErrorCode; /** * Application Use Case: SignupSponsorUseCase * * Handles sponsor registration by creating both a User and a Company atomically. * If any step fails, the operation is rolled back. */ export class SignupSponsorUseCase implements UseCase { constructor( private readonly authRepo: AuthRepository, private readonly companyRepo: CompanyRepository, private readonly passwordService: PasswordHashingService, private readonly logger: Logger, ) {} async execute(input: SignupSponsorInput): Promise> { let createdCompany: Company | null = null; try { // Validate password strength first (before any repository calls) if (!this.isPasswordStrong(input.password)) { return Result.err({ code: 'WEAK_PASSWORD', details: { message: 'Password must be at least 8 characters and contain uppercase, lowercase, and number' }, }); } // Validate display name format (before any repository calls) // We do this by attempting to create a User and catching any validation errors try { // Temporarily create user to validate display name User.create({ id: UserId.create(), displayName: input.displayName, }); } catch (error) { if (error instanceof Error) { if (error.message.includes('Name must be at least') || error.message.includes('Name can only contain') || error.message.includes('Please use your real name')) { return Result.err({ code: 'INVALID_DISPLAY_NAME', details: { message: error.message }, }); } } // Re-throw if it's a different error throw error; } // Validate email format const emailVO = EmailAddress.create(input.email); // Check if user exists const existingUser = await this.authRepo.findByEmail(emailVO); if (existingUser) { return Result.err({ code: 'USER_ALREADY_EXISTS', details: { message: 'User already exists' }, }); } // Hash password const hashedPassword = await this.passwordService.hash(input.password); const passwordHashModule = await import('../../domain/value-objects/PasswordHash'); const passwordHash = passwordHashModule.PasswordHash.fromHash(hashedPassword); // Create user with all fields const userId = UserId.create(); const user = User.create({ id: userId, displayName: input.displayName, email: emailVO.value, passwordHash, }); // Create company via repository const company = this.companyRepo.create({ getName: () => input.companyName, getOwnerUserId: () => userId, getContactEmail: () => emailVO.value, }); // Save company first await this.companyRepo.save(company); createdCompany = company; // Link user to company user.setCompanyId(company.getId()); // Save user await this.authRepo.save(user); return Result.ok({ user, company }); } catch (error) { // Rollback: delete company if it was created if (createdCompany && typeof createdCompany.getId === 'function') { try { const companyId = createdCompany.getId(); await this.companyRepo.delete(companyId); this.logger.info('[SignupSponsorUseCase] Rolled back company creation', { companyId, }); } catch (rollbackError) { this.logger.error('[SignupSponsorUseCase] Failed to rollback company creation', rollbackError instanceof Error ? rollbackError : undefined, { companyId: createdCompany.getId ? createdCompany.getId() : 'unknown', }); } } // Handle specific validation errors from User entity if (error instanceof Error) { if (error.message.includes('Name must be at least') || error.message.includes('Name can only contain') || error.message.includes('Please use your real name')) { return Result.err({ code: 'INVALID_DISPLAY_NAME', details: { message: error.message }, }); } if (error.message.includes('Company name')) { return Result.err({ code: 'REPOSITORY_ERROR', details: { message: error.message }, }); } } const message = error instanceof Error && error.message ? error.message : 'Failed to execute SignupSponsorUseCase'; this.logger.error('SignupSponsorUseCase.execute failed', error instanceof Error ? error : undefined, { input, }); return Result.err({ code: 'REPOSITORY_ERROR', details: { message }, }); } } private isPasswordStrong(password: string): boolean { if (password.length < 8) return false; if (!/[a-z]/.test(password)) return false; if (!/[A-Z]/.test(password)) return false; if (!/\d/.test(password)) return false; return true; } }