This commit is contained in:
2025-12-16 15:42:38 +01:00
parent 29410708c8
commit 362894d1a5
147 changed files with 780 additions and 375 deletions

View File

@@ -0,0 +1,57 @@
import { vi, type Mock } from 'vitest';
import { GetCurrentSessionUseCase } from './GetCurrentSessionUseCase';
import { User } from '../../domain/entities/User';
import { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
describe('GetCurrentSessionUseCase', () => {
let useCase: GetCurrentSessionUseCase;
let mockUserRepo: {
findByEmail: Mock;
findById: Mock;
create: Mock;
update: Mock;
emailExists: Mock;
};
beforeEach(() => {
mockUserRepo = {
findByEmail: vi.fn(),
findById: vi.fn(),
create: vi.fn(),
update: vi.fn(),
emailExists: vi.fn(),
};
useCase = new GetCurrentSessionUseCase(mockUserRepo as IUserRepository);
});
it('should return User when user exists', async () => {
const userId = 'user-123';
const storedUser: StoredUser = {
id: userId,
email: 'test@example.com',
displayName: 'Test User',
passwordHash: 'hash',
salt: 'salt',
primaryDriverId: 'driver-123',
createdAt: new Date(),
};
mockUserRepo.findById.mockResolvedValue(storedUser);
const result = await useCase.execute(userId);
expect(mockUserRepo.findById).toHaveBeenCalledWith(userId);
expect(result).toBeInstanceOf(User);
expect(result?.getId().value).toBe(userId);
expect(result?.getDisplayName()).toBe('Test User');
});
it('should return null when user does not exist', async () => {
const userId = 'user-123';
mockUserRepo.findById.mockResolvedValue(null);
const result = await useCase.execute(userId);
expect(mockUserRepo.findById).toHaveBeenCalledWith(userId);
expect(result).toBeNull();
});
});

View File

@@ -1,4 +1,5 @@
import { User } from '../../domain/entities/User';
import { IUserRepository } from '../../domain/repositories/IUserRepository';
// No direct import of apps/api DTOs in core module
/**
@@ -7,9 +8,13 @@ import { User } from '../../domain/entities/User';
* Retrieves the current user session information.
*/
export class GetCurrentSessionUseCase {
constructor(private userRepo: IUserRepository) {}
async execute(userId: string): Promise<User | null> {
// TODO: Implement actual logic to retrieve user and session data
console.warn('GetCurrentSessionUseCase: Method not implemented.');
return null;
const stored = await this.userRepo.findById(userId);
if (!stored) {
return null;
}
return User.fromStored(stored);
}
}

View File

@@ -1,20 +0,0 @@
import { User } from '../../domain/entities/User';
export interface LoginWithIracingCallbackParams {
code: string;
state?: string;
returnTo?: string;
}
/**
* Application Use Case: LoginWithIracingCallbackUseCase
*
* Handles the callback after iRacing authentication.
*/
export class LoginWithIracingCallbackUseCase {
async execute(params: LoginWithIracingCallbackParams): Promise<User> {
// TODO: Implement actual logic for handling iRacing OAuth callback
console.warn('LoginWithIracingCallbackUseCase: Method not implemented.');
throw new Error('Method not implemented.');
}
}

View File

@@ -1,20 +0,0 @@
export interface IracingAuthRedirectResult {
redirectUrl: string;
state: string;
}
/**
* Application Use Case: StartIracingAuthRedirectUseCase
*
* Initiates the iRacing authentication flow.
*/
export class StartIracingAuthRedirectUseCase {
async execute(returnTo?: string): Promise<IracingAuthRedirectResult> {
// TODO: Implement actual logic for initiating iRacing OAuth redirect
console.warn('StartIracingAuthRedirectUseCase: Method not implemented.');
return {
redirectUrl: '/mock-iracing-redirect',
state: 'mock-state',
};
}
}

View File

@@ -0,0 +1,16 @@
import { Achievement, AchievementProps } from '@core/identity/domain/entities/Achievement';
export interface IAchievementRepository {
save(achievement: Achievement): Promise<void>;
findById(id: string): Promise<Achievement | null>;
}
export class CreateAchievementUseCase {
constructor(private readonly achievementRepository: IAchievementRepository) {}
async execute(props: Omit<AchievementProps, 'createdAt'>): Promise<Achievement> {
const achievement = Achievement.create(props);
await this.achievementRepository.save(achievement);
return achievement;
}
}

View File

@@ -0,0 +1,267 @@
import type { AchievementProps } from './entities/Achievement';
// Predefined achievements for drivers
export const DRIVER_ACHIEVEMENTS: Omit<AchievementProps, 'createdAt'>[] = [
{
id: 'first-race',
name: 'First Steps',
description: 'Complete your first race',
category: 'driver',
rarity: 'common',
points: 10,
requirements: [{ type: 'races_completed', value: 1, operator: '>=' }],
isSecret: false,
},
{
id: 'ten-races',
name: 'Getting Started',
description: 'Complete 10 races',
category: 'driver',
rarity: 'common',
points: 25,
requirements: [{ type: 'races_completed', value: 10, operator: '>=' }],
isSecret: false,
},
{
id: 'fifty-races',
name: 'Regular Racer',
description: 'Complete 50 races',
category: 'driver',
rarity: 'uncommon',
points: 50,
requirements: [{ type: 'races_completed', value: 50, operator: '>=' }],
isSecret: false,
},
{
id: 'hundred-races',
name: 'Veteran',
description: 'Complete 100 races',
category: 'driver',
rarity: 'rare',
points: 100,
requirements: [{ type: 'races_completed', value: 100, operator: '>=' }],
isSecret: false,
},
{
id: 'first-win',
name: 'Victory Lane',
description: 'Win your first race',
category: 'driver',
rarity: 'uncommon',
points: 50,
requirements: [{ type: 'wins', value: 1, operator: '>=' }],
isSecret: false,
},
{
id: 'ten-wins',
name: 'Serial Winner',
description: 'Win 10 races',
category: 'driver',
rarity: 'rare',
points: 100,
requirements: [{ type: 'wins', value: 10, operator: '>=' }],
isSecret: false,
},
{
id: 'first-podium',
name: 'Podium Finisher',
description: 'Finish on the podium',
category: 'driver',
rarity: 'common',
points: 25,
requirements: [{ type: 'podiums', value: 1, operator: '>=' }],
isSecret: false,
},
{
id: 'clean-streak-5',
name: 'Clean Racer',
description: 'Complete 5 consecutive races without incidents',
category: 'driver',
rarity: 'uncommon',
points: 50,
requirements: [{ type: 'consecutive_clean', value: 5, operator: '>=' }],
isSecret: false,
},
{
id: 'clean-streak-10',
name: 'Safety First',
description: 'Complete 10 consecutive races without incidents',
category: 'driver',
rarity: 'rare',
points: 100,
requirements: [{ type: 'consecutive_clean', value: 10, operator: '>=' }],
isSecret: false,
},
{
id: 'championship-win',
name: 'Champion',
description: 'Win a championship',
category: 'driver',
rarity: 'epic',
points: 200,
requirements: [{ type: 'championships_won', value: 1, operator: '>=' }],
isSecret: false,
},
{
id: 'triple-crown',
name: 'Triple Crown',
description: 'Win 3 championships',
category: 'driver',
rarity: 'legendary',
points: 500,
requirements: [{ type: 'championships_won', value: 3, operator: '>=' }],
isSecret: false,
},
{
id: 'elite-driver',
name: 'Elite Driver',
description: 'Reach Elite driver rating',
category: 'driver',
rarity: 'epic',
points: 250,
requirements: [{ type: 'rating_threshold', value: 90, operator: '>=' }],
isSecret: false,
},
];
// Predefined achievements for stewards
export const STEWARD_ACHIEVEMENTS: Omit<AchievementProps, 'createdAt'>[] = [
{
id: 'first-protest',
name: 'Justice Served',
description: 'Handle your first protest',
category: 'steward',
rarity: 'common',
points: 15,
requirements: [{ type: 'protests_handled', value: 1, operator: '>=' }],
isSecret: false,
},
{
id: 'ten-protests',
name: 'Fair Judge',
description: 'Handle 10 protests',
category: 'steward',
rarity: 'uncommon',
points: 50,
requirements: [{ type: 'protests_handled', value: 10, operator: '>=' }],
isSecret: false,
},
{
id: 'fifty-protests',
name: 'Senior Steward',
description: 'Handle 50 protests',
category: 'steward',
rarity: 'rare',
points: 100,
requirements: [{ type: 'protests_handled', value: 50, operator: '>=' }],
isSecret: false,
},
{
id: 'hundred-protests',
name: 'Chief Steward',
description: 'Handle 100 protests',
category: 'steward',
rarity: 'epic',
points: 200,
requirements: [{ type: 'protests_handled', value: 100, operator: '>=' }],
isSecret: false,
},
{
id: 'event-steward-10',
name: 'Event Official',
description: 'Steward 10 race events',
category: 'steward',
rarity: 'uncommon',
points: 50,
requirements: [{ type: 'events_stewarded', value: 10, operator: '>=' }],
isSecret: false,
},
{
id: 'trusted-steward',
name: 'Trusted Steward',
description: 'Achieve highly-trusted status',
category: 'steward',
rarity: 'rare',
points: 150,
requirements: [{ type: 'trust_threshold', value: 75, operator: '>=' }],
isSecret: false,
},
];
// Predefined achievements for admins
export const ADMIN_ACHIEVEMENTS: Omit<AchievementProps, 'createdAt'>[] = [
{
id: 'first-league',
name: 'League Founder',
description: 'Create your first league',
category: 'admin',
rarity: 'common',
points: 25,
requirements: [{ type: 'leagues_managed', value: 1, operator: '>=' }],
isSecret: false,
},
{
id: 'first-season',
name: 'Season Organizer',
description: 'Complete your first full season',
category: 'admin',
rarity: 'uncommon',
points: 50,
requirements: [{ type: 'seasons_completed', value: 1, operator: '>=' }],
isSecret: false,
},
{
id: 'five-seasons',
name: 'Experienced Organizer',
description: 'Complete 5 seasons',
category: 'admin',
rarity: 'rare',
points: 100,
requirements: [{ type: 'seasons_completed', value: 5, operator: '>=' }],
isSecret: false,
},
{
id: 'ten-seasons',
name: 'Veteran Organizer',
description: 'Complete 10 seasons',
category: 'admin',
rarity: 'epic',
points: 200,
requirements: [{ type: 'seasons_completed', value: 10, operator: '>=' }],
isSecret: false,
},
{
id: 'large-league',
name: 'Community Builder',
description: 'Manage a league with 50+ members',
category: 'admin',
rarity: 'rare',
points: 150,
requirements: [{ type: 'members_managed', value: 50, operator: '>=' }],
isSecret: false,
},
{
id: 'huge-league',
name: 'Empire Builder',
description: 'Manage a league with 100+ members',
category: 'admin',
rarity: 'epic',
points: 300,
requirements: [{ type: 'members_managed', value: 100, operator: '>=' }],
isSecret: false,
},
];
// Community achievements (for all roles)
export const COMMUNITY_ACHIEVEMENTS: Omit<AchievementProps, 'createdAt'>[] = [
{
id: 'community-leader',
name: 'Community Leader',
description: 'Achieve community leader trust level',
category: 'community',
rarity: 'legendary',
points: 500,
requirements: [{ type: 'trust_threshold', value: 90, operator: '>=' }],
isSecret: false,
},
];

View File

@@ -124,269 +124,3 @@ export class Achievement implements IEntity<string> {
return this.description;
}
}
// Predefined achievements for drivers
export const DRIVER_ACHIEVEMENTS: Omit<AchievementProps, 'createdAt'>[] = [
{
id: 'first-race',
name: 'First Steps',
description: 'Complete your first race',
category: 'driver',
rarity: 'common',
points: 10,
requirements: [{ type: 'races_completed', value: 1, operator: '>=' }],
isSecret: false,
},
{
id: 'ten-races',
name: 'Getting Started',
description: 'Complete 10 races',
category: 'driver',
rarity: 'common',
points: 25,
requirements: [{ type: 'races_completed', value: 10, operator: '>=' }],
isSecret: false,
},
{
id: 'fifty-races',
name: 'Regular Racer',
description: 'Complete 50 races',
category: 'driver',
rarity: 'uncommon',
points: 50,
requirements: [{ type: 'races_completed', value: 50, operator: '>=' }],
isSecret: false,
},
{
id: 'hundred-races',
name: 'Veteran',
description: 'Complete 100 races',
category: 'driver',
rarity: 'rare',
points: 100,
requirements: [{ type: 'races_completed', value: 100, operator: '>=' }],
isSecret: false,
},
{
id: 'first-win',
name: 'Victory Lane',
description: 'Win your first race',
category: 'driver',
rarity: 'uncommon',
points: 50,
requirements: [{ type: 'wins', value: 1, operator: '>=' }],
isSecret: false,
},
{
id: 'ten-wins',
name: 'Serial Winner',
description: 'Win 10 races',
category: 'driver',
rarity: 'rare',
points: 100,
requirements: [{ type: 'wins', value: 10, operator: '>=' }],
isSecret: false,
},
{
id: 'first-podium',
name: 'Podium Finisher',
description: 'Finish on the podium',
category: 'driver',
rarity: 'common',
points: 25,
requirements: [{ type: 'podiums', value: 1, operator: '>=' }],
isSecret: false,
},
{
id: 'clean-streak-5',
name: 'Clean Racer',
description: 'Complete 5 consecutive races without incidents',
category: 'driver',
rarity: 'uncommon',
points: 50,
requirements: [{ type: 'consecutive_clean', value: 5, operator: '>=' }],
isSecret: false,
},
{
id: 'clean-streak-10',
name: 'Safety First',
description: 'Complete 10 consecutive races without incidents',
category: 'driver',
rarity: 'rare',
points: 100,
requirements: [{ type: 'consecutive_clean', value: 10, operator: '>=' }],
isSecret: false,
},
{
id: 'championship-win',
name: 'Champion',
description: 'Win a championship',
category: 'driver',
rarity: 'epic',
points: 200,
requirements: [{ type: 'championships_won', value: 1, operator: '>=' }],
isSecret: false,
},
{
id: 'triple-crown',
name: 'Triple Crown',
description: 'Win 3 championships',
category: 'driver',
rarity: 'legendary',
points: 500,
requirements: [{ type: 'championships_won', value: 3, operator: '>=' }],
isSecret: false,
},
{
id: 'elite-driver',
name: 'Elite Driver',
description: 'Reach Elite driver rating',
category: 'driver',
rarity: 'epic',
points: 250,
requirements: [{ type: 'rating_threshold', value: 90, operator: '>=' }],
isSecret: false,
},
];
// Predefined achievements for stewards
export const STEWARD_ACHIEVEMENTS: Omit<AchievementProps, 'createdAt'>[] = [
{
id: 'first-protest',
name: 'Justice Served',
description: 'Handle your first protest',
category: 'steward',
rarity: 'common',
points: 15,
requirements: [{ type: 'protests_handled', value: 1, operator: '>=' }],
isSecret: false,
},
{
id: 'ten-protests',
name: 'Fair Judge',
description: 'Handle 10 protests',
category: 'steward',
rarity: 'uncommon',
points: 50,
requirements: [{ type: 'protests_handled', value: 10, operator: '>=' }],
isSecret: false,
},
{
id: 'fifty-protests',
name: 'Senior Steward',
description: 'Handle 50 protests',
category: 'steward',
rarity: 'rare',
points: 100,
requirements: [{ type: 'protests_handled', value: 50, operator: '>=' }],
isSecret: false,
},
{
id: 'hundred-protests',
name: 'Chief Steward',
description: 'Handle 100 protests',
category: 'steward',
rarity: 'epic',
points: 200,
requirements: [{ type: 'protests_handled', value: 100, operator: '>=' }],
isSecret: false,
},
{
id: 'event-steward-10',
name: 'Event Official',
description: 'Steward 10 race events',
category: 'steward',
rarity: 'uncommon',
points: 50,
requirements: [{ type: 'events_stewarded', value: 10, operator: '>=' }],
isSecret: false,
},
{
id: 'trusted-steward',
name: 'Trusted Steward',
description: 'Achieve highly-trusted status',
category: 'steward',
rarity: 'rare',
points: 150,
requirements: [{ type: 'trust_threshold', value: 75, operator: '>=' }],
isSecret: false,
},
];
// Predefined achievements for admins
export const ADMIN_ACHIEVEMENTS: Omit<AchievementProps, 'createdAt'>[] = [
{
id: 'first-league',
name: 'League Founder',
description: 'Create your first league',
category: 'admin',
rarity: 'common',
points: 25,
requirements: [{ type: 'leagues_managed', value: 1, operator: '>=' }],
isSecret: false,
},
{
id: 'first-season',
name: 'Season Organizer',
description: 'Complete your first full season',
category: 'admin',
rarity: 'uncommon',
points: 50,
requirements: [{ type: 'seasons_completed', value: 1, operator: '>=' }],
isSecret: false,
},
{
id: 'five-seasons',
name: 'Experienced Organizer',
description: 'Complete 5 seasons',
category: 'admin',
rarity: 'rare',
points: 100,
requirements: [{ type: 'seasons_completed', value: 5, operator: '>=' }],
isSecret: false,
},
{
id: 'ten-seasons',
name: 'Veteran Organizer',
description: 'Complete 10 seasons',
category: 'admin',
rarity: 'epic',
points: 200,
requirements: [{ type: 'seasons_completed', value: 10, operator: '>=' }],
isSecret: false,
},
{
id: 'large-league',
name: 'Community Builder',
description: 'Manage a league with 50+ members',
category: 'admin',
rarity: 'rare',
points: 150,
requirements: [{ type: 'members_managed', value: 50, operator: '>=' }],
isSecret: false,
},
{
id: 'huge-league',
name: 'Empire Builder',
description: 'Manage a league with 100+ members',
category: 'admin',
rarity: 'epic',
points: 300,
requirements: [{ type: 'members_managed', value: 100, operator: '>=' }],
isSecret: false,
},
];
// Community achievements (for all roles)
export const COMMUNITY_ACHIEVEMENTS: Omit<AchievementProps, 'createdAt'>[] = [
{
id: 'community-leader',
name: 'Community Leader',
description: 'Achieve community leader trust level',
category: 'community',
rarity: 'legendary',
points: 500,
requirements: [{ type: 'trust_threshold', value: 90, operator: '>=' }],
isSecret: false,
},
];

