| 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
// 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( ']]>', ']]>', $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( ']]>', ']]>', $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( ']]>', ']]>', $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( ']]>', ']]>', $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();
}