170 lines
5.4 KiB
TypeScript
170 lines
5.4 KiB
TypeScript
import { AdminVoteSessionRepository } from '../../domain/repositories/AdminVoteSessionRepository';
|
|
import { RatingEventRepository } from '../../domain/repositories/RatingEventRepository';
|
|
import { UserRatingRepository } from '../../domain/repositories/UserRatingRepository';
|
|
import { AdminTrustRatingCalculator } from '../../domain/services/AdminTrustRatingCalculator';
|
|
import { RatingSnapshotCalculator } from '../../domain/services/RatingSnapshotCalculator';
|
|
import { RatingEventFactory } from '../../domain/services/RatingEventFactory';
|
|
import { CloseAdminVoteSessionInput, CloseAdminVoteSessionOutput } from '../dtos/AdminVoteSessionDto';
|
|
|
|
/**
|
|
* Use Case: CloseAdminVoteSessionUseCase
|
|
*
|
|
* Closes an admin vote session and generates rating events.
|
|
* This is the key use case that triggers events on close per plans section 7.1.1.
|
|
*
|
|
* Flow:
|
|
* 1. Load and validate session
|
|
* 2. Close session and calculate outcome
|
|
* 3. Create rating events from outcome
|
|
* 4. Append events to ledger for each affected admin
|
|
* 5. Recompute snapshots
|
|
*
|
|
* Per plans section 7.1.1 and 10.2
|
|
*/
|
|
export class CloseAdminVoteSessionUseCase {
|
|
constructor(
|
|
private readonly adminVoteSessionRepository: AdminVoteSessionRepository,
|
|
private readonly ratingEventRepository: RatingEventRepository,
|
|
private readonly userRatingRepository: UserRatingRepository,
|
|
private readonly appendRatingEventsUseCase: any, // Will be typed properly in integration
|
|
) {}
|
|
|
|
async execute(input: CloseAdminVoteSessionInput): Promise<CloseAdminVoteSessionOutput> {
|
|
try {
|
|
// Validate input
|
|
const errors = this.validateInput(input);
|
|
if (errors.length > 0) {
|
|
return {
|
|
success: false,
|
|
voteSessionId: input.voteSessionId,
|
|
errors,
|
|
};
|
|
}
|
|
|
|
// Load the vote session
|
|
const session = await this.adminVoteSessionRepository.findById(input.voteSessionId);
|
|
if (!session) {
|
|
return {
|
|
success: false,
|
|
voteSessionId: input.voteSessionId,
|
|
errors: ['Vote session not found'],
|
|
};
|
|
}
|
|
|
|
// Validate admin ownership
|
|
if (session.adminId !== input.adminId) {
|
|
return {
|
|
success: false,
|
|
voteSessionId: input.voteSessionId,
|
|
errors: ['Admin does not own this vote session'],
|
|
};
|
|
}
|
|
|
|
// Check if already closed
|
|
if (session.closed) {
|
|
return {
|
|
success: false,
|
|
voteSessionId: input.voteSessionId,
|
|
errors: ['Vote session is already closed'],
|
|
};
|
|
}
|
|
|
|
// Check if within voting window
|
|
const now = new Date();
|
|
if (now < session.startDate || now > session.endDate) {
|
|
return {
|
|
success: false,
|
|
voteSessionId: input.voteSessionId,
|
|
errors: ['Cannot close session outside the voting window'],
|
|
};
|
|
}
|
|
|
|
// Close session and calculate outcome
|
|
const outcome = session.close();
|
|
|
|
// Save closed session
|
|
await this.adminVoteSessionRepository.save(session);
|
|
|
|
// Create rating events from outcome
|
|
// Per plans: events are created for the admin being voted on
|
|
const eventsCreated = await this.createRatingEvents(session, outcome);
|
|
|
|
return {
|
|
success: true,
|
|
voteSessionId: input.voteSessionId,
|
|
outcome: {
|
|
percentPositive: outcome.percentPositive,
|
|
count: outcome.count,
|
|
eligibleVoterCount: outcome.eligibleVoterCount,
|
|
participationRate: outcome.participationRate,
|
|
outcome: outcome.outcome,
|
|
},
|
|
eventsCreated,
|
|
};
|
|
|
|
} catch (error) {
|
|
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
return {
|
|
success: false,
|
|
voteSessionId: input.voteSessionId,
|
|
errors: [`Failed to close vote session: ${errorMsg}`],
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create rating events from vote outcome
|
|
* Events are created for the admin being voted on
|
|
* Per plans: no events are created for tie outcomes
|
|
*/
|
|
private async createRatingEvents(session: any, outcome: any): Promise<number> {
|
|
let eventsCreated = 0;
|
|
|
|
// Don't create events for tie outcomes
|
|
if (outcome.outcome === 'tie') {
|
|
return 0;
|
|
}
|
|
|
|
// Use RatingEventFactory to create vote outcome events
|
|
const voteInput = {
|
|
userId: session.adminId, // The admin being voted on
|
|
voteSessionId: session.id,
|
|
outcome: outcome.outcome as 'positive' | 'negative',
|
|
voteCount: outcome.count.total,
|
|
eligibleVoterCount: outcome.eligibleVoterCount,
|
|
percentPositive: outcome.percentPositive,
|
|
};
|
|
|
|
const events = RatingEventFactory.createFromVote(voteInput);
|
|
|
|
// Save each event to ledger
|
|
for (const event of events) {
|
|
await this.ratingEventRepository.save(event);
|
|
eventsCreated++;
|
|
}
|
|
|
|
// Recompute snapshot for the admin
|
|
if (eventsCreated > 0) {
|
|
const allEvents = await this.ratingEventRepository.getAllByUserId(session.adminId);
|
|
const snapshot = RatingSnapshotCalculator.calculate(session.adminId, allEvents);
|
|
await this.userRatingRepository.save(snapshot);
|
|
}
|
|
|
|
return eventsCreated;
|
|
}
|
|
|
|
private validateInput(input: CloseAdminVoteSessionInput): string[] {
|
|
const errors: string[] = [];
|
|
|
|
if (!input.voteSessionId || input.voteSessionId.trim().length === 0) {
|
|
errors.push('voteSessionId is required');
|
|
}
|
|
|
|
if (!input.adminId || input.adminId.trim().length === 0) {
|
|
errors.push('adminId is required');
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
}
|