Files
hub-insurance/wp-content/plugins/esp-instanda-integration/includes/class-go-live.php
T
zbatteandClaude Fable 5 4d7abdbb3b fix(instanda): set env on go-live, correct secrets docblock, add feed advisory
- F-03: switch_to_live() now sets espinstanda_env=live so environment() actually
  resolves to Live after go-live (was leaving real quotes on the Test endpoint).
- F-04: correct the class-config docblock overstating the at-rest guarantee; it
  only holds when SPG_INSTANDA_KEY is a constant (else the key is self-provisioned
  into the same DB as the ciphertext).
- F-02: add advisory A5 "exactly one active INSTANDA feed" to catch a stray
  test-form feed that also creates quotes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:06:10 -06:00

238 lines
10 KiB
PHP

<?php
/**
* Go_Live - the test-mode gate. Computes the blocking (B1-B5) and advisory checklist
* items, stores acknowledgements, and performs the capability-gated, audited switch
* from "locked to Test" to "live-enabled". The switch is only possible when every
* blocking item passes; rolling back to Test is always available.
*
* @package ESP\Instanda
*/
declare(strict_types=1);
namespace ESP\Instanda;
final class Go_Live {
public const ROLES = [
'event_host' => '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<array{key:string,label:string,detail:string,pass:bool}> */
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<array{key:string,label:string,detail:string,pass:bool}> */
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],
['key' => 'A5', 'label' => 'Exactly one active INSTANDA feed', 'detail' => $this->feed_summary(), 'pass' => $this->active_feed_count() === 1],
];
}
/** Count active INSTANDA feeds across all forms (should be exactly one at go-live). */
private function active_feed_count(): int {
if (!class_exists(__NAMESPACE__ . '\\Instanda_AddOn')) {
return 0;
}
$feeds = Instanda_AddOn::get_instance()->get_feeds();
return count(array_filter((array) $feeds, static fn ($f) => !empty($f['is_active'])));
}
private function feed_summary(): string {
$n = $this->active_feed_count();
if ($n === 1) {
return '1 active feed';
}
return $n === 0
? 'No active feed - the quote form will not push to INSTANDA'
: "{$n} active feeds - deactivate all but the canonical quote form (a stray test-form feed also creates quotes)";
}
/** @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');
// Switching the mode must also select the Live environment, mirroring
// return_to_test(); otherwise environment() stays clamped to Test and real
// customer quotes would keep hitting the Test endpoint after go-live.
update_option('espinstanda_env', '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<string> 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'];
}
}