false,
'error' => __( 'Scheduler not ready.', 'marginbeacon' ),
);
$scheduler = Plugin::instance()->scheduler();
if ( $scheduler ) {
$result = ( new PreflightJobService( $scheduler ) )->enqueue( $coupon_id );
}
if ( empty( $result['success'] ) ) {
add_settings_error(
'aksmb_preflight',
'fail',
isset( $result['error'] ) ? $result['error'] : __( 'Preflight failed.', 'marginbeacon' ),
'error'
);
return;
}
( new FreshnessService() )->refresh_summary_cache();
wp_safe_redirect(
Tabs::url(
Tabs::PREFLIGHT,
array(
'scan_id' => (int) $result['scan_id'],
'queued' => 1,
)
)
);
exit;
}
/**
* @return void
*/
public function render() {
if ( ! Capabilities::current_user_can_manage() ) {
wp_die( esc_html__( 'You do not have permission to access MarginBeacon.', 'marginbeacon' ) );
}
settings_errors( 'aksmb_preflight' );
$this->render_flash_notice();
$repo = new FindingsRepository();
$scan_id = isset( $_GET['scan_id'] ) ? absint( $_GET['scan_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$scan = $scan_id ? $repo->get_scan( $scan_id ) : null;
$findings = $scan ? $repo->get_findings_for_scan( $scan_id ) : array();
$recent = $repo->recent_preflight_scans( 10 );
$search = isset( $_GET['aksmb_coupon_s'] ) ? sanitize_text_field( wp_unslash( $_GET['aksmb_coupon_s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$status = isset( $_GET['aksmb_coupon_status'] ) ? sanitize_key( wp_unslash( $_GET['aksmb_coupon_status'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$type = isset( $_GET['aksmb_coupon_type'] ) ? sanitize_key( wp_unslash( $_GET['aksmb_coupon_type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( strlen( $search ) > 64 ) {
$search = substr( $search, 0, 64 );
}
$inventory = $this->build_inventory( $repo, $search, $status, $type );
$coupons = array();
foreach ( $inventory as $row ) {
if ( 'publish' === $row['status'] ) {
$coupons[] = array(
'id' => $row['id'],
'code' => $row['code'],
'type' => $row['type'],
'amount' => $row['amount'],
);
}
}
?>
$fresh,
'label' => Freshness::label( $fresh ),
);
}
/**
* Render findings table body HTML for a scan (testable without full admin bootstrap).
*
* @param array $scan Scan row (unused except for presence checks).
* @param array $findings Finding rows.
* @return string HTML fragment.
*/
public function render_findings_rows_html( array $scan, array $findings ) {
unset( $scan );
if ( empty( $findings ) ) {
return '| ' . esc_html__( 'No eligible products for this coupon.', 'marginbeacon' ) . ' |
';
}
$presenter = new FindingResultPresenter();
$html = '';
foreach ( $findings as $f ) {
if ( ! is_array( $f ) ) {
continue;
}
$details = $presenter->details_from( $f );
$badge = $this->freshness_badge_for_finding( $f );
$sev = isset( $f['severity'] ) ? (string) $f['severity'] : 'unknown';
$currency = isset( $f['currency'] ) ? (string) $f['currency'] : null;
$html .= ''
. '| ' . $presenter->product_cell_html( $f, $details ) . ' | '
. '' . $presenter->coupon_cell_html( $details ) . ' | '
. '' . esc_html( $presenter->coupon_type_cell( $details ) ) . ' | '
. '' . esc_html( $presenter->coupon_discount_cell( $details, $currency ) ) . ' | '
. '' . esc_html( Severity::label( $sev ) ) . ' | '
. ''
. esc_html( $badge['label'] ) . ' | '
. '
';
}
return $html;
}
/**
* One-shot notice after Pro actions (pairwise redirect).
*
* @return void
*/
private function render_flash_notice() {
if ( ! isset( $_GET['aksmb_ack'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
$ack = wp_unslash( $_GET['aksmb_ack'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Exact equality gate only.
$ok = is_string( $ack ) && '1' === $ack;
$msg = isset( $_GET['aksmb_msg'] ) ? sanitize_text_field( rawurldecode( wp_unslash( $_GET['aksmb_msg'] ) ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( '' === $msg ) {
return;
}
$class = $ok ? 'notice-success' : 'notice-error';
printf(
'',
esc_attr( $class ),
esc_html( $msg )
);
}
/**
* @param string $status Post status.
* @return string
*/
private function coupon_status_label( $status ) {
switch ( (string) $status ) {
case 'publish':
return __( 'Published', 'marginbeacon' );
case 'draft':
return __( 'Draft', 'marginbeacon' );
case 'pending':
return __( 'Pending', 'marginbeacon' );
case 'private':
return __( 'Private', 'marginbeacon' );
default:
return (string) $status;
}
}
/**
* Native coupon inventory rows for Free (FR-020).
*
* @param FindingsRepository $repo Findings repo.
* @param string $search Code search (bounded).
* @param string $status Post status filter.
* @param string $type Discount type filter.
* @return array>
*/
private function build_inventory( FindingsRepository $repo, $search, $status, $type ) {
$post_status = array( 'publish', 'draft', 'pending', 'private' );
if ( $status && in_array( $status, $post_status, true ) ) {
$post_status = array( $status );
}
$query = array(
'post_type' => 'shop_coupon',
'post_status' => $post_status,
'posts_per_page' => 100,
'orderby' => 'title',
'order' => 'ASC',
'no_found_rows' => true,
'update_post_meta_cache' => true,
'update_post_term_cache' => false,
);
if ( '' !== $search ) {
$query['s'] = $search;
}
$posts = get_posts( $query );
$ids = array();
foreach ( (array) $posts as $post ) {
$ids[] = (int) $post->ID;
}
$latest = $repo->latest_selected_by_coupon_ids( $ids );
$out = array();
foreach ( (array) $posts as $post ) {
$c = new \WC_Coupon( (int) $post->ID );
if ( ! $c->get_id() ) {
continue;
}
$discount_type = $c->get_discount_type();
if ( $type && $type !== $discount_type ) {
continue;
}
$expires = $c->get_date_expires();
$expiry = $expires ? $expires->date_i18n( get_option( 'date_format' ) ) : __( 'None', 'marginbeacon' );
$scan_row = isset( $latest[ (int) $post->ID ] ) ? $latest[ (int) $post->ID ] : null;
$counts = $scan_row && isset( $scan_row['counts'] ) && is_array( $scan_row['counts'] ) ? $scan_row['counts'] : array();
$fidelity = isset( $counts['calculation_fidelity'] ) ? (string) $counts['calculation_fidelity'] : '';
$affected = isset( $counts['affected'] ) ? (int) $counts['affected'] : ( isset( $counts['processed'] ) ? (int) $counts['processed'] : null );
$out[] = array(
'id' => (int) $post->ID,
'code' => $c->get_code(),
'status' => $post->post_status,
'status_label' => $this->coupon_status_label( $post->post_status ),
'type' => $discount_type,
'amount' => (string) $c->get_amount(),
'expiry' => $expiry,
'scan_id' => $scan_row ? (int) $scan_row['id'] : 0,
'last_preflight' => $scan_row && ! empty( $scan_row['completed_at_gmt'] )
? (string) $scan_row['completed_at_gmt'] . ' UTC'
: ( $scan_row ? (string) $scan_row['status'] : '' ),
'severity' => $scan_row && ! empty( $scan_row['worst_severity'] ) ? (string) $scan_row['worst_severity'] : null,
'freshness' => $scan_row && ! empty( $scan_row['worst_freshness'] ) ? (string) $scan_row['worst_freshness'] : null,
'coverage' => $fidelity ? $fidelity : '—',
'affected' => $affected,
);
}
return $out;
}
/**
* CSS class for signed money display (gross profit polarity).
*
* @param string|null $value Decimal string or null.
* @return string
*/
private function money_polarity_class( $value ) {
if ( null === $value || '' === $value ) {
return 'aksmb-money';
}
if ( ! is_numeric( $value ) ) {
return 'aksmb-money';
}
$cmp = function_exists( 'bccomp' )
? bccomp( (string) $value, '0', 8 )
: ( preg_match( '/^-/', trim( (string) $value ) ) ? -1 : ( preg_match( '/^[1-9]|^0\.\d*[1-9]/', ltrim( trim( (string) $value ), '0' ) ) ? 1 : 0 ) );
if ( $cmp < 0 ) {
return 'aksmb-money aksmb-money--negative';
}
if ( $cmp > 0 ) {
return 'aksmb-money aksmb-money--positive';
}
return 'aksmb-money aksmb-money--zero';
}
}