403Webshell
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 :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/html/wp-content/themes/matreshka/inc/api-endpoints.php
<?php
// Custom API Endpoints for Mobile App
// Auth: WooCommerce API keys (OAuth 1.0 / query params / Basic Auth HTTPS)
add_action( 'rest_api_init', 'matreshka_register_api_endpoints' );

function matreshka_register_api_endpoints() {
	$auth = [ 'permission_callback' => function() { return current_user_can( 'read' ); } ];

	$routes = [
		'/banners'              => 'matreshka_api_get_banners',
		'/categories'           => 'matreshka_api_get_categories',
		'/category-products'    => 'matreshka_api_get_category_products',
		'/product/(?P<id>\d+)'  => 'matreshka_api_get_product',
		'/products/search'      => 'matreshka_api_search_products',
		'/products/popular'     => 'matreshka_api_get_popular_products',
		'/products/new'         => 'matreshka_api_get_new_products',
		'/promo/first-order'    => 'matreshka_api_first_order_promo',
		'/promotions'                  => 'matreshka_api_get_promotions',
		'/promotion/(?P<id>\d+)'       => 'matreshka_api_get_promotion',
		'/gift-certificates'           => 'matreshka_api_get_gift_certificates',
		'/bonus-card'                  => 'matreshka_api_get_bonus_card',
		'/contacts'                    => 'matreshka_api_get_contacts',
		'/vacancies'                   => 'matreshka_api_get_vacancies',
	];

	foreach ( $routes as $route => $cb ) {
		register_rest_route( 'wc/v3', $route, array_merge( [ 'methods' => 'GET', 'callback' => $cb ], $auth ) );
	}

	// Auth endpoint (POST)
	register_rest_route( 'wc/v3', '/auth', [
		'methods' => 'POST', 'callback' => 'matreshka_api_auth',
		'permission_callback' => function() { return current_user_can( 'read' ); },
	] );

	// === Registration endpoints (public, no WC auth required) ===
	$public = [ 'permission_callback' => '__return_true' ];

	register_rest_route( 'wc/v3', '/auth/send-sms', [
		'methods' => 'POST', 'callback' => 'matreshka_api_reg_send_sms',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/auth/verify-code', [
		'methods' => 'POST', 'callback' => 'matreshka_api_reg_verify_code',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/auth/save-profile', [
		'methods' => 'POST', 'callback' => 'matreshka_api_reg_save_profile',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/auth/save-address', [
		'methods' => 'POST', 'callback' => 'matreshka_api_reg_save_address',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/auth/skip-address', [
		'methods' => 'POST', 'callback' => 'matreshka_api_reg_skip_address',
		'permission_callback' => '__return_true',
	] );

	// === Wishlist endpoints (token auth) ===
	register_rest_route( 'wc/v3', '/wishlist', [
		'methods' => 'GET', 'callback' => 'matreshka_api_get_wishlist',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/wishlist/add', [
		'methods' => 'POST', 'callback' => 'matreshka_api_add_to_wishlist',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/wishlist/remove', [
		'methods' => 'POST', 'callback' => 'matreshka_api_remove_from_wishlist',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/wishlist/toggle', [
		'methods' => 'POST', 'callback' => 'matreshka_api_toggle_wishlist',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/wishlist/ids', [
		'methods' => 'GET', 'callback' => 'matreshka_api_get_wishlist_ids',
		'permission_callback' => '__return_true',
	] );

	// --- Cashback ---
	register_rest_route( 'wc/v3', '/cashback/settings', [
		'methods' => 'GET', 'callback' => 'matreshka_api_cashback_settings',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/cashback/balance', [
		'methods' => 'GET', 'callback' => 'matreshka_api_cashback_balance',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/cashback/categories', [
		'methods' => 'GET', 'callback' => 'matreshka_api_cashback_categories',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/cashback/categories/save', [
		'methods' => 'POST', 'callback' => 'matreshka_api_cashback_save_categories',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/cashback/estimate', [
		'methods' => 'POST', 'callback' => 'matreshka_api_cashback_estimate',
		'permission_callback' => '__return_true',
	] );

	// === Profile management endpoints (token auth) ===
	register_rest_route( 'wc/v3', '/profile/update', [
		'methods' => 'POST', 'callback' => 'matreshka_api_update_profile',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/profile/update-address', [
		'methods' => 'POST', 'callback' => 'matreshka_api_update_address',
		'permission_callback' => '__return_true',
	] );

	// === Order history endpoint (token auth) ===
	register_rest_route( 'wc/v3', '/orders/history', [
		'methods' => 'GET', 'callback' => 'matreshka_api_get_order_history',
		'permission_callback' => '__return_true',
	] );

	// === Notifications endpoints (token auth) ===
	register_rest_route( 'wc/v3', '/notifications', [
		'methods' => 'GET', 'callback' => 'matreshka_api_get_notifications',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/notifications/unread-count', [
		'methods' => 'GET', 'callback' => 'matreshka_api_get_unread_count',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/notifications/read', [
		'methods' => 'POST', 'callback' => 'matreshka_api_mark_notification_read',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/notifications/read-all', [
		'methods' => 'POST', 'callback' => 'matreshka_api_mark_all_notifications_read',
		'permission_callback' => '__return_true',
	] );
	register_rest_route( 'wc/v3', '/notifications/clear', [
		'methods' => 'POST', 'callback' => 'matreshka_api_clear_notifications',
		'permission_callback' => '__return_true',
	] );

	// === Shipping methods endpoint (WC auth) ===
	register_rest_route( 'wc/v3', '/shipping-methods', [
		'methods' => 'GET', 'callback' => 'matreshka_api_get_shipping_methods',
		'permission_callback' => '__return_true',
	] );

	// === Payment methods endpoint (public) ===
	register_rest_route( 'wc/v3', '/payment-methods', [
		'methods' => 'GET', 'callback' => 'matreshka_api_get_payment_methods',
		'permission_callback' => '__return_true',
	] );
}

// === HELPERS ===

function matreshka_format_product_card( $product ) {
	$id = $product->get_id();
	return [
		'id' => $id, 'name' => $product->get_name(), 'slug' => $product->get_slug(),
		'price' => $product->get_price(), 'regular_price' => $product->get_regular_price(),
		'sale_price' => $product->get_sale_price(), 'on_sale' => $product->is_on_sale(),
		'stock_status' => $product->get_stock_status(),
		'image' => wp_get_attachment_image_url( $product->get_image_id(), 'woocommerce_thumbnail' ) ?: '',
		'categories' => array_map( fn( $t ) => [ 'id' => $t->term_id, 'name' => $t->name, 'slug' => $t->slug ],
			wp_get_post_terms( $id, 'product_cat' ) ),
		'custom' => matreshka_get_product_custom_fields( $id ),
	];
}

function matreshka_get_product_custom_fields( $id, $full = false ) {
	$fields = [ 'proteins', 'fats', 'carbs', 'calories', 'weight', 'storage_life', 'storage', 'country' ];
	$data = [];
	foreach ( $fields as $f ) $data[ $f ] = get_field( $f, $id ) ?: '';
	if ( $full ) {
		foreach ( [ 'proteins_label', 'fats_label', 'carbs_label', 'calories_label' ] as $f )
			$data[ $f ] = get_field( $f, $id ) ?: '';
	}
	return $data;
}

function matreshka_format_category( $cat ) {
	$thumb_id = get_term_meta( $cat->term_id, 'thumbnail_id', true );
	return [
		'id' => $cat->term_id, 'name' => $cat->name, 'slug' => $cat->slug,
		'parent' => $cat->parent, 'count' => $cat->count,
		'image' => $thumb_id ? wp_get_attachment_image_url( $thumb_id, 'woocommerce_thumbnail' ) : '',
		'bg_color' => get_term_meta( $cat->term_id, 'category_bg_color', true ) ?: '',
	];
}

function matreshka_build_category_tree( $cats, $parent = 0 ) {
	$tree = [];
	foreach ( $cats as $cat ) {
		if ( $cat->parent != $parent ) continue;
		$item = matreshka_format_category( $cat );
		$children = matreshka_build_category_tree( $cats, $cat->term_id );
		if ( $children ) $item['children'] = $children;
		$tree[] = $item;
	}
	return $tree;
}

// === BANNERS ===

function matreshka_api_get_banners( $request ) {
	$banners = new WP_Query( [
		'post_type' => 'banner',
		'posts_per_page' => -1,
		'post_status' => 'publish',
		'meta_key' => 'banner_priority',
		'orderby' => 'meta_value_num',
		'order' => 'ASC',
		'meta_query' => [
			[
				'key' => 'banner_priority',
				'compare' => 'EXISTS',
				'type' => 'NUMERIC',
			],
		],
	] );

	$data = [];
	while ( $banners->have_posts() ) {
		$banners->the_post();
		$id = get_the_ID();
		$img_id = get_post_thumbnail_id( $id );
		$product_id  = get_field( 'banner_product', $id );
		$category_id = get_field( 'banner_category', $id );

		$item = [
			'id'    => $id,
			'title' => get_the_title(),
			'image' => wp_get_attachment_image_url( $img_id, 'full' ) ?: '',
		];

		if ( $product_id && ( $product = wc_get_product( $product_id ) ) ) {
			$item['product'] = [
				'id' => $product_id, 'name' => $product->get_name(),
				'slug' => $product->get_slug(),
			];
		}

		if ( $category_id && ( $cat = get_term( $category_id, 'product_cat' ) ) && ! is_wp_error( $cat ) ) {
			$item['category'] = [
				'id' => $cat->term_id, 'name' => $cat->name, 'slug' => $cat->slug,
			];
		}

		$data[] = $item;
	}
	wp_reset_postdata();

	return new WP_REST_Response( [ 'success' => true, 'data' => $data, 'count' => count( $data ) ], 200 );
}

// === CATEGORIES (hierarchical) ===

function matreshka_api_get_categories( $request ) {
	$cats = get_terms( [
		'taxonomy'   => 'product_cat',
		'hide_empty' => true,
		'pad_counts' => true,
		'orderby'    => 'menu_order',
		'order'      => 'ASC',
	] );

	if ( is_wp_error( $cats ) ) return new WP_REST_Response( [ 'success' => false, 'message' => 'Error' ], 500 );

	// Filter out Misc category
	$cats = array_filter( $cats, function( $cat ) {
		return $cat->slug !== 'misc';
	} );

	$tree = matreshka_build_category_tree( $cats );
	return new WP_REST_Response( [ 'success' => true, 'data' => $tree, 'count' => count( $tree ) ], 200 );
}

// === PRODUCTS BY CATEGORY ===

function matreshka_api_get_category_products( $request ) {
	$cat_ids = $request->get_param( 'category' );
	if ( ! $cat_ids ) return new WP_REST_Response( [ 'success' => false, 'message' => 'category param required' ], 400 );

	$per_page = absint( $request->get_param( 'per_page' ) ) ?: 20;
	$page     = absint( $request->get_param( 'page' ) ) ?: 1;
	$orderby  = $request->get_param( 'orderby' ) ?: 'date';
	$order    = $request->get_param( 'order' ) ?: 'DESC';

	$args = [
		'post_type' => 'product', 'post_status' => 'publish',
		'posts_per_page' => $per_page, 'paged' => $page,
		'orderby' => $orderby, 'order' => $order,
		'tax_query' => [ [
			'taxonomy' => 'product_cat',
			'field' => 'term_id',
			'terms' => array_map( 'absint', explode( ',', $cat_ids ) ),
		] ],
	];

	$query = new WP_Query( $args );
	$data = [];
	while ( $query->have_posts() ) {
		$query->the_post();
		$product = wc_get_product( get_the_ID() );
		if ( $product ) $data[] = matreshka_format_product_card( $product );
	}
	wp_reset_postdata();

	return new WP_REST_Response( [
		'success' => true, 'data' => $data,
		'total' => $query->found_posts, 'pages' => $query->max_num_pages,
		'page' => $page, 'per_page' => $per_page,
	], 200 );
}

