__( 'You have unsaved changes.', 'refmora' ), 'saving' => __( 'Saving your changes...', 'refmora' ), 'clearing_trash' => __( 'Clearing Trash...', 'refmora' ), 'clear_trash_done' => __( 'Trash cleared.', 'refmora' ), 'clear_trash_partial' => __( 'Some items could not be deleted.', 'refmora' ), 'clear_trash_error' => __( 'Clearing could not finish. Some items may have been deleted. Refresh Trash before trying again.', 'refmora' ), /* translators: %d: number of records permanently deleted so far. */ 'trash_removed' => __( 'Deleted: %d', 'refmora' ), ) ); } } ); add_action( 'admin_post_refm_payout', array( __CLASS__, 'payout_action' ) ); add_action( 'admin_post_refm_commission', array( __CLASS__, 'commission_action' ) ); add_action( 'admin_post_refm_record', array( __CLASS__, 'record_action' ) ); add_action( 'admin_post_refm_clear_trash', array( __CLASS__, 'clear_trash_action' ) ); add_action( 'wp_ajax_refm_clear_trash', array( __CLASS__, 'clear_trash_ajax' ) ); add_action( 'admin_post_refm_affiliate', array( __CLASS__, 'affiliate_action' ) ); add_filter( 'plugin_action_links_' . plugin_basename( REFM_FILE ), function ( $links ) { array_unshift( $links, '' . esc_html__( 'Settings', 'refmora' ) . '' ); return $links; } ); } public static function pending_affiliates_count() { $query = new WP_User_Query( array( 'fields' => 'ID', 'number' => 1, 'count_total' => true, 'cache_results' => true, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required enrollment/pending predicates; WordPress caches IDs and totals and invalidates them when user metadata changes. 'meta_query' => array( 'relation' => 'AND', array( 'key' => '_refm_enrolled', 'value' => 'yes' ), array( 'key' => '_refm_approval', 'value' => 'pending' ), ), ) ); return absint( $query->get_total() ); } public static function pending_counts() { global $wpdb; $commissions = REFM_Store::table( 'commissions' ); $payouts = REFM_Store::table( 'payouts' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Custom affiliate tables have no WordPress CRUD API; read current totals, history and privacy records without stale cross-request caches. $counts = $wpdb->get_row( $wpdb->prepare( 'SELECT (SELECT COUNT(*) FROM %i WHERE status=%s) AS commissions, (SELECT COUNT(*) FROM %i WHERE status=%s) AS payouts', $commissions, 'pending', $payouts, 'pending' ), ARRAY_A ); return array( 'affiliates' => self::pending_affiliates_count(), 'commissions' => absint( $counts['commissions'] ?? 0 ), 'payouts' => absint( $counts['payouts'] ?? 0 ) ); } public static function pending_badge( $count ) { $count = absint( $count ); if ( ! $count ) { return ''; } /* translators: %s: formatted count of pending items. */ $label = sprintf( _n( '%s pending item', '%s pending items', $count, 'refmora' ), number_format_i18n( $count ) ); return '' . esc_html( $label ) . ''; } public static function menu() { $title = 'Refmora Affiliate'; $count = current_user_can( 'manage_woocommerce' ) ? self::pending_affiliates_count() : 0; if ( $count ) { /* translators: %s: formatted count of pending affiliate applications. */ $description = sprintf( _n( '%s pending affiliate application', '%s pending affiliate applications', $count, 'refmora' ), number_format_i18n( $count ) ); $title .= ' ' . esc_html( $description ) . ''; } add_submenu_page( 'woocommerce', 'Refmora Affiliate', $title, 'manage_woocommerce', 'refmora', array( __CLASS__, 'render' ) ); } public static function payout_action() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Access denied.', 'refmora' ), 403 ); } if ( 'POST' !== ( isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '' ) ) { wp_die( esc_html__( 'Use the settings form to make changes.', 'refmora' ), '', array( 'response' => 405 ) ); } $id = absint( isset( $_POST['payout_id'] ) && is_scalar( $_POST['payout_id'] ) ? wp_unslash( $_POST['payout_id'] ) : 0 ); if ( ! isset( $_POST['_wpnonce'] ) || ! is_string( $_POST['_wpnonce'] ) ) { wp_die( esc_html__( 'Your session expired. Refresh and try again.', 'refmora' ), '', array( 'response' => 403 ) ); } check_admin_referer( 'refm_payout_' . $id ); $status = sanitize_key( ( isset( $_POST[ 'status' ] ) && is_scalar( $_POST[ 'status' ] ) ? wp_unslash( $_POST[ 'status' ] ) : '' ) ); $note = sanitize_textarea_field( ( isset( $_POST[ 'note' ] ) && is_scalar( $_POST[ 'note' ] ) ? wp_unslash( $_POST[ 'note' ] ) : '' ) ); $result = REFM_Payouts::transition( $id, $status, $note ); $url = add_query_arg( array( 'page' => 'refmora', 'tab' => 'payouts', 'refm_message' => is_wp_error( $result ) ? $result->get_error_message() : __( 'Withdrawal updated.', 'refmora' ), 'refm_error' => is_wp_error( $result ) ? 1 : 0 ), admin_url( 'admin.php' ) ); wp_safe_redirect( $url ); exit; } /** Process a nonce-protected manager decision, never a GET request. */ public static function commission_action() { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read the scalar ID solely to select its nonce; admin_request() verifies capability, POST and that nonce before any mutation. $id = absint( REFM_Security::value( $_POST, 'commission_id' ) ); REFM_Security::admin_request( 'refm_commission_' . $id ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- admin_request() above verified POST, manage_woocommerce and this commission's nonce. $decision = sanitize_key( wp_unslash( REFM_Security::value( $_POST, 'decision' ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- admin_request() above verified POST, manage_woocommerce and this commission's nonce. $version = sanitize_text_field( wp_unslash( REFM_Security::value( $_POST, 'review_version' ) ) ); $result = REFM_Commissions::review( $id, $decision, $version ); $message = is_wp_error( $result ) ? $result->get_error_message() : ( 'confirmed' === $decision ? __( 'Commission confirmed. Earnings are now available for withdrawal.', 'refmora' ) : __( 'Commission rejected.', 'refmora' ) ); $url = add_query_arg( array( 'page' => 'refmora', 'tab' => 'commissions', 'refm_error' => is_wp_error( $result ) ? 1 : 0, 'refm_message' => $message, ), admin_url( 'admin.php' ) ); wp_safe_redirect( $url ); exit; } /** Handle administrator record removal with a type-, ID- and operation-specific nonce. */ public static function record_action() { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read only scalar routing values to choose the nonce checked immediately below, before any mutation. $type = sanitize_key( REFM_Security::value( $_POST, 'record_type' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read only the scalar record ID for nonce selection. $id = absint( REFM_Security::value( $_POST, 'record_id' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read only the scalar operation for nonce selection. $operation = sanitize_key( REFM_Security::value( $_POST, 'operation' ) ); REFM_Security::admin_request( 'refm_record_' . $type . '_' . $id . '_' . $operation ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- admin_request() verified POST, manage_woocommerce and the record-specific nonce above. $version = sanitize_text_field( wp_unslash( REFM_Security::value( $_POST, 'record_version' ) ) ); if ( 'affiliates' === $type && 'delete' === $operation ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- The record-specific nonce and POST capability guard have already passed. $confirmation = sanitize_text_field( wp_unslash( REFM_Security::value( $_POST, 'confirmation' ) ) ); $result = REFM_Records::delete_affiliate( $id, $confirmation, $version ); } else { $result = REFM_Records::change( $type, $id, $operation, $version ); } $url = add_query_arg( array( 'page' => 'refmora', 'tab' => in_array( $type, array( 'affiliates', 'commissions', 'payouts' ), true ) ? $type : 'affiliates', 'view' => 'affiliates' !== $type && in_array( $operation, array( 'restore', 'delete' ), true ) ? 'trash' : '', 'refm_error' => is_wp_error( $result ) ? 1 : 0, 'refm_message' => is_wp_error( $result ) ? $result->get_error_message() : $result, ), admin_url( 'admin.php' ) ); wp_safe_redirect( $url ); exit; } /** @return string Nonce-verified record type for a Clear All Trash request. */ private static function clear_trash_request() { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This scalar chooses the type-specific nonce verified immediately below, before clearing any record. $type = sanitize_key( REFM_Security::value( $_POST, 'record_type' ) ); REFM_Security::admin_request( 'refm_clear_trash_' . $type ); return $type; } /** Process one bounded batch so a large Trash does not require one long request. */ public static function clear_trash_ajax() { $type = self::clear_trash_request(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- clear_trash_request() verified POST, capability and the type-specific nonce above. $cursor = absint( REFM_Security::value( $_POST, 'cursor', '0' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- clear_trash_request() verified POST, capability and the type-specific nonce above. $ceiling = REFM_Security::value( $_POST, 'ceiling' ); $result = REFM_Records::clear_trash( $type, $cursor, '' === $ceiling ? null : absint( $ceiling ) ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ), 'access' === $result->get_error_code() ? 403 : 400 ); } wp_send_json_success( $result ); } /** Clear ordinary-sized Trash without JavaScript, reporting any partial result. */ public static function clear_trash_action() { $type = self::clear_trash_request(); $cursor = 0; $ceiling = null; $deleted = 0; $skipped = 0; $done = false; $error = ''; $started = microtime( true ); do { $result = REFM_Records::clear_trash( $type, $cursor, $ceiling ); if ( is_wp_error( $result ) ) { $error = $result->get_error_message(); break; } $deleted += $result['deleted']; $skipped += $result['skipped']; $cursor = $result['cursor']; $ceiling = $result['ceiling']; $done = $result['done']; if ( '' === $error && '' !== $result['first_error'] ) { $error = $result['first_error']; } } while ( ! $done && microtime( true ) - $started < 10 ); /* translators: %d: number of permanently deleted records. */ $message = sprintf( _n( '%d item permanently deleted.', '%d items permanently deleted.', $deleted, 'refmora' ), $deleted ); if ( $skipped || ! $done || $error ) { $message .= ' ' . ( $error ?: __( 'Some items were not deleted. Resolve any related withdrawals and run Clear All Trash again.', 'refmora' ) ); } $url = add_query_arg( array( 'page' => 'refmora', 'tab' => in_array( $type, array( 'commissions', 'payouts' ), true ) ? $type : 'commissions', 'view' => 'trash', 'refm_error' => $skipped || ! $done || $error ? 1 : 0, 'refm_message' => $message, ), admin_url( 'admin.php' ) ); wp_safe_redirect( $url ); exit; } /** @return bool Whether an administrator requested the reversible Trash view. */ public static function trash_view() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only list routing; deletion and restoration require separate POST actions and nonces. return REFM_Records::allowed() && 'trash' === REFM_Security::value( $_GET, 'view' ); } /** * Render deletion or restoration for the exact record displayed. * * @param string $type Record type. * @param object|int $row Financial row or affiliate user ID. */ public static function record_actions( $type, $row ) { if ( ! REFM_Records::allowed() ) { return; } $affiliate = 'affiliates' === $type; if ( ! $affiliate && ( 'processing' === $row->status || ( 'commissions' === $type && $row->payout_id ) ) ) { echo '' . esc_html__( 'Resolve withdrawal first', 'refmora' ) . ''; return; } $id = $affiliate ? absint( $row ) : absint( $row->id ); $operations = $affiliate ? array( 'delete' ) : ( empty( $row->trashed_at ) ? array( 'trash' ) : array( 'restore', 'delete' ) ); $version = $affiliate ? REFM_Records::affiliate_version( $id ) : REFM_Records::version( $row ); if ( $affiliate ) { echo '
' . esc_html__( 'Delete', 'refmora' ) . '

' . esc_html__( 'Permanently delete this affiliate profile, commissions, withdrawals, referred-customer associations and click history. Their WordPress account and WooCommerce orders remain. This cannot be undone.', 'refmora' ) . '

'; } else { echo '
'; } foreach ( $operations as $operation ) { echo '
'; wp_nonce_field( 'refm_record_' . $type . '_' . $id . '_' . $operation ); if ( $affiliate ) { echo ''; } $confirm = 'payouts' === $type ? __( 'Move this withdrawal to Trash? Pending or approved requests will be cancelled and reserved funds released. Restore will not reopen a cancelled request.', 'refmora' ) : __( 'Move this commission to Trash? Unpaid earnings will be removed until restored. Paid history and refund adjustments are preserved.', 'refmora' ); if ( 'delete' === $operation && ! $affiliate ) { $confirm = __( 'Permanently delete this record? This cannot be undone. Removing commission history may change financial totals.', 'refmora' ); } $delete_link = ! $affiliate && in_array( $operation, array( 'trash', 'delete' ), true ); $button_class = 'restore' === $operation ? 'refm-restore-link' : ( $delete_link ? 'refm-delete-link' : 'button refm-primary refm-danger' ); echo '
'; } if ( $affiliate ) { echo '
'; } else { echo ''; } } /** * Render record views with their full, unfiltered item totals. * * @param string $type Financial record type. * @param bool $trash Whether Trash is selected. */ public static function record_views( $type, $trash ) { if ( ! REFM_Records::allowed() ) { return; } $counts = REFM_Records::counts( $type ); $views = array( '' => __( 'Records', 'refmora' ), 'trash' => __( 'Trash', 'refmora' ), ); echo '
'; echo ''; if ( $trash ) { $return_url = add_query_arg( array( 'page' => 'refmora', 'tab' => $type, 'view' => 'trash' ), admin_url( 'admin.php' ) ); $confirm = __( 'Permanently delete every item in this Trash, across all pages and filters? This cannot be undone and may change financial totals.', 'refmora' ); echo '
'; wp_nonce_field( 'refm_clear_trash_' . $type ); echo '
'; } echo '
'; } public static function affiliate_action() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Access denied.', 'refmora' ), 403 ); } if ( 'POST' !== ( isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '' ) ) { wp_die( esc_html__( 'Use the settings form to make changes.', 'refmora' ), '', array( 'response' => 405 ) ); } $id = absint( isset( $_POST['affiliate_id'] ) && is_scalar( $_POST['affiliate_id'] ) ? wp_unslash( $_POST['affiliate_id'] ) : 0 ); if ( ! isset( $_POST['_wpnonce'] ) || ! is_string( $_POST['_wpnonce'] ) ) { wp_die( esc_html__( 'Your session expired. Refresh and try again.', 'refmora' ), '', array( 'response' => 403 ) ); } check_admin_referer( 'refm_affiliate_' . $id ); $operation = sanitize_key( ( isset( $_POST[ 'operation' ] ) && is_scalar( $_POST[ 'operation' ] ) ? wp_unslash( $_POST[ 'operation' ] ) : '' ) ); $result = true; if ( in_array( $operation, array( 'approved', 'rejected' ), true ) ) { $result = REFM_Affiliates::review( $id, $operation ); } elseif ( get_userdata( $id ) && in_array( $operation, array( 'block', 'unblock' ), true ) ) { $result = REFM_Affiliates::set_blocked( $id, 'block' === $operation ); } else { $result = new WP_Error( 'invalid', __( 'Invalid affiliate action.', 'refmora' ) ); } wp_safe_redirect( add_query_arg( array( 'page' => 'refmora', 'tab' => 'affiliates', 'refm_error' => is_wp_error( $result ) ? 1 : 0, 'refm_message' => is_wp_error( $result ) ? $result->get_error_message() : __( 'Affiliate account updated.', 'refmora' ) ), admin_url( 'admin.php' ) ) ); exit; } public static function render() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } $tabs = array( 'overview' => __( 'Overview', 'refmora' ), 'affiliates' => __( 'Affiliates', 'refmora' ), 'refer_links' => __( 'Refer Links', 'refmora' ), 'customers' => __( 'Customers', 'refmora' ), 'commissions' => __( 'Commissions', 'refmora' ), 'payouts' => __( 'Withdrawals', 'refmora' ), 'settings' => __( 'Settings', 'refmora' ), 'sharing' => __( 'Sharing', 'refmora' ), 'emails' => __( 'Emails', 'refmora' ), 'customization' => __( 'Customization', 'refmora' ) ); $descriptions = array( 'overview' => __( 'A clear view of your partner program and recent activity.', 'refmora' ), 'affiliates' => __( 'Review applications and manage your affiliate community.', 'refmora' ), 'commissions' => __( 'Follow order earnings, adjustments and settled commissions.', 'refmora' ), 'payouts' => __( 'Review withdrawal requests and record your manual payments.', 'refmora' ), 'settings' => __( 'Set the rules for joining, commissions, referrals and withdrawals.', 'refmora' ), 'sharing' => __( 'Choose how partners share your products.', 'refmora' ), 'emails' => __( 'Keep affiliates informed with clear, personal notifications.', 'refmora' ), 'customization' => __( 'Make the affiliate experience feel like your brand.', 'refmora' ) ); $tabs['tools'] = __( 'Tools', 'refmora' ); if ( current_user_can( 'manage_options' ) ) { $tabs['license'] = class_exists( 'REFM_Pro_License', false ) ? __( 'License', 'refmora' ) : __( 'Premium', 'refmora' ); } $descriptions['refer_links'] = __( 'Browse referral URLs and their tracked clicks.', 'refmora' ); $descriptions['customers'] = __( 'Customers registered through affiliate referral links.', 'refmora' ); $descriptions['tools'] = __( 'Back up and restore settings, reset defaults, clear Refmora data and export your program records.', 'refmora' ); $descriptions['license'] = __( 'Manage your license and receive updates from WP Methods.', 'refmora' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only, sanitized account/admin routing, search, pagination or feedback; no record mutation. $tab = sanitize_key( ( isset( $_GET[ 'tab' ] ) && is_scalar( $_GET[ 'tab' ] ) ? wp_unslash( $_GET[ 'tab' ] ) : 'overview' ) ); if ( ! isset( $tabs[ $tab ] ) ) { $tab = 'overview'; } $active = REFM_Settings::program_enabled(); $range = 'overview' === $tab ? REFM_Reports::requested_range() : array(); echo '
' . esc_html__( 'Skip to panel content', 'refmora' ) . '
WP METHODS

Refmora' . esc_html( REFM_VERSION ) . ' ' . esc_html( class_exists( 'REFM_Pro_License', false ) ? __( 'Pro', 'refmora' ) : __( 'Free', 'refmora' ) ) . '

' . esc_html( $active ? __( 'Program active', 'refmora' ) : __( 'Program paused', 'refmora' ) ) . '' . esc_html__( 'Affiliate Area', 'refmora' ) . wp_kses( REFM_UI::icon( 'arrow' ), REFM_UI::allowed_html() ) . '
' . esc_html__( 'YOUR PARTNER PROGRAM', 'refmora' ) . '

' . esc_html( $tabs[ $tab ] ) . '

' . esc_html( $descriptions[ $tab ] ) . '

'; if ( 'overview' === $tab ) { REFM_Reports::controls( $range ); } echo '
'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only, sanitized account/admin routing, search, pagination or feedback; no record mutation. if ( '1' === sanitize_text_field( ( isset( $_GET[ 'saved' ] ) && is_scalar( $_GET[ 'saved' ] ) ? wp_unslash( $_GET[ 'saved' ] ) : '' ) ) ) { REFM_UI::toast( __( 'Settings saved.', 'refmora' ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only, sanitized account/admin routing, search, pagination or feedback; no record mutation. if ( isset( $_GET['refm_message'] ) && is_scalar( $_GET['refm_message'] ) ) { REFM_UI::toast( sanitize_text_field( wp_unslash( $_GET['refm_message'] ) ), '1' === sanitize_text_field( ( isset( $_GET[ 'refm_error' ] ) && is_scalar( $_GET[ 'refm_error' ] ) ? wp_unslash( $_GET[ 'refm_error' ] ) : '' ) ) ); } if ( 'settings' === $tab ) { self::settings(); } elseif ( 'overview' === $tab ) { self::overview( $range ); } elseif ( 'affiliates' === $tab ) { self::affiliates(); } elseif ( 'refer_links' === $tab ) { REFM_Refer_Links::render( null, 'admin' ); } elseif ( 'customers' === $tab ) { self::customers(); } elseif ( 'sharing' === $tab ) { REFM_Sharing::render(); } elseif ( 'emails' === $tab ) { REFM_Emails::render(); } elseif ( 'customization' === $tab ) { REFM_Design::render(); } elseif ( 'license' === $tab ) { REFM_License::render(); } elseif ( 'tools' === $tab ) { REFM_Tools::render(); } else { self::records( $tab ); } REFM_License::activation_notice( true ); echo '
'; } private static function overview( $range ) { $currency = REFM_Settings::get( 'currency' ); $stats = REFM_Reports::summary( $range ); echo '
'; echo wp_kses( REFM_UI::metric( __( 'Pending commissions', 'refmora' ), REFM_Store::money( $stats['pending'], $currency ), 'clock' ), REFM_UI::allowed_html() ); echo wp_kses( REFM_UI::metric( __( 'Available earnings', 'refmora' ), REFM_Store::money( $stats['available'], $currency ), 'wallet' ), REFM_UI::allowed_html() ); echo wp_kses( REFM_UI::metric( __( 'Awaiting payout', 'refmora' ), REFM_Store::money( $stats['requested'], $currency ), 'send' ), REFM_UI::allowed_html() ); echo wp_kses( REFM_UI::metric( __( 'Completed payouts', 'refmora' ), REFM_Store::money( $stats['paid'], $currency ), 'award' ), REFM_UI::allowed_html() ); echo wp_kses( REFM_UI::metric( __( 'Referral clicks', 'refmora' ), number_format_i18n( $stats['clicks'] ), 'click', '', admin_url( 'admin.php?page=refmora&tab=refer_links' ) . '#refm-refer-links' ), REFM_UI::allowed_html() ); echo wp_kses( REFM_UI::metric( __( 'Registered customers', 'refmora' ), number_format_i18n( $stats['customers'] ), 'user', '', admin_url( 'admin.php?page=refmora&tab=customers' ) . '#refm-customers' ), REFM_UI::allowed_html() ); echo '

' . esc_html__( 'Commission summaries use creation dates and current balances, including refund adjustments. Awaiting payouts uses request dates. Latest lists below show the newest records overall.', 'refmora' ) . '

'; /** Separate add-ons can render an overview report using the same validated date range. */ do_action( 'refm_admin_overview_chart', $range, $currency ); echo '
'; REFM_Refer_Links::render( null, 'overview' ); REFM_Reports::activity_donut( $range, $stats['clicks'], REFM_Reports::converted( $range ) ); echo '
'; self::overview_lists(); echo '

' . esc_html__( 'Program at a glance', 'refmora' ) . '

'; $rate = 'percent' === REFM_Settings::get( 'commission_type' ) ? esc_html( REFM_Settings::get( 'commission_rate' ) ) . '%' : wp_kses_post( REFM_Store::money( REFM_Settings::get( 'commission_rate' ) ) ) . ' ' . esc_html__( 'per unit', 'refmora' ); $values = array( __( 'Program', 'refmora' ) => REFM_Settings::program_enabled() ? esc_html__( 'Active', 'refmora' ) : esc_html__( 'Paused', 'refmora' ), __( 'Default commission', 'refmora' ) => $rate, __( 'Cookie duration', 'refmora' ) => (int) REFM_Settings::get( 'cookie_days' ) . ' ' . esc_html__( 'days', 'refmora' ), __( 'Minimum withdrawal', 'refmora' ) => wp_kses_post( REFM_Store::money( REFM_Settings::get( 'minimum' ) ) ), __( 'Active payout methods', 'refmora' ) => count( REFM_Settings::methods() ) ); foreach ( $values as $label => $value ) { echo '
' . esc_html( $label ) . '
' . wp_kses_post( $value ) . '
'; } echo '
' . esc_html__( 'Manage settings', 'refmora' ) . '

' . esc_html__( 'How it works', 'refmora' ) . '

  1. ' . esc_html__( 'Customers join from My Account → Affiliate Area.', 'refmora' ) . '
  2. ' . esc_html__( 'A referral link sets a signed cookie and attributes new orders.', 'refmora' ) . '
  3. ' . esc_html__( 'Completed orders release commissions for withdrawal.', 'refmora' ) . '
  4. ' . esc_html__( 'Approve a request, then send it through PayPal or Stripe. For manual methods, send it yourself and mark it paid.', 'refmora' ) . '

' . esc_html__( 'Commission excludes tax and shipping. Refunds adjust balances automatically. Automatic payouts require a configured provider and an explicit administrator Send payment action.', 'refmora' ) . '

'; } public static function field( $key, $label, $help, $type = 'text', $options = array(), $attributes = '' ) { $value = REFM_Settings::get( $key ); echo '
'; if ( 'select' === $type ) { echo ''; } else { echo wp_kses( '', REFM_UI::allowed_html() ); } echo '

' . esc_html( $help ) . '

'; } /** Render accessible status choices using the same allowlist as settings validation. */ public static function commission_statuses_field() { $selected = REFM_Settings::clean_commission_statuses( REFM_Settings::get( 'commission_statuses' ) ); echo '
' . esc_html__( 'Commission tracking order statuses', 'refmora' ) . '
'; foreach ( REFM_Settings::commission_order_statuses() as $status => $label ) { echo ''; } echo '

' . esc_html__( 'Choose which order statuses show pending commissions. Completed is always included and follows the availability rule below. Unselected statuses do not create commissions; existing commissions are rejected if an order moves to an unselected status. Cancelled, Failed and Refunded always reject commissions. Changes take effect on the next order status change or commission sync; saving does not recalculate past orders.', 'refmora' ) . '

'; } private static function overview_lists() { global $wpdb; $table = REFM_Store::table( 'payouts' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Custom affiliate tables have no WordPress CRUD API; read current totals, history and privacy records without stale cross-request caches. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE trashed_at IS NULL ORDER BY id DESC LIMIT 5', $table ) ); echo '