View File

@@ -4,8 +4,6 @@
* Repository interface for User entity operations.
*/
import type { AuthenticatedUserDTO } from '../../application/dto/AuthenticatedUserDTO';
export interface UserCredentials {
email: string;
passwordHash: string;

View File

@@ -89,6 +89,18 @@ export class EntityMappers {
static toRaceDTO(race: Race | null): RaceDTO | null {
if (!race) return null;
const sessionTypeMap = {
practice: 'practice' as const,
qualifying: 'qualifying' as const,
q1: 'qualifying' as const,
q2: 'qualifying' as const,
q3: 'qualifying' as const,
sprint: 'race' as const,
main: 'race' as const,
timeTrial: 'practice' as const,
};
return {
id: race.id,
leagueId: race.leagueId,
@@ -97,7 +109,7 @@ export class EntityMappers {
trackId: race.trackId ?? '',
car: race.car,
carId: race.carId ?? '',
sessionType: race.sessionType,
sessionType: sessionTypeMap[race.sessionType.value],
status: race.status,
...(race.strengthOfField !== undefined
? { strengthOfField: race.strengthOfField }
@@ -112,6 +124,17 @@ export class EntityMappers {
}
static toRaceDTOs(races: Race[]): RaceDTO[] {
const sessionTypeMap = {
practice: 'practice' as const,
qualifying: 'qualifying' as const,
q1: 'qualifying' as const,
q2: 'qualifying' as const,
q3: 'qualifying' as const,
sprint: 'race' as const,
main: 'race' as const,
timeTrial: 'practice' as const,
};
return races.map((race) => ({
id: race.id,
leagueId: race.leagueId,
@@ -120,7 +143,7 @@ export class EntityMappers {
trackId: race.trackId ?? '',
car: race.car,
carId: race.carId ?? '',
sessionType: race.sessionType,
sessionType: sessionTypeMap[race.sessionType.value],
status: race.status,
...(race.strengthOfField !== undefined
? { strengthOfField: race.strengthOfField }

View File

@@ -0,0 +1,174 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import { AcceptSponsorshipRequestUseCase } from './AcceptSponsorshipRequestUseCase';
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { INotificationService } from '@core/notifications/application/ports/INotificationService';
import type { IPaymentGateway } from '../ports/IPaymentGateway';
import type { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository';
import type { Logger } from '@core/shared/application';
import { SponsorshipRequest } from '../../domain/entities/SponsorshipRequest';
import { Season } from '../../domain/entities/Season';
import { LeagueWallet } from '../../domain/entities/LeagueWallet';
import { Money } from '../../domain/value-objects/Money';
describe('AcceptSponsorshipRequestUseCase', () => {
let mockSponsorshipRequestRepo: {
findById: Mock;
update: Mock;
};
let mockSeasonSponsorshipRepo: {
create: Mock;
};
let mockSeasonRepo: {
findById: Mock;
};
let mockNotificationService: {
sendNotification: Mock;
};
let mockPaymentGateway: {
processPayment: Mock;
};
let mockWalletRepo: {
findById: Mock;
update: Mock;
};
let mockLeagueWalletRepo: {
findById: Mock;
update: Mock;
};
let mockLogger: {
debug: Mock;
info: Mock;
warn: Mock;
error: Mock;
};
beforeEach(() => {
mockSponsorshipRequestRepo = {
findById: vi.fn(),
update: vi.fn(),
};
mockSeasonSponsorshipRepo = {
create: vi.fn(),
};
mockSeasonRepo = {
findById: vi.fn(),
};
mockNotificationService = {
sendNotification: vi.fn(),
};
mockPaymentGateway = {
processPayment: vi.fn(),
};
mockWalletRepo = {
findById: vi.fn(),
update: vi.fn(),
};
mockLeagueWalletRepo = {
findById: vi.fn(),
update: vi.fn(),
};
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
});
it('should send notification to sponsor, process payment, and update wallets when accepting season sponsorship', async () => {
const useCase = new AcceptSponsorshipRequestUseCase(
mockSponsorshipRequestRepo as unknown as ISponsorshipRequestRepository,
mockSeasonSponsorshipRepo as unknown as ISeasonSponsorshipRepository,
mockSeasonRepo as unknown as ISeasonRepository,
mockNotificationService as unknown as INotificationService,
mockPaymentGateway as unknown as IPaymentGateway,
mockWalletRepo as unknown as IWalletRepository,
mockLeagueWalletRepo as unknown as ILeagueWalletRepository,
mockLogger as unknown as Logger,
);
const request = SponsorshipRequest.create({
id: 'req1',
sponsorId: 'sponsor1',
entityId: 'season1',
entityType: 'season',
tier: 'main',
offeredAmount: Money.create(1000),
status: 'pending',
});
const season = Season.create({
id: 'season1',
leagueId: 'league1',
gameId: 'game1',
name: 'Season 1',
startDate: new Date(),
endDate: new Date(),
});
mockSponsorshipRequestRepo.findById.mockResolvedValue(request);
mockSeasonRepo.findById.mockResolvedValue(season);
mockNotificationService.sendNotification.mockResolvedValue(undefined);
mockPaymentGateway.processPayment.mockResolvedValue({
success: true,
transactionId: 'txn1',
timestamp: new Date(),
});
mockWalletRepo.findById.mockResolvedValue({
id: 'sponsor1',
leagueId: 'league1',
balance: 2000,
totalRevenue: 0,
totalPlatformFees: 0,
totalWithdrawn: 0,
currency: 'USD',
createdAt: new Date(),
});
const leagueWallet = LeagueWallet.create({
id: 'league1',
leagueId: 'league1',
balance: Money.create(500),
});
mockLeagueWalletRepo.findById.mockResolvedValue(leagueWallet);
const result = await useCase.execute({
requestId: 'req1',
respondedBy: 'driver1',
});
expect(result).toBeDefined();
expect(mockNotificationService.sendNotification).toHaveBeenCalledWith({
recipientId: 'sponsor1',
type: 'sponsorship_request_accepted',
title: 'Sponsorship Accepted',
body: 'Your sponsorship request for Season 1 has been accepted.',
channel: 'in_app',
urgency: 'toast',
data: {
requestId: 'req1',
sponsorshipId: expect.any(String),
},
});
expect(mockPaymentGateway.processPayment).toHaveBeenCalledWith(
Money.create(1000),
'sponsor1',
'Sponsorship payment for season season1',
{ requestId: 'req1' }
);
expect(mockWalletRepo.update).toHaveBeenCalledWith(
expect.objectContaining({
id: 'sponsor1',
balance: 1000,
})
);
expect(mockLeagueWalletRepo.update).toHaveBeenCalledWith(
expect.objectContaining({
id: 'league1',
balance: expect.objectContaining({ amount: 1400 }),
})
);
});
});

View File

@@ -9,6 +9,10 @@ import type { Logger } from '@core/shared/application';
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { INotificationService } from '@core/notifications/application/ports/INotificationService';
import type { IPaymentGateway } from '../ports/IPaymentGateway';
import type { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository';
import { SeasonSponsorship } from '../../domain/entities/SeasonSponsorship';
import type { AsyncUseCase } from '@core/shared/application';
@@ -32,6 +36,10 @@ export class AcceptSponsorshipRequestUseCase
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
private readonly seasonRepository: ISeasonRepository,
private readonly notificationService: INotificationService,
private readonly paymentGateway: IPaymentGateway,
private readonly walletRepository: IWalletRepository,
private readonly leagueWalletRepository: ILeagueWalletRepository,
private readonly logger: Logger,
) {}
@@ -79,12 +87,59 @@ export class AcceptSponsorshipRequestUseCase
});
await this.seasonSponsorshipRepo.create(sponsorship);
this.logger.info(`Season sponsorship ${sponsorshipId} created for request ${dto.requestId}.`, { sponsorshipId, requestId: dto.requestId });
}
// TODO: In a real implementation, we would:
// 1. Create notification for the sponsor
// 2. Process payment
// 3. Update wallet balances
// Notify the sponsor
await this.notificationService.sendNotification({
recipientId: request.sponsorId,
type: 'sponsorship_request_accepted',
title: 'Sponsorship Accepted',
body: `Your sponsorship request for ${season.name} has been accepted.`,
channel: 'in_app',
urgency: 'toast',
data: {
requestId: request.id,
sponsorshipId,
},
});
// Process payment
const paymentResult = await this.paymentGateway.processPayment(
request.offeredAmount,
request.sponsorId,
`Sponsorship payment for ${request.entityType} ${request.entityId}`,
{ requestId: request.id }
);
if (!paymentResult.success) {
this.logger.error(`Payment failed for sponsorship request ${request.id}: ${paymentResult.error}`, undefined, { requestId: request.id });
throw new Error('Payment processing failed');
}
// Update wallets
const sponsorWallet = await this.walletRepository.findById(request.sponsorId);
if (!sponsorWallet) {
this.logger.error(`Sponsor wallet not found for ${request.sponsorId}`, undefined, { sponsorId: request.sponsorId });
throw new Error('Sponsor wallet not found');
}
const leagueWallet = await this.leagueWalletRepository.findById(season.leagueId);
if (!leagueWallet) {
this.logger.error(`League wallet not found for ${season.leagueId}`, undefined, { leagueId: season.leagueId });
throw new Error('League wallet not found');
}
const netAmount = acceptedRequest.getNetAmount();
// Deduct from sponsor wallet
const updatedSponsorWallet = {
...sponsorWallet,
balance: sponsorWallet.balance - request.offeredAmount.amount,
};
await this.walletRepository.update(updatedSponsorWallet);
// Add to league wallet
const updatedLeagueWallet = leagueWallet.addFunds(netAmount, paymentResult.transactionId!);
await this.leagueWalletRepository.update(updatedLeagueWallet);
}
this.logger.info(`Sponsorship request ${acceptedRequest.id} successfully accepted.`, { requestId: acceptedRequest.id, sponsorshipId });
@@ -97,8 +152,9 @@ export class AcceptSponsorshipRequestUseCase
netAmount: acceptedRequest.getNetAmount().amount,
};
} catch (error) {
this.logger.error(`Failed to accept sponsorship request ${dto.requestId}: ${error.message}`, { requestId: dto.requestId, error: error.message, stack: error.stack });
throw error;
const err = error instanceof Error ? error : new Error(String(error));
this.logger.error(`Failed to accept sponsorship request ${dto.requestId}: ${err.message}`, err, { requestId: dto.requestId });
throw err;
}
}
}

View File

@@ -50,7 +50,7 @@ export class ApplyForSponsorshipUseCase
// Validate sponsor exists
const sponsor = await this.sponsorRepo.findById(dto.sponsorId);
if (!sponsor) {
this.logger.error('Sponsor not found', { sponsorId: dto.sponsorId });
this.logger.error('Sponsor not found', undefined, { sponsorId: dto.sponsorId });
throw new EntityNotFoundError({ entity: 'sponsor', id: dto.sponsorId });
}

View File

@@ -97,7 +97,7 @@ export class ApplyPenaltyUseCase
return { penaltyId: penalty.id };
} catch (error) {
this.logger.error('ApplyPenaltyUseCase: Failed to apply penalty', { command, error: error.message });
this.logger.error('ApplyPenaltyUseCase: Failed to apply penalty', error, { command });
throw error;
}
}

View File

@@ -54,7 +54,7 @@ export class ApproveTeamJoinRequestUseCase
await this.membershipRepository.removeJoinRequest(requestId);
this.logger.info(`Team join request with ID ${requestId} removed`);
} catch (error) {
this.logger.error(`Failed to approve team join request ${requestId}:`, error);
this.logger.error(`Failed to approve team join request ${requestId}`, error instanceof Error ? error : new Error(String(error)));
throw error;
}
}

View File

@@ -37,7 +37,7 @@ export class CancelRaceUseCase
await this.raceRepository.update(cancelledRace);
this.logger.info(`[CancelRaceUseCase] Race ${raceId} cancelled successfully.`);
} catch (error) {
this.logger.error(`[CancelRaceUseCase] Error cancelling race ${raceId}:`, error);
this.logger.error(`[CancelRaceUseCase] Error cancelling race ${raceId}`, error instanceof Error ? error : new Error(String(error)));
throw error;
}
}

