__( 'You do not have permission to do this.', ACHP_TEXT_DOMAIN ) ], 403 ); } check_ajax_referer( self::SHOW_TOKEN_NONCE, 'nonce' ); $token = get_option( 'achp_access_token' ); $expiry = (int) get_option( 'achp_access_token_expiry' ); if ( empty( $token ) ) { wp_send_json_success( [ 'has_token' => false, 'message' => __( 'No token is currently stored. Click "Check Payment Status" once to force one to be generated, then try this again.', ACHP_TEXT_DOMAIN ), ] ); } $options = get_option( 'woocommerce_ach_processing_settings', [] ); $testmode = isset( $options['testmode'] ) && 'yes' === $options['testmode']; $configured_status_url = $testmode ? ( $options['test_status_check_api_endpoint'] ?? '' ) : ( $options['status_check_api_endpoint'] ?? '' ); $result = [ 'has_token' => true, 'token' => $token, 'expiry' => $expiry ? gmdate( 'Y-m-d H:i:s', $expiry ) . ' UTC' : '(unknown)', 'expiry_unix' => $expiry, 'is_expired' => $expiry > 0 && $expiry < time(), 'testmode' => $testmode, 'login_url' => ( $testmode ? ( $options['test_api_endpoint'] ?? '' ) : ( $options['api_endpoint'] ?? '' ) ) . 'EnsembleLogin', 'status_check_url' => ! empty( $configured_status_url ) ? $configured_status_url : ACHP_Main_Class::$checkStatusUrl . ' (default — ' . ( $testmode ? 'test_status_check_api_endpoint' : 'status_check_api_endpoint' ) . ' is not set)', 'decoded' => self::decode_jwt_if_possible( $token ), ]; wp_send_json_success( $result ); } /** * Best-effort JWT decode for display purposes only — this does NOT * verify the signature (no public key/secret is available here), * it just base64url-decodes the header and payload segments so an * identity engineer can see claims like "iss"/"aud" without needing * to paste a live bearer token into a third-party decoding site. * Returns null if the token isn't a standard 3-segment JWT. */ private static function decode_jwt_if_possible( $token ) { $parts = explode( '.', (string) $token ); if ( 3 !== count( $parts ) ) { return null; } $decode_segment = static function ( $segment ) { $padded = strtr( $segment, '-_', '+/' ); $padded .= str_repeat( '=', ( 4 - strlen( $padded ) % 4 ) % 4 ); $raw = base64_decode( $padded, true ); if ( false === $raw ) { return null; } $json = json_decode( $raw, true ); return null !== $json ? $json : $raw; }; $header = $decode_segment( $parts[0] ); $payload = $decode_segment( $parts[1] ); if ( null === $header && null === $payload ) { return null; } return [ 'header' => $header, 'payload' => $payload, ]; } /** * Manual trigger for ACHP_Status_Poller::purge_all_scheduled(), fired * by the "Purge Stale Schedules" button on the gateway settings * screen. Use this to force cleanup on demand rather than waiting * for the version-gated migration to run on the next page load. */ public function ajax_purge_schedules() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( [ 'message' => __( 'You do not have permission to do this.', ACHP_TEXT_DOMAIN ) ], 403 ); } check_ajax_referer( self::PURGE_NONCE, 'nonce' ); if ( ! class_exists( 'ACHP_Status_Poller' ) ) { wp_send_json_error( [ 'message' => __( 'Status poller is not available.', ACHP_TEXT_DOMAIN ) ], 500 ); } try { $cancelled = ACHP_Status_Poller::purge_all_scheduled(); } catch ( \Throwable $e ) { wc_get_logger()->error( sprintf( 'Manual schedule purge failed: %s', $e->getMessage() ), [ 'source' => 'ach-processing' ] ); wp_send_json_error( [ 'message' => __( 'Purge failed. See WooCommerce → Status → Logs (ach-processing) for details.', ACHP_TEXT_DOMAIN ) ], 500 ); } $summary = sprintf( /* translators: %d: number of stale scheduled actions removed. */ _n( 'Purge complete. Removed %d stale scheduled action.', 'Purge complete. Removed %d stale scheduled actions.', (int) $cancelled, ACHP_TEXT_DOMAIN ), (int) $cancelled ); wp_send_json_success( [ 'message' => $summary, 'cancelled' => (int) $cancelled, ] ); } /** * Manual trigger for the status poller, fired by the "Check Status" * button on the gateway settings screen. */ public function ajax_run_poll_now() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_send_json_error( [ 'message' => __( 'You do not have permission to run the status check.', ACHP_TEXT_DOMAIN ) ], 403 ); } check_ajax_referer( self::POLL_NOW_NONCE, 'nonce' ); if ( ! class_exists( 'ACHP_Status_Poller' ) ) { wp_send_json_error( [ 'message' => __( 'Status poller is not available.', ACHP_TEXT_DOMAIN ) ], 500 ); } try { $stats = ACHP_Status_Poller::run( true ); // Manual trigger bypasses the scheduling floor. } catch ( \Throwable $e ) { wc_get_logger()->error( sprintf( 'Manual status poll failed: %s', $e->getMessage() ), [ 'source' => 'ach-processing' ] ); wp_send_json_error( [ 'message' => __( 'Status check failed. See WooCommerce → Status → Logs (ach-processing) for details.', ACHP_TEXT_DOMAIN ) ], 500 ); } $stats = is_array( $stats ) ? $stats : []; $updated = (int) ( $stats['updated'] ?? 0 ); $summary = sprintf( /* translators: %d: number of orders whose status was updated. */ _n( 'Status check complete. %d order updated.', 'Status check complete. %d orders updated.', $updated, ACHP_TEXT_DOMAIN ), $updated ); wp_send_json_success( [ 'message' => $summary, 'stats' => $stats, ] ); } public function webhook_callback() { $request_body = file_get_contents( 'php://input' ); wc_get_logger()->debug( print_r( $request_body, true ), [ 'source' => 'ach-processing-webhook' ] ); } public function add_token_notice() { // Only surface this on the WooCommerce settings screen to avoid // polluting every admin page. $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || 'woocommerce_page_wc-settings' !== $screen->id ) { return; } $token = get_option( 'achp_access_token' ); $expiry = (int) get_option( 'achp_access_token_expiry' ); $now = time(); $missing = empty( $token ) || 0 === $expiry; $expired = ! $missing && $now > $expiry; if ( ! $missing && ! $expired ) { return; } $message = $missing ? __( 'ACH Processing access token is missing. Verify API credentials and save settings to generate one.', ACHP_TEXT_DOMAIN ) : __( 'ACH Processing access token has expired. It will be refreshed automatically on the next scheduled run.', ACHP_TEXT_DOMAIN ); ?>