' . esc_html__( 'Latest withdrawal requests', 'refmora' ) . '

' . esc_html__( 'See more withdrawals', 'refmora' ) . '
'; foreach ( $rows as $row ) { echo ''; } if ( ! $rows ) { echo ''; } echo '
' . esc_html__( 'Affiliate', 'refmora' ) . '' . esc_html__( 'Amount', 'refmora' ) . '' . esc_html__( 'Payment method', 'refmora' ) . '' . esc_html__( 'Status', 'refmora' ) . '
' . wp_kses_post( self::identity( $row->affiliate_id ) ) . '' . wp_kses_post( REFM_Store::money( $row->amount, $row->currency ) ) . '' . wp_kses( REFM_UI::payment_method( $row->method ), REFM_UI::allowed_html() ) . '' . wp_kses_post( REFM_Store::status( $row->status ) ) . '
' . esc_html__( 'No withdrawal requests yet.', 'refmora' ) . '
'; // phpcs:ignore WordPress.DB.SlowDBQuery -- Required enrolled-affiliate predicate; only five users are fetched, no total count, with native WordPress query caching. $users = get_users( array( 'count_total' => false, 'cache_results' => true, 'meta_key' => '_refm_enrolled', 'meta_value' => 'yes', 'number' => 5, 'orderby' => 'ID', 'order' => 'DESC' ) ); echo '

