| Server IP : 104.171.130.249 / Your IP : 216.73.216.146 Web Server : nginx/1.25.5 System : Linux 0dac65491b7e 6.8.0-134-generic #134-Ubuntu SMP PREEMPT_DYNAMIC Fri Jun 26 18:43:11 UTC 2026 x86_64 User : root ( 0) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : OFF | Perl : ON | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : /var/www/html/wp-content/themes/matreshka/inc/ |
Upload File : |
<?php
/**
* Cashback System for Matreshka
*
* Features:
* - Admin settings page (global %, per-category %, whitelist, user limit)
* - Cashback accrual on completed orders
* - Cashback redemption at checkout (as WC fee)
* - User category selection (from admin whitelist)
* - Cart/checkout cashback display
* - Admin reset all users' category selections
*
* @package Matreshka
*/
if ( ! defined( 'ABSPATH' ) ) exit;
// ================================================================
// 1. ADMIN SETTINGS PAGE
// ================================================================
add_action( 'admin_menu', function() {
add_menu_page(
'Настройки кешбека',
'Кешбек',
'manage_options',
'matreshka-cashback',
'matreshka_cashback_admin_page',
'dashicons-money-alt',
58
);
} );
add_action( 'admin_init', function() {
register_setting( 'matreshka_cashback_group', 'matreshka_cashback_default_percent', [
'type' => 'number', 'default' => 0.5,
'sanitize_callback' => function( $v ) { return max( 0, min( 100, floatval( $v ) ) ); },
] );
register_setting( 'matreshka_cashback_group', 'matreshka_cashback_category_percents', [
'type' => 'array',
'sanitize_callback' => 'matreshka_sanitize_category_percents',
] );
register_setting( 'matreshka_cashback_group', 'matreshka_cashback_whitelist', [
'type' => 'array',
'sanitize_callback' => function( $v ) { return is_array( $v ) ? array_map( 'absint', $v ) : []; },
] );
register_setting( 'matreshka_cashback_group', 'matreshka_cashback_user_limit', [
'type' => 'integer', 'default' => 3,
'sanitize_callback' => function( $v ) { return max( 1, absint( $v ) ); },
] );
} );
function matreshka_sanitize_category_percents( $data ) {
if ( ! is_array( $data ) ) return [];
$clean = [];
foreach ( $data as $cat_id => $pct ) {
$cat_id = absint( $cat_id );
$pct = floatval( $pct );
if ( $cat_id && $pct > 0 ) {
$clean[ $cat_id ] = min( 100, $pct );
}
}
return $clean;
}
/**
* Get all cashback settings
*/
function matreshka_cashback_settings() {
static $cache = null;
if ( $cache !== null ) return $cache;
$cache = [
'default_percent' => floatval( get_option( 'matreshka_cashback_default_percent', 0.5 ) ),
'category_percents' => get_option( 'matreshka_cashback_category_percents', [] ) ?: [],
'whitelist' => get_option( 'matreshka_cashback_whitelist', [] ) ?: [],
'user_limit' => absint( get_option( 'matreshka_cashback_user_limit', 3 ) ),
];
return $cache;
}
/**
* Admin page renderer
*/
function matreshka_cashback_admin_page() {
// Handle reset action
if ( isset( $_POST['matreshka_reset_cashback_categories'] ) && check_admin_referer( 'matreshka_reset_cashback' ) ) {
$users = get_users( [ 'fields' => 'ID' ] );
foreach ( $users as $uid ) {
delete_user_meta( $uid, 'matreshka_cashback_categories' );
}
echo '<div class="notice notice-success"><p>Категории кешбека сброшены для всех пользователей.</p></div>';
}
$settings = matreshka_cashback_settings();
$categories = get_terms( [ 'taxonomy' => 'product_cat', 'hide_empty' => false, 'orderby' => 'name' ] );
if ( is_wp_error( $categories ) ) $categories = [];
?>
<div class="wrap">
<h1>Настройки кешбека</h1>
<form method="post" action="options.php">
<?php settings_fields( 'matreshka_cashback_group' ); ?>
<h2>Общие настройки</h2>
<table class="form-table">
<tr>
<th><label for="cb_default">Общий % кешбека</label></th>
<td>
<input type="number" id="cb_default" name="matreshka_cashback_default_percent"
value="<?php echo esc_attr( $settings['default_percent'] ); ?>"
step="0.1" min="0" max="100" style="width:80px;"> %
<p class="description">Применяется ко всем категориям, если не задан индивидуальный %.</p>
</td>
</tr>
<tr>
<th><label for="cb_limit">Макс. категорий для пользователя</label></th>
<td>
<input type="number" id="cb_limit" name="matreshka_cashback_user_limit"
value="<?php echo esc_attr( $settings['user_limit'] ); ?>"
min="1" max="20" style="width:60px;">
<p class="description">Сколько категорий кешбека может активировать 1 пользователь.</p>
</td>
</tr>
</table>
<h2>Белый список категорий <small>(для выбора пользователями)</small></h2>
<p class="description">Отметьте категории, из которых пользователи смогут выбрать себе категории повышенного кешбека.</p>
<table class="widefat striped" style="max-width:700px;">
<thead>
<tr>
<th style="width:30px;"> </th>
<th>Категория</th>
<th style="width:120px;">Кешбек (%)</th>
</tr>
</thead>
<tbody>
<?php foreach ( $categories as $cat ) :
$in_whitelist = in_array( $cat->term_id, $settings['whitelist'] );
$pct = $settings['category_percents'][ $cat->term_id ] ?? '';
?>
<tr>
<td>
<input type="checkbox" name="matreshka_cashback_whitelist[]"
value="<?php echo esc_attr( $cat->term_id ); ?>"
<?php checked( $in_whitelist ); ?>>
</td>
<td><?php echo esc_html( $cat->name ); ?> <small class="text-muted">(<?php echo $cat->count; ?> товаров)</small></td>
<td>
<input type="number" name="matreshka_cashback_category_percents[<?php echo esc_attr( $cat->term_id ); ?>]"
value="<?php echo esc_attr( $pct ); ?>"
step="0.1" min="0" max="100" style="width:80px;"
placeholder="<?php echo esc_attr( $settings['default_percent'] ); ?>">
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php submit_button( 'Сохранить настройки' ); ?>
</form>
<hr>
<h2>Сброс категорий</h2>
<p>Удалить у всех пользователей выбранные категории кешбека. Пользователи смогут выбрать заново.</p>
<form method="post">
<?php wp_nonce_field( 'matreshka_reset_cashback' ); ?>
<button type="submit" name="matreshka_reset_cashback_categories" value="1"
class="button button-secondary" onclick="return confirm('Вы уверены? Категории кешбека будут сброшены у ВСЕХ пользователей.');">
🔄 Сбросить категории всех пользователей
</button>
</form>
</div>
<?php
}
// ================================================================
// 2. CASHBACK CALCULATION HELPERS
// ================================================================
/**
* Get cashback percent for a specific product
* Priority: user selected category > category custom % > default %
*/
function matreshka_get_product_cashback_percent( $product_id, $user_id = null ) {
$settings = matreshka_cashback_settings();
$default = $settings['default_percent'];
// Get product categories
$cats = wp_get_post_terms( $product_id, 'product_cat', [ 'fields' => 'ids' ] );
if ( is_wp_error( $cats ) || empty( $cats ) ) return $default;
// Check if user has selected cashback categories
$user_cats = [];
if ( $user_id ) {
$user_cats = get_user_meta( $user_id, 'matreshka_cashback_categories', true );
$user_cats = is_array( $user_cats ) ? $user_cats : [];
}
$best_percent = $default;
foreach ( $cats as $cat_id ) {
// Elevated rate only if user selected this category
if ( in_array( $cat_id, $user_cats ) && isset( $settings['category_percents'][ $cat_id ] ) ) {
$best_percent = max( $best_percent, $settings['category_percents'][ $cat_id ] );
}
}
return $best_percent;
}
/**
* Calculate total cashback for the current cart
*/
function matreshka_calculate_cart_cashback( $user_id = null ) {
if ( ! $user_id && is_user_logged_in() ) {
$user_id = get_current_user_id();
}
if ( ! $user_id || ! WC()->cart ) return 0;
$settings = matreshka_cashback_settings();
$user_cats = get_user_meta( $user_id, 'matreshka_cashback_categories', true );
$user_cats = is_array( $user_cats ) ? $user_cats : [];
$total_cb = 0;
foreach ( WC()->cart->get_cart() as $item ) {
$product_id = $item['product_id'];
$line_total = $item['line_total']; // After discounts
$cats = wp_get_post_terms( $product_id, 'product_cat', [ 'fields' => 'ids' ] );
if ( is_wp_error( $cats ) ) $cats = [];
$percent = $settings['default_percent'];
// Check if any product category is in user's selected cashback categories
foreach ( $cats as $cat_id ) {
if ( in_array( $cat_id, $user_cats ) && isset( $settings['category_percents'][ $cat_id ] ) ) {
$percent = max( $percent, $settings['category_percents'][ $cat_id ] );
}
}
$total_cb += $line_total * ( $percent / 100 );
}
return round( $total_cb, 2 );
}
/**
* Calculate cashback for a completed order
*/
function matreshka_calculate_order_cashback( $order ) {
$user_id = $order->get_user_id();
if ( ! $user_id ) return 0;
$settings = matreshka_cashback_settings();
$user_cats = get_user_meta( $user_id, 'matreshka_cashback_categories', true );
$user_cats = is_array( $user_cats ) ? $user_cats : [];
$total_cb = 0;
foreach ( $order->get_items() as $item ) {
$product_id = $item->get_product_id();
$line_total = $item->get_total(); // After discounts
$cats = wp_get_post_terms( $product_id, 'product_cat', [ 'fields' => 'ids' ] );
if ( is_wp_error( $cats ) ) $cats = [];
$percent = $settings['default_percent'];
foreach ( $cats as $cat_id ) {
if ( in_array( $cat_id, $user_cats ) && isset( $settings['category_percents'][ $cat_id ] ) ) {
$percent = max( $percent, $settings['category_percents'][ $cat_id ] );
}
}
$total_cb += $line_total * ( $percent / 100 );
}
return round( $total_cb, 2 );
}
// ================================================================
// 3. ORDER HOOKS — ACCRUAL ON COMPLETED
// ================================================================
add_action( 'woocommerce_order_status_completed', 'matreshka_cashback_award', 20 );
function matreshka_cashback_award( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) return;
// Don't award twice
if ( $order->get_meta( '_matreshka_cashback_awarded' ) ) return;
$user_id = $order->get_user_id();
if ( ! $user_id ) return;
$cashback = matreshka_calculate_order_cashback( $order );
if ( $cashback <= 0 ) return;
// Convert rubles to bonus points (1 ₽ = 10 бонусов, rate 0.1)
$bonus_points = floor( $cashback / 0.1 );
matreshka_add_user_bonuses( $user_id, $bonus_points );
$order->update_meta_data( '_matreshka_cashback_awarded', $bonus_points );
$order->update_meta_data( '_matreshka_cashback_amount', $cashback );
$order->save();
$order->add_order_note( sprintf( 'Кешбек начислен: %s (%d бонусов)', matreshka_bonuses_to_money( $bonus_points ), $bonus_points ) );
}
// ================================================================
// 4. CASHBACK REDEMPTION AT CHECKOUT (as negative fee)
// ================================================================
/**
* Return the maximum number of whole bonus points that can be redeemed now.
*
* One point is worth 0.1 ₽. Bonuses may pay for up to 50% of the products'
* price after coupon discounts; delivery and other fees are not discountable.
*/
function matreshka_get_redeemable_bonus_points( $cart = null, $user_id = 0 ) {
$cart = $cart ?: ( WC()->cart ?? null );
$user_id = $user_id ?: get_current_user_id();
if ( ! $cart || ! $user_id ) {
return 0;
}
$balance = matreshka_get_user_bonuses( $user_id );
$products_total = max( 0, (float) $cart->get_cart_contents_total() );
$max_by_order = (int) floor( ( $products_total * 0.5 ) / 0.1 + 0.00001 );
return max( 0, min( $balance, $max_by_order ) );
}
/**
* Convert whole bonus points into a WooCommerce fee amount.
*/
function matreshka_bonus_redemption_amount( $points ) {
return round( absint( $points ) * 0.1, wc_get_price_decimals() );
}
// Store the number of bonus points to redeem in the session.
add_action( 'wp_ajax_matreshka_apply_cashback', 'matreshka_ajax_apply_cashback' );
function matreshka_ajax_apply_cashback() {
check_ajax_referer( 'matreshka_cashback_nonce', 'nonce' );
if ( ! is_user_logged_in() ) {
wp_send_json_error( [ 'message' => 'Не авторизован' ] );
}
$requested_points = isset( $_POST['bonus_points'] ) ? absint( wp_unslash( $_POST['bonus_points'] ) ) : 0;
$max_points = matreshka_get_redeemable_bonus_points();
if ( $requested_points <= 0 || $max_points <= 0 ) {
WC()->session->set( 'matreshka_cashback_redeem_points', 0 );
WC()->session->set( 'matreshka_cashback_redeem', 0 ); // Clear the legacy session value.
wp_send_json_success( [ 'message' => 'Кешбек снят', 'applied' => 0 ] );
}
$points = min( $requested_points, $max_points );
WC()->session->set( 'matreshka_cashback_redeem_points', $points );
WC()->session->set( 'matreshka_cashback_redeem', 0 ); // Clear the legacy session value.
wp_send_json_success( [
'message' => 'Бонусы применены',
'applied_points' => $points,
'applied_amount' => matreshka_bonus_redemption_amount( $points ),
] );
}
// Remove cashback
add_action( 'wp_ajax_matreshka_remove_cashback', 'matreshka_ajax_remove_cashback' );
function matreshka_ajax_remove_cashback() {
check_ajax_referer( 'matreshka_cashback_nonce', 'nonce' );
if ( ! is_user_logged_in() ) {
wp_send_json_error( [ 'message' => 'Не авторизован' ] );
}
WC()->session->set( 'matreshka_cashback_redeem_points', 0 );
WC()->session->set( 'matreshka_cashback_redeem', 0 ); // Clear the legacy session value.
wp_send_json_success( [ 'message' => 'Кешбек снят' ] );
}
// Add negative fee for cashback redemption (with server-side revalidation)
add_action( 'woocommerce_cart_calculate_fees', 'matreshka_cashback_add_fee' );
function matreshka_cashback_add_fee( $cart ) {
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) return;
if ( ! is_user_logged_in() ) return;
$points = WC()->session ? absint( WC()->session->get( 'matreshka_cashback_redeem_points', 0 ) ) : 0;
if ( $points <= 0 ) return;
// Revalidate the balance and 50% rule every time WooCommerce recalculates.
$points = min( $points, matreshka_get_redeemable_bonus_points( $cart ) );
if ( $points > 0 ) {
$cart->add_fee( 'Списание бонусов', -matreshka_bonus_redemption_amount( $points ), false );
} else {
WC()->session->set( 'matreshka_cashback_redeem_points', 0 );
}
}
// Deduct bonuses when order is placed (with final revalidation)
add_action( 'woocommerce_checkout_order_processed', 'matreshka_cashback_deduct_on_order', 10, 3 );
function matreshka_cashback_deduct_on_order( $order_id, $posted_data, $order ) {
if ( $order->get_meta( '_matreshka_cashback_redeemed_points' ) ) return;
$user_id = $order->get_user_id();
if ( ! $user_id ) return;
// Deduct exactly the amount that was added to this order as a discount.
$redeem = 0.0;
foreach ( $order->get_items( 'fee' ) as $fee ) {
if ( 'Списание бонусов' === $fee->get_name() ) {
$redeem += abs( (float) $fee->get_total() );
}
}
$points = (int) round( $redeem / 0.1 );
$points = min( $points, matreshka_get_user_bonuses( $user_id ) );
if ( $points <= 0 ) {
if ( WC()->session ) {
WC()->session->set( 'matreshka_cashback_redeem_points', 0 );
}
return;
}
$redeem = matreshka_bonus_redemption_amount( $points );
matreshka_subtract_user_bonuses( $user_id, $points );
$order->update_meta_data( '_matreshka_cashback_redeemed', $redeem );
$order->update_meta_data( '_matreshka_cashback_redeemed_points', $points );
$order->save();
$order->add_order_note( sprintf( 'Списано бонусами: %s (%d бонусов)', matreshka_bonuses_to_money( $points ), $points ) );
// Clear session
WC()->session->set( 'matreshka_cashback_redeem_points', 0 );
}
// ================================================================
// 5. CART & CHECKOUT DISPLAY HOOKS
// ================================================================
// Add cashback info line in cart totals
add_action( 'woocommerce_after_cart_totals', 'matreshka_cashback_cart_display' );
function matreshka_cashback_cart_display() {
if ( ! is_user_logged_in() ) return;
$cashback = matreshka_calculate_cart_cashback();
if ( $cashback <= 0 ) return;
$cashback_bonuses = floor( $cashback / 0.1 );
$user_balance = matreshka_get_user_bonuses( get_current_user_id() );
$tpl = get_template_directory_uri();
?>
<div class="matreshka-cashback-info">
<div class="cashback-info-row">
<div class="cashback-info-icon">
<img src="<?php echo esc_url( $tpl . '/assets/icons/UndoLeftRound.svg' ); ?>" alt="" width="24" height="24">
</div>
<span class="cashback-info-label montserrat-bold">Кешбек</span>
</div>
<div class="cashback-info-body">
<span class="cashback-info-title montserrat-bold">+<?php echo esc_html( $cashback_bonuses ); ?> бонусов</span>
<span class="cashback-info-sub montserrat-medium">≈ <?php echo matreshka_bonuses_to_money( $cashback_bonuses ); ?></span>
</div>
<?php if ( $user_balance > 0 ) : ?>
<div class="cashback-info-balance montserrat-medium">
<img src="<?php echo esc_url( $tpl . '/assets/icons/matreshka.svg' ); ?>" alt="" width="16" height="16">
<span>Сейчас на счёте <strong><?php echo esc_html( $user_balance ); ?></strong> бонусов (<?php echo matreshka_bonuses_to_money( $user_balance ); ?>)</span>
</div>
<?php endif; ?>
<span class="cashback-info-note montserrat-medium fw-bold">Начисляются после выполнения заказа</span>
</div>
<?php
}
// Show the applied redemption on checkout. The amount is selected in the cart.
add_action( 'woocommerce_review_order_before_submit', 'matreshka_cashback_checkout_display' );
function matreshka_cashback_checkout_display() {
if ( ! is_user_logged_in() ) return;
$user_id = get_current_user_id();
$balance = matreshka_get_user_bonuses( $user_id );
$cashback = matreshka_calculate_cart_cashback();
$cashback_bonuses = floor( $cashback / 0.1 );
$redeem_points = WC()->session ? absint( WC()->session->get( 'matreshka_cashback_redeem_points', 0 ) ) : 0;
$redeem_points = min( $redeem_points, matreshka_get_redeemable_bonus_points() );
?>
<div class="matreshka-cashback-checkout" id="cashback-checkout">
<?php if ( $cashback > 0 ) : ?>
<div class="cashback-earn montserrat-medium mb-2">
<img src="<?php echo esc_url( get_template_directory_uri() . '/assets/icons/UndoLeftRound.svg' ); ?>" alt="" width="18" class="me-1">
Кешбек: <strong class="text-success">+<?php echo esc_html( $cashback_bonuses ); ?> бонусов</strong>
<span class="text-dark-opacity">(<?php echo matreshka_bonuses_to_money( $cashback_bonuses ); ?>)</span>
</div>
<?php endif; ?>
<?php if ( $balance > 0 ) : ?>
<div class="cashback-balance montserrat-medium">
На счёте: <strong><?php echo esc_html( $balance ); ?> бонусов</strong>
<span class="text-dark-opacity">(<?php echo matreshka_bonuses_to_money( $balance ); ?>)</span>
</div>
<?php endif; ?>
<?php if ( $redeem_points > 0 ) : ?>
<div class="cashback-earn montserrat-medium mt-2">
Списано бонусами: <strong><?php echo esc_html( $redeem_points ); ?></strong>
<span class="text-dark-opacity">(<?php echo matreshka_bonuses_to_money( $redeem_points ); ?>)</span>
</div>
<?php endif; ?>
</div>
<?php
}
// Enqueue cashback CSS globally + JS data on checkout
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_style( 'matreshka-cashback', get_template_directory_uri() . '/assets/css/cashback.css', [], '1.3.1' );
if ( ! is_checkout() && ! is_cart() ) return;
if ( ! is_user_logged_in() ) return;
$cb_balance = matreshka_get_user_bonuses( get_current_user_id() );
wp_localize_script( 'jquery', 'matreshka_cashback', [
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'matreshka_cashback_nonce' ),
'balance' => $cb_balance,
'balance_money' => $cb_balance * 0.1,
] );
} );
// ================================================================
// 6. USER CATEGORY SELECTION
// ================================================================
// Route: /cashback-categories/
add_action( 'init', function() {
add_rewrite_rule( '^cashback-categories/?$', 'index.php?matreshka_cashback_page=1', 'top' );
} );
add_filter( 'query_vars', function( $vars ) {
$vars[] = 'matreshka_cashback_page';
return $vars;
} );
add_filter( 'template_include', function( $template ) {
if ( get_query_var( 'matreshka_cashback_page' ) ) {
$t = get_theme_file_path( 'page-cashback-categories.php' );
if ( file_exists( $t ) ) return $t;
}
return $template;
} );
// Flush rewrite once
add_action( 'init', function() {
if ( ! get_option( 'matreshka_cashback_rewrite_v1' ) ) {
flush_rewrite_rules();
update_option( 'matreshka_cashback_rewrite_v1', 1 );
}
}, 99 );
// AJAX: Save user's selected cashback categories
add_action( 'wp_ajax_matreshka_save_cashback_categories', 'matreshka_save_cashback_categories' );
function matreshka_save_cashback_categories() {
check_ajax_referer( 'matreshka_cashback_nonce', 'nonce' );
if ( ! is_user_logged_in() ) wp_send_json_error( [ 'message' => 'Не авторизован' ] );
$settings = matreshka_cashback_settings();
$selected = isset( $_POST['categories'] ) ? array_map( 'absint', (array) $_POST['categories'] ) : [];
// Validate: only from whitelist
$selected = array_intersect( $selected, $settings['whitelist'] );
// Enforce limit
$selected = array_slice( $selected, 0, $settings['user_limit'] );
update_user_meta( get_current_user_id(), 'matreshka_cashback_categories', $selected );
wp_send_json_success( [ 'message' => 'Категории сохранены', 'selected' => $selected ] );
}
/**
* Get user's selected cashback categories
*/
function matreshka_get_user_cashback_categories( $user_id = null ) {
if ( ! $user_id ) $user_id = get_current_user_id();
$cats = get_user_meta( $user_id, 'matreshka_cashback_categories', true );
return is_array( $cats ) ? $cats : [];
}
/**
* Get formatted whitelist categories with their percentages
*/
function matreshka_get_cashback_whitelist_data() {
$settings = matreshka_cashback_settings();
$data = [];
foreach ( $settings['whitelist'] as $cat_id ) {
$term = get_term( $cat_id, 'product_cat' );
if ( ! $term || is_wp_error( $term ) ) continue;
$data[] = [
'id' => $cat_id,
'name' => $term->name,
'slug' => $term->slug,
'percent' => $settings['category_percents'][ $cat_id ] ?? $settings['default_percent'],
'image' => matreshka_get_category_thumb_url( $cat_id ),
];
}
return $data;
}
function matreshka_get_category_thumb_url( $cat_id ) {
$thumb_id = get_term_meta( $cat_id, 'thumbnail_id', true );
return $thumb_id ? wp_get_attachment_image_url( $thumb_id, 'woocommerce_thumbnail' ) : '';
}