View File

@@ -1,4 +1,5 @@
import type { UseCase } from '@core/shared/application/UseCase';
import type { Logger } from '@core/shared/application';
import type { IRaceEventRepository } from '../../domain/repositories/IRaceEventRepository';
import type { IDomainEventPublisher } from '@core/shared/domain';
import type { RaceEventStewardingClosedEvent } from '../../domain/events/RaceEventStewardingClosed';
@@ -20,6 +21,8 @@ export class CloseRaceEventStewardingUseCase
implements UseCase<CloseRaceEventStewardingCommand, void, void, void>
{
constructor(
private readonly logger: Logger,
private readonly raceEventRepository: IRaceEventRepository,
private readonly domainEventPublisher: IDomainEventPublisher,
) {}
@@ -58,7 +61,7 @@ export class CloseRaceEventStewardingUseCase
await this.domainEventPublisher.publish(event);
} catch (error) {
console.error(`Failed to close stewarding for race event ${raceEvent.id}:`, error);
this.logger.error(`Failed to close stewarding for race event ${raceEvent.id}`, error instanceof Error ? error : new Error(String(error)));
// In production, this would trigger alerts/monitoring
}
}

View File

@@ -81,8 +81,8 @@ export class CompleteRaceUseCaseWithRatings
const completedRace = race.complete();
await this.raceRepository.update(completedRace);
this.logger.info(`Race ID: ${raceId} completed successfully.`);
} catch (error: any) {
this.logger.error(`Error completing race ${raceId}: ${error.message}`);
} catch (error) {
this.logger.error(`Error completing race ${raceId}`, error instanceof Error ? error : new Error(String(error)));
throw error;
}
}