' . esc_html__( 'Latest affiliates', 'refmora' ) . '

' . esc_html__( 'See more affiliates', 'refmora' ) . '
'; foreach ( $users as $user ) { echo ''; } if ( ! $users ) { echo ''; } echo '
' . esc_html__( 'Affiliate', 'refmora' ) . '' . esc_html__( 'Account', 'refmora' ) . '
' . wp_kses_post( self::identity( $user->ID ) ) . '' . ( 'yes' === get_user_meta( $user->ID, '_refm_blocked', true ) ? wp_kses_post( REFM_Store::status( 'blocked' ) ) : wp_kses_post( REFM_Store::status( REFM_Affiliates::status( $user->ID ) ) ) ) . '
' . esc_html__( 'No affiliates yet.', 'refmora' ) . '
'; } private static function settings() { echo '
'; wp_nonce_field( 'refm_settings' ); echo ''; echo '

' . wp_kses( REFM_UI::icon( 'affiliates' ), REFM_UI::allowed_html() ) . esc_html__( 'Program & enrollment', 'refmora' ) . '

' . esc_html__( 'Pausing stops new enrollments, referrals and withdrawals. Existing order commissions and history continue to update.', 'refmora' ) . '

'; self::field( 'approval_required', __( 'Affiliate account approval', 'refmora' ), __( 'New accounts join automatically by default. Enable review to approve or reject new applications from the Affiliates tab. Existing affiliates remain approved; pending applications still need review.', 'refmora' ), 'select', array( 'no' => __( 'Join automatically', 'refmora' ), 'yes' => __( 'Require administrator approval', 'refmora' ) ) ); echo '

