namespace, '/settings', array( array( 'methods' => 'GET', 'callback' => array( $this, 'get_settings' ), 'permission_callback' => array( $this, 'check_permission' ), ), array( 'methods' => 'PUT', 'callback' => array( $this, 'update_settings' ), 'permission_callback' => array( $this, 'check_permission' ), 'args' => $this->schema(), ), ) ); } /** * Default settings shape. * * @return array */ private function defaults(): array { return array( 'cost_basis' => 'average', 'tax_in_profit' => false, 'forecast_window' => 30, 'coverage_days' => 30, 'safety_days' => 7, 'default_lead_time' => 7, 'payment_fee_percent' => 0.0, 'payment_fee_fixed' => 0.0, 'shipping_alloc' => 'value', 'other_cost_percent' => 0.0, 'other_cost_fixed' => 0.0, 'revenue_statuses' => array( 'processing', 'completed' ), 'po_logo' => '', 'delete_data_on_uninstall' => false, ); } /** * Return the current settings merged over defaults. */ public function get_settings(): WP_REST_Response { $saved = get_option( self::OPTION, array() ); $saved = is_array( $saved ) ? $saved : array(); return new WP_REST_Response( array_merge( $this->defaults(), $saved ), 200 ); } /** * Persist submitted settings. * * @param WP_REST_Request $request Request. */ public function update_settings( WP_REST_Request $request ): WP_REST_Response { $current = array_merge( $this->defaults(), (array) get_option( self::OPTION, array() ) ); $incoming = array( 'cost_basis' => $this->sanitize_enum( $request->get_param( 'cost_basis' ), array( 'average', 'last' ), $current['cost_basis'] ), 'tax_in_profit' => null === $request->get_param( 'tax_in_profit' ) ? $current['tax_in_profit'] : (bool) $request->get_param( 'tax_in_profit' ), 'forecast_window' => $this->clamp_int( $request->get_param( 'forecast_window' ), 1, 365, $current['forecast_window'] ), 'coverage_days' => $this->clamp_int( $request->get_param( 'coverage_days' ), 0, 365, $current['coverage_days'] ), 'safety_days' => $this->clamp_int( $request->get_param( 'safety_days' ), 0, 180, $current['safety_days'] ), 'default_lead_time' => $this->clamp_int( $request->get_param( 'default_lead_time' ), 0, 365, $current['default_lead_time'] ), 'payment_fee_percent' => $this->clamp_float( $request->get_param( 'payment_fee_percent' ), 0, 100, (float) $current['payment_fee_percent'] ), 'payment_fee_fixed' => $this->clamp_float( $request->get_param( 'payment_fee_fixed' ), 0, PHP_INT_MAX, (float) $current['payment_fee_fixed'] ), 'shipping_alloc' => $this->sanitize_enum( $request->get_param( 'shipping_alloc' ), array( 'value', 'none' ), $current['shipping_alloc'] ), 'other_cost_percent' => $this->clamp_float( $request->get_param( 'other_cost_percent' ), 0, 100, (float) $current['other_cost_percent'] ), 'other_cost_fixed' => $this->clamp_float( $request->get_param( 'other_cost_fixed' ), 0, PHP_INT_MAX, (float) $current['other_cost_fixed'] ), 'revenue_statuses' => $this->sanitize_statuses( $request->get_param( 'revenue_statuses' ), (array) $current['revenue_statuses'] ), 'po_logo' => null === $request->get_param( 'po_logo' ) ? (string) $current['po_logo'] : esc_url_raw( (string) $request->get_param( 'po_logo' ) ), 'delete_data_on_uninstall' => null === $request->get_param( 'delete_data_on_uninstall' ) ? (bool) $current['delete_data_on_uninstall'] : (bool) $request->get_param( 'delete_data_on_uninstall' ), ); update_option( self::OPTION, $incoming, false ); return new WP_REST_Response( $incoming, 200 ); } /** * Argument schema for validation hints. * * @return array> */ private function schema(): array { return array( 'cost_basis' => array( 'type' => 'string' ), 'tax_in_profit' => array( 'type' => 'boolean' ), 'forecast_window' => array( 'type' => 'integer' ), 'coverage_days' => array( 'type' => 'integer' ), 'safety_days' => array( 'type' => 'integer' ), 'default_lead_time' => array( 'type' => 'integer' ), 'payment_fee_percent' => array( 'type' => 'number' ), 'payment_fee_fixed' => array( 'type' => 'number' ), 'shipping_alloc' => array( 'type' => 'string' ), 'other_cost_percent' => array( 'type' => 'number' ), 'other_cost_fixed' => array( 'type' => 'number' ), 'revenue_statuses' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ), ), 'po_logo' => array( 'type' => 'string' ), 'delete_data_on_uninstall' => array( 'type' => 'boolean' ), ); } /** * Sanitise a value against an allow-list. * * @param mixed $value Raw value. * @param string[] $allowed Allowed values. * @param string $fallback Fallback when invalid. */ private function sanitize_enum( $value, array $allowed, string $fallback ): string { $value = is_string( $value ) ? sanitize_key( $value ) : ''; return in_array( $value, $allowed, true ) ? $value : $fallback; } /** * Sanitise a submitted list of order-status slugs. * * Only slugs WooCommerce actually registers are kept; an empty or invalid * result falls back to the previous value so the option can never end up with * zero counted statuses (which would blank every revenue figure). * * @param mixed $value Raw value (expected array of slugs). * @param string[] $fallback Fallback when invalid/empty. * @return string[] */ private function sanitize_statuses( $value, array $fallback ): array { if ( ! is_array( $value ) ) { return $fallback; } $valid = function_exists( 'wc_get_order_statuses' ) ? array_map( static fn( $s ) => (string) preg_replace( '/^wc-/', '', (string) $s ), array_keys( wc_get_order_statuses() ) ) : array( 'pending', 'processing', 'on-hold', 'completed', 'refunded', 'cancelled', 'failed' ); $clean = array_values( array_intersect( $valid, array_unique( array_map( 'sanitize_key', $value ) ) ) ); return empty( $clean ) ? $fallback : $clean; } /** * Clamp an integer within bounds. * * @param mixed $value Raw value. * @param int $min Minimum. * @param int $max Maximum. * @param int $fallback Fallback when null. */ private function clamp_int( $value, int $min, int $max, int $fallback ): int { if ( null === $value || '' === $value ) { return $fallback; } return max( $min, min( $max, (int) $value ) ); } /** * Clamp a float within bounds. * * @param mixed $value Raw value. * @param float $min Minimum. * @param float $max Maximum. * @param float $fallback Fallback when null. */ private function clamp_float( $value, float $min, float $max, float $fallback ): float { if ( null === $value || '' === $value ) { return $fallback; } return max( $min, min( $max, (float) $value ) ); } }