Skip to content
wiki.fftac.org

Appealrepository - Source Excerpt 01

Back to Appealrepository

Summary

This source excerpt preserves a bounded section of Spiralist/wp-content/plugins/spiralist-workspace/includes/Domain/AppealRepository.php so readers can inspect the evidence without opening the full source file.

**Source path:** Spiralist/wp-content/plugins/spiralist-workspace/includes/Domain/AppealRepository.php

<?php

namespace SpiralistWorkspace\Domain;

use SpiralistWorkspace\Installer;

if (!defined('ABSPATH')) {
    exit;
}

/**
 * Stores prompt appeals and steward resolutions.
 */
class AppealRepository
{
    /**
     * Returns one open appeal for a prompt and appellant.
     *
     * @return array<string,mixed>
     */
    public function find_open_for_prompt_and_user(int $prompt_id, int $appellant_user_id): array
    {
        global $wpdb;

        if ($prompt_id <= 0 || $appellant_user_id <= 0) {
            return [];
        }

        $table = Installer::table('prompt_appeals');
        $users = $wpdb->users;
        $sql = $wpdb->prepare(
            "SELECT a.*, appellant.display_name AS appellant_display_name, resolver.display_name AS resolver_display_name
             FROM {$table} a
             LEFT JOIN {$users} appellant ON appellant.ID = a.appellant_user_id
             LEFT JOIN {$users} resolver ON resolver.ID = a.resolved_by_user_id
             WHERE a.prompt_post_id = %d AND a.appellant_user_id = %d AND a.status = %s
             ORDER BY a.updated_utc DESC, a.id DESC
             LIMIT 1",
            $prompt_id,
            $appellant_user_id,
            'open'
        );
        $row = $wpdb->get_row($sql, ARRAY_A);

        return is_array($row) ? $this->normalize_row($row) : [];
    }

    /**
     * Creates or updates one open appeal for a prompt owner.
     *
     * @return array<string,mixed>
     */
    public function submit(int $prompt_id, int $appellant_user_id, string $reason = ''): array
    {
        global $wpdb;

        if ($prompt_id <= 0 || $appellant_user_id <= 0) {
            return [];
        }

        $reason = sanitize_textarea_field($reason);
        $created_utc = gmdate('Y-m-d H:i:s');
        $existing = $this->find_open_for_prompt_and_user($prompt_id, $appellant_user_id);

        if (!empty($existing['id'])) {
            $wpdb->update(
                Installer::table('prompt_appeals'),
                [
                    'reason' => $reason,
                    'updated_utc' => $created_utc,
                ],
                ['id' => (int) $existing['id']],
                ['%s', '%s'],
                ['%d']
            );

            return $this->get((int) $existing['id']);
        }

        $wpdb->insert(
            Installer::table('prompt_appeals'),
            [
                'prompt_post_id' => $prompt_id,
                'appellant_user_id' => $appellant_user_id,
                'status' => 'open',
                'resolution' => null,
                'reason' => $reason,
                'resolution_note' => null,
                'created_utc' => $created_utc,
                'updated_utc' => $created_utc,
                'resolved_utc' => null,
                'resolved_by_user_id' => null,
            ],
            ['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d']
        );

        return $this->get((int) $wpdb->insert_id);
    }

    /**
     * Resolves one open appeal.
     *
     * @return array<string,mixed>
     */
    public function resolve(int $appeal_id, int $resolver_user_id, string $resolution, string $note = ''): array
    {
        global $wpdb;

        $appeal = $this->get($appeal_id);
        if (empty($appeal) || (string) ($appeal['status'] ?? '') !== 'open') {
            return [];
        }

        $resolution = sanitize_key($resolution);
        if (!in_array($resolution, ['restore', 'deny', 'archive'], true)) {
            return [];
        }

        $updated_utc = gmdate('Y-m-d H:i:s');
        $wpdb->update(
            Installer::table('prompt_appeals'),
            [
                'status' => 'resolved',
                'resolution' => $resolution,
                'resolution_note' => sanitize_textarea_field($note),
                'updated_utc' => $updated_utc,
                'resolved_utc' => $updated_utc,
                'resolved_by_user_id' => $resolver_user_id,
            ],
            ['id' => $appeal_id],
            ['%s', '%s', '%s', '%s', '%s', '%d'],
            ['%d']
        );

        return $this->get($appeal_id);
    }