' . esc_html__( 'Join Affiliate page', 'refmora' ) . '

' . esc_html__( 'Customize the invitation customers see in the Affiliate Area before joining.', 'refmora' ) . '

'; self::field( 'join_title', __( 'Join Affiliate title', 'refmora' ), __( 'Heading on the page before joining.', 'refmora' ), 'text', array(), 'maxlength="180" required' ); self::field( 'join_button_text', __( 'Join button text', 'refmora' ), __( 'Label for the button that submits the affiliate application.', 'refmora' ), 'text', array(), 'maxlength="180" required' ); echo '

' . esc_html__( 'Plain text with line breaks. Leave empty to hide the description.', 'refmora' ) . '

'; self::field( 'join_show_rate', __( 'Default commission rate before joining', 'refmora' ), __( 'Choose whether customers see your default percentage or fixed amount on the Join Affiliate page. Product-specific rates may differ.', 'refmora' ), 'select', array( 'no' => __( 'Hide', 'refmora' ), 'yes' => __( 'Show', 'refmora' ) ) ); echo '

' . esc_html__( 'Disabled by default. When enabled, applicants must provide a website or social profile URL.', 'refmora' ) . '

' . esc_html__( 'Disabled by default. When enabled, applicants must describe their promotion plan. View answers in the Affiliates list.', 'refmora' ) . '

