Files
hub-insurance/wp-content/plugins/esp-instanda-integration/includes/class-transactions-table.php
T
2026-07-02 15:54:39 -06:00

180 lines
7.5 KiB
PHP

<?php
/**
* Transactions browser - a WP_List_Table over the operational store with status
* views and a nonce + capability-gated, cross-env-guarded, async Resubmit.
*
* Resubmit never blocks the admin: it schedules a single cron event and returns.
* A "possible duplicate" record requires explicit confirmation before resubmit
* (INSTANDA has no idempotency, so resubmitting a request that actually succeeded
* would create a second quote).
*
* @package ESP\Instanda
*/
declare(strict_types=1);
namespace ESP\Instanda;
if (!class_exists('\WP_List_Table') && defined('ABSPATH')) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
final class Transactions_List_Table extends \WP_List_Table {
private Transaction_Store $store;
public function __construct() {
parent::__construct(['singular' => 'transaction', 'plural' => 'transactions', 'ajax' => false]);
$this->store = new Transaction_Store();
}
public function get_columns(): array {
return [
'cb' => '<input type="checkbox" />',
'submitted' => 'Submitted',
'email' => 'Customer email',
'role' => 'Role',
'status' => 'Status',
'env' => 'Env',
'quote_ref' => 'quoteRef',
'retries' => 'Retries',
'row_actions'=> 'Actions',
];
}
protected function get_views(): array {
$counts = $this->store->counts();
$current = (string) ($_GET['status'] ?? 'all');
$base = admin_url('admin.php?page=espinstanda-transactions');
$make = static function (string $key, string $label, int $n) use ($current, $base): string {
$url = $key === 'all' ? $base : add_query_arg('status', $key, $base);
$class = $current === $key ? ' class="current"' : '';
return sprintf('<a href="%s"%s>%s <span class="count">(%d)</span></a>', esc_url($url), $class, esc_html($label), $n);
};
return [
'all' => $make('all', 'All', $counts['all']),
'delivered' => $make('delivered', 'Delivered', $counts['delivered']),
'failed' => $make('failed', 'Failed', $counts['failed']),
'pending' => $make('pending', 'Pending', $counts['pending']),
];
}
protected function get_bulk_actions(): array {
return ['resubmit' => 'Resubmit'];
}
public function prepare_items(): void {
$per_page = 25;
$page = max(1, (int) $this->get_pagenum());
$status = (string) ($_GET['status'] ?? 'all');
$filters = $status !== 'all' ? ['status' => $status] : [];
$this->items = $this->store->query($filters, $page, $per_page);
$this->_column_headers = [$this->get_columns(), [], []];
$counts = $this->store->counts();
$total = $status !== 'all' ? ($counts[$status] ?? 0) : $counts['all'];
$this->set_pagination_args(['total_items' => $total, 'per_page' => $per_page]);
}
protected function column_cb($item): string {
return sprintf('<input type="checkbox" name="tx[]" value="%d" />', $item->id);
}
protected function column_default($item, $column_name): string {
return match ($column_name) {
'submitted' => esc_html($item->created_at) . '<br><span class="mono">#' . $item->id . '</span>',
'email' => esc_html($item->customer_email),
'role' => esc_html($item->role_label),
'status' => $this->status_badge($item),
'env' => '<span class="espinstanda-env">' . esc_html(strtoupper($item->environment)) . '</span>',
'quote_ref' => esc_html($item->quote_ref ?? '—'),
'retries' => (string) $item->retry_count,
'row_actions' => $this->row_action_buttons($item),
default => '',
};
}
private function status_badge(Transaction $item): string {
if ($item->possible_duplicate) {
return '<span class="espinstanda-badge dup">Failed · possible dup</span>';
}
$label = ucfirst($item->status);
return '<span class="espinstanda-badge ' . esc_attr($item->status) . '">' . esc_html($label) . '</span>';
}
private function row_action_buttons(Transaction $item): string {
$detail = add_query_arg(['page' => 'espinstanda-transactions', 'tx' => $item->id], admin_url('admin.php'));
$view = sprintf('<a class="button button-small" href="%s">View log</a>', esc_url($detail));
if ($item->status === Tx_Status::Delivered->value) {
return $view;
}
$cross_env = $item->environment !== Config::environment()->value;
$label = $item->possible_duplicate ? 'Resubmit…' : 'Resubmit';
$resubmit = sprintf(
'<button class="button button-small espinstanda-resubmit" data-tx="%d" data-dup="%d" %s>%s</button>',
$item->id,
$item->possible_duplicate ? 1 : 0,
$cross_env ? 'disabled title="Blocked - stored environment differs from the active one"' : '',
esc_html($label)
);
return $view . ' ' . $resubmit;
}
}
/** Async resubmit worker - runs on the esp_instanda_resubmit cron event. */
final class Resubmit_Handler {
public static function schedule(int $tx_id): void {
wp_schedule_single_event(time() + 1, 'esp_instanda_resubmit', [$tx_id]);
}
public static function run(int $tx_id): void {
$store = new Transaction_Store();
$tx = $store->find($tx_id);
if ($tx === null) {
return;
}
if ($tx->environment !== Config::environment()->value) {
$store->append_event($tx_id, 'Resubmit blocked - cross-environment.', 'error');
return;
}
// Self-recovery guard: skip if the customer already has a recent successful quote.
$recent = $store->recent_success_for_email($tx->customer_email, DAY_IN_SECONDS);
if ($recent !== null && $recent->id !== $tx->id) {
$store->append_event($tx_id, 'Resubmit skipped - customer already has a recent successful quote.');
return;
}
$body = json_decode($tx->redacted_payload, true);
if (!is_array($body)) {
$store->append_event($tx_id, 'Resubmit aborted - stored payload unreadable.', 'error');
return;
}
$result = (new Api_Client())->start_quote($body, $tx->dedupe_key);
if ($result->ok && $result->quote_ref !== null) {
$store->mark_success($tx_id, $result->quote_ref, $result->url_single_use, $result->http_status);
Failure_Surface::bump();
if ($tx->entry_id) {
Entry_Integration::record($tx->entry_id, $result->quote_ref, (string) $result->url_single_use, 'delivered', $tx->environment);
} else {
// Stranded lead (no entry) - mint a fresh link and email the customer (option B).
$fresh = (string) $result->url_single_use;
if ($fresh === '') {
$fresh = (string) (new Api_Client())->continue_quote($result->quote_ref)->url_single_use;
}
if ($fresh !== '') {
(new Notifier())->customer_quote_ready_email($tx, $fresh);
}
}
} else {
$store->mark_failed($tx_id, $result->http_status, $result->response_excerpt, $result->error_string());
Failure_Surface::bump();
}
}
}