// === SINGLE PRODUCT ===

function matreshka_api_get_product( $request ) {
	$product = wc_get_product( $request['id'] );
	if ( ! $product ) return new WP_REST_Response( [ 'success' => false, 'message' => 'Product not found' ], 404 );

	$id = $product->get_id();
	$gallery = array_map( fn( $img_id ) => [
		'id' => $img_id,
		'full' => wp_get_attachment_image_url( $img_id, 'full' ) ?: '',
		'thumbnail' => wp_get_attachment_image_url( $img_id, 'woocommerce_thumbnail' ) ?: '',
	], $product->get_gallery_image_ids() );

	$main_img_id = $product->get_image_id();
	if ( $main_img_id ) {
		array_unshift( $gallery, [
			'id' => (int) $main_img_id,
			'full' => wp_get_attachment_image_url( $main_img_id, 'full' ) ?: '',
			'thumbnail' => wp_get_attachment_image_url( $main_img_id, 'woocommerce_thumbnail' ) ?: '',
		] );
	}

	$data = [
		'id' => $id, 'name' => $product->get_name(), 'slug' => $product->get_slug(),
		'sku' => $product->get_sku(),
		'description' => $product->get_description(), 'short_description' => $product->get_short_description(),
		'price' => $product->get_price(), 'regular_price' => $product->get_regular_price(),
		'sale_price' => $product->get_sale_price(), 'on_sale' => $product->is_on_sale(),
		'stock_status' => $product->get_stock_status(), 'stock_quantity' => $product->get_stock_quantity(),
		'gallery' => $gallery,
		'categories' => array_map( fn( $t ) => [ 'id' => $t->term_id, 'name' => $t->name, 'slug' => $t->slug ],
			wp_get_post_terms( $id, 'product_cat' ) ),
		'custom' => matreshka_get_product_custom_fields( $id, true ),
	];

	return new WP_REST_Response( [ 'success' => true, 'data' => $data ], 200 );
}

// === SEARCH ===

function matreshka_api_search_products( $request ) {
	$q = sanitize_text_field( $request->get_param( 'q' ) );
	if ( ! $q ) return new WP_REST_Response( [ 'success' => false, 'message' => 'q param required' ], 400 );

	$per_page = absint( $request->get_param( 'per_page' ) ) ?: 20;
	$page     = absint( $request->get_param( 'page' ) ) ?: 1;

	$query = new WP_Query( [
		'post_type' => 'product', 'post_status' => 'publish',
		's' => $q, 'posts_per_page' => $per_page, 'paged' => $page,
	] );

	$data = [];
	while ( $query->have_posts() ) {
		$query->the_post();
		$product = wc_get_product( get_the_ID() );
		if ( $product ) $data[] = matreshka_format_product_card( $product );
	}
	wp_reset_postdata();

	return new WP_REST_Response( [
		'success' => true, 'data' => $data,
		'total' => $query->found_posts, 'pages' => $query->max_num_pages,
		'page' => $page, 'per_page' => $per_page,
	], 200 );
}

// === POPULAR PRODUCTS ===

function matreshka_api_get_popular_products( $request ) {
	$per_page = absint( $request->get_param( 'per_page' ) ) ?: 20;
	$page     = absint( $request->get_param( 'page' ) ) ?: 1;

	$query = new WP_Query( [
		'post_type' => 'product', 'post_status' => 'publish',
		'posts_per_page' => $per_page, 'paged' => $page,
		'meta_key' => 'total_sales', 'orderby' => 'meta_value_num', 'order' => 'DESC',
	] );

	$data = [];
	while ( $query->have_posts() ) {
		$query->the_post();
		$product = wc_get_product( get_the_ID() );
		if ( $product ) $data[] = matreshka_format_product_card( $product );
	}
	wp_reset_postdata();

	return new WP_REST_Response( [
		'success' => true, 'data' => $data,
		'total' => $query->found_posts, 'pages' => $query->max_num_pages,
		'page' => $page, 'per_page' => $per_page,
	], 200 );
}

// === NEW PRODUCTS ===

function matreshka_api_get_new_products( $request ) {
	$per_page = absint( $request->get_param( 'per_page' ) ) ?: 50;
	$page     = absint( $request->get_param( 'page' ) ) ?: 1;

	$query = new WP_Query( [
		'post_type' => 'product',
		'post_status' => 'publish',
		'posts_per_page' => $per_page,
		'paged' => $page,
		'orderby' => 'date',
		'order' => 'DESC',
		'meta_query' => [
			[
				'key' => '_stock_status',
				'value' => 'instock',
				'compare' => '=',
			],
		],
	] );

	$data = [];
	while ( $query->have_posts() ) {
		$query->the_post();
		$product = wc_get_product( get_the_ID() );
		if ( $product ) $data[] = matreshka_format_product_card( $product );
	}
	wp_reset_postdata();

	return new WP_REST_Response( [
		'success' => true,
		'data' => $data,
		'total' => $query->found_posts,
		'pages' => $query->max_num_pages,
		'page' => $page,
		'per_page' => $per_page,
	], 200 );
}

// === AUTH ===

function matreshka_api_auth( $request ) {
	$email = sanitize_email( $request->get_param( 'email' ) );
	$password = $request->get_param( 'password' );

	if ( ! $email || ! $password ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Email and password required' ], 400 );
	}

	$user = wp_authenticate( $email, $password );
	if ( is_wp_error( $user ) ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Invalid email or password' ], 401 );
	}

	$customer = new WC_Customer( $user->ID );
	if ( ! $customer->get_id() ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Customer not found' ], 404 );
	}

	return new WP_REST_Response( [ 'success' => true, 'data' => [
		'id' => $customer->get_id(),
		'email' => $customer->get_email(),
		'first_name' => $customer->get_first_name(),
		'last_name' => $customer->get_last_name(),
		'billing' => [
			'first_name' => $customer->get_billing_first_name(),
			'last_name' => $customer->get_billing_last_name(),
			'phone' => $customer->get_billing_phone(),
			'email' => $customer->get_billing_email(),
			'city' => $customer->get_billing_city(),
			'address_1' => $customer->get_billing_address_1(),
		],
		'promo' => matreshka_api_get_customer_promo( $user->ID ),
	] ], 200 );
}

// === PROMO: first order discount ===

function matreshka_api_get_customer_promo( $user_id ) {
	if ( get_user_meta( $user_id, '_matreshka_first_order_used', true ) ) {
		return [ 'first_order' => [ 'available' => false ] ];
	}
	$orders = wc_get_orders( [
		'customer_id' => $user_id, 'status' => [ 'completed', 'processing' ],
		'limit' => 1, 'return' => 'ids',
	] );
	$available = empty( $orders );
	return [ 'first_order' => [
		'available' => $available,
		'discount' => 500, 'min_order' => 1500, 'currency' => 'RUB',
		'title' => 'Скидка 500 ₽ на первый заказ от 1500 ₽',
	] ];
}

function matreshka_api_first_order_promo( $request ) {
	$customer_id = absint( $request->get_param( 'customer_id' ) );
	if ( ! $customer_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'customer_id required' ], 400 );
	}
	return new WP_REST_Response( [
		'success' => true, 'data' => matreshka_api_get_customer_promo( $customer_id ),
	], 200 );
}

// ================================================================
// === REGISTRATION API (mobile app) ===
// === Flow: send-sms → verify-code → save-profile → save-address
// ================================================================

/**
 * Helper: format user data for API response
 */
function matreshka_api_format_user( $user_id ) {
	$user = get_userdata( $user_id );
	if ( ! $user ) return null;

	$email = $user->user_email;
	if ( strpos( $email, '@phone.matreshka.local' ) !== false ) $email = '';

	return [
		'id'             => $user_id,
		'first_name'     => $user->first_name,
		'email'          => $email,
		'phone'          => get_user_meta( $user_id, '_matreshka_phone', true ),
		'birthday'       => get_user_meta( $user_id, '_matreshka_birthday', true ),
		'email_verified' => (bool) get_user_meta( $user_id, '_matreshka_email_verified', true ),
		'reg_step'       => get_user_meta( $user_id, '_matreshka_reg_step', true ) ?: '1',
		'address'        => [
			'street'          => get_user_meta( $user_id, 'shipping_address_1', true ),
			'entrance'        => get_user_meta( $user_id, '_matreshka_entrance', true ),
			'intercom'        => get_user_meta( $user_id, '_matreshka_intercom', true ),
			'floor'           => get_user_meta( $user_id, '_matreshka_floor', true ),
			'apartment'       => get_user_meta( $user_id, '_matreshka_apartment', true ),
			'courier_comment' => get_user_meta( $user_id, '_matreshka_courier_comment', true ),
		],
		'promo' => function_exists( 'matreshka_api_get_customer_promo' )
			? matreshka_api_get_customer_promo( $user_id ) : null,
	];
}

/**
 * POST /wc/v3/auth/send-sms
 * Body: { phone: "79991234567" }
 */
function matreshka_api_reg_send_sms( $request ) {
	if ( ! defined( 'MATRESHKA_SMS_API_ID' ) ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'SMS not configured' ], 500 );
	}

	$phone = preg_replace( '/\D/', '', sanitize_text_field( $request->get_param( 'phone' ) ?? '' ) );
	if ( strlen( $phone ) !== 11 || $phone[0] !== '7' ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Invalid phone number. Use format: 79991234567' ], 400 );
	}

	// IP rate limit
	$ip      = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
	$ip_key  = 'matreshka_sms_ip_' . md5( $ip );
	$ip_hits = (int) get_transient( $ip_key );
	if ( $ip_hits >= MATRESHKA_SMS_IP_LIMIT ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Too many requests. Try later.' ], 429 );
	}

	// Phone cooldown
	$key      = 'matreshka_sms_' . $phone;
	$existing = get_transient( $key );
	if ( $existing && ( time() - $existing['sent_at'] ) < MATRESHKA_SMS_COOLDOWN ) {
		$wait = MATRESHKA_SMS_COOLDOWN - ( time() - $existing['sent_at'] );
		return new WP_REST_Response( [ 'success' => false, 'message' => "Wait {$wait} seconds", 'wait' => $wait ], 429 );
	}

	$code = str_pad( random_int( 0, 999999 ), 6, '0', STR_PAD_LEFT );

	set_transient( $key, [
		'code'     => $code,
		'sent_at'  => time(),
		'attempts' => 0,
	], MATRESHKA_SMS_CODE_TTL );

	$result = matreshka_sms_send( $phone, "Матрёшка: ваш код {$code}", $ip );

	if ( ! $result['ok'] ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'SMS sending failed' ], 502 );
	}

	set_transient( $ip_key, $ip_hits + 1, HOUR_IN_SECONDS );

	return new WP_REST_Response( [ 'success' => true, 'message' => 'Code sent', 'cooldown' => MATRESHKA_SMS_COOLDOWN ], 200 );
}

