Files
gridpilot.gg/core/identity/application/use-cases/CloseAdminVoteSessionUseCase.ts
2026-01-16 16:46:57 +01:00

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;
}
}