'transaction', 'plural' => 'transactions', 'ajax' => false]); $this->store = new Transaction_Store(); } public function get_columns(): array { return [ 'cb' => '', '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('%s (%d)', 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('', $item->id); } protected function column_default($item, $column_name): string { return match ($column_name) { 'submitted' => esc_html($item->created_at) . '
#' . $item->id . '', 'email' => esc_html($item->customer_email), 'role' => esc_html($item->role_label), 'status' => $this->status_badge($item), 'env' => '' . esc_html(strtoupper($item->environment)) . '', '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 'Failed · possible dup'; } $label = ucfirst($item->status); return '' . esc_html($label) . ''; } private function row_action_buttons(Transaction $item): string { $detail = add_query_arg(['page' => 'espinstanda-transactions', 'tx' => $item->id], admin_url('admin.php')); $view = sprintf('View log', 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( '', $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(); } } }