service = $service; $this->forecasts = $forecasts; $this->adjustments = $adjustments; } /** * {@inheritDoc} */ public function register_routes(): void { register_rest_route( $this->namespace, '/products', array( array( 'methods' => 'GET', 'callback' => array( $this, 'index' ), 'permission_callback' => array( $this, 'check_permission' ), 'args' => array( 'search' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'page' => array( 'type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint', ), 'per_page' => array( 'type' => 'integer', 'default' => 20, 'sanitize_callback' => 'absint', ), ), ), ) ); register_rest_route( $this->namespace, '/products/bulk-supplier', array( array( 'methods' => 'POST', 'callback' => array( $this, 'bulk_supplier' ), 'permission_callback' => array( $this, 'check_permission' ), ), ) ); register_rest_route( $this->namespace, '/products/search', array( array( 'methods' => 'GET', 'callback' => array( $this, 'search' ), 'permission_callback' => array( $this, 'check_permission' ), 'args' => array( 'term' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), ), ), ) ); register_rest_route( $this->namespace, '/products/(?P\d+)', array( 'args' => array( 'id' => array( 'type' => 'integer', 'sanitize_callback' => 'absint', ), ), array( 'methods' => 'GET', 'callback' => array( $this, 'show' ), 'permission_callback' => array( $this, 'check_permission' ), ), ) ); register_rest_route( $this->namespace, '/products/(?P\d+)/enable-stock', array( 'args' => array( 'id' => array( 'type' => 'integer', 'sanitize_callback' => 'absint', ), ), array( 'methods' => 'POST', 'callback' => array( $this, 'enable_stock' ), 'permission_callback' => array( $this, 'check_permission' ), ), ) ); register_rest_route( $this->namespace, '/products/(?P\d+)/adjust-stock', array( 'args' => array( 'id' => array( 'type' => 'integer', 'sanitize_callback' => 'absint', ), ), array( 'methods' => 'POST', 'callback' => array( $this, 'adjust_stock' ), 'permission_callback' => array( $this, 'check_permission' ), ), ) ); register_rest_route( $this->namespace, '/products/(?P\d+)/stock-mode', array( 'args' => array( 'id' => array( 'type' => 'integer', 'sanitize_callback' => 'absint', ), ), array( 'methods' => 'POST', 'callback' => array( $this, 'stock_mode' ), 'permission_callback' => array( $this, 'check_permission' ), ), ) ); register_rest_route( $this->namespace, '/products/(?P\d+)/stock-history', array( 'args' => array( 'id' => array( 'type' => 'integer', 'sanitize_callback' => 'absint', ), ), array( 'methods' => 'GET', 'callback' => array( $this, 'stock_history' ), 'permission_callback' => array( $this, 'check_permission' ), ), ) ); register_rest_route( $this->namespace, '/products/(?P\d+)/disable-stock', array( 'args' => array( 'id' => array( 'type' => 'integer', 'sanitize_callback' => 'absint', ), ), array( 'methods' => 'POST', 'callback' => array( $this, 'disable_stock' ), 'permission_callback' => array( $this, 'check_permission' ), ), ) ); register_rest_route( $this->namespace, '/products/(?P\d+)/purchasing', array( 'args' => array( 'id' => array( 'type' => 'integer', 'sanitize_callback' => 'absint', ), ), array( 'methods' => 'GET', 'callback' => array( $this, 'get_unit' ), 'permission_callback' => array( $this, 'check_permission' ), ), array( 'methods' => 'PUT, PATCH', 'callback' => array( $this, 'update_unit' ), 'permission_callback' => array( $this, 'check_permission' ), ), ) ); } /** * GET /products * * @param WP_REST_Request $request Request. */ public function index( WP_REST_Request $request ): WP_REST_Response { $result = $this->service->list_products( array( 'search' => (string) $request->get_param( 'search' ), 'page' => $this->int_param( $request, 'page', 1 ), 'per_page' => $this->int_param( $request, 'per_page', 20 ), ) ); $response = new WP_REST_Response( $result['items'], 200 ); $response->header( 'X-WP-Total', (string) $result['total'] ); $response->header( 'X-WP-TotalPages', (string) $result['pages'] ); return $response; } /** * POST /products/bulk-supplier * * @param WP_REST_Request $request Request. * @return WP_REST_Response|\WP_Error */ public function bulk_supplier( WP_REST_Request $request ) { $body = (array) $request->get_json_params(); $product_ids = array_map( 'intval', (array) ( $body['product_ids'] ?? array() ) ); $supplier_id = (int) ( $body['supplier_id'] ?? 0 ); $cost = array_key_exists( 'purchase_cost', $body ) && '' !== $body['purchase_cost'] && null !== $body['purchase_cost'] ? (float) $body['purchase_cost'] : null; if ( empty( $product_ids ) || $supplier_id <= 0 ) { return new \WP_Error( 'profit_engine_bulk_invalid', __( 'Select products and a supplier.', 'profit-engine' ), array( 'status' => 400 ) ); } return new WP_REST_Response( $this->service->bulk_assign_supplier( $product_ids, $supplier_id, $cost ), 200 ); } /** * GET /products/search * * @param WP_REST_Request $request Request. */ public function search( WP_REST_Request $request ): WP_REST_Response { $units = $this->service->search_units( (string) $request->get_param( 'term' ), 20 ); return new WP_REST_Response( $units, 200 ); } /** * GET /products/{id} * * @param WP_REST_Request $request Request. * @return WP_REST_Response|\WP_Error */ public function show( WP_REST_Request $request ) { $result = $this->service->get_product( (int) $request['id'] ); if ( is_wp_error( $result ) ) { return $result; } return new WP_REST_Response( $result, 200 ); } /** * POST /products/{id}/enable-stock * * @param WP_REST_Request $request Request. * @return WP_REST_Response|\WP_Error */ public function enable_stock( WP_REST_Request $request ) { $body = (array) $request->get_json_params(); $qty = array_key_exists( 'stock', $body ) && '' !== $body['stock'] && null !== $body['stock'] ? max( 0.0, (float) $body['stock'] ) : null; // Variable products: { "stocks": { "": qty, ... } }, one quantity each. $per_unit = array(); foreach ( (array) ( $body['stocks'] ?? array() ) as $unit_id => $unit_qty ) { if ( (int) $unit_id > 0 && is_numeric( $unit_qty ) ) { $per_unit[ (int) $unit_id ] = max( 0.0, (float) $unit_qty ); } } $result = $this->service->enable_stock_tracking( (int) $request['id'], $qty, $per_unit ); return is_wp_error( $result ) ? $result : new WP_REST_Response( $result, 200 ); } /** * POST /products/{id}/adjust-stock * * Body: { "counted": { "": qty }, "reason": "count", "note": "" }. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|\WP_Error */ public function adjust_stock( WP_REST_Request $request ) { if ( ! $this->adjustments ) { return new \WP_Error( 'profit_engine_unavailable', __( 'Stock adjustments are not available.', 'profit-engine' ), array( 'status' => 500 ) ); } $body = (array) $request->get_json_params(); $counted = array(); foreach ( (array) ( $body['counted'] ?? array() ) as $id => $qty ) { $counted[ (int) $id ] = $qty; } $product_id = (int) $request['id']; $result = $this->adjustments->adjust( $product_id, $counted, sanitize_key( (string) ( $body['reason'] ?? '' ) ), sanitize_text_field( (string) ( $body['note'] ?? '' ) ) ); if ( is_wp_error( $result ) ) { return $result; } if ( $result['changed'] ) { $this->recompute_forecasts( $product_id ); } return new WP_REST_Response( $result, 200 ); } /** * POST /products/{id}/stock-mode * * Body: { "mode": "shared", "stock": qty } or { "mode": "variation", "stocks": { id: qty } }. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|\WP_Error */ public function stock_mode( WP_REST_Request $request ) { if ( ! $this->adjustments ) { return new \WP_Error( 'profit_engine_unavailable', __( 'Stock adjustments are not available.', 'profit-engine' ), array( 'status' => 500 ) ); } $body = (array) $request->get_json_params(); $stocks = array(); foreach ( (array) ( $body['stocks'] ?? array() ) as $id => $qty ) { if ( (int) $id > 0 && is_numeric( $qty ) ) { $stocks[ (int) $id ] = (float) $qty; } } $product_id = (int) $request['id']; $result = $this->adjustments->set_mode( $product_id, sanitize_key( (string) ( $body['mode'] ?? '' ) ), isset( $body['stock'] ) && is_numeric( $body['stock'] ) ? (float) $body['stock'] : null, $stocks ); if ( is_wp_error( $result ) ) { return $result; } $this->recompute_forecasts( $product_id ); return new WP_REST_Response( $result, 200 ); } /** * GET /products/{id}/stock-history — recent stock movements. * * @param WP_REST_Request $request Request. * @return WP_REST_Response */ public function stock_history( WP_REST_Request $request ): WP_REST_Response { $rows = $this->adjustments ? $this->adjustments->history( (int) $request['id'], 10 ) : array(); return new WP_REST_Response( $rows, 200 ); } /** * Recompute forecasts for every unit of a product, so stock changes show * in the reorder list right away. * * @param int $product_id Product ID. */ private function recompute_forecasts( int $product_id ): void { if ( ! $this->forecasts ) { return; } $detail = $this->service->get_product( $product_id ); foreach ( is_array( $detail ) ? $detail['units'] : array() as $unit ) { $this->forecasts->recompute_one( (int) $unit['id'] ); } } /** * POST /products/{id}/disable-stock * * Also recomputes the product's forecasts right away, so it leaves the * reorder list now rather than after the nightly recompute. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|\WP_Error */ public function disable_stock( WP_REST_Request $request ) { $product_id = (int) $request['id']; $result = $this->service->disable_stock_tracking( $product_id ); if ( is_wp_error( $result ) ) { return $result; } $this->recompute_forecasts( $product_id ); return new WP_REST_Response( $result, 200 ); } /** * GET /products/{id}/purchasing * * @param WP_REST_Request $request Request. * @return WP_REST_Response|\WP_Error */ public function get_unit( WP_REST_Request $request ) { $result = $this->service->get_unit( (int) $request['id'] ); if ( is_wp_error( $result ) ) { return $result; } return new WP_REST_Response( $result, 200 ); } /** * PUT /products/{id}/purchasing * * @param WP_REST_Request $request Request. * @return WP_REST_Response|\WP_Error */ public function update_unit( WP_REST_Request $request ) { $result = $this->service->update_unit( (int) $request['id'], (array) $request->get_json_params() ); if ( is_wp_error( $result ) ) { return $result; } return new WP_REST_Response( $result, 200 ); } }