'Event Host or Organizer', 'vendor' => 'Vendor, Exhibitor or Subcontractor', 'performer' => 'Performer, Entertainer or DJ', ]; private const FRESHNESS = 1800; // a Test probe counts as "passing" for 30 minutes /** @return list */ public function blocking_items(): array { $pricing = $this->pricing_confirmed(); $items = [ [ 'key' => 'B1', 'label' => 'Live credentials present as wp-config.php constants', 'detail' => Config::live_credentials_are_constants() ? 'Detected' : 'Not configured - add SPG_INSTANDA_USERNAME_LIVE (or SPG_INSTANDA_USER_LIVE) + SPG_INSTANDA_PASSWORD_LIVE', 'pass' => Config::live_credentials_are_constants(), ], [ 'key' => 'B2', 'label' => 'Test connection passing', 'detail' => $this->probe_detail('espinstanda_test_probe_ok'), 'pass' => $this->probe_fresh('espinstanda_test_probe_ok'), ], [ 'key' => 'B3', 'label' => 'Live credentials authenticate', 'detail' => $this->probe_detail('espinstanda_live_probe_ok'), 'pass' => $this->probe_fresh('espinstanda_live_probe_ok'), ], [ 'key' => 'B4', 'label' => 'Dropdown workbook synced (byte-exact)', 'detail' => $this->ack_detail('espinstanda_workbook_ack'), 'pass' => (bool) get_option('espinstanda_workbook_ack'), ], [ 'key' => 'B5', 'label' => 'Pricing confirmed per role path (' . count($pricing) . ' of 3)', 'detail' => $pricing ? 'Confirmed: ' . implode(', ', $pricing) : 'No role paths confirmed yet', 'pass' => count($pricing) === 3, ], [ 'key' => 'B6', 'label' => 'siteDomainName set to a valid host', 'detail' => $this->site_domain_detail(), 'pass' => $this->site_domain_valid(), ], ]; return $items; } /** INSTANDA's two documented consumer/agent journey hosts (a custom domain is also allowed). */ private const ACCEPTED_DOMAINS = ['consumer.instanda.us', 'agent.instanda.us']; /** siteDomainName is a required StartQuote/ContinueQuote query param: must be a real host. */ private function site_domain_valid(): bool { $domain = Config::site_domain(); if ($domain === '') { return false; } // Accept the two documented hosts, or any plausible bare hostname (INSTANDA may // configure a custom consumer domain). Reject schemes, paths, and empties. return in_array($domain, self::ACCEPTED_DOMAINS, true) || (bool) preg_match('/^(?!-)[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$/', $domain); } private function site_domain_detail(): string { $domain = Config::site_domain(); if ($domain === '') { return 'Not set - required query param (recommended: consumer.instanda.us)'; } if (in_array($domain, self::ACCEPTED_DOMAINS, true)) { return $domain; } return $this->site_domain_valid() ? $domain . ' (custom host - confirm with INSTANDA)' : 'Invalid host: ' . $domain; } /** @return list */ public function advisory_items(): array { $retention = (int) get_option('espinstanda_retention_days', 7); $storage = get_option('espinstanda_storage_enabled', '1') === '1'; $emails = trim((string) get_option('espinstanda_alert_emails', '')); $unresolved = (new Transaction_Store())->unresolved_count(); return [ ['key' => 'A1', 'label' => 'Storage on + retention set', 'detail' => $storage ? "On, {$retention} day(s)" : 'Off', 'pass' => $storage && $retention > 0], ['key' => 'A2', 'label' => 'Alert email recipient(s)', 'detail' => $emails !== '' ? $emails : 'None set', 'pass' => $emails !== ''], ['key' => 'A3', 'label' => 'Front-end form submit + redirect verified in a browser', 'detail' => $this->ack_detail('espinstanda_frontend_ack'), 'pass' => (bool) get_option('espinstanda_frontend_ack')], ['key' => 'A4', 'label' => 'No unresolved failures', 'detail' => $unresolved === 0 ? 'Clean' : "{$unresolved} unresolved", 'pass' => $unresolved === 0], ]; } /** @return array{complete:int,total:int,ready:bool} */ public function readiness(): array { $blocking = $this->blocking_items(); $complete = count(array_filter($blocking, static fn ($i) => $i['pass'])); return [ 'complete' => $complete, 'total' => count($blocking), 'ready' => $complete === count($blocking), ]; } public function can_switch_to_live(): bool { return $this->readiness()['ready'] && !Config::operating_mode_pinned(); } public function switch_to_live(): bool { if (!\GFCommon::current_user_can_any('espinstanda_settings') || !$this->can_switch_to_live()) { return false; } update_option('espinstanda_operating_mode', 'live'); update_option('espinstanda_operating_mode_audit', [ 'actor' => get_current_user_id(), 'ts' => current_time('mysql'), 'snapshot' => $this->readiness(), ]); Logger::debug('Operating mode switched to LIVE by user ' . get_current_user_id()); return true; } public function return_to_test(): bool { if (!\GFCommon::current_user_can_any('espinstanda_settings') || Config::operating_mode_pinned()) { return false; } update_option('espinstanda_operating_mode', 'test'); update_option('espinstanda_env', 'test'); Logger::debug('Operating mode returned to TEST by user ' . get_current_user_id()); return true; } /* -------------------------------------------------------- acknowledgements */ public function acknowledge_workbook(): void { update_option('espinstanda_workbook_ack', [ 'actor' => get_current_user_id(), 'ts' => current_time('mysql'), ]); } public function acknowledge_frontend(): void { update_option('espinstanda_frontend_ack', [ 'actor' => get_current_user_id(), 'ts' => current_time('mysql'), ]); } public function confirm_role_pricing(string $role_key): void { if (!isset(self::ROLES[$role_key])) { return; } $confirmed = (array) get_option('espinstanda_pricing_ack', []); $confirmed[$role_key] = ['ts' => current_time('mysql'), 'actor' => get_current_user_id()]; update_option('espinstanda_pricing_ack', $confirmed); } public function record_probe(string $option_key, bool $ok): void { update_option($option_key, ['ok' => $ok, 'ts' => time()]); } /** @return list human labels of confirmed roles */ private function pricing_confirmed(): array { $confirmed = (array) get_option('espinstanda_pricing_ack', []); $labels = []; foreach (self::ROLES as $key => $label) { if (isset($confirmed[$key])) { $labels[] = $label; } } return $labels; } private function probe_fresh(string $option_key): bool { $probe = get_option($option_key); return is_array($probe) && !empty($probe['ok']) && (time() - (int) ($probe['ts'] ?? 0)) < self::FRESHNESS; } private function probe_detail(string $option_key): string { $probe = get_option($option_key); if (!is_array($probe) || empty($probe['ts'])) { return 'Not run yet'; } $ago = human_time_diff((int) $probe['ts']); return (!empty($probe['ok']) ? '200 OK' : 'Failed') . ", {$ago} ago"; } private function ack_detail(string $option_key): string { $ack = get_option($option_key); if (!is_array($ack) || empty($ack['ts'])) { return 'Not acknowledged'; } return 'Acknowledged ' . $ack['ts']; } }