service = $service; } /** Register authenticated POST actions and owner-scoped displays. */ public function register() { add_filter( 'devoynx_rewards_admin_tabs', array( $this, 'tabs' ) ); add_action( 'devoynx_rewards_admin_tab', array( $this, 'render_admin' ) ); add_action( 'devoynx_rewards_wallet_after_activity', array( $this, 'render_wallet' ), 24 ); add_action( 'admin_post_devoynx_rewards_basic_referral_settings', array( $this, 'save' ) ); add_action( 'admin_post_devoynx_rewards_basic_referral_accept', array( $this, 'accept' ) ); add_action( 'admin_post_devoynx_rewards_basic_referral_reconcile', array( $this, 'reconcile' ) ); } /** * Register the Free basic-referral settings tab. * * @param array $tabs Navigation. * @return array */ public function tabs( $tabs ) { $tabs['basic-referrals'] = __( 'Basic referrals', 'devoynx-rewards' ); return $tabs; } /** * Gate writes before each handler's nonce check. * * @param bool $merchant Whether merchant permission is required. */ private function authorize( $merchant = false ) { if ( 'POST' !== sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? '' ) ) || ! get_current_user_id() || ( $merchant && ! current_user_can( 'manage_devoynx_rewards' ) ) ) { wp_die( esc_html__( 'Permission denied.', 'devoynx-rewards' ), 403 ); } } /** Save bounded, explicit merchant economics. */ public function save() { $this->authorize( true ); check_admin_referer( 'devoynx_rewards_basic_referral_settings' ); $config = array( 'enabled' => isset( $_POST['enabled'] ) ? 1 : 0 ); foreach ( array( 'referrer_points', 'friend_points' ) as $field ) { $config[ $field ] = isset( $_POST[ $field ] ) && is_scalar( $_POST[ $field ] ) ? min( 100000, absint( $_POST[ $field ] ) ) : 0; } $config['minimum_order'] = isset( $_POST['minimum_order'] ) && is_string( $_POST['minimum_order'] ) ? min( 1000000, max( 0, (float) wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['minimum_order'] ) ) ) ) ) : 10; update_option( Referrals::OPTION, $config, false ); wp_safe_redirect( add_query_arg( array( 'page' => 'devoynx-rewards', 'tab' => 'basic-referrals', 'referral_saved' => 1, ), admin_url( 'admin.php' ) ) ); exit; } /** Consent uses the logged-in identity, never a posted recipient or point amount. */ public function accept() { $this->authorize(); check_admin_referer( 'devoynx_rewards_basic_referral_accept' ); $code = isset( $_POST['referral_code'] ) && is_string( $_POST['referral_code'] ) ? sanitize_text_field( wp_unslash( $_POST['referral_code'] ) ) : ''; $result = $this->service->accept( $code ); wp_safe_redirect( add_query_arg( 'basic_referral_status', is_wp_error( $result ) ? 'unavailable' : 'accepted', wc_get_account_endpoint_url( 'rewards' ) ) . '#devoynx-basic-referral' ); exit; } /** Retry a single real order without manufacturing or bypassing eligibility. */ public function reconcile() { $this->authorize( true ); check_admin_referer( 'devoynx_rewards_basic_referral_reconcile' ); $id = isset( $_POST['order_id'] ) && is_scalar( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0; $result = $this->service->process( $id ); $status = is_wp_error( $result ) ? 'failed' : ( $result['status'] ?? 'none' ); wp_safe_redirect( add_query_arg( array( 'page' => 'devoynx-rewards', 'tab' => 'basic-referrals', 'referral_reconcile' => $status, ), admin_url( 'admin.php' ) ) ); exit; } /** * Localized state labels, never raw internal error or payload strings. * * @param string $status State. * @return string */ public function label( $status ) { $labels = array( 'pending' => __( 'Waiting for a qualifying payment', 'devoynx-rewards' ), 'awarded' => __( 'Awarded; refunds may reduce points', 'devoynx-rewards' ), 'reversed' => __( 'Reversed after refund', 'devoynx-rewards' ), 'erased' => __( 'Closed by privacy request', 'devoynx-rewards' ), ); return $labels[ $status ] ?? __( 'Not eligible or needs review', 'devoynx-rewards' ); } /** * Distinguish a processing failure from an unpaid invitation. * * @param array $row Receipt. * @return string */ private function progress( $row ) { $data = json_decode( $row['payload'], true ); return ! empty( $data['needs_review'] ) ? __( 'Processing needs review; contact the store if it persists', 'devoynx-rewards' ) : $this->label( $row['status'] ); } /** * Render policy, real recent receipts and an explicit reconciliation tool. * * @param string $tab Active tab. */ public function render_admin( $tab ) { if ( 'basic-referrals' !== $tab || ! current_user_can( 'manage_devoynx_rewards' ) ) { return; } $config = $this->service->settings(); global $wpdb; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only pagination on a capability-gated screen. $page = isset( $_GET['referral_page'] ) ? max( 1, absint( $_GET['referral_page'] ) ) : 1; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Capability-gated, bounded current report on the custom workflow table. $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i WHERE kind = 'basic_referral' ORDER BY id DESC LIMIT 26 OFFSET %d", $wpdb->prefix . 'devoynx_rewards_program_records', 25 * ( $page - 1 ) ), ARRAY_A ); ?>

__( 'Points for the inviter', 'devoynx-rewards' ), 'friend_points' => __( 'Points for the friend', 'devoynx-rewards' ), ) as $field => $label ) : ?>

label( $status ) ); ?>

progress( $row ) ); ?>
service->settings(); $row = ( new ProgramStore() )->find( 'referral:friend:' . $user_id ); global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Owner-filtered aggregate counts contain no friend's identity or purchase details. $counts = $wpdb->get_results( $wpdb->prepare( "SELECT status,COUNT(*) total FROM %i WHERE kind = 'basic_referral' AND group_key = %s GROUP BY status", $wpdb->prefix . 'devoynx_rewards_program_records', 'inviter:' . $user_id ), ARRAY_A ); if ( ! $config['enabled'] && ! $row && ! $counts ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only prefill never accepts attribution. $incoming = isset( $_GET['dyr_invite'] ) && is_string( $_GET['dyr_invite'] ) ? sanitize_text_field( wp_unslash( $_GET['dyr_invite'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only failure message; success comes from the persisted owner receipt. $failed = isset( $_GET['basic_referral_status'] ) && 'unavailable' === $_GET['basic_referral_status']; $optout = get_user_meta( $user_id, '_devoynx_rewards_referral_optout', true ); ?>

service->code( $user_id ); ?>

service->error()->get_error_message() ); ?>

progress( $row ) ); ?>

label( $count['status'] ) ); ?>