// Pure helpers for the moderation-appeals feature (Phase 6c). No DB access — // just the status/type vocabularies and row-shaping so both the model layer and // the tests can reason about appeal state without a database. // Terminal statuses: an appeal that has reached one of these is closed and can // no longer be withdrawn, claimed, or resolved. const TERMINAL = new Set(['approved', 'denied', 'withdrawn']) // Active statuses: an appeal that still occupies the "one active appeal per // action" slot. A player cannot open a second appeal for a mod_action while one // of these is outstanding. const ACTIVE = new Set(['pending', 'under_review']) // Only bans and mutes are appealable (kicks/warns are not — a kick is not a // standing state, and a warn carries no access restriction to reverse). const APPEALABLE_TYPES = new Set(['ban', 'mute']) // True if a status is one an appeal can still transition away from. function isTerminal(status) { return TERMINAL.has(status) } function isActive(status) { return ACTIVE.has(status) } function isAppealableType(actionType) { return APPEALABLE_TYPES.has(actionType) } // Map an approve/deny resolution to the reversal_status the row should carry. // A denial never reverses; an approval only reverses when the underlying action // is appealable (ban/mute) and the bot call succeeded. function reversalStatusFor({ status, actionType, botOk }) { if (status !== 'approved') return 'none' if (!APPEALABLE_TYPES.has(actionType)) return 'none' return botOk ? 'done' : 'failed' } module.exports = { TERMINAL, ACTIVE, APPEALABLE_TYPES, isTerminal, isActive, isAppealableType, reversalStatusFor, }