/**
 * POST /wc/v3/auth/verify-code
 * Body: { phone: "79991234567", code: "123456" }
 * Returns: user data + auth token
 * - Existing user (reg_step=complete): logs in, returns full profile
 * - Existing user (reg_step!=complete): logs in, returns current step
 * - New user: creates account, returns reg_step=2
 */
function matreshka_api_reg_verify_code( $request ) {
	$phone = preg_replace( '/\D/', '', sanitize_text_field( $request->get_param( 'phone' ) ?? '' ) );
	$code  = sanitize_text_field( $request->get_param( 'code' ) ?? '' );
	$key   = 'matreshka_sms_' . $phone;
	$data  = get_transient( $key );

	if ( ! $data ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Code expired. Request a new one.' ], 410 );
	}

	if ( $data['attempts'] >= 5 ) {
		delete_transient( $key );
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Too many attempts. Request a new code.' ], 429 );
	}

	if ( $data['code'] !== $code ) {
		$data['attempts']++;
		set_transient( $key, $data, MATRESHKA_SMS_CODE_TTL );
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Invalid code', 'attempts_left' => 5 - $data['attempts'] ], 401 );
	}

	delete_transient( $key );

	// Find or create user
	$users = get_users( [ 'meta_key' => '_matreshka_phone', 'meta_value' => $phone, 'number' => 1 ] );

	if ( ! empty( $users ) ) {
		$user_id = $users[0]->ID;
	} else {
		$username = 'user_' . $phone;
		$user_id  = wp_create_user( $username, wp_generate_password( 16 ), $phone . '@phone.matreshka.local' );

		if ( is_wp_error( $user_id ) ) {
			return new WP_REST_Response( [ 'success' => false, 'message' => 'Account creation failed' ], 500 );
		}

		$user = new WP_User( $user_id );
		$user->set_role( 'customer' );

		update_user_meta( $user_id, '_matreshka_phone', $phone );
		update_user_meta( $user_id, '_matreshka_reg_step', '2' );
		update_user_meta( $user_id, 'billing_phone', '+' . $phone );
	}

	// Generate application password for API auth
	$app_pass = matreshka_api_get_or_create_app_password( $user_id );

	return new WP_REST_Response( [
		'success' => true,
		'data'    => matreshka_api_format_user( $user_id ),
		'token'   => $app_pass,
	], 200 );
}

/**
 * Generate or retrieve application password for mobile app auth
 */
function matreshka_api_get_or_create_app_password( $user_id ) {
	// Check if app password already exists
	$existing = get_user_meta( $user_id, '_matreshka_app_token', true );
	if ( $existing ) return $existing;

	// Generate a new token
	$token = wp_generate_password( 40, false );
	update_user_meta( $user_id, '_matreshka_app_token', $token );

	return $token;
}

/**
 * POST /wc/v3/auth/save-profile
 * Headers: X-Auth-Token: <token>
 * Body: { name, email, birthday?, want_receipts?, consent_personal_data, want_promotions? }
 */
function matreshka_api_reg_save_profile( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	$name     = sanitize_text_field( $request->get_param( 'name' ) ?? '' );
	$email    = sanitize_email( $request->get_param( 'email' ) ?? '' );
	$birthday = sanitize_text_field( $request->get_param( 'birthday' ) ?? '' );

	if ( ! $name || ! $email ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'name and email are required' ], 400 );
	}

	if ( ! $request->get_param( 'consent_personal_data' ) ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Personal data consent required' ], 400 );
	}

	if ( $birthday && ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $birthday ) ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Invalid birthday format (YYYY-MM-DD)' ], 400 );
	}

	// Check unique email
	$email_user = get_user_by( 'email', $email );
	if ( $email_user && $email_user->ID !== $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Email already in use' ], 409 );
	}

	wp_update_user( [
		'ID' => $user_id, 'user_email' => $email,
		'first_name' => $name, 'display_name' => $name,
	] );

	update_user_meta( $user_id, '_matreshka_birthday', $birthday );
	update_user_meta( $user_id, '_matreshka_want_receipts', ! empty( $request->get_param( 'want_receipts' ) ) ? 1 : 0 );
	update_user_meta( $user_id, '_matreshka_consent_personal_data', 1 );
	update_user_meta( $user_id, '_matreshka_want_promotions', ! empty( $request->get_param( 'want_promotions' ) ) ? 1 : 0 );
	update_user_meta( $user_id, 'billing_first_name', $name );
	update_user_meta( $user_id, 'billing_email', $email );

	// Send verification email
	if ( function_exists( 'matreshka_send_verification_email' ) ) {
		matreshka_send_verification_email( $user_id, $email );
	}

	update_user_meta( $user_id, '_matreshka_reg_step', '3' );

	return new WP_REST_Response( [
		'success' => true,
		'data'    => matreshka_api_format_user( $user_id ),
	], 200 );
}

/**
 * POST /wc/v3/auth/save-address
 * Headers: X-Auth-Token: <token>
 * Body: { address, entrance?, intercom?, floor?, apartment?, courier_comment? }
 */
function matreshka_api_reg_save_address( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	$address = sanitize_text_field( $request->get_param( 'address' ) ?? '' );
	if ( ! $address ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'address is required' ], 400 );
	}

	update_user_meta( $user_id, 'shipping_address_1', $address );
	update_user_meta( $user_id, 'billing_address_1', $address );
	update_user_meta( $user_id, '_matreshka_entrance', sanitize_text_field( $request->get_param( 'entrance' ) ?? '' ) );
	update_user_meta( $user_id, '_matreshka_intercom', sanitize_text_field( $request->get_param( 'intercom' ) ?? '' ) );
	update_user_meta( $user_id, '_matreshka_floor', sanitize_text_field( $request->get_param( 'floor' ) ?? '' ) );
	update_user_meta( $user_id, '_matreshka_apartment', sanitize_text_field( $request->get_param( 'apartment' ) ?? '' ) );
	update_user_meta( $user_id, '_matreshka_courier_comment', sanitize_text_field( $request->get_param( 'courier_comment' ) ?? '' ) );

	update_user_meta( $user_id, '_matreshka_reg_step', 'complete' );

	return new WP_REST_Response( [
		'success' => true,
		'data'    => matreshka_api_format_user( $user_id ),
	], 200 );
}

/**
 * POST /wc/v3/auth/skip-address
 * Headers: X-Auth-Token: <token>
 */
function matreshka_api_reg_skip_address( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	update_user_meta( $user_id, '_matreshka_reg_step', 'complete' );

	return new WP_REST_Response( [
		'success' => true,
		'data'    => matreshka_api_format_user( $user_id ),
	], 200 );
}

/**
 * Auth helper: validate X-Auth-Token header → return user_id or false
 */
function matreshka_api_auth_by_token( $request ) {
	$token = $request->get_header( 'X-Auth-Token' );
	if ( ! $token ) return false;

	$users = get_users( [ 'meta_key' => '_matreshka_app_token', 'meta_value' => $token, 'number' => 1 ] );
	if ( empty( $users ) ) return false;

	return $users[0]->ID;
}

// ================================================================
// === PROMOTIONS API ===
// ================================================================

/**
 * Helper: format a single promotion for API response
 */
function matreshka_api_format_promotion( $promo_id ) {
	$start_date  = get_post_meta( $promo_id, '_promo_date_start', true );
	$end_date    = get_post_meta( $promo_id, '_promo_date_end', true );
	$badge_text  = get_post_meta( $promo_id, '_promo_badge_text', true );
	$badge_color = get_post_meta( $promo_id, '_promo_badge_color', true ) ?: '#A92231';
	$short_desc  = get_post_meta( $promo_id, '_promo_short_description', true );
	$conditions  = get_post_meta( $promo_id, '_promo_conditions', true );
	$hero_id     = get_post_meta( $promo_id, '_promo_hero_image', true );
	$linked_ids  = get_post_meta( $promo_id, '_promo_linked_products', true );
	$cat_id      = get_post_meta( $promo_id, '_promo_category', true );
	$redirect    = get_post_meta( $promo_id, '_promo_redirect_to_category', true );

	$days_left  = function_exists( 'matreshka_promo_days_left' ) ? matreshka_promo_days_left( $end_date ) : 0;
	$date_label = function_exists( 'matreshka_promo_date_label' ) ? matreshka_promo_date_label( $end_date ) : '';

	// Thumbnail
	$thumb = get_the_post_thumbnail_url( $promo_id, 'medium_large' ) ?: '';

	// Hero image
	$hero_url = $hero_id ? wp_get_attachment_image_url( $hero_id, 'full' ) : '';

	// Category
	$category = null;
	if ( $cat_id ) {
		$cat = get_term( (int) $cat_id, 'product_cat' );
		if ( $cat && ! is_wp_error( $cat ) ) {
			$category = [
				'id'   => $cat->term_id,
				'name' => $cat->name,
				'slug' => $cat->slug,
			];
		}
	}

	// Linked products
	$products = [];
	if ( $linked_ids && function_exists( 'wc_get_product' ) ) {
		$pids = array_filter( array_map( 'absint', explode( ',', $linked_ids ) ) );
		foreach ( $pids as $pid ) {
			$product = wc_get_product( $pid );
			if ( $product && $product->get_status() === 'publish' ) {
				$products[] = matreshka_format_product_card( $product );
			}
		}
	}

	return [
		'id'              => $promo_id,
		'title'           => get_the_title( $promo_id ),
		'slug'            => get_post_field( 'post_name', $promo_id ),
		'content'         => apply_filters( 'the_content', get_post_field( 'post_content', $promo_id ) ),
		'short_description' => $short_desc ?: '',
		'image'           => $thumb,
		'hero_image'      => $hero_url ?: '',
		'date_start'      => $start_date ?: '',
		'date_end'        => $end_date ?: '',
		'date_label'      => $date_label,
		'days_left'       => $days_left,
		'badge'           => [
			'text'  => $badge_text ?: '',
			'color' => $badge_color,
		],
		'conditions'      => $conditions ?: '',
		'category'        => $category,
		'redirect_to_category' => (bool) ( $redirect && $cat_id ),
		'products'        => $products,
	];
}

/**
 * GET /wc/v3/promotions
 * Params: status (active|upcoming|ended|all), per_page, page
 */
