$audit_id ] : []; $result = ( new Client() )->get( '/payments/status', $query ); } catch ( Consent_Required_Exception $e ) { set_transient( self::CACHE_KEY, 'error', self::CACHE_TTL_ERROR ); return false; } catch ( Backend_Exception $e ) { set_transient( self::CACHE_KEY, 'error', self::CACHE_TTL_ERROR ); return false; } return self::apply_status( $result ); } /** * Record a `/payments/status` or `/payments/verify` response. Returns true * when it reported a paid site. */ public static function apply_status( $result, string $fallback_reference = '' ): bool { if ( ! is_array( $result ) || empty( $result['paid'] ) ) { set_transient( self::CACHE_KEY, 'unpaid', self::CACHE_TTL ); return false; } $reference = (string) ( $result['reference'] ?? $fallback_reference ); $audit_id = isset( $result['audit_id'] ) ? (string) $result['audit_id'] : ''; self::mark_paid( $reference, $audit_id ); return true; } /** * Flip local state to paid and park the wizard on the apply step. * * `$audit_id` is the snapshot the payment actually belongs to — the backend * widens its lookup beyond the audit we asked about, so adopting the one it * returns keeps later audit-scoped calls pointed at the right snapshot. */ public static function mark_paid( string $reference, string $audit_id = '' ): void { $state = self::read_state(); $already = ! empty( $state['paid'] ) && ( $state['entitlementRef'] ?? '' ) === $reference && ( '' === $audit_id || ( $state['auditId'] ?? '' ) === $audit_id ); if ( $already ) { return; } $state['paid'] = true; $state['entitlementRef'] = $reference; $state['step'] = 'apply'; $state['pendingRef'] = ''; // checkout resolved — stop resuming it $state['syncSuppressed'] = false; // settled — nothing left to suppress if ( '' !== $audit_id ) { $state['auditId'] = $audit_id; } if ( ! in_array( 'pay', $state['completedSteps'] ?? [], true ) ) { $state['completedSteps'][] = 'pay'; } update_option( self::OPTION, $state, false ); delete_transient( self::CACHE_KEY ); } public static function defaults(): array { return [ 'step' => 'welcome', 'completedSteps' => [], 'paid' => false, 'auditId' => '', 'entitlementRef' => '', 'skipped' => false, 'pendingRef' => '', 'syncSuppressed' => false, ]; } /** * Whether the background reconcile in sync() is currently held off. * * Set by a wizard reset. The flag lives in the option rather than in a * one-off argument because the wizard re-reads state immediately after * resetting: a guard that covered only the reset response would be undone * by the very next GET. Onboarding_Controller clears it once the user * walks back to the pay step, and mark_paid() clears it on settlement. */ public static function is_sync_suppressed(): bool { $state = self::read_state(); return ! empty( $state['syncSuppressed'] ); } public static function set_sync_suppressed( bool $suppressed ): void { $state = self::read_state(); $state['syncSuppressed'] = $suppressed; update_option( self::OPTION, $state, false ); } public static function read_state(): array { $stored = get_option( self::OPTION, [] ); if ( ! is_array( $stored ) ) { $stored = []; } return array_merge( self::defaults(), $stored ); } }