| 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
/**
* Product Management API Endpoints
*
* Internal endpoints for supermarket programmers to manage products
* via REST API. Requires WooCommerce + ACF.
*
* @package Matreshka
*/
defined( 'ABSPATH' ) || exit;
add_action( 'rest_api_init', 'matreshka_register_product_management_endpoints' );
function matreshka_register_product_management_endpoints() {
$write_auth = [
'permission_callback' => function () {
return current_user_can( 'edit_products' );
},
];
// Create single product
register_rest_route( 'wc/v3', '/products/create', [
'methods' => 'POST',
'callback' => 'matreshka_api_create_product',
...$write_auth,
] );
// Batch create products
register_rest_route( 'wc/v3', '/products/batch-create', [
'methods' => 'POST',
'callback' => 'matreshka_api_batch_create_products',
...$write_auth,
] );
// Update single product (by ID or SKU)
register_rest_route( 'wc/v3', '/products/update/(?P<id>[\w.-]+)', [
'methods' => 'PUT',
'callback' => 'matreshka_api_update_product',
...$write_auth,
] );
// Batch update products
register_rest_route( 'wc/v3', '/products/batch-update', [
'methods' => 'PUT',
'callback' => 'matreshka_api_batch_update_products',
...$write_auth,
] );
// Quick update price/stock (single, by ID or SKU)
register_rest_route( 'wc/v3', '/products/quick-update/(?P<id>[\w.-]+)', [
'methods' => 'PATCH',
'callback' => 'matreshka_api_quick_update_product',
...$write_auth,
] );
// Quick update price/stock (batch)
register_rest_route( 'wc/v3', '/products/batch-quick-update', [
'methods' => 'PATCH',
'callback' => 'matreshka_api_batch_quick_update_products',
...$write_auth,
] );
// Delete single product (by ID or SKU)
register_rest_route( 'wc/v3', '/products/delete/(?P<id>[\w.-]+)', [
'methods' => 'DELETE',
'callback' => 'matreshka_api_delete_product',
'permission_callback' => function () {
return current_user_can( 'delete_products' );
},
] );
// Batch delete products
register_rest_route( 'wc/v3', '/products/batch-delete', [
'methods' => 'DELETE',
'callback' => 'matreshka_api_batch_delete_products',
'permission_callback' => function () {
return current_user_can( 'delete_products' );
},
] );
$read_auth = [
'permission_callback' => function () {
return current_user_can( 'edit_products' );
},
];
// Get single product (by ID or SKU)
register_rest_route( 'wc/v3', '/products/get/(?P<id>[\w.-]+)', [
'methods' => 'GET',
'callback' => 'matreshka_api_manage_get_product',
...$read_auth,
] );
// Get list of products
register_rest_route( 'wc/v3', '/products/list', [
'methods' => 'GET',
'callback' => 'matreshka_api_manage_list_products',
...$read_auth,
] );
}
/* ──────────────────────────────────────────────────────────────
HELPERS
────────────────────────────────────────────────────────────── */
/**
* Resolve a product identifier (numeric ID or SKU string) to a product ID.
*
* @param string|int $identifier Product ID or SKU.
* @return int Product ID, or 0 if not found.
*/
function matreshka_resolve_product_id( $identifier ) {
if ( is_numeric( $identifier ) ) {
return absint( $identifier );
}
$identifier = sanitize_text_field( $identifier );
$product_id = wc_get_product_id_by_sku( $identifier );
return $product_id ? absint( $product_id ) : 0;
}
/**
* Resolve product ID from a batch item that may contain 'id' or 'sku'.
*
* @param array $item Batch item with 'id' and/or 'sku'.
* @return int Product ID, or 0 if not found.
*/
function matreshka_resolve_batch_product_id( $item ) {
if ( ! empty( $item['id'] ) && is_numeric( $item['id'] ) ) {
return absint( $item['id'] );
}
if ( ! empty( $item['sku'] ) ) {
return matreshka_resolve_product_id( $item['sku'] );
}
return 0;
}
/**
* Sideload an image from a URL into the WP Media Library.
*
* @param string $url Remote image URL.
* @param int $post_id Post to attach image to (0 = unattached).
* @param string $description Alt text / description.
* @return int|WP_Error Attachment ID on success.
*/
function matreshka_sideload_image( $url, $post_id = 0, $description = '' ) {
if ( ! function_exists( 'media_sideload_image' ) ) {
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
}
// Validate URL scheme
$parsed = wp_parse_url( $url );
if ( empty( $parsed['scheme'] ) || ! in_array( $parsed['scheme'], [ 'http', 'https' ], true ) ) {
return new WP_Error( 'invalid_url', 'Image URL must use http or https scheme.' );
}
// Download to temp file
$tmp = download_url( $url, 30 );
if ( is_wp_error( $tmp ) ) {
return $tmp;
}
// Validate MIME type
$file_info = wp_check_filetype( basename( wp_parse_url( $url, PHP_URL_PATH ) ) );
$allowed = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ];
$real_mime = mime_content_type( $tmp );
if ( ! in_array( $real_mime, $allowed, true ) ) {
@unlink( $tmp );
return new WP_Error( 'invalid_mime', 'File is not an allowed image type. Allowed: jpg, png, gif, webp.' );
}
$file_array = [
'name' => basename( wp_parse_url( $url, PHP_URL_PATH ) ),
'tmp_name' => $tmp,
];
$attachment_id = media_handle_sideload( $file_array, $post_id, $description );
if ( is_wp_error( $attachment_id ) ) {
@unlink( $tmp );
}
return $attachment_id;
}
/**
* Validate required fields in product data.
*
* @param array $data Product data array.
* @return true|WP_Error
*/
function matreshka_validate_product_data( $data ) {
$errors = [];
if ( empty( $data['name'] ) ) {
$errors[] = 'name is required';
}
if ( ! isset( $data['regular_price'] ) || $data['regular_price'] === '' ) {
$errors[] = 'regular_price is required';
}
if ( empty( $data['categories'] ) || ! is_array( $data['categories'] ) ) {
$errors[] = 'categories is required (array of category IDs)';
}
if ( empty( $data['image_url'] ) ) {
$errors[] = 'image_url is required (main product image)';
}
if ( ! empty( $errors ) ) {
return new WP_Error(
'validation_error',
'Validation failed: ' . implode( '; ', $errors ),
[ 'status' => 400, 'errors' => $errors ]
);
}
return true;
}
/**
* Create a single WooCommerce product from data array.
*
* @param array $data Product data.
* @return array|WP_Error Product result or error.
*/
function matreshka_create_product_from_data( $data ) {
// Validate required fields
$valid = matreshka_validate_product_data( $data );
if ( is_wp_error( $valid ) ) {
return $valid;
}
$product = new WC_Product_Simple();
// ─── Core fields ───
$product->set_name( sanitize_text_field( $data['name'] ) );
$product->set_regular_price( sanitize_text_field( (string) $data['regular_price'] ) );
$product->set_status( sanitize_text_field( $data['status'] ?? 'publish' ) );
$product->set_catalog_visibility( 'visible' );
if ( ! empty( $data['sku'] ) ) {
$product->set_sku( sanitize_text_field( $data['sku'] ) );
}
if ( isset( $data['sale_price'] ) && $data['sale_price'] !== '' ) {
$product->set_sale_price( sanitize_text_field( (string) $data['sale_price'] ) );
}
if ( ! empty( $data['description'] ) ) {
$product->set_description( wp_kses_post( $data['description'] ) );
}
if ( ! empty( $data['short_description'] ) ) {
$product->set_short_description( wp_kses_post( $data['short_description'] ) );
}
// Stock
$stock_status = sanitize_text_field( $data['stock_status'] ?? 'instock' );
if ( in_array( $stock_status, [ 'instock', 'outofstock', 'onbackorder' ], true ) ) {
$product->set_stock_status( $stock_status );
}
if ( isset( $data['stock_quantity'] ) && $data['stock_quantity'] !== '' ) {
$product->set_manage_stock( true );
$product->set_stock_quantity( absint( $data['stock_quantity'] ) );
}
// Weight & dimensions (WooCommerce native)
if ( ! empty( $data['wc_weight'] ) ) {
$product->set_weight( sanitize_text_field( $data['wc_weight'] ) );
}
// ─── Categories ───
$category_ids = [];
foreach ( (array) $data['categories'] as $cat ) {
$cat_id = is_numeric( $cat ) ? absint( $cat ) : 0;
if ( $cat_id && term_exists( $cat_id, 'product_cat' ) ) {
$category_ids[] = $cat_id;
}
}
if ( ! empty( $category_ids ) ) {
$product->set_category_ids( $category_ids );
}
// Save product first to get an ID for image attachment
$product_id = $product->save();
if ( ! $product_id ) {
return new WP_Error( 'create_failed', 'Failed to create product.', [ 'status' => 500 ] );
}
// ─── Main image ───
$image_errors = [];
if ( ! empty( $data['image_url'] ) ) {
$img_id = matreshka_sideload_image(
esc_url_raw( $data['image_url'] ),
$product_id,
sanitize_text_field( $data['name'] )
);
if ( is_wp_error( $img_id ) ) {
$image_errors[] = 'main image: ' . $img_id->get_error_message();
} else {
$product->set_image_id( $img_id );
}
}
// ─── Gallery images ───
$gallery_ids = [];
if ( ! empty( $data['gallery_image_urls'] ) && is_array( $data['gallery_image_urls'] ) ) {
foreach ( $data['gallery_image_urls'] as $idx => $gallery_url ) {
$gal_id = matreshka_sideload_image(
esc_url_raw( $gallery_url ),
$product_id,
sanitize_text_field( $data['name'] ) . ' — gallery ' . ( $idx + 1 )
);
if ( is_wp_error( $gal_id ) ) {
$image_errors[] = 'gallery image #' . ( $idx + 1 ) . ': ' . $gal_id->get_error_message();
} else {
$gallery_ids[] = $gal_id;
}
}
}
if ( ! empty( $gallery_ids ) ) {
$product->set_gallery_image_ids( $gallery_ids );
}
// Save again with images
$product->save();
// ─── ACF custom fields ───
$acf_fields = [
'proteins', 'fats', 'carbs', 'calories',
'weight', 'storage_life', 'storage', 'country',
'proteins_label', 'fats_label', 'carbs_label', 'calories_label',
];
foreach ( $acf_fields as $field ) {
if ( isset( $data[ $field ] ) && $data[ $field ] !== '' ) {
update_field( $field, sanitize_text_field( $data[ $field ] ), $product_id );
}
}
// ─── Build response ───
$response = [
'id' => $product_id,
'name' => $product->get_name(),
'slug' => $product->get_slug(),
'sku' => $product->get_sku(),
'status' => $product->get_status(),
'permalink' => get_permalink( $product_id ),
'regular_price' => $product->get_regular_price(),
'sale_price' => $product->get_sale_price(),
'stock_status' => $product->get_stock_status(),
'image' => wp_get_attachment_image_url( $product->get_image_id(), 'woocommerce_thumbnail' ) ?: '',
'gallery' => array_map( function ( $img_id ) {
return [
'id' => $img_id,
'thumbnail' => wp_get_attachment_image_url( $img_id, 'woocommerce_thumbnail' ) ?: '',
'full' => wp_get_attachment_image_url( $img_id, 'full' ) ?: '',
];
}, $gallery_ids ),
'categories' => array_map( function ( $cat_id ) {
$term = get_term( $cat_id, 'product_cat' );
return $term && ! is_wp_error( $term )
? [ 'id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug ]
: null;
}, $category_ids ),
'custom' => matreshka_get_product_custom_fields( $product_id, true ),
];
if ( ! empty( $image_errors ) ) {
$response['image_warnings'] = $image_errors;
}
return $response;
}
/* ──────────────────────────────────────────────────────────────
ENDPOINTS
────────────────────────────────────────────────────────────── */
/**
* POST /wc/v3/products/create
*
* Create a single product.
*/
function matreshka_api_create_product( WP_REST_Request $request ) {
$data = $request->get_json_params();
$result = matreshka_create_product_from_data( $data );
if ( is_wp_error( $result ) ) {
return new WP_REST_Response( [
'success' => false,
'code' => $result->get_error_code(),
'message' => $result->get_error_message(),
'errors' => $result->get_error_data()['errors'] ?? [],
], $result->get_error_data()['status'] ?? 400 );
}
return new WP_REST_Response( [
'success' => true,
'data' => $result,
], 201 );
}
/**
* POST /wc/v3/products/batch-create
*
* Create multiple products in a single request.
* Body: { "products": [ {...}, {...}, ... ] }
* Max 50 products per batch.
*/
function matreshka_api_batch_create_products( WP_REST_Request $request ) {
$body = $request->get_json_params();
$products = $body['products'] ?? [];
if ( empty( $products ) || ! is_array( $products ) ) {
return new WP_REST_Response( [
'success' => false,
'message' => 'products array is required.',
], 400 );
}
$max_batch = 50;
if ( count( $products ) > $max_batch ) {
return new WP_REST_Response( [
'success' => false,
'message' => "Maximum {$max_batch} products per batch request.",
], 400 );
}
$results = [
'created' => [],
'errors' => [],
];
foreach ( $products as $index => $product_data ) {
$result = matreshka_create_product_from_data( $product_data );
if ( is_wp_error( $result ) ) {
$results['errors'][] = [
'index' => $index,
'name' => $product_data['name'] ?? '(unnamed)',
'code' => $result->get_error_code(),
'message' => $result->get_error_message(),
];
} else {
$results['created'][] = $result;
}
}
$status = empty( $results['errors'] ) ? 201 : 207;
return new WP_REST_Response( [
'success' => empty( $results['errors'] ),
'total' => count( $products ),
'created_count' => count( $results['created'] ),
'error_count' => count( $results['errors'] ),
'created' => $results['created'],
'errors' => $results['errors'],
], $status );
}
/* ──────────────────────────────────────────────────────────────
UPDATE HELPERS
────────────────────────────────────────────────────────────── */
/**
* Build a standardized product response array.
*/
function matreshka_build_product_response( $product ) {
$id = $product->get_id();
$gallery_ids = $product->get_gallery_image_ids();
return [
'id' => $id,
'name' => $product->get_name(),
'slug' => $product->get_slug(),
'sku' => $product->get_sku(),
'status' => $product->get_status(),
'permalink' => get_permalink( $id ),
'regular_price' => $product->get_regular_price(),
'sale_price' => $product->get_sale_price(),
'date_on_sale_to' => $product->get_date_on_sale_to() ? $product->get_date_on_sale_to()->date( 'Y-m-d H:i:s' ) : '',
'stock_status' => $product->get_stock_status(),
'stock_quantity' => $product->get_stock_quantity(),
'image' => wp_get_attachment_image_url( $product->get_image_id(), 'woocommerce_thumbnail' ) ?: '',
'gallery' => array_map( function ( $img_id ) {
return [
'id' => $img_id,
'thumbnail' => wp_get_attachment_image_url( $img_id, 'woocommerce_thumbnail' ) ?: '',
'full' => wp_get_attachment_image_url( $img_id, 'full' ) ?: '',
];
}, $gallery_ids ),
'categories' => array_map( function ( $cat_id ) {
$term = get_term( $cat_id, 'product_cat' );
return $term && ! is_wp_error( $term )
? [ 'id' => $term->term_id, 'name' => $term->name, 'slug' => $term->slug ]
: null;
}, $product->get_category_ids() ),
'custom' => matreshka_get_product_custom_fields( $id, true ),
];
}
/**
* Update an existing WooCommerce product from data array.
* Only provided fields are updated (partial update / PATCH semantics).
*
* @param int $product_id Existing product ID.
* @param array $data Fields to update.
* @return array|WP_Error Product response or error.
*/
function matreshka_update_product_from_data( $product_id, $data ) {
$product = wc_get_product( $product_id );
if ( ! $product || ! $product instanceof WC_Product ) {
return new WP_Error( 'not_found', 'Product not found.', [ 'status' => 404 ] );
}
// ─── Core fields (only if provided) ───
if ( isset( $data['name'] ) && $data['name'] !== '' ) {
$product->set_name( sanitize_text_field( $data['name'] ) );
}
if ( isset( $data['regular_price'] ) && $data['regular_price'] !== '' ) {
$product->set_regular_price( sanitize_text_field( (string) $data['regular_price'] ) );
}
if ( array_key_exists( 'sale_price', $data ) ) {
if ( $data['sale_price'] === '' || $data['sale_price'] === null ) {
$product->set_sale_price( '' );
} else {
$product->set_sale_price( sanitize_text_field( (string) $data['sale_price'] ) );
}
}
if ( array_key_exists( 'date_on_sale_to', $data ) ) {
if ( empty( $data['date_on_sale_to'] ) ) {
$product->set_date_on_sale_to( '' );
} else {
$product->set_date_on_sale_to( sanitize_text_field( $data['date_on_sale_to'] ) );
}
}
if ( array_key_exists( 'date_on_sale_from', $data ) ) {
if ( empty( $data['date_on_sale_from'] ) ) {
$product->set_date_on_sale_from( '' );
} else {
$product->set_date_on_sale_from( sanitize_text_field( $data['date_on_sale_from'] ) );
}
}
if ( isset( $data['sku'] ) ) {
$product->set_sku( sanitize_text_field( $data['sku'] ) );
}
if ( isset( $data['status'] ) ) {
$product->set_status( sanitize_text_field( $data['status'] ) );
}
if ( isset( $data['description'] ) ) {
$product->set_description( wp_kses_post( $data['description'] ) );
}
if ( isset( $data['short_description'] ) ) {
$product->set_short_description( wp_kses_post( $data['short_description'] ) );
}
// Stock
if ( isset( $data['stock_status'] ) ) {
$stock_status = sanitize_text_field( $data['stock_status'] );
if ( in_array( $stock_status, [ 'instock', 'outofstock', 'onbackorder' ], true ) ) {
$product->set_stock_status( $stock_status );
}
}
if ( isset( $data['stock_quantity'] ) && $data['stock_quantity'] !== '' ) {
$product->set_manage_stock( true );
$product->set_stock_quantity( absint( $data['stock_quantity'] ) );
}
// WC weight
if ( isset( $data['wc_weight'] ) ) {
$product->set_weight( sanitize_text_field( $data['wc_weight'] ) );
}
// ─── Categories ───
if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) {
$category_ids = [];
foreach ( $data['categories'] as $cat ) {
$cat_id = is_numeric( $cat ) ? absint( $cat ) : 0;
if ( $cat_id && term_exists( $cat_id, 'product_cat' ) ) {
$category_ids[] = $cat_id;
}
}
if ( ! empty( $category_ids ) ) {
$product->set_category_ids( $category_ids );
}
}
// ─── Main image ───
$image_errors = [];
if ( ! empty( $data['image_url'] ) ) {
$img_id = matreshka_sideload_image(
esc_url_raw( $data['image_url'] ),
$product_id,
$product->get_name()
);
if ( is_wp_error( $img_id ) ) {
$image_errors[] = 'main image: ' . $img_id->get_error_message();
} else {
$product->set_image_id( $img_id );
}
}
// ─── Gallery images (replace all) ───
if ( isset( $data['gallery_image_urls'] ) && is_array( $data['gallery_image_urls'] ) ) {
$gallery_ids = [];
foreach ( $data['gallery_image_urls'] as $idx => $gallery_url ) {
$gal_id = matreshka_sideload_image(
esc_url_raw( $gallery_url ),
$product_id,
$product->get_name() . ' — gallery ' . ( $idx + 1 )
);
if ( is_wp_error( $gal_id ) ) {
$image_errors[] = 'gallery image #' . ( $idx + 1 ) . ': ' . $gal_id->get_error_message();
} else {
$gallery_ids[] = $gal_id;
}
}
$product->set_gallery_image_ids( $gallery_ids );
}
$product->save();
// ─── ACF custom fields ───
$acf_fields = [
'proteins', 'fats', 'carbs', 'calories',
'weight', 'storage_life', 'storage', 'country',
'proteins_label', 'fats_label', 'carbs_label', 'calories_label',
];
foreach ( $acf_fields as $field ) {
if ( isset( $data[ $field ] ) ) {
update_field( $field, sanitize_text_field( $data[ $field ] ), $product_id );
}
}
$response = matreshka_build_product_response( $product );
if ( ! empty( $image_errors ) ) {
$response['image_warnings'] = $image_errors;
}
return $response;
}
/* ──────────────────────────────────────────────────────────────
UPDATE ENDPOINTS
────────────────────────────────────────────────────────────── */
/**
* PUT /wc/v3/products/update/{id}
*
* Full update of a single product. Only provided fields are changed.
*/
function matreshka_api_update_product( WP_REST_Request $request ) {
$data = $request->get_json_params();
$product_id = matreshka_resolve_product_id( $request['id'] );
if ( ! $product_id ) {
return new WP_REST_Response( [
'success' => false,
'code' => 'not_found',
'message' => 'Product not found by ID or SKU: ' . sanitize_text_field( $request['id'] ),
], 404 );
}
$result = matreshka_update_product_from_data( $product_id, $data );
if ( is_wp_error( $result ) ) {
return new WP_REST_Response( [
'success' => false,
'code' => $result->get_error_code(),
'message' => $result->get_error_message(),
], $result->get_error_data()['status'] ?? 400 );
}
return new WP_REST_Response( [ 'success' => true, 'data' => $result ], 200 );
}
/**
* PUT /wc/v3/products/batch-update
*
* Batch update products.
* Body: { "products": [ { "id": 123, ...fields }, ... ] }
* Max 100 per batch.
*/
function matreshka_api_batch_update_products( WP_REST_Request $request ) {
$body = $request->get_json_params();
$products = $body['products'] ?? [];
if ( empty( $products ) || ! is_array( $products ) ) {
return new WP_REST_Response( [ 'success' => false, 'message' => 'products array is required.' ], 400 );
}
$max_batch = 100;
if ( count( $products ) > $max_batch ) {
return new WP_REST_Response( [ 'success' => false, 'message' => "Maximum {$max_batch} products per batch." ], 400 );
}
$results = [ 'updated' => [], 'errors' => [] ];
foreach ( $products as $index => $item ) {
$pid = matreshka_resolve_batch_product_id( $item );
if ( ! $pid ) {
$results['errors'][] = [ 'index' => $index, 'code' => 'missing_id', 'message' => 'id or sku is required.' ];
continue;
}
$result = matreshka_update_product_from_data( $pid, $item );
if ( is_wp_error( $result ) ) {
$results['errors'][] = [ 'index' => $index, 'id' => $pid, 'code' => $result->get_error_code(), 'message' => $result->get_error_message() ];
} else {
$results['updated'][] = $result;
}
}
return new WP_REST_Response( [
'success' => empty( $results['errors'] ),
'total' => count( $products ),
'updated_count' => count( $results['updated'] ),
'error_count' => count( $results['errors'] ),
'updated' => $results['updated'],
'errors' => $results['errors'],
], empty( $results['errors'] ) ? 200 : 207 );
}
/* ──────────────────────────────────────────────────────────────
QUICK UPDATE (price / sale / stock only)
────────────────────────────────────────────────────────────── */
/**
* Apply quick-update fields to a product.
* Accepts only: regular_price, sale_price, date_on_sale_to, date_on_sale_from, stock_status, stock_quantity.
*
* @return array|WP_Error Compact response.
*/
function matreshka_quick_update_product( $product_id, $data ) {
$product = wc_get_product( $product_id );
if ( ! $product || ! $product instanceof WC_Product ) {
return new WP_Error( 'not_found', 'Product not found.', [ 'status' => 404 ] );
}
$allowed = [ 'regular_price', 'sale_price', 'date_on_sale_to', 'date_on_sale_from', 'stock_status', 'stock_quantity' ];
$has_field = false;
foreach ( $allowed as $key ) {
if ( array_key_exists( $key, $data ) ) { $has_field = true; break; }
}
if ( ! $has_field ) {
return new WP_Error( 'no_fields', 'At least one field required: ' . implode( ', ', $allowed ) . '.', [ 'status' => 400 ] );
}
if ( isset( $data['regular_price'] ) && $data['regular_price'] !== '' ) {
$product->set_regular_price( sanitize_text_field( (string) $data['regular_price'] ) );
}
if ( array_key_exists( 'sale_price', $data ) ) {
$product->set_sale_price( ( $data['sale_price'] === '' || $data['sale_price'] === null ) ? '' : sanitize_text_field( (string) $data['sale_price'] ) );
}
if ( array_key_exists( 'date_on_sale_to', $data ) ) {
$product->set_date_on_sale_to( empty( $data['date_on_sale_to'] ) ? '' : sanitize_text_field( $data['date_on_sale_to'] ) );
}
if ( array_key_exists( 'date_on_sale_from', $data ) ) {
$product->set_date_on_sale_from( empty( $data['date_on_sale_from'] ) ? '' : sanitize_text_field( $data['date_on_sale_from'] ) );
}
if ( isset( $data['stock_status'] ) ) {
$ss = sanitize_text_field( $data['stock_status'] );
if ( in_array( $ss, [ 'instock', 'outofstock', 'onbackorder' ], true ) ) {
$product->set_stock_status( $ss );
}
}
if ( isset( $data['stock_quantity'] ) && $data['stock_quantity'] !== '' ) {
$product->set_manage_stock( true );
$product->set_stock_quantity( absint( $data['stock_quantity'] ) );
}
$product->save();
return [
'id' => $product->get_id(),
'name' => $product->get_name(),
'regular_price' => $product->get_regular_price(),
'sale_price' => $product->get_sale_price(),
'date_on_sale_to' => $product->get_date_on_sale_to() ? $product->get_date_on_sale_to()->date( 'Y-m-d H:i:s' ) : '',
'stock_status' => $product->get_stock_status(),
'stock_quantity' => $product->get_stock_quantity(),
];
}
/**
* PATCH /wc/v3/products/quick-update/{id}
*/
function matreshka_api_quick_update_product( WP_REST_Request $request ) {
$product_id = matreshka_resolve_product_id( $request['id'] );
if ( ! $product_id ) {
return new WP_REST_Response( [
'success' => false,
'code' => 'not_found',
'message' => 'Product not found by ID or SKU: ' . sanitize_text_field( $request['id'] ),
], 404 );
}
$result = matreshka_quick_update_product( $product_id, $request->get_json_params() );
if ( is_wp_error( $result ) ) {
return new WP_REST_Response( [
'success' => false, 'code' => $result->get_error_code(), 'message' => $result->get_error_message(),
], $result->get_error_data()['status'] ?? 400 );
}
return new WP_REST_Response( [ 'success' => true, 'data' => $result ], 200 );
}
/**
* PATCH /wc/v3/products/batch-quick-update
*
* Body: { "products": [ { "id": 123, "regular_price": "99.90", ... }, ... ] }
* Max 200 per batch.
*/
function matreshka_api_batch_quick_update_products( WP_REST_Request $request ) {
$body = $request->get_json_params();
$products = $body['products'] ?? [];
if ( empty( $products ) || ! is_array( $products ) ) {
return new WP_REST_Response( [ 'success' => false, 'message' => 'products array is required.' ], 400 );
}
$max_batch = 200;
if ( count( $products ) > $max_batch ) {
return new WP_REST_Response( [ 'success' => false, 'message' => "Maximum {$max_batch} products per batch." ], 400 );
}
$results = [ 'updated' => [], 'errors' => [] ];
foreach ( $products as $index => $item ) {
$pid = matreshka_resolve_batch_product_id( $item );
if ( ! $pid ) {
$results['errors'][] = [ 'index' => $index, 'code' => 'missing_id', 'message' => 'id or sku is required.' ];
continue;
}
$result = matreshka_quick_update_product( $pid, $item );
if ( is_wp_error( $result ) ) {
$results['errors'][] = [ 'index' => $index, 'id' => $pid, 'code' => $result->get_error_code(), 'message' => $result->get_error_message() ];
} else {
$results['updated'][] = $result;
}
}
return new WP_REST_Response( [
'success' => empty( $results['errors'] ),
'total' => count( $products ),
'updated_count' => count( $results['updated'] ),
'error_count' => count( $results['errors'] ),
'updated' => $results['updated'],
'errors' => $results['errors'],
], empty( $results['errors'] ) ? 200 : 207 );
}
/* ──────────────────────────────────────────────────────────────
DELETE ENDPOINTS
────────────────────────────────────────────────────────────── */
/**
* DELETE /wc/v3/products/delete/{id}
*
* Query param ?force=true to permanently delete (bypass trash).
*/
function matreshka_api_delete_product( WP_REST_Request $request ) {
$product_id = matreshka_resolve_product_id( $request['id'] );
if ( ! $product_id ) {
return new WP_REST_Response( [
'success' => false,
'message' => 'Product not found by ID or SKU: ' . sanitize_text_field( $request['id'] ),
], 404 );
}
$force = filter_var( $request->get_param( 'force' ), FILTER_VALIDATE_BOOLEAN );
$product = wc_get_product( $product_id );
if ( ! $product || ! $product instanceof WC_Product ) {
return new WP_REST_Response( [ 'success' => false, 'message' => 'Product not found.' ], 404 );
}
$name = $product->get_name();
if ( $force ) {
$product->delete( true );
$action = 'permanently_deleted';
} else {
$product->delete( false );
$action = 'trashed';
}
return new WP_REST_Response( [
'success' => true,
'data' => [ 'id' => $product_id, 'name' => $name, 'action' => $action ],
], 200 );
}
/**
* DELETE /wc/v3/products/batch-delete
*
* Body: { "ids": [123, 456, ...], "force": false }
* Max 100 per batch.
*/
function matreshka_api_batch_delete_products( WP_REST_Request $request ) {
$body = $request->get_json_params();
$ids = $body['ids'] ?? [];
$force = ! empty( $body['force'] );
if ( empty( $ids ) || ! is_array( $ids ) ) {
return new WP_REST_Response( [ 'success' => false, 'message' => 'ids array is required.' ], 400 );
}
$max_batch = 100;
if ( count( $ids ) > $max_batch ) {
return new WP_REST_Response( [ 'success' => false, 'message' => "Maximum {$max_batch} products per batch." ], 400 );
}
$results = [ 'deleted' => [], 'errors' => [] ];
foreach ( $ids as $index => $raw_id ) {
$pid = matreshka_resolve_product_id( $raw_id );
$product = $pid ? wc_get_product( $pid ) : null;
if ( ! $product || ! $product instanceof WC_Product ) {
$results['errors'][] = [ 'index' => $index, 'id' => $pid, 'message' => 'Product not found.' ];
continue;
}
$name = $product->get_name();
$action = $force ? 'permanently_deleted' : 'trashed';
$product->delete( $force );
$results['deleted'][] = [ 'id' => $pid, 'name' => $name, 'action' => $action ];
}
return new WP_REST_Response( [
'success' => empty( $results['errors'] ),
'total' => count( $ids ),
'deleted_count' => count( $results['deleted'] ),
'error_count' => count( $results['errors'] ),
'deleted' => $results['deleted'],
'errors' => $results['errors'],
], empty( $results['errors'] ) ? 200 : 207 );
}
/* ──────────────────────────────────────────────────────────────
GET ENDPOINTS
────────────────────────────────────────────────────────────── */
/**
* GET /wc/v3/products/get/{id}
*
* Return a single product by numeric ID or SKU.
*/
function matreshka_api_manage_get_product( WP_REST_Request $request ) {
$product_id = matreshka_resolve_product_id( $request['id'] );
if ( ! $product_id ) {
return new WP_REST_Response( [
'success' => false,
'message' => 'Product not found by ID or SKU: ' . sanitize_text_field( $request['id'] ),
], 404 );
}
$product = wc_get_product( $product_id );
if ( ! $product || ! $product instanceof WC_Product ) {
return new WP_REST_Response( [ 'success' => false, 'message' => 'Product not found.' ], 404 );
}
return new WP_REST_Response( [
'success' => true,
'data' => matreshka_build_product_response( $product ),
], 200 );
}
/**
* GET /wc/v3/products/list
*
* Return a paginated list of products.
*
* Query params:
* per_page — items per page (1–100, default 20)
* page — page number (default 1)
* category — filter by category ID
* status — filter by status (publish, draft, pending, trash)
* search — search by product name
* orderby — date, title, price, id (default date)
* order — asc, desc (default desc)
*/
function matreshka_api_manage_list_products( WP_REST_Request $request ) {
$per_page = absint( $request->get_param( 'per_page' ) ?: 20 );
$per_page = max( 1, min( 100, $per_page ) );
$page = max( 1, absint( $request->get_param( 'page' ) ?: 1 ) );
$args = [
'status' => 'publish',
'limit' => $per_page,
'page' => $page,
'paginate' => true,
];
// Category filter
$category = $request->get_param( 'category' );
if ( $category ) {
if ( is_numeric( $category ) ) {
$args['tax_query'] = [ [
'taxonomy' => 'product_cat',
'field' => 'term_id',
'terms' => absint( $category ),
] ];
} else {
$args['category'] = [ sanitize_text_field( $category ) ];
}
}
// Status filter
$status = $request->get_param( 'status' );
if ( $status && in_array( $status, [ 'publish', 'draft', 'pending', 'trash' ], true ) ) {
$args['status'] = $status;
}
// Search
$search = $request->get_param( 'search' );
if ( $search ) {
$args['s'] = sanitize_text_field( $search );
}
// Ordering
$orderby = $request->get_param( 'orderby' );
if ( $orderby && in_array( $orderby, [ 'date', 'title', 'price', 'id' ], true ) ) {
$args['orderby'] = $orderby;
}
$order = $request->get_param( 'order' );
if ( $order && in_array( strtolower( $order ), [ 'asc', 'desc' ], true ) ) {
$args['order'] = strtoupper( $order );
}
$query = new WC_Product_Query( $args );
$results = $query->get_products();
$products = [];
foreach ( $results->products as $product ) {
$products[] = matreshka_build_product_response( $product );
}
$total = (int) $results->total;
$total_pages = (int) $results->max_num_pages;
return new WP_REST_Response( [
'success' => true,
'data' => $products,
'total' => $total,
'total_pages' => $total_pages,
'page' => $page,
'per_page' => $per_page,
], 200 );
}