function matreshka_api_get_promotions( $request ) {
	$status   = sanitize_text_field( $request->get_param( 'status' ) ?: 'active' );
	$per_page = absint( $request->get_param( 'per_page' ) ) ?: 20;
	$page     = absint( $request->get_param( 'page' ) ) ?: 1;
	$today    = current_time( 'Y-m-d' );

	$args = [
		'post_type'      => 'promotion',
		'post_status'    => 'publish',
		'posts_per_page' => $per_page,
		'paged'          => $page,
	];

	if ( $status === 'active' ) {
		$args['meta_query'] = [
			'relation' => 'AND',
			[ 'key' => '_promo_date_start', 'value' => $today, 'compare' => '<=', 'type' => 'DATE' ],
			[ 'key' => '_promo_date_end',   'value' => $today, 'compare' => '>=', 'type' => 'DATE' ],
			'priority_clause' => [
				'key' => '_promo_priority',
				'compare' => 'EXISTS',
				'type' => 'NUMERIC',
			],
		];
		$args['orderby'] = [
			'priority_clause' => 'ASC',
			'meta_value' => 'ASC',
		];
		$args['meta_key'] = '_promo_date_end';
	} elseif ( $status === 'upcoming' ) {
		$args['meta_query'] = [
			'priority_clause' => [
				'key' => '_promo_priority',
				'compare' => 'EXISTS',
				'type' => 'NUMERIC',
			],
			[ 'key' => '_promo_date_start', 'value' => $today, 'compare' => '>', 'type' => 'DATE' ],
		];
		$args['orderby'] = [
			'priority_clause' => 'ASC',
			'meta_value' => 'ASC',
		];
		$args['meta_key'] = '_promo_date_start';
	} elseif ( $status === 'ended' ) {
		$args['meta_query'] = [
			'priority_clause' => [
				'key' => '_promo_priority',
				'compare' => 'EXISTS',
				'type' => 'NUMERIC',
			],
			[ 'key' => '_promo_date_end', 'value' => $today, 'compare' => '<', 'type' => 'DATE' ],
		];
		$args['orderby'] = [
			'priority_clause' => 'ASC',
			'meta_value' => 'DESC',
		];
		$args['meta_key'] = '_promo_date_end';
	} else {
		// status=all
		$args['meta_query'] = [
			'priority_clause' => [
				'key' => '_promo_priority',
				'compare' => 'EXISTS',
				'type' => 'NUMERIC',
			],
		];
		$args['orderby'] = 'priority_clause';
		$args['order'] = 'ASC';
	}
	// status=all → no meta_query, returns everything

	$query = new WP_Query( $args );
	$data  = [];

	while ( $query->have_posts() ) {
		$query->the_post();
		$item = matreshka_api_format_promotion( get_the_ID() );
		// For list view, omit heavy fields
		unset( $item['content'], $item['products'], $item['conditions'] );
		$data[] = $item;
	}
	wp_reset_postdata();

	return new WP_REST_Response( [
		'success'  => true,
		'data'     => $data,
		'total'    => $query->found_posts,
		'pages'    => $query->max_num_pages,
		'page'     => $page,
		'per_page' => $per_page,
	], 200 );
}

/**
 * GET /wc/v3/promotion/{id}
 */
function matreshka_api_get_promotion( $request ) {
	$id   = absint( $request['id'] );
	$post = get_post( $id );

	if ( ! $post || $post->post_type !== 'promotion' || $post->post_status !== 'publish' ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Promotion not found' ], 404 );
	}

	return new WP_REST_Response( [
		'success' => true,
		'data'    => matreshka_api_format_promotion( $id ),
	], 200 );
}

// ================================================================
// === WISHLIST API ===
// ================================================================

/**
 * GET /wc/v3/wishlist
 * Headers: X-Auth-Token: <token>
 * Returns full product cards for all wishlist items
 */
function matreshka_api_get_wishlist( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	$wishlist = get_user_meta( $user_id, 'matreshka_wishlist', true );
	$wishlist = matreshka_normalize_wishlist( $wishlist, true );
	update_user_meta( $user_id, 'matreshka_wishlist', $wishlist );

	$products = [];
	foreach ( $wishlist as $pid ) {
		$product = wc_get_product( $pid );
		if ( $product ) {
			$products[] = matreshka_format_product_card( $product );
		}
	}

	return new WP_REST_Response( [
		'success' => true,
		'data'    => $products,
		'count'   => count( $products ),
	], 200 );
}

/**
 * GET /wc/v3/wishlist/ids
 * Headers: X-Auth-Token: <token>
 * Returns only product IDs (lightweight)
 */
function matreshka_api_get_wishlist_ids( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	$wishlist = get_user_meta( $user_id, 'matreshka_wishlist', true );
	$wishlist = matreshka_normalize_wishlist( $wishlist, true );
	update_user_meta( $user_id, 'matreshka_wishlist', $wishlist );

	return new WP_REST_Response( [
		'success' => true,
		'data'    => $wishlist,
		'count'   => count( $wishlist ),
	], 200 );
}

/**
 * POST /wc/v3/wishlist/add
 * Headers: X-Auth-Token: <token>
 * Body: { product_id: 123 }
 */
function matreshka_api_add_to_wishlist( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	$product_id = absint( $request->get_param( 'product_id' ) );
	$product = $product_id ? wc_get_product( $product_id ) : false;

	if ( ! $product || ! $product->is_visible() ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Invalid product_id' ], 400 );
	}

	$wishlist = get_user_meta( $user_id, 'matreshka_wishlist', true );
	$wishlist = matreshka_normalize_wishlist( $wishlist, true );

	if ( ! in_array( $product_id, $wishlist, true ) ) {
		$wishlist[] = $product_id;
	}

	update_user_meta( $user_id, 'matreshka_wishlist', $wishlist );

	return new WP_REST_Response( [
		'success'    => true,
		'message'    => 'Product added to wishlist',
		'product_id' => $product_id,
		'count'      => count( $wishlist ),
	], 200 );
}

/**
 * POST /wc/v3/wishlist/remove
 * Headers: X-Auth-Token: <token>
 * Body: { product_id: 123 }
 */
function matreshka_api_remove_from_wishlist( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	$product_id = absint( $request->get_param( 'product_id' ) );
	if ( ! $product_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'product_id is required' ], 400 );
	}

	$wishlist = get_user_meta( $user_id, 'matreshka_wishlist', true );
	$wishlist = matreshka_normalize_wishlist( $wishlist, true );

	$wishlist = array_values( array_diff( $wishlist, [ $product_id ] ) );
	update_user_meta( $user_id, 'matreshka_wishlist', $wishlist );

	return new WP_REST_Response( [
		'success'    => true,
		'message'    => 'Product removed from wishlist',
		'product_id' => $product_id,
		'count'      => count( $wishlist ),
	], 200 );
}

/**
 * POST /wc/v3/wishlist/toggle
 * Headers: X-Auth-Token: <token>
 * Body: { product_id: 123 }
 * Adds if not in wishlist, removes if already there
 */
function matreshka_api_toggle_wishlist( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	$product_id = absint( $request->get_param( 'product_id' ) );
	$product = $product_id ? wc_get_product( $product_id ) : false;

	if ( ! $product || ! $product->is_visible() ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Invalid product_id' ], 400 );
	}

	$wishlist = get_user_meta( $user_id, 'matreshka_wishlist', true );
	$wishlist = matreshka_normalize_wishlist( $wishlist, true );

	$in_wishlist = in_array( $product_id, $wishlist, true );

	if ( $in_wishlist ) {
		$wishlist = array_values( array_diff( $wishlist, [ $product_id ] ) );
		$action = 'removed';
	} else {
		$wishlist[] = $product_id;
		$action = 'added';
	}

	update_user_meta( $user_id, 'matreshka_wishlist', $wishlist );

	return new WP_REST_Response( [
		'success'      => true,
		'action'       => $action,
		'in_wishlist'  => ! $in_wishlist,
		'product_id'   => $product_id,
		'count'        => count( $wishlist ),
	], 200 );
}

// =====================================================
// CASHBACK ENDPOINTS
// =====================================================

/**
 * GET /wc/v3/cashback/settings
 * Public — returns global cashback info & available categories
 */
function matreshka_api_cashback_settings( WP_REST_Request $request ) {
	$settings  = matreshka_cashback_settings();
	$whitelist = matreshka_get_cashback_whitelist_data();

	return new WP_REST_Response( [
		'default_percent' => $settings['default_percent'],
		'user_limit'      => $settings['user_limit'],
		'categories'      => $whitelist,
	], 200 );
}

/**
 * GET /wc/v3/cashback/balance
 * Auth required — returns user's bonus balance
 */
function matreshka_api_cashback_balance( WP_REST_Request $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'code' => 'unauthorized', 'message' => 'Неверный токен' ], 401 );
	}

	$balance   = matreshka_get_user_bonuses( $user_id );
	$settings  = matreshka_cashback_settings();
	$user_cats = matreshka_get_user_cashback_categories( $user_id );

	$selected = [];
	foreach ( $user_cats as $cat_id ) {
		$term = get_term( $cat_id, 'product_cat' );
		if ( $term && ! is_wp_error( $term ) ) {
			$selected[] = [
				'id'      => $cat_id,
				'name'    => $term->name,
				'percent' => $settings['category_percents'][ $cat_id ] ?? $settings['default_percent'],
			];
		}
	}

	return new WP_REST_Response( [
		'balance'             => $balance,
		'balance_money'       => matreshka_bonuses_to_money( $balance ),
		'default_percent'     => $settings['default_percent'],
		'selected_categories' => $selected,
	], 200 );
}

/**
 * GET /wc/v3/cashback/categories
 * Auth required — returns whitelist with user's selections marked
 */
function matreshka_api_cashback_categories( WP_REST_Request $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'code' => 'unauthorized', 'message' => 'Неверный токен' ], 401 );
	}

	$settings  = matreshka_cashback_settings();
	$whitelist = matreshka_get_cashback_whitelist_data();
	$user_cats = matreshka_get_user_cashback_categories( $user_id );

	foreach ( $whitelist as &$cat ) {
		$cat['selected'] = in_array( $cat['id'], $user_cats );
	}

	return new WP_REST_Response( [
		'user_limit' => $settings['user_limit'],
		'selected'   => count( $user_cats ),
		'categories' => $whitelist,
	], 200 );
}

/**
 * POST /wc/v3/cashback/categories/save
 * Auth required — save user's selected categories
 * Body: { "categories": [12, 34, 56] }
 */
function matreshka_api_cashback_save_categories( WP_REST_Request $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'code' => 'unauthorized', 'message' => 'Неверный токен' ], 401 );
	}

	$settings = matreshka_cashback_settings();
	$selected = $request->get_param( 'categories' );
	$selected = is_array( $selected ) ? array_map( 'absint', $selected ) : [];

	// Validate: only from whitelist
	$selected = array_values( array_intersect( $selected, $settings['whitelist'] ) );

	// Enforce limit
	$selected = array_slice( $selected, 0, $settings['user_limit'] );

	update_user_meta( $user_id, 'matreshka_cashback_categories', $selected );

	return new WP_REST_Response( [
		'success'  => true,
		'selected' => $selected,
		'count'    => count( $selected ),
		'limit'    => $settings['user_limit'],
	], 200 );
}

/**
 * POST /wc/v3/cashback/estimate
 * Auth required — estimate cashback for a list of products
 * Body: { "items": [ { "product_id": 123, "quantity": 2 }, ... ] }
 */
function matreshka_api_cashback_estimate( WP_REST_Request $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'code' => 'unauthorized', 'message' => 'Неверный токен' ], 401 );
	}

	$items    = $request->get_param( 'items' );
	if ( ! is_array( $items ) || empty( $items ) ) {
		return new WP_REST_Response( [ 'code' => 'invalid_data', 'message' => 'Передайте items' ], 400 );
	}

	$settings  = matreshka_cashback_settings();
	$user_cats = matreshka_get_user_cashback_categories( $user_id );
	$total_cb  = 0;
	$details   = [];

	foreach ( $items as $item ) {
		$product_id = absint( $item['product_id'] ?? 0 );
		$quantity   = max( 1, absint( $item['quantity'] ?? 1 ) );
		$product    = wc_get_product( $product_id );
		if ( ! $product ) continue;

		$price = floatval( $product->get_price() ) * $quantity;
		$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 ] );
			}
		}

		$cashback = round( $price * ( $percent / 100 ), 2 );
		$total_cb += $cashback;

		$details[] = [
			'product_id' => $product_id,
			'name'       => $product->get_name(),
			'price'      => $price,
			'percent'    => $percent,
			'cashback'   => $cashback,
		];
	}

	$balance = matreshka_get_user_bonuses( $user_id );
	$total_bonuses = floor( $total_cb / 0.1 );

	return new WP_REST_Response( [
		'total_cashback'        => round( $total_cb, 2 ),
		'total_cashback_bonuses' => $total_bonuses,
		'items'                 => $details,
		'balance'               => $balance,
		'balance_money'         => matreshka_bonuses_to_money( $balance ),
	], 200 );
}