'; echo '

' . wp_kses( REFM_UI::icon( 'commissions' ), REFM_UI::allowed_html() ) . esc_html__( 'Commission rules', 'refmora' ) . '

' . esc_html__( 'Set your default earnings. Commissions use the WooCommerce base currency; individual products can override these rates.', 'refmora' ) . '

'; self::field( 'commission_type', __( 'Commission type', 'refmora' ), __( 'Product settings override this default.', 'refmora' ), 'select', array( 'percent' => __( 'Percentage', 'refmora' ), 'fixed' => __( 'Fixed amount per unit', 'refmora' ) ) ); self::field( 'commission_rate', __( 'Default commission', 'refmora' ), __( 'Percentage: 0–100. Fixed: amount per purchased unit, capped at the item total.', 'refmora' ), 'number', array(), 'min="0" max="1000000" step="0.000001" required' ); self::commission_statuses_field(); self::field( 'release_mode', __( 'Commission availability', 'refmora' ), __( 'By default, commissions become available when the order is Completed. Choose a waiting period, or Confirm from Commission List to keep pending commissions on hold until a manager confirms or rejects them. Manual confirmation can release a commission in any selected tracking status. Previously released commissions stay available; refunds and cancellations still adjust earnings.', 'refmora' ), 'select', array( 'completed' => __( 'Immediately after order completion (default)', 'refmora' ), 'delay' => __( 'After a waiting period', 'refmora' ), 'manual' => __( 'Confirm from Commission List', 'refmora' ) ) ); echo '
'; self::field( 'release_days', __( 'Waiting period (days)', 'refmora' ), __( '1 to 365 days, counted from the order completion time. A saved waiting period is not changed by later settings updates.', 'refmora' ), 'number', array(), 'min="1" max="365" step="1" required' ); echo '
'; $base_currency = REFM_Settings::base_currency(); $currency_names = get_woocommerce_currencies(); echo '
' . esc_html__( 'WooCommerce base currency', 'refmora' ) . '

' . esc_html( $base_currency . ' - ' . $currency_names[ $base_currency ] ) . '

' . esc_html__( 'New commissions, fixed per-unit rates, minimum withdrawal and dashboard totals use the base currency from WooCommerce Settings. The Pro add-on can convert FOX/CURCY orders in another currency using a saved exchange rate. Existing transactions retain their original currency.', 'refmora' ) . ' ' . esc_html__( 'WooCommerce Settings', 'refmora' ) . '

'; if ( has_action( 'refm_exchange_settings' ) ) { do_action( 'refm_exchange_settings' ); } else { echo '

' . esc_html__( 'Currency conversion is provided by the separate Pro add-on. Free earns commissions in the WooCommerce base currency.', 'refmora' ) . '

' . wp_kses_post( REFM_License::premium_link() ) . '
'; } echo '

' . wp_kses( REFM_UI::icon( 'clock' ), REFM_UI::allowed_html() ) . esc_html__( 'Referral tracking & repeat purchases', 'refmora' ) . '

' . esc_html__( 'Choose how long referrals last and whether returning customers can earn additional commissions.', 'refmora' ) . '

'; self::field( 'referral_parameter', __( 'Referral link parameter', 'refmora' ), __( 'Default: refm_ref. Use a name such as ref or partner. Letters, numbers, underscores and hyphens only, starting with a letter. Existing refm_ref links continue to work. Exclude both names from page caching.', 'refmora' ), 'text', array(), 'maxlength="60" pattern="[a-zA-Z][a-zA-Z0-9_-]{0,59}" required' ); self::field( 'account_slug', __( 'Affiliate Area URL slug', 'refmora' ), __( 'Default: refmora. Example: partners creates /my-account/partners/. Use letters, numbers and hyphens, starting with a letter. Avoid existing WordPress and account endpoints. The default URL continues to work.', 'refmora' ), 'text', array(), 'maxlength="60" pattern="[a-zA-Z][a-zA-Z0-9-]{0,59}" required' ); self::field( 'cookie_days', __( 'Cookie lifetime (days)', 'refmora' ), __( 'Between 1 and 365 days.', 'refmora' ), 'number', array(), 'min="1" max="365" step="1" required' ); self::field( 'attribution', __( 'Referral attribution', 'refmora' ), __( 'First referral keeps the first valid cookie. Last referral replaces it.', 'refmora' ), 'select', array( 'first' => __( 'First referral', 'refmora' ), 'last' => __( 'Last referral', 'refmora' ) ) ); self::field( 'purchase_policy', __( 'Repeat purchase commissions', 'refmora' ), __( 'Once: only one eligible commission-earning purchase per customer across all affiliates and products. Multiple: each eligible referred order earns commission. Customers are matched by account or billing email. Applies to new orders only.', 'refmora' ), 'select', array( 'multiple' => __( 'Every eligible referred purchase', 'refmora' ), 'once' => __( 'Once per customer', 'refmora' ) ) ); echo '

' . wp_kses( REFM_UI::icon( 'payouts' ), REFM_UI::allowed_html() ) . esc_html__( 'Payout settings', 'refmora' ) . '

'; self::field( 'minimum', __( 'Minimum withdrawal', 'refmora' ), __( 'Customers request their entire available balance, after refund adjustments.', 'refmora' ), 'number', array(), 'min="0.01" max="1000000" step="0.000001" required' ); self::field( 'payout_mode', __( 'Enable payout', 'refmora' ), __( 'Manual methods are paid outside the plugin. Automatic providers send only after approval and Send payment. Pause new withdrawals whenever needed.', 'refmora' ), 'select', array( 'manual' => __( 'Enabled', 'refmora' ), 'disabled' => __( 'Withdrawals paused', 'refmora' ) ) ); self::field( 'method_selection', __( 'Available methods', 'refmora' ), __( 'Single keeps only the first checked method active. Multiple lets customers choose among all checked methods.', 'refmora' ), 'select', array( 'single' => __( 'One active method', 'refmora' ), 'multiple' => __( 'Multiple active methods', 'refmora' ) ) ); echo '
'; echo '

' . esc_html__( 'Payout methods', 'refmora' ) . '

' . esc_html__( 'Add manual methods. Customers choose from active methods. Instructions explain the payment details needed.', 'refmora' ) . '

'; foreach ( REFM_Settings::get( 'methods' ) as $index => $method ) { self::method_row( $index, $method ); } echo '
'; REFM_Gateways::fields(); echo '

' . wp_kses( REFM_UI::icon( 'link' ), REFM_UI::allowed_html() ) . esc_html__( 'Product commissions & tracking', 'refmora' ) . '

