feat(reputation): add ReputationService, rank resolver integration, and GET /api/reputation
This commit is contained in:
106
apps/api/src/reputation/reputation.service.ts
Normal file
106
apps/api/src/reputation/reputation.service.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource, EntityManager } from 'typeorm';
|
||||
import { CharacterReputation } from './entities/character-reputation.entity';
|
||||
import { ReputationFaction } from './entities/reputation-faction.entity';
|
||||
import { reputationFactionNotFound } from './reputation.errors';
|
||||
import { resolveReputationRank } from './reputation-rank';
|
||||
|
||||
export interface ReputationGrantResult {
|
||||
factionKey: string;
|
||||
previousReputation: number;
|
||||
newReputation: number;
|
||||
previousRank: string;
|
||||
newRank: string;
|
||||
rankChanged: boolean;
|
||||
}
|
||||
|
||||
export interface CharacterReputationDto {
|
||||
factionKey: string;
|
||||
factionName: string;
|
||||
reputation: number;
|
||||
rank: string;
|
||||
rankLabel: string;
|
||||
nextThreshold: number | null;
|
||||
}
|
||||
|
||||
type RepositoryScope = Pick<DataSource, 'getRepository'>;
|
||||
|
||||
@Injectable()
|
||||
export class ReputationService {
|
||||
constructor(private readonly dataSource: DataSource) {}
|
||||
|
||||
/** Grants Regional Reputation, server-authoritative (spec §11, §30). */
|
||||
async grantReputation(
|
||||
characterId: string,
|
||||
factionKey: string,
|
||||
amount: number,
|
||||
manager?: EntityManager,
|
||||
): Promise<ReputationGrantResult> {
|
||||
const run = async (txManager: EntityManager): Promise<ReputationGrantResult> => {
|
||||
const factions = txManager.getRepository(ReputationFaction);
|
||||
const reputations = txManager.getRepository(CharacterReputation);
|
||||
|
||||
const faction = await factions.findOneBy({ key: factionKey });
|
||||
if (!faction) {
|
||||
throw reputationFactionNotFound();
|
||||
}
|
||||
|
||||
const existing = await reputations.findOne({
|
||||
where: { characterId, factionId: faction.id },
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
|
||||
const previousReputation = existing?.reputation ?? 0;
|
||||
const newReputation = previousReputation + amount;
|
||||
|
||||
if (existing) {
|
||||
existing.reputation = newReputation;
|
||||
await reputations.save(existing);
|
||||
} else {
|
||||
await reputations.save(
|
||||
reputations.create({ characterId, factionId: faction.id, reputation: newReputation }),
|
||||
);
|
||||
}
|
||||
|
||||
const previousRank = resolveReputationRank(previousReputation);
|
||||
const newRank = resolveReputationRank(newReputation);
|
||||
|
||||
return {
|
||||
factionKey,
|
||||
previousReputation,
|
||||
newReputation,
|
||||
previousRank: previousRank.key,
|
||||
newRank: newRank.key,
|
||||
rankChanged: previousRank.key !== newRank.key,
|
||||
};
|
||||
};
|
||||
|
||||
return manager ? run(manager) : this.dataSource.transaction(run);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every enabled faction, one entry each -- a faction the character has
|
||||
* never interacted with reads as 0 Reputation / Stranger (spec §36,
|
||||
* design R10), not as an absent entry.
|
||||
*/
|
||||
async getCharacterReputation(characterId: string): Promise<CharacterReputationDto[]> {
|
||||
const scope: RepositoryScope = this.dataSource;
|
||||
const factions = await scope.getRepository(ReputationFaction).find({ where: { enabled: true } });
|
||||
const reputations = await scope.getRepository(CharacterReputation).find({ where: { characterId } });
|
||||
|
||||
return factions.map((faction) => {
|
||||
const existing = reputations.find((row) => row.factionId === faction.id);
|
||||
const reputation = existing?.reputation ?? 0;
|
||||
const rank = resolveReputationRank(reputation);
|
||||
|
||||
return {
|
||||
factionKey: faction.key,
|
||||
factionName: faction.name,
|
||||
reputation,
|
||||
rank: rank.key,
|
||||
rankLabel: rank.label,
|
||||
nextThreshold: rank.nextThreshold,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user