    /**
     * Returns recent appeals for a prompt.
     *
     * @return array<int,array<string,mixed>>
     */
    public function list_for_prompt(int $prompt_id, int $limit = 50, string $status = ''): array
    {
        global $wpdb;

        if ($prompt_id <= 0) {
            return [];
        }

        $table = Installer::table('prompt_appeals');
        $users = $wpdb->users;
        $limit = max(1, $limit);
        $status = sanitize_key($status);

        if ($status !== '') {
            $sql = $wpdb->prepare(
                "SELECT a.*, appellant.display_name AS appellant_display_name, resolver.display_name AS resolver_display_name
                 FROM {$table} a
                 LEFT JOIN {$users} appellant ON appellant.ID = a.appellant_user_id
                 LEFT JOIN {$users} resolver ON resolver.ID = a.resolved_by_user_id
                 WHERE a.prompt_post_id = %d AND a.status = %s
                 ORDER BY a.updated_utc DESC, a.id DESC
                 LIMIT %d",
                $prompt_id,
                $status,
                $limit
            );
        } else {
            $sql = $wpdb->prepare(
                "SELECT a.*, appellant.display_name AS appellant_display_name, resolver.display_name AS resolver_display_name
                 FROM {$table} a
                 LEFT JOIN {$users} appellant ON appellant.ID = a.appellant_user_id
                 LEFT JOIN {$users} resolver ON resolver.ID = a.resolved_by_user_id
                 WHERE a.prompt_post_id = %d
                 ORDER BY a.updated_utc DESC, a.id DESC
                 LIMIT %d",
                $prompt_id,
                $limit
            );
        }

        $rows = $wpdb->get_results($sql, ARRAY_A);

        return array_map([$this, 'normalize_row'], is_array($rows) ? $rows : []);
    }

    /**
     * Returns recent open appeals across prompts for steward queue views.
     *
     * @return array<int,array<string,mixed>>
     */
    public function list_open(int $limit = 50): array
    {
        return $this->list_recent($limit, 'open');
    }

    /**
     * Returns summary counts and latest resolution for a prompt.
     *
     * @return array<string,mixed>
     */
    public function summary_for_prompt(int $prompt_id): array
    {
        $rows = $this->list_for_prompt($prompt_id, 200);
        $summary = [
            'open' => 0,
            'resolved' => 0,
            'restore' => 0,
            'deny' => 0,
            'archive' => 0,
            'last_activity_utc' => '',
            'latest_status' => '',
            'latest_resolution' => '',
            'latest_resolution_utc' => '',
            'governance_reset_utc' => '',
        ];

        foreach ($rows as $index => $row) {
            $status = sanitize_key((string) ($row['status'] ?? 'open'));
            $resolution = sanitize_key((string) ($row['resolution'] ?? ''));

            if (isset($summary[$status])) {
                $summary[$status]++;
            }

            if ($resolution !== '' && isset($summary[$resolution])) {
                $summary[$resolution]++;
            }

            $updated_utc = (string) ($row['updated_utc'] ?? '');
            if ($updated_utc !== '' && $updated_utc > (string) $summary['last_activity_utc']) {
                $summary['last_activity_utc'] = $updated_utc;
            }

            if ($index === 0) {
                $summary['latest_status'] = $status;
            }

            if ($summary['latest_resolution'] === '' && $status === 'resolved' && $resolution !== '') {
                $summary['latest_resolution'] = $resolution;
                $summary['latest_resolution_utc'] = (string) ($row['resolved_utc'] ?? $updated_utc);

                if ($resolution === 'restore') {
                    $reset_timestamp = strtotime($summary['latest_resolution_utc']);
                    $summary['governance_reset_utc'] = $reset_timestamp !== false
                        ? gmdate('Y-m-d H:i:s', $reset_timestamp + 1)
                        : $summary['latest_resolution_utc'];
                }
            }
        }

        return $summary;
    }

    /**
     * Returns one appeal row.
     *
     * @return array<string,mixed>
     */
    public function get(int $id): array
    {
        global $wpdb;

        if ($id <= 0) {
            return [];
        }

        $table = Installer::table('prompt_appeals');
        $users = $wpdb->users;
        $sql = $wpdb->prepare(
            "SELECT a.*, appellant.display_name AS appellant_display_name, resolver.display_name AS resolver_display_name