'GET', 'callback' => array( $this, 'get_state' ), 'permission_callback' => $permission, ) ); register_rest_route( self::API_NAMESPACE, '/materials', array( 'methods' => 'POST', 'callback' => array( $this, 'create_material' ), 'permission_callback' => $permission, ) ); register_rest_route( self::API_NAMESPACE, '/materials/(?P[A-Za-z0-9-]+)', array( array( 'methods' => 'PUT', 'callback' => array( $this, 'update_material' ), 'permission_callback' => $permission, ), array( 'methods' => 'DELETE', 'callback' => array( $this, 'delete_material' ), 'permission_callback' => $permission, ), ) ); register_rest_route( self::API_NAMESPACE, '/recipes/(?P\d+)', array( array( 'methods' => 'PUT', 'callback' => array( $this, 'save_recipe' ), 'permission_callback' => $permission, ), array( 'methods' => 'DELETE', 'callback' => array( $this, 'delete_recipe' ), 'permission_callback' => $permission, ), ) ); register_rest_route( self::API_NAMESPACE, '/settings', array( 'methods' => 'PUT', 'callback' => array( $this, 'save_settings' ), 'permission_callback' => $permission, ) ); register_rest_route( self::API_NAMESPACE, '/products', array( 'methods' => 'GET', 'callback' => array( $this, 'search_products' ), 'permission_callback' => $permission, 'args' => array( 'q' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ), ), ) ); register_rest_route( self::API_NAMESPACE, '/products/(?P\d+)', array( 'methods' => 'GET', 'callback' => array( $this, 'get_product' ), 'permission_callback' => $permission, ) ); register_rest_route( self::API_NAMESPACE, '/write', array( 'methods' => 'POST', 'callback' => array( $this, 'write_costs' ), 'permission_callback' => $permission, ) ); } /** * Turn a Validator/store result into a response. * * @param array $result Result array. * @return \WP_REST_Response */ private function respond( array $result ): \WP_REST_Response { if ( $result['ok'] ) { return new \WP_REST_Response( $result['value'], 200 ); } return new \WP_REST_Response( array( 'code' => 'invalid', 'errors' => $result['errors'], ), $result['status'] ?? 422 ); } /** * Everything the page needs on load. * * @return \WP_REST_Response */ public function get_state(): \WP_REST_Response { $materials = new Materials(); $recipes = ( new Recipes() )->all(); $usage = Recipes::usage( $recipes ); $catalog = new Catalog(); $material_list = array(); foreach ( $materials->all() as $material ) { $material['usedInRecipes'] = $usage[ $material['id'] ] ?? 0; $material_list[] = $material; } $products = array(); $listed = array(); foreach ( $recipes as $product_id => $recipe ) { $description = $catalog->describe( (int) $product_id ); if ( null === $description ) { continue; // In the trash or no longer a supported product: its recipe waits. } $products[ $product_id ] = $description; $listed[] = $recipe; } $state = array( 'settings' => ( new Settings() )->get(), 'materials' => $material_list, 'recipes' => $listed, 'products' => (object) $products, 'cogs' => array( 'supported' => Cogs::supported(), 'enabled' => Cogs::enabled(), 'settingsUrl' => Cogs::settings_url(), ), // Per product, variant id => coefficient. Empty here: an add-on fills it in. 'factors' => (object) array(), ); /** * Lets an add-on add to the state the page loads (for example per-variation coefficients under 'factors'). * * @param array $state The state. */ $state = apply_filters( 'madecost_state', $state ); return new \WP_REST_Response( $state, 200 ); } /** * Create a material. * * @param \WP_REST_Request $request Request. */ public function create_material( \WP_REST_Request $request ): \WP_REST_Response { $result = ( new Materials() )->save( $request->get_json_params() ); if ( $result['ok'] ) { $result['value']['usedInRecipes'] = 0; } return $this->respond( $result ); } /** * Update a material. * * @param \WP_REST_Request $request Request. */ public function update_material( \WP_REST_Request $request ): \WP_REST_Response { $id = (string) $request['id']; $result = ( new Materials() )->save( $request->get_json_params(), $id ); if ( $result['ok'] ) { $usage = Recipes::usage( ( new Recipes() )->all() ); $result['value']['usedInRecipes'] = $usage[ $id ] ?? 0; } return $this->respond( $result ); } /** * Delete a material that is not used. * * @param \WP_REST_Request $request Request. */ public function delete_material( \WP_REST_Request $request ): \WP_REST_Response { $id = (string) $request['id']; $usage = Recipes::usage( ( new Recipes() )->all() ); $result = ( new Materials() )->delete( $id, $usage[ $id ] ?? 0 ); if ( $result['ok'] ) { return new \WP_REST_Response( array( 'deleted' => true ), 200 ); } if ( 'in_use' === $result['reason'] ) { return new \WP_REST_Response( array( 'code' => 'in_use', 'recipes' => $result['recipes'], 'errors' => array( 'form' => __( 'This material is used in a recipe. Remove it from the recipes first.', 'madecost' ) ), ), 409 ); } return new \WP_REST_Response( array( 'code' => 'not_found' ), 404 ); } /** * Save a recipe. The response carries the product description so the page can show it at once. * * @param \WP_REST_Request $request Request. */ public function save_recipe( \WP_REST_Request $request ): \WP_REST_Response { $product_id = (int) $request['product_id']; $result = ( new Recipes() )->save( $product_id, $request->get_json_params() ); if ( ! $result['ok'] ) { return $this->respond( $result ); } return new \WP_REST_Response( array( 'recipe' => $result['value'], 'product' => ( new Catalog() )->describe( $product_id ), ), 200 ); } /** * Delete a recipe. * * @param \WP_REST_Request $request Request. */ public function delete_recipe( \WP_REST_Request $request ): \WP_REST_Response { $product_id = (int) $request['product_id']; if ( ! current_user_can( 'edit_post', $product_id ) ) { return new \WP_REST_Response( array( 'code' => 'forbidden' ), 403 ); } $deleted = ( new Recipes() )->delete( $product_id ); return new \WP_REST_Response( array( 'deleted' => $deleted ), $deleted ? 200 : 404 ); } /** * Save the settings. * * @param \WP_REST_Request $request Request. */ public function save_settings( \WP_REST_Request $request ): \WP_REST_Response { return $this->respond( ( new Settings() )->save( $request->get_json_params() ) ); } /** * Search products for the "add a recipe" picker. * * @param \WP_REST_Request $request Request. */ public function search_products( \WP_REST_Request $request ): \WP_REST_Response { return new \WP_REST_Response( ( new Catalog() )->search( (string) $request['q'] ), 200 ); } /** * Describe one product (variants, prices, current costs). * * @param \WP_REST_Request $request Request. */ public function get_product( \WP_REST_Request $request ): \WP_REST_Response { $description = ( new Catalog() )->describe( (int) $request['product_id'] ); if ( null === $description ) { return new \WP_REST_Response( array( 'code' => 'not_found' ), 404 ); } return new \WP_REST_Response( $description, 200 ); } /** * Write costs to WooCommerce. * * @param \WP_REST_Request $request Request. */ public function write_costs( \WP_REST_Request $request ): \WP_REST_Response { $body = $request->get_json_params(); $items = is_array( $body ) && isset( $body['items'] ) && is_array( $body['items'] ) ? $body['items'] : null; if ( null === $items || count( $items ) > 5000 ) { return new \WP_REST_Response( array( 'code' => 'invalid', 'errors' => array( 'form' => __( 'Nothing to write, or too many items at once.', 'madecost' ) ), ), 422 ); } $results = ( new Cogs() )->write( $items ); return new \WP_REST_Response( array( 'results' => $results ), 200 ); } }