// ============================================================
// Gift Certificates
// ============================================================

/**
 * GET /wc/v3/gift-certificates
 *
 * Returns all gift certificate page data:
 * hero, certificates (images + labels), how-it-works steps,
 * why-items, CTA, and editor content (rules/terms).
 * Data is pulled from page with template "page-gift-certificates.php".
 */
function matreshka_api_get_gift_certificates( $request ) {
	// Find the page with gift certificates template
	$pages = get_pages( [
		'meta_key'   => '_wp_page_template',
		'meta_value' => 'page-gift-certificates.php',
		'number'     => 1,
	] );

	if ( empty( $pages ) ) {
		return new WP_REST_Response( [
			'code'    => 'not_found',
			'message' => 'Страница подарочных сертификатов не найдена',
		], 404 );
	}

	$page    = $pages[0];
	$post_id = $page->ID;
	$site_url = get_site_url();

	// Helper: resolve image URL from meta (attachment ID) or fallback
	$get_img = function( $meta_key, $fallback ) use ( $post_id, $site_url ) {
		$img_id = get_post_meta( $post_id, $meta_key, true );
		if ( $img_id ) {
			$url = wp_get_attachment_image_url( $img_id, 'large' );
			return $url ?: $site_url . $fallback;
		}
		return $site_url . $fallback;
	};

	// Helper: get text meta or default
	$get_text = function( $meta_key, $default ) use ( $post_id ) {
		$val = get_post_meta( $post_id, $meta_key, true );
		return ( $val !== '' && $val !== false ) ? $val : $default;
	};

	// --- Hero ---
	$hero = [
		'title'    => $get_text( '_gc_hero_title', 'Подарочные сертификаты' ),
		'subtitle' => $get_text( '_gc_hero_subtitle', 'Подарите близким возможность выбрать лучшие продукты в Матрёшке' ),
		'image'    => $get_img( '_gc_hero_image', '/wp-content/themes/matreshka/assets/img/gift-hero.png' ),
	];

	// --- Certificates ---
	$certificates = [
		[
			'label' => $get_text( '_gc_cert_1_label', '1 000 ₽' ),
			'image' => $get_img( '_gc_cert_1_image', '/wp-content/uploads/2026/02/sert_1000.png' ),
		],
		[
			'label' => $get_text( '_gc_cert_2_label', '3 000 ₽' ),
			'image' => $get_img( '_gc_cert_2_image', '/wp-content/uploads/2026/02/cert_3000.png' ),
		],
		[
			'label' => $get_text( '_gc_cert_3_label', '5 000 ₽' ),
			'image' => $get_img( '_gc_cert_3_image', '/wp-content/uploads/2026/02/cert_5000.png' ),
		],
	];

	// --- How it works ---
	$how_it_works = [
		'title' => $get_text( '_gc_how_title', 'Как это работает?' ),
		'steps' => [
			$get_text( '_gc_how_step_1', 'Выберите номинал сертификата: 1 000 ₽, 3 000 ₽ или 5 000 ₽' ),
			$get_text( '_gc_how_step_2', 'Приобретите сертификат на кассе любого магазина Матрёшка' ),
			$get_text( '_gc_how_step_3', 'Подарите красивую карточку — получатель сам выберет любимые продукты' ),
			$get_text( '_gc_how_step_4', 'Сертификат принимается к оплате во всех магазинах сети Матрёшка' ),
		],
	];

	// --- Why section ---
	$why = [
		'title' => $get_text( '_gc_why_title', 'Почему сертификат Матрёшки — идеальный подарок?' ),
		'items' => [
			$get_text( '_gc_why_item_1', 'Универсальный — подходит каждому, ведь продукты нужны всем' ),
			$get_text( '_gc_why_item_2', 'Без срочности — можно использовать в удобное время' ),
			$get_text( '_gc_why_item_3', 'Свобода выбора — получатель сам решает, что купить' ),
			$get_text( '_gc_why_item_4', 'Красивый дизайн — не стыдно дарить в подарочном конверте' ),
			$get_text( '_gc_why_item_5', 'Для любого повода — день рождения, праздник, благодарность' ),
			$get_text( '_gc_why_item_6', 'Всегда в наличии — приобретайте прямо на кассе' ),
		],
	];

	// --- CTA ---
	$cta = [
		'title'       => $get_text( '_gc_cta_title', 'Подарите радость уже сегодня!' ),
		'text'        => $get_text( '_gc_cta_text', 'Сертификаты доступны на кассе любого магазина Матрёшка. Просто скажите кассиру нужный номинал — и подарок готов!' ),
		'button_text' => $get_text( '_gc_cta_button', 'Найти ближайший магазин' ),
		'button_link' => $get_text( '_gc_cta_link', '/contacts/' ),
	];

	// --- Content (rules/terms from WP editor) ---
	$content_raw = $page->post_content;
	$content_html = apply_filters( 'the_content', $content_raw );
	$content_html = str_replace( ']]>', ']]&gt;', $content_html );

	return new WP_REST_Response( [
		'success'      => true,
		'hero'         => $hero,
		'certificates' => $certificates,
		'how_it_works' => $how_it_works,
		'why'          => $why,
		'cta'          => $cta,
		'content_html' => trim( $content_html ),
		'page_url'     => get_permalink( $post_id ),
	], 200 );
}

// ============================================================
// Bonus Card (Карта Матрёшки)
// ============================================================

function matreshka_api_get_bonus_card( WP_REST_Request $request ) {
	$pages = get_pages( [
		'meta_key'   => '_wp_page_template',
		'meta_value' => 'page-bonus-card.php',
		'number'     => 1,
	] );

	if ( empty( $pages ) ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Bonus card page not found' ], 404 );
	}

	$page    = $pages[0];
	$post_id = $page->ID;

	// Helpers
	$get_text = function( $meta_key, $default ) use ( $post_id ) {
		$val = get_post_meta( $post_id, $meta_key, true );
		return ( $val !== '' && $val !== false ) ? $val : $default;
	};

	$get_img = function( $meta_key, $fallback ) use ( $post_id ) {
		$att_id = get_post_meta( $post_id, $meta_key, true );
		if ( $att_id ) {
			$url = wp_get_attachment_url( $att_id );
			if ( $url ) return $url;
		}
		return home_url( $fallback );
	};

	// --- Hero ---
	$hero = [
		'title'    => $get_text( '_bc_hero_title', 'Карта Матрёшки' ),
		'subtitle' => $get_text( '_bc_hero_subtitle', 'Копите бонусы с каждой покупки и оплачивайте ими до 50% чека' ),
		'image'    => $get_img( '_bc_hero_image', '/wp-content/themes/matreshka/assets/img/card_main.png' ),
	];

	// --- Benefits ---
	$benefits = [
		'title' => $get_text( '_bc_benefits_title', 'Преимущества карты' ),
		'items' => [
			$get_text( '_bc_benefit_1', 'Бонусы с каждой покупки — до 5% возвращается на карту' ),
			$get_text( '_bc_benefit_2', 'Оплачивайте бонусами до 50% от суммы чека' ),
			$get_text( '_bc_benefit_3', 'Повышенный кешбек в выбранных категориях' ),
			$get_text( '_bc_benefit_4', 'Бессрочные бонусы при покупке раз в 12 месяцев' ),
		],
	];

	// --- How to get ---
	$how_to_get = [
		'title' => $get_text( '_bc_howget_title', 'Как получить карту?' ),
		'steps' => [
			[
				'method' => 'cashier',
				'text'   => $get_text( '_bc_howget_step_1', 'Сообщите свой номер телефона кассиру на кассе супермаркета и дождитесь звонка для активации' ),
			],
			[
				'method' => 'telegram',
				'text'   => $get_text( '_bc_howget_step_2', 'Активируйте карту онлайн через Telegram бота @matreshka_lpr_bot' ),
				'url'    => $get_text( '_bc_tg_bot_url', 'https://t.me/matreshka_lpr_bot' ),
			],
		],
	];

	// --- Info image ---
	$info_image = $get_img( '_bc_info_image', '/wp-content/themes/matreshka/assets/img/card_info.jpeg' );

	// --- CTA ---
	$cta = [
		'title'       => $get_text( '_bc_cta_title', 'Начните копить бонусы уже сегодня!' ),
		'text'        => $get_text( '_bc_cta_text', 'Получите карту Матрёшки на кассе любого нашего магазина или активируйте онлайн через Telegram бота.' ),
		'button_text' => $get_text( '_bc_cta_button', 'Активировать через Telegram' ),
		'button_link' => $get_text( '_bc_cta_link', 'https://t.me/matreshka_lpr_bot' ),
	];

	// --- Content (rules / conditions from WP editor) ---
	$content_raw  = $page->post_content;
	$content_html = apply_filters( 'the_content', $content_raw );
	$content_html = str_replace( ']]>', ']]&gt;', $content_html );

	return new WP_REST_Response( [
		'success'    => true,
		'hero'       => $hero,
		'benefits'   => $benefits,
		'how_to_get' => $how_to_get,
		'info_image' => $info_image,
		'cta'        => $cta,
		'content_html' => trim( $content_html ),
		'page_url'   => get_permalink( $post_id ),
	], 200 );
}

// ============================================================
// Contacts (Контакты)
// ============================================================

function matreshka_api_get_contacts( WP_REST_Request $request ) {
	// Find the page with contacts template
	$pages = get_pages( [
		'meta_key'   => '_wp_page_template',
		'meta_value' => 'page-contacts.php',
		'number'     => 1,
	] );

	if ( empty( $pages ) ) {
		return new WP_REST_Response( [
			'success' => false,
			'message' => 'Contacts page not found',
		], 404 );
	}

	$page    = $pages[0];
	$post_id = $page->ID;

	// --- Helper: read footer option (ACF → WP Options fallback) ---
	$get_opt = function( $acf_name, $wp_option ) {
		$val = function_exists('get_field') ? get_field( $acf_name, 'option' ) : '';
		if ( empty( $val ) ) {
			$val = get_option( $wp_option );
		}
		return $val ?: '';
	};

	// --- Phones ---
	$phones = [];
	$phone_1 = $get_opt( 'footer_phone_1', 'matreshka_footer_phone_1' );
	$phone_2 = $get_opt( 'footer_phone_2', 'matreshka_footer_phone_2' );
	if ( ! empty( $phone_1 ) ) {
		$phones[] = [
			'number'    => $phone_1,
			'formatted' => preg_replace( '/\s+|\(|\)|-/', '', $phone_1 ),
		];
	}
	if ( ! empty( $phone_2 ) ) {
		$phones[] = [
			'number'    => $phone_2,
			'formatted' => preg_replace( '/\s+|\(|\)|-/', '', $phone_2 ),
		];
	}

	// --- Email ---
	$email = $get_opt( 'footer_email', 'matreshka_footer_email' );

	// --- Hours ---
	$hours = $get_opt( 'footer_hours', 'matreshka_footer_hours' );

	// --- Social links ---
	$social = [];
	$vk = $get_opt( 'footer_vk', 'matreshka_footer_vk' );
	if ( ! empty( $vk ) ) {
		$social[] = [ 'network' => 'vk', 'url' => $vk ];
	}
	$instagram = $get_opt( 'footer_instagram', 'matreshka_footer_instagram' );
	if ( ! empty( $instagram ) ) {
		$social[] = [ 'network' => 'instagram', 'url' => $instagram ];
	}
	$telegram = $get_opt( 'footer_telegram', 'matreshka_footer_telegram' );
	if ( ! empty( $telegram ) ) {
		$social[] = [ 'network' => 'telegram', 'url' => $telegram ];
	}

	// --- Hero ---
	$hero = [
		'title'    => get_the_title( $post_id ),
		'subtitle' => 'Мы всегда рады помочь вам! Свяжитесь с нами любым удобным способом.',
		'image'    => get_template_directory_uri() . '/assets/img/hotline.jpg',
	];

	// --- Stores (from metabox) ---
	$raw_stores = get_post_meta( $post_id, '_matreshka_stores', true );
	$stores = [];
	if ( is_array( $raw_stores ) ) {
		foreach ( $raw_stores as $store ) {
			if ( empty( $store['name'] ) && empty( $store['address'] ) ) {
				continue;
			}
			$stores[] = [
				'name'    => $store['name'] ?? '',
				'address' => $store['address'] ?? '',
				'phone'   => $store['phone'] ?? '',
				'hours'   => $store['hours'] ?? '',
			];
		}
	}

	// --- Content (from WP editor) ---
	$content_raw  = $page->post_content;
	$content_html = apply_filters( 'the_content', $content_raw );
	$content_html = str_replace( ']]>', ']]&gt;', $content_html );

	return new WP_REST_Response( [
		'success'      => true,
		'hero'         => $hero,
		'phones'       => $phones,
		'email'        => $email,
		'hours'        => $hours,
		'social'       => $social,
		'stores'       => $stores,
		'stores_count' => count( $stores ),
		'content_html' => trim( $content_html ),
		'page_url'     => get_permalink( $post_id ),
	], 200 );
}