View File

@@ -4,7 +4,6 @@ import { Season } from '../../domain/entities/Season';
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
import type { AsyncUseCase } from '@core/shared/application';
import type { Logger } from '@core/shared/application';
import type {
@@ -129,11 +128,7 @@ export class CreateLeagueWithSeasonAndScoringUseCase
this.logger.debug('CreateLeagueWithSeasonAndScoringUseCase completed successfully.', { result });
return result;
} catch (error) {
this.logger.error('Error during CreateLeagueWithSeasonAndScoringUseCase execution.', {
command,
error: error.message,
stack: error.stack,
});
this.logger.error('Error during CreateLeagueWithSeasonAndScoringUseCase execution.', error, { command });
throw error;
}
}

View File

@@ -5,7 +5,6 @@ import type {
AllTeamsResultDTO,
} from '../presenters/IAllTeamsPresenter';
import type { UseCase } from '@core/shared/application';
import type { Team } from '../../domain/entities/Team';
import { Logger } from "@core/shared/application";
/**
@@ -54,7 +53,7 @@ export class GetAllTeamsUseCase
presenter.present(dto);
this.logger.info('Successfully retrieved all teams.');
} catch (error) {
this.logger.error('Error retrieving all teams:', error);
this.logger.error('Error retrieving all teams', error instanceof Error ? error : new Error(String(error)));
throw error; // Re-throw the error after logging
}
}

View File

@@ -1,6 +1,6 @@
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { IGetLeagueAdminPermissionsPresenter, GetLeagueAdminPermissionsResultDTO, GetLeagueAdminPermissionsViewModel } from '../presenters/IGetLeagueAdminPermissionsPresenter';
import type { IGetLeagueAdminPermissionsPresenter, GetLeagueAdminPermissionsViewModel } from '../presenters/IGetLeagueAdminPermissionsPresenter';
import type { UseCase } from '@core/shared/application/UseCase';
export interface GetLeagueAdminPermissionsUseCaseParams {

View File

@@ -1,5 +1,6 @@
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
import type { IGetLeagueAdminPresenter, GetLeagueAdminResultDTO, GetLeagueAdminViewModel } from '../presenters/IGetLeagueAdminPresenter';
import { GetLeagueAdminPermissionsViewModel } from '../presenters/IGetLeagueAdminPermissionsPresenter';
import type { IGetLeagueAdminPresenter } from '../presenters/IGetLeagueAdminPresenter';
import type { UseCase } from '@core/shared/application/UseCase';
export interface GetLeagueAdminUseCaseParams {
@@ -14,7 +15,7 @@ export interface GetLeagueAdminResultDTO {
// Additional data would be populated by combining multiple use cases
}
export class GetLeagueAdminUseCase implements UseCase<GetLeagueAdminUseCaseParams, GetLeagueAdminResultDTO, GetLeagueAdminViewModel, IGetLeagueAdminPresenter> {
export class GetLeagueAdminUseCase implements UseCase<GetLeagueAdminUseCaseParams, GetLeagueAdminResultDTO, GetLeagueAdminPermissionsViewModel, IGetLeagueAdminPresenter> {
constructor(
private readonly leagueRepository: ILeagueRepository,
) {}