' . esc_html__( 'Edit a product → Product data → General to set a percentage, fixed amount per unit, or disable commission for that product. Parent product settings cover all variations.', 'refmora' ) . '

' . esc_html( str_replace( 'refm_ref', implode( ', ', REFM_Settings::referral_keys() ), __( 'Caching: exclude refm_ref query strings from full-page cache, and keep My Account, Cart and Checkout uncached. With LiteSpeed, add refm_ref under Cache → Excludes → Do Not Cache Query Strings; do not add it to Drop Query String. Hosting/CDN caches must also bypass referral URLs and logged-in users. Purge existing page caches after applying these rules. Guest product pages remain cacheable. Review the Refmora privacy policy text in WordPress Settings → Privacy and connect cookie consent through the refm_tracking_allowed filter when required.', 'refmora' ) ) ) . '

'; echo '

' . wp_kses( REFM_UI::icon( 'affiliates' ), REFM_UI::allowed_html() ) . esc_html__( 'Affiliate Terms & Conditions', 'refmora' ) . '

' . esc_html__( 'Customers see this content in the Affiliate Area before joining and must accept it. Leave it empty to join without a terms checkbox. Updates apply to new applications; existing affiliates keep their enrollment.', 'refmora' ) . '

'; wp_editor( REFM_Settings::terms(), 'refm_terms_content', array( 'textarea_name' => 'refm[terms_content]', 'textarea_rows' => 10, 'media_buttons' => false, 'teeny' => true, 'quicktags' => array( 'buttons' => 'strong,em,link,ul,ol,li,close' ) ) ); echo '
'; echo '
'; REFM_Currency::recovery_form(); REFM_UI::footer( __( 'Save settings', 'refmora' ), 'refm-program-settings-form' ); } public static function method_row( $index, $method, $removable = true ) { $prefix = 'refm[methods][' . $index . ']'; echo '
'; REFM_UI::payment_icon_field( $prefix . '[icon]', $method['icon'] ?? '', __( 'Payment icon', 'refmora' ) ); echo '
' . ( $removable ? '' : '' ) . '
'; } private static function identity( $id ) { $user = get_userdata( $id ); return $user ? '' . esc_html( $user->display_name ) . '' . esc_html( $user->user_email ) . '' : '#' . absint( $id ); } private static function search_term() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only, sanitized account/admin routing, search, pagination or feedback; no record mutation. return mb_substr( sanitize_text_field( ( isset( $_GET[ 'q' ] ) && is_scalar( $_GET[ 'q' ] ) ? wp_unslash( $_GET[ 'q' ] ) : '' ) ), 0, 160 ); } private static function list_statuses( $tab ) { return 'affiliates' === $tab ? array( 'pending', 'approved', 'rejected', 'blocked' ) : ( 'payouts' === $tab ? array( 'pending', 'approved', 'processing', 'completed', 'tested', 'failed', 'rejected' ) : array( 'pending', 'available', 'reserved', 'paid', 'rejected' ) ); } private static function list_status( $tab ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only, sanitized account/admin routing, search, pagination or feedback; no record mutation. $status = sanitize_key( ( isset( $_GET[ 'status' ] ) && is_scalar( $_GET[ 'status' ] ) ? wp_unslash( $_GET[ 'status' ] ) : '' ) ); return in_array( $status, self::list_statuses( $tab ), true ) ? $status : ''; } private static function filters( $tab ) { $search = self::search_term(); $status = self::list_status( $tab ); $placeholder = 'affiliates' === $tab ? __( 'Name, email or user ID', 'refmora' ) : ( 'commissions' === $tab ? __( 'Affiliate, email, order or commission ID', 'refmora' ) : __( 'Affiliate, email or withdrawal ID', 'refmora' ) ); echo '
'; if ( '' !== $search || '' !== $status ) { echo '' . esc_html__( 'Reset', 'refmora' ) . ''; } if ( in_array( $tab, array( 'commissions', 'payouts' ), true ) && self::trash_view() ) { echo ''; } echo '
'; } /** Show only referral-attributed registrations to authorized store managers. */ private static function customers() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } global $wpdb; $search = self::search_term(); $like = '%' . $wpdb->esc_like( $search ) . '%'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page navigation; manager capability is checked before querying. $page = REFM_Security::page( $_GET, 'paged' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Fresh pagination over the custom registration-attribution table, with prepared search terms. $count = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i AS c LEFT JOIN %i AS u ON u.ID=c.customer_id WHERE (%s=%s OR u.display_name LIKE %s OR u.user_email LIKE %s OR CAST(c.customer_id AS CHAR)=%s OR CAST(c.affiliate_id AS CHAR)=%s)', REFM_Store::table( 'customers' ), $wpdb->users, $search, '', $like, $like, $search, $search ) ); $page = min( $page, max( 1, (int) ceil( $count / self::PAGE_SIZE ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Fetch a bounded page of referral customers; affiliate names are primed together below. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT c.customer_id,c.affiliate_id,c.created_at FROM %i AS c LEFT JOIN %i AS u ON u.ID=c.customer_id WHERE (%s=%s OR u.display_name LIKE %s OR u.user_email LIKE %s OR CAST(c.customer_id AS CHAR)=%s OR CAST(c.affiliate_id AS CHAR)=%s) ORDER BY c.created_at DESC,c.customer_id DESC LIMIT %d OFFSET %d', REFM_Store::table( 'customers' ), $wpdb->users, $search, '', $like, $like, $search, $search, self::PAGE_SIZE, ( $page - 1 ) * self::PAGE_SIZE ) ); if ( $rows ) { cache_users( array_unique( array_map( 'absint', array_merge( array_column( $rows, 'customer_id' ), array_column( $rows, 'affiliate_id' ) ) ) ) ); } echo '

' . esc_html__( 'Registered customers', 'refmora' ) . '

'; $serial = ( $page - 1 ) * self::PAGE_SIZE; foreach ( $rows as $row ) { $customer = get_userdata( $row->customer_id ); $affiliate = get_userdata( $row->affiliate_id ); echo ''; } if ( ! $rows ) { echo ''; } echo '
' . esc_html__( 'SL', 'refmora' ) . '' . esc_html__( 'Customer', 'refmora' ) . '' . esc_html__( 'Email', 'refmora' ) . '' . esc_html__( 'Affiliate', 'refmora' ) . '' . esc_html__( 'Registered', 'refmora' ) . '
' . esc_html( number_format_i18n( ++$serial ) ) . '' . esc_html( $customer ? $customer->display_name : __( 'Deleted customer', 'refmora' ) ) . '' . esc_html( $customer ? $customer->user_email : '' ) . '' . esc_html( $affiliate ? $affiliate->display_name : __( 'Deleted affiliate', 'refmora' ) ) . '' . esc_html( get_date_from_gmt( $row->created_at, get_option( 'date_format' ) ) ) . '
' . esc_html__( 'No registered customers found.', 'refmora' ) . '
'; self::pagination( $count, $page, 'customers' ); echo '
'; } private static function affiliates() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only, sanitized account/admin routing, search, pagination or feedback; no record mutation. $page = REFM_Security::page_number( absint( ( isset( $_GET[ 'paged' ] ) && is_scalar( $_GET[ 'paged' ] ) ? wp_unslash( $_GET[ 'paged' ] ) : '1' ) ) ); // phpcs:ignore WordPress.DB.SlowDBQuery -- Required enrollment predicate; results are paginated at ten users and WordPress caches query IDs/totals. $args = array( 'cache_results' => true, 'meta_key' => '_refm_enrolled', 'meta_value' => 'yes', 'number' => self::PAGE_SIZE, 'offset' => ( $page - 1 ) * self::PAGE_SIZE, 'orderby' => 'ID', 'order' => 'DESC' ); $search = self::search_term(); $status = self::list_status( 'affiliates' ); if ( '' !== $search ) { $args['search'] = '*' . ltrim( $search, '#' ) . '*'; $args['search_columns'] = array( 'user_login', 'user_email', 'display_name', 'ID' ); } // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required account-status predicate for this paginated, cached user query. if ( 'blocked' === $status ) { $args['meta_query'] = array( array( 'key' => '_refm_blocked', 'value' => 'yes' ) ); } elseif ( '' !== $status ) { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Preserve legacy approval and blocked-account semantics; native user-query caches are invalidated by metadata updates. $args['meta_query'] = array( 'relation' => 'AND', array( 'relation' => 'OR', array( 'key' => '_refm_blocked', 'compare' => 'NOT EXISTS' ), array( 'key' => '_refm_blocked', 'value' => 'yes', 'compare' => '!=' ) ) ); $args['meta_query'][] = 'approved' === $status ? array( 'relation' => 'OR', array( 'key' => '_refm_approval', 'compare' => 'NOT EXISTS' ), array( 'key' => '_refm_approval', 'value' => array( 'pending', 'rejected' ), 'compare' => 'NOT IN' ) ) : array( 'key' => '_refm_approval', 'value' => $status ); } $query = new WP_User_Query( $args ); $count = (int) $query->get_total(); $last = max( 1, (int) ceil( $count / self::PAGE_SIZE ) ); if ( $page > $last ) { $page = $last; $args['offset'] = ( $page - 1 ) * self::PAGE_SIZE; $args['count_total'] = false; $query = new WP_User_Query( $args ); } $users = $query->get_results(); $currency = REFM_Settings::get( 'currency' ); $earnings = array(); if ( $users ) { global $wpdb; $ids = array_map( 'absint', wp_list_pluck( $users, 'ID' ) ); $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); $args = array_merge( array( REFM_Store::table( 'commissions' ), $currency ), $ids ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders -- One page-scoped aggregate; only generated numeric placeholders are interpolated and every ID/currency is prepared. $earnings = $wpdb->get_results( $wpdb->prepare( "SELECT affiliate_id,COALESCE(SUM(amount),0) AS earned FROM %i WHERE currency=%s AND affiliate_id IN ($placeholders) AND status IN ('available','reserved','paid') GROUP BY affiliate_id", $args ), OBJECT_K ); } $serial = ( $page - 1 ) * self::PAGE_SIZE; echo '

' . esc_html__( 'Affiliates', 'refmora' ) . '

'; self::filters( 'affiliates' ); echo '
'; foreach ( $users as $user ) { $blocked = 'yes' === get_user_meta( $user->ID, '_refm_blocked', true ); $stats = REFM_Store::referral_stats( $user->ID ); $approval = REFM_Affiliates::status( $user->ID ); echo ''; } if ( ! $query->get_results() ) { echo ''; } echo '
' . esc_html__( 'SL', 'refmora' ) . '' . esc_html__( 'Affiliate', 'refmora' ) . '' . esc_html__( 'Available balance', 'refmora' ) . '' . esc_html__( 'Total earned', 'refmora' ) . '' . esc_html__( 'Clicks', 'refmora' ) . '' . esc_html__( 'Customers', 'refmora' ) . '' . esc_html__( 'Account', 'refmora' ) . '' . esc_html__( 'Action', 'refmora' ) . '
' . esc_html( number_format_i18n( ++$serial ) ) . '' . wp_kses_post( self::identity( $user->ID ) ) . '' . wp_kses_post( REFM_Store::money( REFM_Store::balance( $user->ID, REFM_Settings::get( 'currency' ) ) ) ) . '' . wp_kses_post( REFM_Store::money( $earnings[ $user->ID ]->earned ?? 0, $currency ) ) . '' . esc_html( number_format_i18n( $stats['clicks'] ) ) . '' . esc_html( number_format_i18n( $stats['customers'] ) ) . '' . ( $blocked ? wp_kses_post( REFM_Store::status( 'blocked' ) ) : wp_kses_post( REFM_Store::status( $approval ) ) ) . ''; self::application_details( $user->ID ); $actions = 'pending' === $approval ? array( 'approved' => __( 'Approve', 'refmora' ), 'rejected' => __( 'Reject', 'refmora' ) ) : ( 'rejected' === $approval ? array( 'approved' => __( 'Approve', 'refmora' ) ) : array( $blocked ? 'unblock' : 'block' => $blocked ? __( 'Unblock', 'refmora' ) : __( 'Block', 'refmora' ) ) ); echo '
'; wp_nonce_field( 'refm_affiliate_' . $user->ID ); foreach ( $actions as $operation => $label ) { $icon = 'approved' === $operation ? 'check-circle' : ( 'rejected' === $operation ? 'close' : ( 'block' === $operation ? 'lock' : 'shield' ) ); echo ''; } echo '
'; self::record_actions( 'affiliates', $user->ID ); echo '
' . esc_html( '' !== $search || '' !== $status ? __( 'No affiliates match your search or filters.', 'refmora' ) : __( 'Customers who join the program will appear here.', 'refmora' ) ) . '
'; self::pagination( $count, esc_html( $page ), 'affiliates', $status ); echo '
'; } private static function application_details( $id ) { $application = get_user_meta( $id, '_refm_application', true ); if ( ! is_array( $application ) || ( empty( $application['website'] ) && empty( $application['promotion'] ) ) ) { return; } echo '
' . esc_html__( 'Application details', 'refmora' ) . ''; $url = esc_url( REFM_Security::value( $application, 'website' ), array( 'http', 'https' ) ); if ( $url ) { echo '

' . esc_html__( 'Website / Promote URL', 'refmora' ) . '
' . esc_html( REFM_Security::value( $application, 'website' ) ) . '

'; } if ( ! empty( $application['promotion'] ) ) { echo '

' . esc_html__( 'Promotion plan', 'refmora' ) . '
' . nl2br( esc_html( REFM_Security::value( $application, 'promotion' ) ) ) . '

'; } echo '
'; } /** * Render actions only for unsettled commissions awaiting a manager decision. * * @param object $row Commission record. * @param WC_Order|null $order Current WooCommerce order. */ public static function commission_actions( $row, $order ) { if ( REFM_Commissions::can_review( $row, $order ) ) { echo '
'; wp_nonce_field( 'refm_commission_' . $row->id ); echo '
'; return; } $decision = $row->review_status ?? ''; if ( in_array( $decision, array( 'confirmed', 'rejected' ), true ) ) { echo '' . esc_html( 'confirmed' === $decision ? __( 'Confirmed', 'refmora' ) : __( 'Rejected', 'refmora' ) ) . ''; } } private static function records( $type ) { global $wpdb; $manual_review = 'commissions' === $type && 'manual' === REFM_Settings::get( 'release_mode' ); $record_actions = REFM_Records::allowed(); $action_column = $manual_review || $record_actions; $trash = (int) self::trash_view(); $table = REFM_Store::table( $type ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only, sanitized account/admin routing, search, pagination or feedback; no record mutation. $page = REFM_Security::page_number( absint( ( isset( $_GET[ 'paged' ] ) && is_scalar( $_GET[ 'paged' ] ) ? wp_unslash( $_GET[ 'paged' ] ) : '1' ) ) ); $status = self::list_status( $type ); $search = self::search_term(); $like = '%' . $wpdb->esc_like( $search ) . '%'; $number = ltrim( $search, '#' ); $numeric = ctype_digit( $number ) && strlen( $number ) <= 18; $number = $numeric ? (int) $number : 0; $order_column = 'commissions' === $type ? 'order_id' : 'id'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Custom affiliate tables have no WordPress CRUD API; read current totals, history and privacy records without stale cross-request caches. $count = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i r LEFT JOIN %i u ON u.ID=r.affiliate_id WHERE (%s=\'\' OR r.status=%s) AND ((%d=1 AND r.trashed_at IS NOT NULL) OR (%d=0 AND r.trashed_at IS NULL)) AND (%s=\'\' OR u.user_login LIKE %s OR u.user_email LIKE %s OR u.display_name LIKE %s OR (%d=1 AND (r.id=%d OR r.affiliate_id=%d OR r.%i=%d)))', $table, $wpdb->users, $status, $status, $trash, $trash, $search, $like, $like, $like, (int) $numeric, $number, $number, $order_column, $number ) ); $page = min( $page, max( 1, (int) ceil( $count / self::PAGE_SIZE ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Custom affiliate tables have no WordPress CRUD API; read current totals, history and privacy records without stale cross-request caches. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT r.* FROM %i r LEFT JOIN %i u ON u.ID=r.affiliate_id WHERE (%s=\'\' OR r.status=%s) AND ((%d=1 AND r.trashed_at IS NOT NULL) OR (%d=0 AND r.trashed_at IS NULL)) AND (%s=\'\' OR u.user_login LIKE %s OR u.user_email LIKE %s OR u.display_name LIKE %s OR (%d=1 AND (r.id=%d OR r.affiliate_id=%d OR r.%i=%d))) ORDER BY r.id DESC LIMIT %d OFFSET %d', $table, $wpdb->users, $status, $status, $trash, $trash, $search, $like, $like, $like, (int) $numeric, $number, $number, $order_column, $number, self::PAGE_SIZE, ( $page - 1 ) * self::PAGE_SIZE ) ); echo '

' . esc_html( 'payouts' === $type ? __( 'Withdrawal requests', 'refmora' ) : __( 'Commission ledger', 'refmora' ) ) . '

'; self::filters( $type ); echo '
'; if ( $record_actions ) { self::record_views( $type, $trash ); if ( $trash ) { echo '

' . esc_html__( 'Restore returns history to Records using the current rules. Delete permanently removes the record and cannot be undone. Cancelled withdrawals stay cancelled when restored.', 'refmora' ) . '

'; } } if ( 'payouts' === $type ) { echo '

' . esc_html__( 'Approve each request first. For manual methods, send money outside the plugin and choose Mark paid. For PayPal or Stripe, choose Send payment; completion requires provider confirmation. Sandbox/Test does not settle real earnings. Notes are visible to the affiliate.', 'refmora' ) . '

'; } echo '
#' . esc_html( 'payouts' === $type ? __( 'Affiliate', 'refmora' ) : __( 'Affiliate', 'refmora' ) ) . '' . esc_html__( 'Amount', 'refmora' ) . '' . esc_html__( 'Status', 'refmora' ) . '' . esc_html__( 'Date', 'refmora' ) . '' . esc_html( 'payouts' === $type ? __( 'Payment & action', 'refmora' ) : __( 'Order / paid', 'refmora' ) ) . ''; if ( $action_column ) { echo '' . esc_html__( 'Action', 'refmora' ) . ''; } echo ''; foreach ( $rows as $row ) { echo '' . absint( $row->id ) . '' . wp_kses_post( self::identity( $row->affiliate_id ) ); echo '' . wp_kses_post( REFM_Store::money( $row->amount, $row->currency ) ) . '' . wp_kses_post( REFM_Store::status( $row->status ) ) . '' . esc_html( get_date_from_gmt( $row->created_at, get_option( 'date_format' ) ) ) . ''; if ( 'commissions' === $type ) { $order = wc_get_order( $row->order_id ); if ( 'repeat' === $row->eligibility ) { echo '' . esc_html__( 'Repeat purchase excluded', 'refmora' ) . ''; } if ( $order ) { echo '#' . absint( $row->order_id ) . ''; } else { echo '#' . absint( $row->order_id ); } echo '' . esc_html__( 'Settled:', 'refmora' ) . ' ' . wp_kses_post( REFM_Store::money( $row->paid_amount, $row->currency ) ) . ''; } else { echo wp_kses( REFM_UI::payment_method( $row->method ), REFM_UI::allowed_html() ); echo '
' . esc_html__( 'Payment details', 'refmora' ) . '
' . nl2br( esc_html( $row->details ) ); $gateway_job = REFM_Gateways::job( $row->id ); REFM_Gateways::summary( $row->id ); echo '
'; if ( in_array( $row->status, array( 'pending', 'approved', 'processing' ), true ) ) { echo '
'; wp_nonce_field( 'refm_payout_' . $row->id ); echo '

' . esc_html__( 'Visible to the affiliate. Add a note or payment reference.', 'refmora' ) . '

'; if ( 'processing' === $row->status ) { echo ''; } elseif ( 'pending' === $row->status ) { echo ''; } elseif ( $gateway_job ) { echo ''; } else { echo ''; } if ( 'processing' !== $row->status ) { echo ''; } echo '
'; } else { echo '
' . wp_kses( REFM_UI::icon( 'note' ), REFM_UI::allowed_html() ) . esc_html__( 'Admin note', 'refmora' ) . '

' . ( $row->note ? nl2br( esc_html( $row->note ) ) : esc_html__( 'No admin note.', 'refmora' ) ) . '

'; } echo '
'; } echo ''; if ( $action_column ) { echo ''; if ( $manual_review && ! $trash ) { self::commission_actions( $row, $order ); } if ( $record_actions ) { self::record_actions( $type, $row ); } echo ''; } echo ''; } if ( ! $rows ) { echo '' . esc_html__( 'No records found.', 'refmora' ) . ''; } echo '
'; self::pagination( $count, esc_html( $page ), $type, $status ); echo '
'; } private static function pagination( $count, $page, $tab, $status = '' ) { $total = (int) ceil( $count / self::PAGE_SIZE ); if ( $total <= 1 ) { return; } $page = min( max( 1, $page ), $total ); $base = add_query_arg( array( 'page' => 'refmora', 'tab' => $tab, 'view' => self::trash_view() ? 'trash' : '', 'status' => $status, 'q' => self::search_term(), 'paged' => 999999999 ), admin_url( 'admin.php' ) ); $clean_url = function ( $url ) { return remove_query_arg( array( 'saved', 'refm_message', 'refm_error' ), $url ); }; add_filter( 'get_pagenum_link', $clean_url ); try { $links = paginate_links( array( 'base' => str_replace( '999999999', '%#%', $base ), 'format' => '', 'current' => $page, 'total' => $total, 'type' => 'array', 'add_args' => false, 'mid_size' => 1, 'end_size' => 1, 'prev_text' => __( 'Previous', 'refmora' ), 'next_text' => __( 'Next', 'refmora' ) ) ); } finally { remove_filter( 'get_pagenum_link', $clean_url ); } /* translators: %1$d: current page, %2$d: total pages. */ echo ''; } }