// ============================================================
// Vacancies (Вакансии)
// ============================================================

function matreshka_api_get_vacancies( WP_REST_Request $request ) {
	$pages = get_pages( [
		'meta_key'   => '_wp_page_template',
		'meta_value' => 'page-vacancies.php',
		'number'     => 1,
	] );

	if ( empty( $pages ) ) {
		return new WP_REST_Response( [
			'success' => false,
			'message' => 'Vacancies page not found',
		], 404 );
	}

	$page    = $pages[0];
	$post_id = $page->ID;

	$get = function( $key, $default = '' ) use ( $post_id ) {
		$val = get_post_meta( $post_id, $key, true );
		return ( $val !== '' && $val !== false ) ? $val : $default;
	};

	// --- Hero ---
	$hero = [
		'title'    => $get( '_vac_hero_title', 'Работа, которая вас понимает' ),
		'subtitle' => $get( '_vac_hero_subtitle', 'Присоединяйтесь к команде супермаркетов «Матрёшка» — мы ценим каждого сотрудника и создаём комфортные условия для работы и роста.' ),
	];

	// --- Why us ---
	$why = [
		'title' => $get( '_vac_why_title', 'Почему люди выбирают нас' ),
		'items' => [
			[ 'title' => $get( '_vac_why_1_title', 'Ценим' ),       'text' => $get( '_vac_why_1_text', 'Здесь в центре внимания люди и их потребности.' ) ],
			[ 'title' => $get( '_vac_why_2_title', 'Развиваем' ),   'text' => $get( '_vac_why_2_text', 'Обучение с первого дня, наставничество и карьерные перспективы.' ) ],
			[ 'title' => $get( '_vac_why_3_title', 'Выручаем' ),   'text' => $get( '_vac_why_3_text', 'Гибкий график, стабильная зарплата, скидки и бонусы.' ) ],
			[ 'title' => $get( '_vac_why_4_title', 'Поддерживаем' ), 'text' => $get( '_vac_why_4_text', 'Официальное трудоустройство, полный соцпакет.' ) ],
		],
	];

	// --- Stats ---
	$stats = [
		[ 'number' => $get( '_vac_stat_1_num', '500+' ),  'text' => $get( '_vac_stat_1_text', 'сотрудников в нашей команде' ) ],
		[ 'number' => $get( '_vac_stat_2_num', '10+' ),   'text' => $get( '_vac_stat_2_text', 'магазинов в сети' ) ],
		[ 'number' => $get( '_vac_stat_3_num', '1000+' ), 'text' => $get( '_vac_stat_3_text', 'довольных покупателей каждый день' ) ],
	];

	// --- What we offer ---
	$offer = [
		'title' => $get( '_vac_offer_title', 'Что мы предлагаем' ),
		'items' => [
			$get( '_vac_offer_1', 'Официальное оформление с первого дня' ),
			$get( '_vac_offer_2', 'Стабильная заработная плата без задержек' ),
			$get( '_vac_offer_3', 'Скидка для сотрудников на весь ассортимент' ),
			$get( '_vac_offer_4', 'Удобный график работы — подберём под вас' ),
			$get( '_vac_offer_5', 'Дружный коллектив и наставничество' ),
			$get( '_vac_offer_6', 'Карьерный рост и внутреннее обучение' ),
		],
	];

	// --- Culture ---
	$culture = [
		'title' => $get( '_vac_culture_title', 'Наша атмосфера и ценности' ),
		'items' => [
			[ 'title' => $get( '_vac_culture_1_title', 'Живём общими ценностями' ), 'text' => $get( '_vac_culture_1_text', 'Клиент — партнёрство — качество — результат' ) ],
			[ 'title' => $get( '_vac_culture_2_title', 'Говорим спасибо' ), 'text' => $get( '_vac_culture_2_text', 'Это не сложно, но важно — благодарность объединяет команду' ) ],
			[ 'title' => $get( '_vac_culture_3_title', 'Совершенствуем мастерство' ), 'text' => $get( '_vac_culture_3_text', 'Вместе достигаем большего и растём профессионально' ) ],
			[ 'title' => $get( '_vac_culture_4_title', 'Заботимся о людях' ), 'text' => $get( '_vac_culture_4_text', 'Покупатели, партнёры, сотрудники — в центре всего' ) ],
		],
	];

	// --- Vacancy images ---
	$raw_images = get_post_meta( $post_id, '_vac_images', true );
	$images = [];
	if ( is_array( $raw_images ) ) {
		foreach ( $raw_images as $att_id ) {
			$url = wp_get_attachment_image_url( $att_id, 'large' );
			if ( $url ) {
				$images[] = [
					'id'  => (int) $att_id,
					'url' => $url,
					'alt' => get_post_meta( $att_id, '_wp_attachment_image_alt', true ) ?: 'Вакансия',
				];
			}
		}
	}

	// --- CTA ---
	$cta_phone = function_exists('get_field') ? get_field('footer_phone_1', 'option') : '';
	if ( empty( $cta_phone ) ) { $cta_phone = get_option('matreshka_footer_phone_1'); }

	$cta = [
		'title' => $get( '_vac_cta_title', 'Присоединяйтесь к нашей команде!' ),
		'text'  => $get( '_vac_cta_text', 'Приходите в ближайший магазин Матрёшка и узнайте об открытых вакансиях у администратора.' ),
		'phone' => $cta_phone ?: '',
	];

	// --- Content ---
	$content_raw  = $page->post_content;
	$content_html = apply_filters( 'the_content', $content_raw );
	$content_html = str_replace( ']]>', ']]&gt;', $content_html );

	return new WP_REST_Response( [
		'success'      => true,
		'hero'         => $hero,
		'why'          => $why,
		'stats'        => $stats,
		'offer'        => $offer,
		'culture'      => $culture,
		'vacancy_images' => $images,
		'vacancy_count'  => count( $images ),
		'cta'          => $cta,
		'content_html' => trim( $content_html ),
		'page_url'     => get_permalink( $post_id ),
	], 200 );
}

// ============================================================
// SUBSTITUTION & PACKING PREFERENCES — REST API ORDER META
// ============================================================

/**
 * Save substitution/packing preferences when order is created via REST API.
 * Mobile app passes these as top-level fields in POST /wc/v3/orders body.
 * Hook fires after order object is saved.
 */
add_action( 'woocommerce_rest_insert_shop_order_object', 'matreshka_api_save_substitution_meta', 10, 3 );

function matreshka_api_save_substitution_meta( $order, $request, $creating ) {
	if ( ! $creating ) {
		return;
	}

	$params  = $request->get_json_params();
	$user_id = $order->get_customer_id();

	$valid_types = [ 'call_replace', 'no_replace', 'auto_replace' ];
	$labels = [
		'call_replace' => 'Позвонить и заменить',
		'no_replace'   => 'Не заменять',
		'auto_replace' => 'Не спрашивать и заменить',
	];

	// substitution_type — fallback: user meta → default call_replace
	$type = null;
	if ( ! empty( $params['substitution_type'] ) && in_array( $params['substitution_type'], $valid_types, true ) ) {
		$type = sanitize_text_field( $params['substitution_type'] );
	} elseif ( $user_id ) {
		$saved = get_user_meta( $user_id, '_matreshka_substitution_type', true );
		if ( $saved && in_array( $saved, $valid_types, true ) ) {
			$type = $saved;
		}
	}
	if ( ! $type ) {
		$type = 'call_replace';
	}
	$order->update_meta_data( '_matreshka_substitution_type',  $type );
	$order->update_meta_data( '_matreshka_substitution_label', $labels[ $type ] ?? $type );

	// packer_comment
	if ( isset( $params['packer_comment'] ) ) {
		$order->update_meta_data( '_matreshka_packer_comment', sanitize_textarea_field( $params['packer_comment'] ) );
	}

	// less_bags — fallback: user meta
	if ( isset( $params['less_bags'] ) ) {
		$less_bags = ! empty( $params['less_bags'] ) ? 'yes' : 'no';
	} elseif ( $user_id ) {
		$less_bags = get_user_meta( $user_id, '_matreshka_less_bags', true ) ?: 'no';
	} else {
		$less_bags = 'no';
	}
	$order->update_meta_data( '_matreshka_less_bags', $less_bags );

	// vegs_in_bags — fallback: user meta
	if ( isset( $params['vegs_in_bags'] ) ) {
		$vegs_in_bags = ! empty( $params['vegs_in_bags'] ) ? 'yes' : 'no';
	} elseif ( $user_id ) {
		$vegs_in_bags = get_user_meta( $user_id, '_matreshka_vegs_in_bags', true ) ?: 'no';
	} else {
		$vegs_in_bags = 'no';
	}
	$order->update_meta_data( '_matreshka_vegs_in_bags', $vegs_in_bags );

	// leave_at_door
	if ( isset( $params['leave_at_door'] ) ) {
		$order->update_meta_data( '_matreshka_leave_at_door', ! empty( $params['leave_at_door'] ) ? 'yes' : 'no' );
	}

	// courier_comment
	if ( isset( $params['courier_comment'] ) ) {
		$order->update_meta_data( '_matreshka_courier_comment', sanitize_textarea_field( $params['courier_comment'] ) );
	}

	$order->save();

	// Save substitution prefs to user meta for future orders
	if ( $user_id ) {
		update_user_meta( $user_id, '_matreshka_substitution_type', $type );
		if ( isset( $params['packer_comment'] ) ) {
			update_user_meta( $user_id, '_matreshka_packer_comment', sanitize_textarea_field( $params['packer_comment'] ) );
		}
		update_user_meta( $user_id, '_matreshka_less_bags',    $less_bags );
		update_user_meta( $user_id, '_matreshka_vegs_in_bags', $vegs_in_bags );
	}
}

/**
 * Expose substitution preferences in REST API order response.
 */
