feat(AdminController, AdminService, AdminRouter, localization): add character death cleanup feature
All checks were successful
Deploy to production / deploy (push) Successful in 1m52s

- Implemented `adminCleanupCharacterDeathArtifacts` method in AdminService to remove stale character-bound data after death/inheritance, including knowledge, debtors prism, and political offices.
- Added corresponding route in AdminRouter for triggering the cleanup process via an API endpoint.
- Enhanced AdminController to handle requests for the new cleanup feature, ensuring proper error handling and response formatting.
- Updated frontend components to include a user interface for initiating the cleanup, with localization support in both English and German for improved user experience.
This commit is contained in:
Torsten Schulz (local)
2026-04-20 15:43:44 +02:00
parent 8ce15441bf
commit 267711fca6
6 changed files with 311 additions and 1 deletions

View File

@@ -16,6 +16,7 @@ class AdminController {
this.adminForceFalukantPregnancy = this.adminForceFalukantPregnancy.bind(this);
this.adminClearFalukantPregnancy = this.adminClearFalukantPregnancy.bind(this);
this.adminForceFalukantBirth = this.adminForceFalukantBirth.bind(this);
this.adminCleanupCharacterDeathArtifacts = this.adminCleanupCharacterDeathArtifacts.bind(this);
this.adminGetPotentialFathersForCharacter = this.adminGetPotentialFathersForCharacter.bind(this);
this.getFalukantUserBranches = this.getFalukantUserBranches.bind(this);
this.updateFalukantStock = this.updateFalukantStock.bind(this);
@@ -507,6 +508,21 @@ class AdminController {
}
}
async adminCleanupCharacterDeathArtifacts(req, res) {
try {
const { userid: userId } = req.headers;
const { characterId } = req.params;
const response = await AdminService.adminCleanupCharacterDeathArtifacts(userId, characterId);
res.status(200).json(response);
} catch (error) {
console.log(error);
const status = error.message === 'noaccess'
? 403
: (['invalidCharacter', 'notfound'].includes(error.message) ? 400 : 500);
res.status(status).json({ error: error.message });
}
}
async getFalukantUserBranches(req, res) {
try {
const { userid: userId } = req.headers;

View File

@@ -55,6 +55,7 @@ router.get('/falukant/character/:characterId/potential-fathers', authenticate, a
router.post('/falukant/character/force-pregnancy', authenticate, adminController.adminForceFalukantPregnancy);
router.post('/falukant/character/clear-pregnancy', authenticate, adminController.adminClearFalukantPregnancy);
router.post('/falukant/character/force-birth', authenticate, adminController.adminForceFalukantBirth);
router.post('/falukant/character/:characterId/death-cleanup', authenticate, adminController.adminCleanupCharacterDeathArtifacts);
router.get('/falukant/branches/:falukantUserId', authenticate, adminController.getFalukantUserBranches);
router.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock);
router.post('/falukant/stock', authenticate, adminController.addFalukantStock);

View File

@@ -10,7 +10,7 @@ import UserParamType from "../models/type/user_param.js";
import ContactMessage from "../models/service/contactmessage.js";
import ContactService from "./ContactService.js";
import { sendAnswerEmail } from './emailService.js';
import { Op, Sequelize } from 'sequelize';
import { Op, QueryTypes, Sequelize } from 'sequelize';
import FalukantUser from "../models/falukant/data/user.js";
import FalukantCharacter from "../models/falukant/data/character.js";
import FalukantPredefineFirstname from "../models/falukant/predefine/firstname.js";
@@ -32,6 +32,10 @@ import ChildRelation from "../models/falukant/data/child_relation.js";
import Relationship from "../models/falukant/data/relationship.js";
import RelationshipType from "../models/falukant/type/relationship.js";
import RelationshipState from "../models/falukant/data/relationship_state.js";
import Knowledge from '../models/falukant/data/product_knowledge.js';
import DebtorsPrism from '../models/falukant/data/debtors_prism.js';
import Candidate from '../models/falukant/data/candidate.js';
import PoliticalOffice from '../models/falukant/data/political_office.js';
import { sequelize } from '../utils/sequelize.js';
import npcCreationJobService from './npcCreationJobService.js';
import VocabService from './vocabService.js';
@@ -1172,6 +1176,255 @@ class AdminService {
return { success: true, childCharacterId, gender: childGender };
}
async adminCleanupCharacterDeathArtifacts(userId, characterId) {
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
throw new Error('noaccess');
}
const parsedCharacterId = Number(characterId);
if (!Number.isFinite(parsedCharacterId) || parsedCharacterId <= 0) {
throw new Error('invalidCharacter');
}
const character = await FalukantCharacter.findByPk(parsedCharacterId, {
attributes: ['id', 'userId']
});
if (!character) {
throw new Error('notfound');
}
const summary = {
characterId: parsedCharacterId,
knowledgeDeleted: 0,
debtorsPrismDeleted: 0,
politicalOfficeArchived: 0,
politicalOfficeDeleted: 0,
electionCandidateDeleted: 0,
characterDeleted: false
};
await sequelize.transaction(async (t) => {
summary.knowledgeDeleted = await Knowledge.destroy({
where: { characterId: parsedCharacterId },
transaction: t
});
summary.debtorsPrismDeleted = await DebtorsPrism.destroy({
where: { characterId: parsedCharacterId },
transaction: t
});
summary.electionCandidateDeleted = await Candidate.destroy({
where: { characterId: parsedCharacterId },
transaction: t
});
const archivedRows = await sequelize.query(
`
INSERT INTO falukant_log.political_office_history (
character_id,
office_type_id,
start_date,
end_date,
created_at,
updated_at
)
SELECT
po.character_id,
po.office_type_id,
COALESCE(po.created_at, NOW()),
NOW(),
NOW(),
NOW()
FROM falukant_data.political_office po
WHERE po.character_id = :characterId
RETURNING id
`,
{
replacements: { characterId: parsedCharacterId },
type: QueryTypes.SELECT,
transaction: t
}
);
summary.politicalOfficeArchived = archivedRows.length;
summary.politicalOfficeDeleted = await PoliticalOffice.destroy({
where: { characterId: parsedCharacterId },
transaction: t
});
await sequelize.query(
`
DELETE FROM falukant_data.occupied_political_office
WHERE character_id = :characterId
`,
{
replacements: { characterId: parsedCharacterId },
type: QueryTypes.DELETE,
transaction: t
}
);
await sequelize.query(
`
DELETE FROM falukant_data.political_benefit_last_tick
WHERE character_id = :characterId
`,
{
replacements: { characterId: parsedCharacterId },
type: QueryTypes.DELETE,
transaction: t
}
);
await sequelize.query(
`
DELETE FROM falukant_data.falukant_character_trait
WHERE character_id = :characterId
`,
{
replacements: { characterId: parsedCharacterId },
type: QueryTypes.DELETE,
transaction: t
}
);
await sequelize.query(
`
DELETE FROM falukant_data.church_application
WHERE character_id = :characterId
OR supervisor_id = :characterId
`,
{
replacements: { characterId: parsedCharacterId },
type: QueryTypes.DELETE,
transaction: t
}
);
await sequelize.query(
`
DELETE FROM falukant_data.church_office
WHERE character_id = :characterId
OR supervisor_id = :characterId
`,
{
replacements: { characterId: parsedCharacterId },
type: QueryTypes.DELETE,
transaction: t
}
);
await sequelize.query(
`
DELETE FROM falukant_data.political_appointment
WHERE appointer_character_id = :characterId
OR target_character_id = :characterId
`,
{
replacements: { characterId: parsedCharacterId },
type: QueryTypes.DELETE,
transaction: t
}
);
await sequelize.query(
`
DELETE FROM falukant_data.marriage_proposals
WHERE requester_character_id = :characterId
OR proposed_character_id = :characterId
`,
{
replacements: { characterId: parsedCharacterId },
type: QueryTypes.DELETE,
transaction: t
}
);
await sequelize.query(
`
DELETE FROM falukant_data.child_relation
WHERE father_character_id = :characterId
OR mother_character_id = :characterId
OR child_character_id = :characterId
`,
{
replacements: { characterId: parsedCharacterId },
type: QueryTypes.DELETE,
transaction: t
}
);
await sequelize.query(
`
DELETE FROM falukant_data.relationship_state
WHERE relationship_id IN (
SELECT id FROM falukant_data.relationship
WHERE character1_id = :characterId
OR character2_id = :characterId
)
`,
{
replacements: { characterId: parsedCharacterId },
type: QueryTypes.DELETE,
transaction: t
}
);
await sequelize.query(
`
DELETE FROM falukant_data.relationship
WHERE character1_id = :characterId
OR character2_id = :characterId
`,
{
replacements: { characterId: parsedCharacterId },
type: QueryTypes.DELETE,
transaction: t
}
);
await sequelize.query(
`
DELETE FROM falukant_data.region_tax_history
WHERE setter_character_id = :characterId
`,
{
replacements: { characterId: parsedCharacterId },
type: QueryTypes.DELETE,
transaction: t
}
);
await sequelize.query(
`
DELETE FROM falukant_log.health_activity
WHERE character_id = :characterId
`,
{
replacements: { characterId: parsedCharacterId },
type: QueryTypes.DELETE,
transaction: t
}
);
await sequelize.query(
`
DELETE FROM falukant_log.political_office_history
WHERE character_id = :characterId
`,
{
replacements: { characterId: parsedCharacterId },
type: QueryTypes.DELETE,
transaction: t
}
);
const deletedCharacters = await FalukantCharacter.destroy({
where: { id: parsedCharacterId },
transaction: t
});
summary.characterDeleted = deletedCharacters > 0;
});
if (character.userId) {
const falukantUser = await FalukantUser.findByPk(character.userId, {
include: [{ model: User, as: 'user', attributes: ['hashedId'] }]
});
const hashedId = falukantUser?.user?.hashedId;
if (hashedId) {
await notifyUser(hashedId, 'falukantUpdateStatus', {});
await notifyUser(hashedId, 'familychanged', {});
}
}
return { success: true, ...summary };
}
// --- User Administration ---
async searchUsers(requestingHashedUserId, query) {
if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) {