add_filter( 'woocommerce_rest_prepare_shop_order_object', 'matreshka_api_add_substitution_to_response', 10, 3 );

function matreshka_api_add_substitution_to_response( $response, $order, $request ) {
	$data = $response->get_data();

	$data['substitution'] = [
		'type'           => $order->get_meta( '_matreshka_substitution_type' ) ?: null,
		'label'          => $order->get_meta( '_matreshka_substitution_label' ) ?: null,
		'packer_comment' => $order->get_meta( '_matreshka_packer_comment' ) ?: null,
		'less_bags'      => $order->get_meta( '_matreshka_less_bags' ) === 'yes',
		'vegs_in_bags'   => $order->get_meta( '_matreshka_vegs_in_bags' ) === 'yes',
		'leave_at_door'  => $order->get_meta( '_matreshka_leave_at_door' ) === 'yes',
	];

	$response->set_data( $data );
	return $response;
}

// ================================================================
// === Profile Management Endpoints
// ================================================================

/**
 * POST /wc/v3/profile/update
 * Headers: X-Auth-Token: <token>
 * Body: { name?, email?, birthday? }
 *
 * Updates the authenticated user's profile fields.
 * All fields are optional — only provided fields are updated.
 */
function matreshka_api_update_profile( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	$name     = $request->get_param( 'name' );
	$email    = $request->get_param( 'email' );
	$birthday = $request->get_param( 'birthday' );

	// At least one field must be provided
	if ( $name === null && $email === null && $birthday === null ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'No fields to update' ], 400 );
	}

	$wp_update = [ 'ID' => $user_id ];

	// Name
	if ( $name !== null ) {
		$name = sanitize_text_field( $name );
		if ( ! $name ) {
			return new WP_REST_Response( [ 'success' => false, 'message' => 'name cannot be empty' ], 400 );
		}
		$wp_update['first_name']   = $name;
		$wp_update['display_name'] = $name;
		update_user_meta( $user_id, 'billing_first_name', $name );
	}

	// Email
	if ( $email !== null ) {
		$email = sanitize_email( $email );
		if ( ! is_email( $email ) ) {
			return new WP_REST_Response( [ 'success' => false, 'message' => 'Invalid email format' ], 400 );
		}
		$email_user = get_user_by( 'email', $email );
		if ( $email_user && $email_user->ID !== $user_id ) {
			return new WP_REST_Response( [ 'success' => false, 'message' => 'Email already in use' ], 409 );
		}
		$wp_update['user_email'] = $email;
		update_user_meta( $user_id, 'billing_email', $email );
	}

	wp_update_user( $wp_update );

	// Birthday
	if ( $birthday !== null ) {
		$birthday = sanitize_text_field( $birthday );
		if ( $birthday && ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $birthday ) ) {
			return new WP_REST_Response( [ 'success' => false, 'message' => 'Invalid birthday format (YYYY-MM-DD)' ], 400 );
		}
		update_user_meta( $user_id, '_matreshka_birthday', $birthday );
	}

	return new WP_REST_Response( [
		'success' => true,
		'message' => 'Profile updated',
		'data'    => matreshka_api_format_user( $user_id ),
	], 200 );
}

/**
 * POST /wc/v3/profile/update-address
 * Headers: X-Auth-Token: <token>
 * Body: { address, entrance?, intercom?, floor?, apartment?, courier_comment? }
 *
 * Updates the authenticated user's delivery address.
 */
function matreshka_api_update_address( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	$address = sanitize_text_field( $request->get_param( 'address' ) ?? '' );
	if ( ! $address ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'address is required' ], 400 );
	}

	update_user_meta( $user_id, 'shipping_address_1', $address );
	update_user_meta( $user_id, 'billing_address_1', $address );
	update_user_meta( $user_id, '_matreshka_entrance', sanitize_text_field( $request->get_param( 'entrance' ) ?? '' ) );
	update_user_meta( $user_id, '_matreshka_intercom', sanitize_text_field( $request->get_param( 'intercom' ) ?? '' ) );
	update_user_meta( $user_id, '_matreshka_floor', sanitize_text_field( $request->get_param( 'floor' ) ?? '' ) );
	update_user_meta( $user_id, '_matreshka_apartment', sanitize_text_field( $request->get_param( 'apartment' ) ?? '' ) );
	update_user_meta( $user_id, '_matreshka_courier_comment', sanitize_text_field( $request->get_param( 'courier_comment' ) ?? '' ) );

	return new WP_REST_Response( [
		'success' => true,
		'message' => 'Address updated',
		'data'    => matreshka_api_format_user( $user_id ),
	], 200 );
}

// ================================================================
// === Notifications API
// ================================================================

/**
 * Format a single notification for API response
 */
function matreshka_api_format_notification( $notification, $index ) {
	return [
		'index'    => $index,
		'title'    => $notification['title'] ?? '',
		'content'  => $notification['content'] ?? '',
		'date'     => isset( $notification['date'] ) ? date( 'c', $notification['date'] ) : '',
		'timestamp'=> $notification['date'] ?? 0,
		'read'     => ! empty( $notification['read'] ),
		'type'     => $notification['type'] ?? 'general',
		'order_id' => $notification['order_id'] ?? null,
	];
}

/**
 * GET /wc/v3/notifications
 * Headers: X-Auth-Token: <token>
 * Params: limit (int, default -1 = all), page (int, default 1), per_page (int, default 20)
 *
 * Returns user notifications list
 */
function matreshka_api_get_notifications( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	$per_page = absint( $request->get_param( 'per_page' ) ) ?: 20;
	$page     = absint( $request->get_param( 'page' ) ) ?: 1;

	// Get all notifications (sorted newest first)
	$all = matreshka_get_user_notifications( $user_id, -1 );
	$total = count( $all );

	// Paginate
	$offset = ( $page - 1 ) * $per_page;
	$items  = array_slice( $all, $offset, $per_page );

	$data = [];
	foreach ( $items as $i => $n ) {
		$data[] = matreshka_api_format_notification( $n, $offset + $i );
	}

	$unread = 0;
	foreach ( $all as $n ) {
		if ( empty( $n['read'] ) ) $unread++;
	}

	return new WP_REST_Response( [
		'success'      => true,
		'data'         => $data,
		'total'        => $total,
		'unread_count' => $unread,
		'page'         => $page,
		'per_page'     => $per_page,
		'total_pages'  => ceil( $total / $per_page ),
	], 200 );
}

/**
 * GET /wc/v3/notifications/unread-count
 * Headers: X-Auth-Token: <token>
 *
 * Returns only the unread notifications count (lightweight)
 */
function matreshka_api_get_unread_count( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	return new WP_REST_Response( [
		'success'      => true,
		'unread_count' => matreshka_get_unread_notifications_count( $user_id ),
	], 200 );
}

/**
 * POST /wc/v3/notifications/read
 * Headers: X-Auth-Token: <token>
 * Body: { index: 0 }
 *
 * Mark a single notification as read by its index
 */
function matreshka_api_mark_notification_read( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	$index = $request->get_param( 'index' );
	if ( $index === null || $index === '' ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'index is required' ], 400 );
	}

	$index = absint( $index );
	$result = matreshka_mark_notification_read( $user_id, $index );

	if ( ! $result ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Notification not found' ], 404 );
	}

	return new WP_REST_Response( [
		'success'      => true,
		'message'      => 'Notification marked as read',
		'unread_count' => matreshka_get_unread_notifications_count( $user_id ),
	], 200 );
}

/**
 * POST /wc/v3/notifications/read-all
 * Headers: X-Auth-Token: <token>
 *
 * Mark all notifications as read
 */
function matreshka_api_mark_all_notifications_read( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	$notifications = get_user_meta( $user_id, 'matreshka_notifications', true );

	if ( ! empty( $notifications ) && is_array( $notifications ) ) {
		foreach ( $notifications as &$n ) {
			$n['read'] = true;
		}
		unset( $n );
		update_user_meta( $user_id, 'matreshka_notifications', $notifications );
	}

	return new WP_REST_Response( [
		'success'      => true,
		'message'      => 'All notifications marked as read',
		'unread_count' => 0,
	], 200 );
}

/**
 * POST /wc/v3/notifications/clear
 * Headers: X-Auth-Token: <token>
 *
 * Delete all notifications
 */
function matreshka_api_clear_notifications( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	matreshka_clear_all_notifications( $user_id );

	return new WP_REST_Response( [
		'success' => true,
		'message' => 'All notifications cleared',
	], 200 );
}

// ================================================================
// === Order History API
// ================================================================

/**
 * GET /wc/v3/orders/history
 * Headers: X-Auth-Token: <token>
 * Params: page (int, default 1), per_page (int, default 20), status (string, optional)
 *
 * Returns paginated order history for the authenticated user
 */
function matreshka_api_get_order_history( $request ) {
	$user_id = matreshka_api_auth_by_token( $request );
	if ( ! $user_id ) {
		return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized' ], 401 );
	}

	$per_page = absint( $request->get_param( 'per_page' ) ) ?: 20;
	$page     = absint( $request->get_param( 'page' ) ) ?: 1;
	$status   = sanitize_text_field( $request->get_param( 'status' ) ?? '' );

	$allowed_statuses = [ 'completed', 'processing', 'on-hold', 'pending', 'cancelled', 'refunded', 'failed' ];

	if ( $status && in_array( $status, $allowed_statuses, true ) ) {
		$query_status = [ $status ];
	} else {
		$query_status = $allowed_statuses;
	}

	$orders = wc_get_orders( [
		'customer' => $user_id,
		'limit'    => $per_page,
		'paged'    => $page,
		'orderby'  => 'date',
		'order'    => 'DESC',
		'status'   => $query_status,
		'paginate' => true,
	] );

	$status_labels = [
		'completed'  => 'Выполнен',
		'processing' => 'В обработке',
		'on-hold'    => 'На удержании',
		'pending'    => 'Ожидает оплаты',
		'cancelled'  => 'Отменён',
		'refunded'   => 'Возврат',
		'failed'     => 'Ошибка',
	];

	$data = [];

	foreach ( $orders->orders as $order ) {
		$order_id    = $order->get_id();
		$order_date  = $order->get_date_created();
		$order_status = $order->get_status();

		// Build items array
		$items = [];
		foreach ( $order->get_items() as $item ) {
			$product = $item->get_product();
			$item_data = [
				'product_id' => $item->get_product_id(),
				'name'       => $item->get_name(),
				'quantity'   => $item->get_quantity(),
				'price'      => (float) $item->get_total(),
				'image'      => '',
			];

			if ( $product ) {
				$img_id = $product->get_image_id();
				if ( $img_id ) {
					$item_data['image'] = wp_get_attachment_image_url( $img_id, 'woocommerce_thumbnail' ) ?: '';
				}
			}

			$items[] = $item_data;
		}

		// Cashback data
		$bonus_points  = (int) $order->get_meta( '_matreshka_cashback_awarded' );
		$cashback_amt  = (float) $order->get_meta( '_matreshka_cashback_amount' );

		// Substitution & delivery prefs
		$substitution = [
			'type'           => $order->get_meta( '_matreshka_substitution_type' ) ?: null,
			'label'          => $order->get_meta( '_matreshka_substitution_label' ) ?: null,
			'packer_comment' => $order->get_meta( '_matreshka_packer_comment' ) ?: null,
			'less_bags'      => $order->get_meta( '_matreshka_less_bags' ) === 'yes',
			'vegs_in_bags'   => $order->get_meta( '_matreshka_vegs_in_bags' ) === 'yes',
			'leave_at_door'  => $order->get_meta( '_matreshka_leave_at_door' ) === 'yes',
		];

		// Shipping method info
		$ship_type = $order->get_meta( '_matreshka_shipping_type' ) ?: 'delivery';
		$shipping_info = [
			'method' => $ship_type,
			'label'  => $ship_type === 'pickup' ? 'Самовывоз' : 'Доставка',
			'cost'   => (float) $order->get_shipping_total(),
		];
		if ( $ship_type === 'pickup' ) {
			$shipping_info['store'] = [
				'name'    => $order->get_meta( '_matreshka_pickup_store_name' ) ?: null,
				'address' => $order->get_meta( '_matreshka_pickup_store_address' ) ?: null,
				'phone'   => $order->get_meta( '_matreshka_pickup_store_phone' ) ?: null,
				'hours'   => $order->get_meta( '_matreshka_pickup_store_hours' ) ?: null,
			];
		}

		$data[] = [
			'id'              => $order_id,
			'phone'           => $order->get_billing_phone(),
			'status'          => $order_status,
			'status_label'    => $status_labels[ $order_status ] ?? ucfirst( $order_status ),
			'date'            => $order_date ? $order_date->date( 'c' ) : '',
			'timestamp'       => $order_date ? $order_date->getTimestamp() : 0,
			'total'           => (float) $order->get_total(),
			'currency'        => $order->get_currency(),
			'payment_method'  => $order->get_payment_method_title(),
			'items_count'     => count( $items ),
			'items'           => $items,
			'cashback'        => [
				'awarded'       => $bonus_points > 0,
				'bonus_points'  => $bonus_points,
				'amount'        => $cashback_amt,
			],
			'substitution'    => $substitution,
			'shipping'        => $shipping_info,
			'shipping_address'=> $order->get_address( 'shipping' ),
			'order_notes'     => $order->get_customer_note(),
		];
	}

	return new WP_REST_Response( [
		'success'     => true,
		'data'        => $data,
		'total'       => (int) $orders->total,
		'page'        => $page,
		'per_page'    => $per_page,
		'total_pages' => (int) $orders->max_num_pages,
	], 200 );
}

/* ══════════════════════════════════════════════════════════════
 * Shipping Methods API
 * ══════════════════════════════════════════════════════════════ */

/**
 * GET /wc/v3/shipping-methods
 *
 * Returns available shipping methods with costs & pickup stores.
 * Optionally accepts X-Auth-Token to return user's saved preference.
 */
function matreshka_api_get_shipping_methods( $request ) {
	$costs  = function_exists( 'matreshka_get_shipping_costs' ) ? matreshka_get_shipping_costs() : [ 'delivery' => 389, 'packaging' => 49.99 ];
	$stores = function_exists( 'matreshka_get_pickup_stores' ) ? array_values( matreshka_get_pickup_stores() ) : [];

	// Format stores for API
	$api_stores = [];
	foreach ( $stores as $i => $store ) {
		$api_stores[] = [
			'index'   => $i,
			'name'    => $store['name'] ?? '',
			'address' => $store['address'] ?? '',
			'phone'   => $store['phone'] ?? '',
			'hours'   => $store['hours'] ?? '',
		];
	}

	$methods = [
		[
			'id'          => 'delivery',
			'title'       => 'Доставка',
			'description' => 'Курьерская доставка до двери',
			'cost'        => round( $costs['delivery'] + $costs['packaging'], 2 ),
			'cost_details' => [
				'delivery'  => (float) $costs['delivery'],
				'packaging' => (float) $costs['packaging'],
			],
		],
		[
			'id'          => 'pickup',
			'title'       => 'Самовывоз',
			'description' => 'Забрать из магазина',
			'cost'        => round( $costs['packaging'], 2 ),
			'cost_details' => [
				'packaging' => (float) $costs['packaging'],
			],
			'stores'      => $api_stores,
		],
	];

	// If user is authenticated, include their saved preference
	$user_pref = null;
	$user_id   = matreshka_api_auth_by_token( $request );
	if ( $user_id ) {
		$saved_type  = get_user_meta( $user_id, '_matreshka_shipping_type', true ) ?: 'delivery';
		$saved_store = (int) get_user_meta( $user_id, '_matreshka_pickup_store', true );
		$user_pref = [
			'method'      => $saved_type,
			'store_index' => $saved_type === 'pickup' ? $saved_store : null,
		];
	}

	$response = [
		'success'  => true,
		'methods'  => $methods,
		'default'  => 'delivery',
	];

	if ( $user_pref ) {
		$response['user_preference'] = $user_pref;
	}

	return new WP_REST_Response( $response, 200 );
}

/* ──────────────────────────────────────────────────────────────
 * Hook: Save shipping data when order is created via REST API
 *
 * When the mobile app creates an order via POST /wc/v3/orders,
 * it passes shipping_type and pickup_store_index in meta_data.
 * This hook processes those values.
 * ────────────────────────────────────────────────────────────── */

/* ══════════════════════════════════════════════════════════════
 * Payment Methods API
 * ══════════════════════════════════════════════════════════════ */

/**
 * GET /wc/v3/payment-methods
 *
 * Returns available payment gateways.
 * Optionally accepts X-Auth-Token to return user's last used method.
 */
function matreshka_api_get_payment_methods( $request ) {
	// Icon mapping for known gateways
	$icon_map = [
		'cod'    => 'money',
		'bacs'   => 'account_balance',
		'cheque' => 'receipt_long',
	];

	// Ensure WooCommerce is loaded
	if ( ! function_exists( 'WC' ) || ! WC()->payment_gateways ) {
		return new WP_REST_Response( [
			'success' => false,
			'message' => 'Payment gateways not available',
		], 500 );
	}

	$gateways  = WC()->payment_gateways->get_available_payment_gateways();
	$methods   = [];

	foreach ( $gateways as $gateway ) {
		$methods[] = [
			'id'          => $gateway->id,
			'title'       => $gateway->get_title(),
			'description' => $gateway->get_description() ?: '',
			'icon'        => $icon_map[ $gateway->id ] ?? 'payments',
		];
	}

	// If user is authenticated, include their last used payment method
	$user_pref = null;
	$user_id   = matreshka_api_auth_by_token( $request );
	if ( $user_id ) {
		$saved = get_user_meta( $user_id, '_matreshka_payment_method', true );
		if ( $saved && isset( $gateways[ $saved ] ) ) {
			$user_pref = $saved;
		}
	}

	$default = ! empty( $methods ) ? $methods[0]['id'] : null;

	$response = [
		'success' => true,
		'methods' => $methods,
		'default' => $default,
	];

	if ( $user_pref ) {
		$response['user_preference'] = $user_pref;
	}

	return new WP_REST_Response( $response, 200 );
}

/* ──────────────────────────────────────────────────────────────
 * Hook: Save payment method when order is created via REST API
 * ────────────────────────────────────────────────────────────── */

add_action( 'woocommerce_rest_insert_shop_order_object', 'matreshka_api_save_payment_method_pref', 10, 3 );
function matreshka_api_save_payment_method_pref( $order, $request, $creating ) {
	if ( ! $creating ) return;

	$payment_method = $order->get_payment_method();
	$customer_id    = $order->get_customer_id();

	if ( $customer_id && $payment_method ) {
		update_user_meta( $customer_id, '_matreshka_payment_method', $payment_method );
	}
}

add_action( 'woocommerce_rest_insert_shop_order_object', 'matreshka_api_save_shipping_on_order_create', 10, 3 );
function matreshka_api_save_shipping_on_order_create( $order, $request, $creating ) {
	if ( ! $creating ) {
		return;
	}

	$params = $request->get_json_params();

	// Accept shipping_type at top level or in meta_data
	$shipping_type   = null;
	$store_index     = null;

	// Check top-level
	if ( isset( $params['shipping_type'] ) ) {
		$shipping_type = sanitize_text_field( $params['shipping_type'] );
	}
	if ( isset( $params['pickup_store_index'] ) ) {
		$store_index = absint( $params['pickup_store_index'] );
	}

	// Check meta_data array
	if ( ! $shipping_type && ! empty( $params['meta_data'] ) && is_array( $params['meta_data'] ) ) {
		foreach ( $params['meta_data'] as $meta ) {
			if ( ( $meta['key'] ?? '' ) === 'shipping_type' ) {
				$shipping_type = sanitize_text_field( $meta['value'] ?? '' );
			}
			if ( ( $meta['key'] ?? '' ) === 'pickup_store_index' ) {
				$store_index = absint( $meta['value'] ?? 0 );
			}
		}
	}

	// Fallback: use user meta preference
	if ( ! $shipping_type ) {
		$customer_id = $order->get_customer_id();
		if ( $customer_id ) {
			$shipping_type = get_user_meta( $customer_id, '_matreshka_shipping_type', true ) ?: 'delivery';
			if ( $shipping_type === 'pickup' && $store_index === null ) {
				$store_index = (int) get_user_meta( $customer_id, '_matreshka_pickup_store', true );
			}
		} else {
			$shipping_type = 'delivery';
		}
	}

	if ( ! in_array( $shipping_type, [ 'delivery', 'pickup' ], true ) ) {
		$shipping_type = 'delivery';
	}

	// Save shipping type
	$order->update_meta_data( '_matreshka_shipping_type', $shipping_type );

	// Save pickup store data
	if ( $shipping_type === 'pickup' && $store_index !== null ) {
		$stores = function_exists( 'matreshka_get_pickup_stores' ) ? array_values( matreshka_get_pickup_stores() ) : [];

		if ( isset( $stores[ $store_index ] ) ) {
			$store = $stores[ $store_index ];
			$order->update_meta_data( '_matreshka_pickup_store_name',    $store['name'] ?? '' );
			$order->update_meta_data( '_matreshka_pickup_store_address', $store['address'] ?? '' );
			$order->update_meta_data( '_matreshka_pickup_store_phone',   $store['phone'] ?? '' );
			$order->update_meta_data( '_matreshka_pickup_store_hours',   $store['hours'] ?? '' );
		}
	}

	// Add the correct shipping line to the order
	$costs = function_exists( 'matreshka_get_shipping_costs' ) ? matreshka_get_shipping_costs() : [ 'delivery' => 389, 'packaging' => 49.99 ];

	// Remove existing shipping lines (WC may have added default)
	foreach ( $order->get_items( 'shipping' ) as $item_id => $item ) {
		$order->remove_item( $item_id );
	}

	$shipping_item = new WC_Order_Item_Shipping();
	if ( $shipping_type === 'delivery' ) {
		$shipping_item->set_method_title( 'Доставка' );
		$shipping_item->set_method_id( 'matreshka_delivery' );
		$shipping_item->set_total( $costs['delivery'] + $costs['packaging'] );
	} else {
		$shipping_item->set_method_title( 'Самовывоз' );
		$shipping_item->set_method_id( 'matreshka_pickup' );
		$shipping_item->set_total( $costs['packaging'] );
	}
	$order->add_item( $shipping_item );

	// Persist to user meta
	$customer_id = $order->get_customer_id();
	if ( $customer_id ) {
		update_user_meta( $customer_id, '_matreshka_shipping_type', $shipping_type );
		if ( $shipping_type === 'pickup' && $store_index !== null ) {
			update_user_meta( $customer_id, '_matreshka_pickup_store', $store_index );
		}
	}

	$order->calculate_totals();
	$order->save();
}

Youez - 2016 - github.com/yon3zu
LinuXploit