'orderworth'], $args)), admin_url('admin.php')); } public static function request_integer($value): int { if (!is_string($value) || !preg_match('/^(0|[1-9][0-9]*)$/D', $value) || strlen($value) > strlen((string) PHP_INT_MAX) || (strlen($value) === strlen((string) PHP_INT_MAX) && strcmp($value, (string) PHP_INT_MAX) > 0)) { return -1; } return (int) $value; } public static function authorize(): void { if (!current_user_can('manage_woocommerce')) { wp_die(esc_html__('You do not have permission to view store costs.', 'orderworth'), '', ['response'=>403]); } } /** Reject non-string nonce fields before WordPress attempts string conversion. */ public static function validate_nonce_input(): void { foreach (['nonce', '_wpnonce', '_ajax_nonce', 'owwc_forget_nonce'] as $field) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This validates nonce shape before core verifies its value; it never authorizes a request. if (isset($_REQUEST[$field]) && !is_string($_REQUEST[$field])) { wp_die(esc_html__('Invalid security token. Reload the form and try again.', 'orderworth'), '', ['response' => 403]); } } } public static function guard(string $action, bool $ajax = false): void { self::authorize(); if ($ajax) { OWWC_Admin::validate_nonce_input(); check_ajax_referer('owwc_reports', 'nonce'); } else { OWWC_Admin::validate_nonce_input(); check_admin_referer($action); } } public static function assets(string $hook): void { if ($hook !== 'toplevel_page_orderworth') { return; } wp_enqueue_style('owwc-admin', plugins_url('assets/admin.css', OWWC_FILE), [], OWWC_VERSION); wp_enqueue_script('owwc-report-core', plugins_url('assets/report-core.js', OWWC_FILE), [], OWWC_VERSION, true); wp_enqueue_script('owwc-admin', plugins_url('assets/admin.js', OWWC_FILE), ['owwc-report-core', 'wp-i18n'], OWWC_VERSION, true); wp_set_script_translations('owwc-admin', 'orderworth', dirname(OWWC_FILE) . '/languages'); wp_enqueue_script('owwc-setup', plugins_url('assets/setup.js', OWWC_FILE), ['owwc-admin', 'wp-i18n'], OWWC_VERSION, true); wp_set_script_translations('owwc-setup', 'orderworth', dirname(OWWC_FILE) . '/languages'); wp_localize_script('owwc-admin', 'OWWC', [ 'ajax'=>admin_url('admin-ajax.php'), 'nonce'=>wp_create_nonce('owwc_reports'), 'review'=>self::url(['view'=>'order']), 'currency'=>OWWC_Reports::unit()['currency'], 'decimals'=>OWWC_Reports::unit()['decimals'], 'messages'=>OWWC_Settings::messages(), 'pro'=>(bool) apply_filters('owwc_pro_enabled', false), ]); } /** Keep the published Pro pair usable while Free is replaced first. */ public static function legacy_pro_assets(string $hook): void { if ($hook !== 'toplevel_page_orderworth' || !current_user_can('manage_woocommerce') || !defined('OWWC_PRO_VERSION') || !in_array(OWWC_PRO_VERSION, ['1.10.4', '1.10.5'], true) || !is_callable(['OWWC_Pro_Access', 'with_access'])) { return; } try { OWWC_Pro_Access::with_access(static function () { // Only these known releases' asset entry points expect the former screen ID. // Do not replay admin hooks or bypass the ordinary licence/capability checks. foreach (['OWWC_Pro_Stripe_Fees', 'OWWC_Pro_Actual_Expenses', 'OWWC_Pro_Returns', 'OWWC_Pro_Fee_Review', 'OWWC_Pro_Cost_Import', 'OWWC_Pro_Cost_Ledger', 'OWWC_Pro_Report_Profiles', 'OWWC_Pro_Workflows', 'OWWC_Pro_Reporting', 'OWWC_Pro_Background_Reports', 'OWWC_Pro_Report_Exports', 'OWWC_Pro_Automations', 'OWWC_Pro_Commercial_Workflows', 'OWWC_Pro_ShipStation_Import'] as $class) { if (is_callable([$class, 'assets'])) { $class::assets('woocommerce_page_orderworth'); } } }); } catch (OWWC_Pro_Access_Exception $error) { // Locked Pro keeps its usual Free-only behavior. return; } } /** Apply the shared workspace presentation after the edition-specific styles. */ public static function workspace_assets(string $hook): void { if ($hook !== 'toplevel_page_orderworth' || !current_user_can('manage_woocommerce')) { return; } $dependencies = ['owwc-admin']; foreach (wp_styles()->queue as $handle) { if (is_string($handle) && strpos($handle, 'owwc-') === 0 && $handle !== 'owwc-workspace') { $dependencies[] = $handle; } } wp_enqueue_style('owwc-workspace', plugins_url('assets/workspace.css', OWWC_FILE), array_values(array_unique($dependencies)), OWWC_VERSION); wp_enqueue_script('owwc-workspace', plugins_url('assets/workspace.js', OWWC_FILE), [], OWWC_VERSION, true); } private static function flash(string $message, bool $error = false): void { set_transient('owwc_notice_' . get_current_user_id(), ['message'=>$message,'error'=>$error], 60); } public static function save_settings(): void { self::authorize(); OWWC_Admin::validate_nonce_input(); check_admin_referer('owwc_settings'); try { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- OWWC_Settings::sanitize strictly validates each numeric field before saving; stripping text could change its value. $input = isset($_POST['owwc']) && is_array($_POST['owwc']) ? wp_unslash($_POST['owwc']) : []; if (array_key_exists('confirmed', $input) && $input['confirmed'] !== '1') { wp_die(esc_html__('Invalid confirmation value. Your assumptions were not changed.', 'orderworth'), '', ['response'=>400]); } update_option('owwc_settings', OWWC_Settings::sanitize($input), false); self::flash(__('Assumptions saved. Existing order snapshots are not rewritten.', 'orderworth')); } catch (InvalidArgumentException $e) { self::flash(OWWC_Settings::translate_error($e->getMessage()), true); } wp_safe_redirect(self::url(['view'=>'settings'])); exit; } public static function save_review(): void { self::authorize(); OWWC_Admin::validate_nonce_input(); check_admin_referer('owwc_review'); if (array_key_exists('reviewed', $_POST) && $_POST['reviewed'] !== '1') { wp_die(esc_html__('Invalid refund-review confirmation. Your saved costs were not changed.', 'orderworth'), '', ['response'=>400]); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- request_integer rejects non-string, non-canonical, negative and overflowing values before conversion; -1 is rejected by the caller. $id = OWWC_Admin::request_integer(wp_unslash($_POST['order_id'] ?? '0')); if ($id < 1) { wp_die(esc_html__('Invalid order identifier.', 'orderworth'), '', ['response'=>400]); } $order = wc_get_order($id); if (!$order || $order->get_type() !== 'shop_order') { wp_die(esc_html__('Order unavailable.', 'orderworth'), '', ['response'=>404]); } try { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- review_form_input checks the complete form shape; store_review validates every supplied numeric cost before any write. $cost = isset($_POST['cost']) && is_array($_POST['cost']) ? wp_unslash($_POST['cost']) : null; $input = self::review_form_input($order, ['cost'=>$cost, 'owwc_review_complete'=>isset($_POST['owwc_review_complete']) && $_POST['owwc_review_complete'] === '1' ? '1' : null]); self::store_review($order, $input, isset($_POST['reviewed']) && $_POST['reviewed'] === '1', isset($_POST['signature']) && is_string($_POST['signature']) ? sanitize_text_field(wp_unslash($_POST['signature'])) : ''); self::flash(__('Review saved. Return to the attention list and run a fresh scan for updated totals.', 'orderworth')); } catch (InvalidArgumentException $e) { self::flash(OWWC_Settings::translate_error($e->getMessage()), true); } wp_safe_redirect(self::url(['view'=>'order','order_id'=>$id])); exit; } /** Require a complete browser form before treating blank costs as removal. */ public static function review_form_input($order, array $request): array { $input = $request['cost'] ?? null; $fields = ['cogs','payment_cost','shipping_cost','packaging_cost']; if ((float) $order->get_total_refunded() > 0) { $fields[] = 'recovered_cogs'; } $complete = ($request['owwc_review_complete'] ?? null) === '1' && is_array($input); foreach ($fields as $field) { if (!is_array($input) || !array_key_exists($field, $input) || !is_string($input[$field])) { $complete = false; } } if (!$complete) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- The admin handler escapes this plain-text error for HTML or sends JSON consumed by textContent. throw new InvalidArgumentException(__('The review form was incomplete. Reload it and try again. Your saved costs were not changed.', 'orderworth')); } return $input; } public static function store_review($order, array $input, bool $reviewed, string $signature): void { if (!$order instanceof WC_Order || $order->get_type() !== 'shop_order') { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Plain-text validation error is caught by the admin handler and escaped for HTML or sent as JSON to textContent; escaping here would corrupt those messages. throw new InvalidArgumentException(__('Order unavailable.', 'orderworth')); } if (!current_user_can('manage_woocommerce') || !current_user_can('edit_shop_order', $order->get_id())) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Plain-text validation error is caught by the admin handler and escaped for HTML or sent as JSON to textContent; escaping here would corrupt those messages. throw new InvalidArgumentException(__('You do not have permission to edit this order.', 'orderworth')); } $key = 'owwc_review_lock_' . $order->get_id(); $lock = OWWC_Lock::acquire($key); if (!$lock) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Plain-text validation error is caught by the admin handler and escaped for HTML or sent as JSON to textContent; escaping here would corrupt those messages. throw new InvalidArgumentException(__('Another review is being saved for this order. Reopen it before retrying.', 'orderworth')); } try { // A caller may have loaded this object before another request saved. // Construct a fresh CRUD object instead of reusing that cached instance. $class = get_class($order); $fresh = new $class($order->get_id()); $fresh->read_meta_data(true); if (!$fresh->get_id() || $fresh->get_type() !== 'shop_order') { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Plain-text validation error is caught by the admin handler and escaped for HTML or sent as JSON to textContent; escaping here would corrupt those messages. throw new InvalidArgumentException(__('Order unavailable.', 'orderworth')); } self::write_review($fresh, $input, $reviewed, $signature); } finally { OWWC_Lock::release($key, $lock); } } private static function write_review($order, array $input, bool $reviewed, string $signature): void { if (!$signature || !hash_equals(OWWC_Profit_Calculator::review_signature($order), $signature)) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Plain-text validation error is caught by the admin handler and escaped for HTML or sent as JSON to textContent; escaping here would corrupt those messages. throw new InvalidArgumentException(__('The order, review or assumptions changed since you opened it. Reload this review before saving. Your changes were not applied.', 'orderworth')); } $values = []; foreach (['cogs','payment_cost','shipping_cost','packaging_cost'] as $key) { if (isset($input[$key]) && $input[$key] !== '') { $values[$key] = OWWC_Settings::number($input[$key]); } } if (isset($values['payment_cost'])) { $values['payment_cost_basis'] = OWWC_Reports::unit(); } if ($reviewed && (float) $order->get_total_refunded() > 0) { $values['recovered_cogs'] = OWWC_Settings::number($input['recovered_cogs'] ?? ''); $row = OWWC_Profit_Calculator::calculate($order); $native = array_column($row['lines'], 'cogs'); $cost = isset($values['cogs']) ? \OrderWorth\Engine::money($values['cogs'], OWWC_Reports::unit()['decimals']) : ($native && !in_array(null, $native, true) ? array_sum($native) : null); if ($cost === null || \OrderWorth\Engine::money($values['recovered_cogs'], OWWC_Reports::unit()['decimals']) > $cost) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Plain-text validation error is caught by the admin handler and escaped for HTML or sent as JSON to textContent; escaping here would corrupt those messages. throw new InvalidArgumentException(__('Confirm the total original product cost first. Recovered cost cannot exceed it. Nothing was saved.', 'orderworth')); } $values['refund_signature'] = OWWC_Profit_Calculator::refund_signature($order); } $values['reviewed_at'] = gmdate('c'); $values['reviewed_by'] = get_current_user_id(); $values = apply_filters('owwc_review_values', $values, $order); $order->update_meta_data('_owwc_overrides', $values); if (!$order->get_meta('_owwc_assumptions', true) && OWWC_Settings::get()['confirmed']) { $assumptions = OWWC_Settings::assumptions($order); if (empty($assumptions['conditional_rules_unavailable'])) { $order->update_meta_data('_owwc_assumptions', $assumptions); } } $order->save_meta_data(); if (class_exists('OWWC_Guide')) { OWWC_Guide::review_saved($order); } } public static function review_open(): void { self::authorize(); OWWC_Admin::validate_nonce_input(); check_ajax_referer('owwc_reports', 'nonce'); try { $token = isset($_POST['token']) && is_string($_POST['token']) ? sanitize_text_field(wp_unslash($_POST['token'])) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- request_integer rejects non-string, non-canonical, negative and overflowing values before conversion; -1 is rejected by the caller. $order = OWWC_Reports::review_order($token, OWWC_Admin::request_integer(wp_unslash($_POST['order_id'] ?? '0'))); ob_start(); self::order($order, true); $html = ob_get_clean(); wp_send_json_success(['html'=>$html, 'order_id'=>$order->get_id()]); } catch (InvalidArgumentException $e) { wp_send_json_error(['message'=>OWWC_Settings::translate_error($e->getMessage())], 400); } catch (Throwable $e) { wp_send_json_error(['message'=>__('Review unavailable. Start a fresh scan and retry.', 'orderworth')], 400); } } public static function review_save(): void { self::authorize(); OWWC_Admin::validate_nonce_input(); check_ajax_referer('owwc_reports', 'nonce'); if (array_key_exists('reviewed', $_POST) && $_POST['reviewed'] !== '1') { wp_send_json_error(['message'=>__('Invalid refund-review confirmation. Your saved costs were not changed.', 'orderworth')], 400); } try { $token = isset($_POST['token']) && is_string($_POST['token']) ? sanitize_text_field(wp_unslash($_POST['token'])) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- request_integer rejects non-string, non-canonical, negative and overflowing values before conversion; -1 is rejected by the caller. $order = OWWC_Reports::review_order($token, OWWC_Admin::request_integer(wp_unslash($_POST['order_id'] ?? '0'))); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- review_form_input checks the complete form shape; store_review validates every supplied numeric cost before any write. $cost = isset($_POST['cost']) && is_array($_POST['cost']) ? wp_unslash($_POST['cost']) : null; $input = self::review_form_input($order, ['cost'=>$cost, 'owwc_review_complete'=>isset($_POST['owwc_review_complete']) && $_POST['owwc_review_complete'] === '1' ? '1' : null]); do_action('owwc_scan_review_will_save', $token, $order->get_id()); self::store_review($order, $input, isset($_POST['reviewed']) && $_POST['reviewed'] === '1', isset($_POST['signature']) && is_string($_POST['signature']) ? sanitize_text_field(wp_unslash($_POST['signature'])) : ''); wp_send_json_success(apply_filters('owwc_scan_reviewed_result', ['row'=>OWWC_Reports::row($order->get_id())], $token)); } catch (InvalidArgumentException $e) { wp_send_json_error(['message'=>OWWC_Settings::translate_error($e->getMessage())], 409); } catch (Throwable $e) { wp_send_json_error(['message'=>__('Save could not be confirmed. Reopen the review to check its saved state before retrying.', 'orderworth')], 500); } } public static function start(): void { self::authorize(); OWWC_Admin::validate_nonce_input(); check_ajax_referer('owwc_reports', 'nonce'); try { foreach (['from','to','compare'] as $field) { if (isset($_POST[$field]) && !is_string($_POST[$field])) { wp_send_json_error(['message'=>__('Invalid report request.', 'orderworth')], 400); } } if (isset($_POST['compare']) && !in_array($_POST['compare'], ['0','1'], true)) { wp_send_json_error(['message'=>__('Invalid report request.', 'orderworth')], 400); } [$from, $to] = OWWC_Reports::default_range(); $from = isset($_POST['from']) && is_string($_POST['from']) ? sanitize_text_field(wp_unslash($_POST['from'])) : $from; $to = isset($_POST['to']) && is_string($_POST['to']) ? sanitize_text_field(wp_unslash($_POST['to'])) : $to; wp_send_json_success(OWWC_Reports::create($from, $to, isset($_POST['compare']) && $_POST['compare'] === '1')); } catch (InvalidArgumentException $e) { wp_send_json_error(['message'=>OWWC_Settings::translate_error($e->getMessage())], 400); } catch (LengthException $e) { wp_send_json_error(['message'=>OWWC_Settings::translate_error($e->getMessage())], 400); } catch (Throwable $e) { wp_send_json_error(['message'=>__('Could not start the scan. Check the date range and site logs, then retry.', 'orderworth')], 400); } } public static function batch(): void { self::authorize(); OWWC_Admin::validate_nonce_input(); check_ajax_referer('owwc_reports', 'nonce'); try { $token = isset($_POST['token']) && is_string($_POST['token']) ? sanitize_text_field(wp_unslash($_POST['token'])) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- request_integer rejects non-string, non-canonical, negative and overflowing values before conversion; -1 is rejected by the caller. wp_send_json_success(OWWC_Reports::batch($token, OWWC_Admin::request_integer(wp_unslash($_POST['group'] ?? '0')), OWWC_Admin::request_integer(wp_unslash($_POST['offset'] ?? '0')))); } catch (Throwable $e) { wp_send_json_error(['message'=>__('Scan expired or could not finish. Totals are incomplete: start a new scan.', 'orderworth')], 400); } } public static function columns(array $columns): array { $columns['owwc_margin'] = __('Est. order profit', 'orderworth'); return $columns; } public static function column(string $column, $value): void { if ($column !== 'owwc_margin' || !current_user_can('manage_woocommerce')) { return; } $order = is_object($value) ? $value : wc_get_order($value); if (!$order) { return; } $row = OWWC_Profit_Calculator::calculate($order); $label = $row['ready'] ? self::amount($row['profit'], $row['currency'], $row['decimals']) : __('Needs review', 'orderworth'); echo '' . esc_html($label) . ''; } private static function amount(int $value, string $currency, int $decimals): string { return $currency . ' ' . number_format($value / pow(10, $decimals), $decimals, '.', ','); } public static function page(): void { if (!current_user_can('manage_woocommerce')) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only navigation. Every mutation has a separate capability and nonce check. $view = isset($_GET['view']) && is_string($_GET['view']) ? sanitize_key(wp_unslash($_GET['view'])) : ''; // A saved Pro URL still has a useful, read-only destination without active Pro. // Resolve before the title and navigation so their selected destination agrees. $pro_previews = [ 'review'=>'pro_insights', 'period-review'=>'pro_insights', 'advanced'=>'pro_insights', 'background'=>'pro_insights', 'storage'=>'pro_insights', 'automations'=>'pro_schedules', 'digests'=>'pro_schedules', 'workflows'=>'pro_compare', 'fee_review'=>'pro_compare', 'actual_expenses'=>'pro_compare', 'cost_import'=>'pro_compare', 'cost_history'=>'pro_compare', 'fx'=>'pro_compare', ]; if (isset($pro_previews[$view]) && !apply_filters('owwc_pro_enabled', false)) { $view = $pro_previews[$view]; } $titles = [ ''=>__('Profit overview', 'orderworth'), 'settings'=>__('Cost assumptions', 'orderworth'), 'setup'=>__('Setup & import', 'orderworth'), 'order'=>__('Order review', 'orderworth'), 'guide'=>__('How it works', 'orderworth'), 'tutorial'=>__('Quickstart', 'orderworth'), 'progress'=>__('Local progress & preferences', 'orderworth'), 'privacy'=>__('Privacy choices', 'orderworth'), 'pro_compare'=>__('Explore Pro', 'orderworth'), 'pro_expenses'=>__('Expense rules · PRO', 'orderworth'), 'pro_insights'=>__('Profit insights · PRO', 'orderworth'), 'pro_schedules'=>__('Scheduled reports · PRO', 'orderworth'), 'workflows'=>__('Workflows', 'orderworth'), 'review'=>__('Review follow-through', 'orderworth'), 'period-review'=>__('Period review', 'orderworth'), 'advanced'=>__('Advanced reports', 'orderworth'), 'background'=>__('Background reports', 'orderworth'), 'storage'=>__('Report storage', 'orderworth'), 'automations'=>__('Scheduled reports & alerts', 'orderworth'), 'digests'=>__('Scheduled summaries', 'orderworth'), 'fee_review'=>__('Payment fees', 'orderworth'), 'actual_expenses'=>__('Actual expenses', 'orderworth'), 'cost_import'=>__('Bulk costs', 'orderworth'), 'cost_history'=>__('Cost history', 'orderworth'), 'fx'=>__('Exchange rates', 'orderworth'), ]; $title = $titles[$view] ?? __('OrderWorth workspace', 'orderworth'); echo '
'; self::navigation($view); echo '

' . esc_html__('OrderWorth', 'orderworth') . '' . esc_html($title) . '

'; OWWC_Focus::control(); echo '' . esc_html__('Back to WordPress', 'orderworth') . '' . esc_html(apply_filters('owwc_pro_enabled', false) ? __('Pro', 'orderworth') : __('Free', 'orderworth')) . '

' . esc_html($title) . '

'; if ($view === '') { echo '

' . esc_html__('Choose a period, review the cost basis, then read the estimate.', 'orderworth') . '

'; } echo '
'; OWWC_Consent::notice(); $notice = get_transient('owwc_notice_' . get_current_user_id()); if ($notice) { echo '

' . esc_html($notice['message']) . '

'; delete_transient('owwc_notice_' . get_current_user_id()); } echo '

' . esc_html__('Estimates exclude overhead, advertising, income tax and costs you have not supplied. This is not an accounting statement or live monitoring service.', 'orderworth') . '

'; OWWC_Review_Request::prompt($view); if ($view === 'settings') { self::settings(); } elseif ($view === 'setup') { OWWC_Setup::page(); } elseif ($view === 'order') { self::order(); } elseif ($view === 'guide') { self::guide(); } elseif ($view === 'tutorial') { OWWC_Guide::page(); } elseif ($view !== '' && has_action('owwc_admin_view_' . $view)) { do_action('owwc_admin_view_' . $view); } else { self::dashboard(); } echo '
'; } private static function navigation(string $view): void { $groups = [ 'reports'=>['title'=>__('Reporting', 'orderworth'),'items'=>[]], 'costs'=>['title'=>__('Costs', 'orderworth'),'items'=>[ ['url'=>self::url(['view'=>'setup']),'view'=>'setup','label'=>__('Setup & import', 'orderworth')], ['url'=>self::url(['view'=>'settings']),'view'=>'settings','label'=>__('Cost assumptions', 'orderworth')], ]], 'automation'=>['title'=>__('Automation', 'orderworth'),'items'=>[]], 'help'=>['title'=>__('Help & preferences', 'orderworth'),'items'=>[ ['url'=>self::url(['view'=>'tutorial']),'view'=>'tutorial','label'=>__('Quickstart', 'orderworth')], ['url'=>self::url(['view'=>'guide']),'view'=>'guide','label'=>__('How it works', 'orderworth')], ['url'=>self::url(['view'=>'progress']),'view'=>'progress','label'=>__('Local progress & preferences', 'orderworth')], ['url'=>self::url(['view'=>'privacy']),'view'=>'privacy','label'=>__('Privacy choices', 'orderworth')], ]], ]; if (OWWC_Review_Request::directory_ready()) { $groups['help']['items'][] = ['url'=>self::url(['view'=>'progress']).'#owwc-review-preferences','view'=>'','label'=>__('Review on WordPress.org', 'orderworth')]; } ob_start(); do_action('owwc_admin_nav'); $extra = ob_get_clean(); preg_match_all('/]*href=([\'\"])(.*?)\1[^>]*>(.*?)<\/a>/is', (string)$extra, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $url=html_entity_decode($match[2], ENT_QUOTES, 'UTF-8'); $query=[]; parse_str((string)wp_parse_url($url, PHP_URL_QUERY), $query); $target=isset($query['view']) && is_string($query['view']) ? sanitize_key($query['view']) : ''; $item=['url'=>$url,'view'=>$target,'label'=>wp_strip_all_tags(html_entity_decode($match[3], ENT_QUOTES, 'UTF-8'))]; if ($target === '' && ($query['page'] ?? '') === 'orderworth-shared-reports') { $groups['reports']['items'][]=$item; continue; } if ($target==='workflows') { $groups['automation']['items'][]=$item; continue; } $key=in_array($target,['fee_review','actual_expenses','cost_import','cost_history','fx'],true)?'costs':(in_array($target,['automations','digests'],true)?'automation':($target===''?'help':'reports')); $groups[$key]['items'][]=$item; } if (apply_filters('owwc_pro_enabled', false)) { if (has_action('owwc_admin_view_review')) { $groups['reports']['items'][]=['url'=>self::url(['view'=>'review']),'view'=>'review','label'=>__('Review follow-through', 'orderworth')]; } if (has_action('owwc_admin_view_period-review')) { $groups['reports']['items'][]=['url'=>self::url(['view'=>'period-review']),'view'=>'period-review','label'=>__('Period review', 'orderworth')]; } } echo ''; } private static function dashboard(): void { [$from, $to] = OWWC_Reports::default_range(); $confirmed = OWWC_Settings::get()['confirmed']; OWWC_Guide::banner(); if (!$confirmed) { echo ('

' . esc_html__('Before your first report: ', 'orderworth') . '' . esc_html__('confirm payment, shipping and packaging expenses', 'orderworth') . '' . '. ' . '' . esc_html__('Show me how', 'orderworth') . '' . '.' . '

'); } echo '

' . esc_html__('Report period', 'orderworth') . '

'; echo ('
' . '' . '
' . '' . ''); do_action('owwc_report_date_controls'); echo ('' . ''); do_action('owwc_report_control_links'); echo '

' . esc_html__('Report controls are loading. If they remain unavailable, reload this page.', 'orderworth') . '

'; do_action('owwc_scan_tools'); echo '
' . esc_html__('How this report is collected', 'orderworth') . '

' . esc_html__('Orders are collected and processed in bounded batches. A complete total appears only after collection and calculation finish. Use Resume after interruption before the private scan expires. Order data can change while it runs; this is not an accounting snapshot.', 'orderworth') . '

' . esc_html(apply_filters('owwc_report_range_description', __('Free includes recent and older dates with manual continuation. Reports allow up to 3,660 calendar days, including both end dates; use another period for a longer history. Pro adds comparisons, product reports, exports and background collection that continues after you close the page.', 'orderworth'))) . '

'; echo ('

' . esc_html__('No report run yet. Totals appear after every batch succeeds.', 'orderworth') . '

' . '' . '
'); echo '' . '' . '
' . '

' . esc_html__('Order review', 'orderworth') . '

' . '' . '
' . '

' . '

' . '
' . '
' . '
'); do_action('owwc_report_tools'); } private static function field(string $name, string $label, string $value, string $help = '', bool $required = true, string $prefix = 'owwc'): void { echo ''; } private static function settings(): void { $s = OWWC_Settings::get(); echo ('
' . '

' . esc_html__('Make your assumptions explicit', 'orderworth') . '

' . '

' . esc_html(sprintf(/* translators: 1: report value or label. */ __('All monetary values use %1$s. Enter decimals with a dot. Zero is allowed only when it is genuinely the assumption you intend.', 'orderworth'), OWWC_Reports::unit()['currency'])) . '

' . '
' . ''); wp_nonce_field('owwc_settings'); echo '
'; self::field('fee_percent', __('Payment fee (%)', 'orderworth'), (string) $s['fee_percent'], __('Up to 12 decimal places. Applied to the original charged total, including sales tax and shipping. Not a PayPal or gateway reconciliation.', 'orderworth')); self::field('fee_fixed', __('Payment fee (fixed per order)', 'orderworth'), (string) $s['fee_fixed'], __('Retained on refunds unless manually overridden on the order.', 'orderworth')); self::field('shipping_cost', __('Your shipping expense per order', 'orderworth'), (string) $s['shipping_cost'], __('What you pay to ship, not what the customer pays. Use a manual order override when needed.', 'orderworth')); self::field('packaging_cost', __('Packaging expense per order', 'orderworth'), (string) $s['packaging_cost']); self::field('threshold', __('Target order margin (%)', 'orderworth'), (string) $s['threshold'], __('Up to 12 decimal places. A review threshold, not promised recoverable revenue.', 'orderworth')); echo ('
' . '' . '

' . '' . '

' . '
' . '

' . esc_html__('New processing/completed orders capture these assumptions. Older orders use current assumptions until a manual review captures them. Changing settings never silently rewrites an existing snapshot.', 'orderworth') . '

' . '
'); OWWC_Pro_Previews::settings_card(); $saved_rules = (array) get_option('owwc_pro_rules', []); if ((!empty($saved_rules['payments']) || !empty($saved_rules['shipping'])) && !apply_filters('owwc_conditional_rules_available', false)) { echo '

' . esc_html(OWWC_Settings::messages()['conditional_rules_unavailable']) . '

'; } do_action('owwc_settings_after'); } private static function order($order = null, bool $inline = false): void { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended -- Read-only selection after capability checks; request_integer rejects non-string, non-canonical, negative and overflowing values before conversion; -1 is rejected by the caller. $id = OWWC_Admin::request_integer(wp_unslash($_GET['order_id'] ?? '0')); $order = $order ?: ($id > 0 ? wc_get_order($id) : false); if (!$order || $order->get_type() !== 'shop_order') { echo ('

' . esc_html__('Order unavailable.', 'orderworth') . '

'); return; } if (!current_user_can('edit_shop_order', $order->get_id())) { echo ('

' . esc_html__('You do not have permission to review this order.', 'orderworth') . '

'); return; } $row = OWWC_Profit_Calculator::calculate($order); $v = (array) $order->get_meta('_owwc_overrides', true); $shipping_keys = []; foreach ($order->get_items('shipping') as $shipping_item) { $shipping_keys[] = $shipping_item->get_method_id() . ':' . $shipping_item->get_instance_id(); } echo ('

' . esc_html__('Payment method ID: ', 'orderworth') . '' . esc_html($order->get_payment_method()) . '' . esc_html__(' · Shipping method IDs: ', 'orderworth') . '' . esc_html(implode(', ', $shipping_keys) ?: 'none') . '' . '

'); echo ('
' . '

' . esc_html(sprintf(/* translators: 1: report value or label. */ __('Review order #%1$s', 'orderworth'), $order->get_order_number())) . '

' . '

' . '' . esc_html__('Open the WooCommerce order', 'orderworth') . '' . '

' . '

' . esc_html__('Basis: ', 'orderworth') . '' . esc_html($row['assumption_source'] === 'snapshot' ? __('Recorded assumptions', 'orderworth') : __('Current assumptions — not historical actuals', 'orderworth')) . '' . esc_html__('. Native item costs come from this order, never today’s product cost.', 'orderworth') . '

'); if ($row['issues']) { echo ''; } echo '
'; foreach (['revenue'=>__('Revenue after sales tax and refunds', 'orderworth'),'net_cogs'=>__('Product costs minus confirmed recovery', 'orderworth'),'payment_cost'=>__('Payment expense', 'orderworth'),'shipping_cost'=>__('Shipping expense', 'orderworth'),'packaging_cost'=>__('Packaging expense', 'orderworth'),'profit'=>__('Estimated order profit', 'orderworth')] as $key=>$label) { echo '
' . esc_html($label) . '
' . esc_html(isset($row[$key]) ? self::amount($row[$key], $row['currency'], $row['decimals']) : __('Not available', 'orderworth')) . '
'; } do_action('owwc_order_details', $order, $row, $inline); echo ('
' . '

' . esc_html__('Discounts are already included in order revenue; they are not subtracted again. Customer-paid shipping is revenue, not your shipping expense.', 'orderworth') . '

' . '
'); do_action('owwc_order_review_before', $order, $row); echo ('
' . '

' . esc_html__('Confirm missing or actual costs', 'orderworth') . '

' . '

' . esc_html__('Leave a field blank to use the recorded cost or assumption. Enter an explicit 0 to confirm a genuine zero. These fields change only OrderWorth metadata, not order charges or WooCommerce costs.', 'orderworth') . '

' . '
' . '' . ''); wp_nonce_field('owwc_review'); echo ''; echo '
'; self::field('cogs', __('Total original product costs for this order', 'orderworth'), (string) ($v['cogs'] ?? ''), __('All quantities combined, before any refund recovery.', 'orderworth'), false, 'cost'); self::field('payment_cost', __('Total payment expense', 'orderworth'), (string) ($v['payment_cost'] ?? ''), __('Net expense retained after refunds. Include any extra refund fee.', 'orderworth'), false, 'cost'); self::field('shipping_cost', __('Total shipping expense', 'orderworth'), (string) ($v['shipping_cost'] ?? ''), __('Include outbound and return shipping if you pay both.', 'orderworth'), false, 'cost'); self::field('packaging_cost', __('Total packaging expense', 'orderworth'), (string) ($v['packaging_cost'] ?? ''), '', false, 'cost'); if ((float) $order->get_total_refunded() > 0) { self::field('recovered_cogs', __('Product cost recovered from returned stock', 'orderworth'), (string) ($v['recovered_cogs'] ?? ''), __('Required to confirm a refund. Enter 0 if nothing recoverable was returned. Cannot exceed original product costs.', 'orderworth'), false, 'cost'); } echo '
'; if ((float) $order->get_total_refunded() > 0) { $reviewed = isset($v['refund_signature']) && hash_equals(OWWC_Profit_Calculator::refund_signature($order), $v['refund_signature']); echo ('

' . '' . '

' . '

' . esc_html__('A new or changed refund requires a fresh review. This does not infer physical restocking from a monetary refund.', 'orderworth') . '

'); } echo (''); if ($inline) { echo (' ' . ''); } // Keep this last: PHP input truncation must never turn missing controls into intentional removals. echo ''; echo ('
' . '

' . esc_html__('Saving captures current assumptions for an older order if no snapshot exists. Clear manual fields and save to return those fields to their recorded/default basis.', 'orderworth') . '

' . '
'); } private static function guide(): void { echo ('
' . '

' . esc_html__('The estimate, in plain language', 'orderworth') . '

' . '

' . '' . esc_html__('Revenue after tax and refunds − product cost after recovery − payment expense − shipping expense − packaging expense.', 'orderworth') . '' . '

' . '
    ' . '
  1. ' . esc_html__('Enable WooCommerce’s native Cost of Goods Sold feature and record product costs before new orders are created. Existing orders without trustworthy item costs require review.', 'orderworth') . '
  2. ' . '
  3. ' . esc_html__('Confirm your default expenses in Cost assumptions. Defaults are estimates, not automatically imported gateway or carrier charges.', 'orderworth') . '
  4. ' . '
  5. ' . esc_html__('Run a complete scan. Review missing data first, then losses and margins below your chosen target.', 'orderworth') . '
  6. ' . '
  7. ' . esc_html__('Open an order to see the breakdown and record actual expenses. Use in-page Save & review next to update one reviewed order; run a fresh scan after other store changes.', 'orderworth') . '
  8. ' . '
' . '

' . esc_html__('Scope and exclusions', 'orderworth') . '

' . '

' . esc_html__('OrderWorth Free targets a single-currency store with simple products and variations. Pro supports historical currency normalization through Exchange Rates. Subscriptions, bundles and marketplace splits remain outside scope. Not every third-party extension can be detected automatically; reconcile a few known orders against your records.', 'orderworth') . '

' . '

' . esc_html__('Missing native costs — including an unconfirmed zero — block the profit total until reviewed. Ads, rent, salaries, income tax and other overhead are excluded. An order’s later refunds affect the period in which the original order was created: this is not a cash-flow-by-refund-date report.', 'orderworth') . '

' . '

' . esc_html__('Privacy and maintenance', 'orderworth') . '

' . '

' . esc_html__('Free reports run locally. Optional domain sharing and marketing email signup are separate, unchecked choices in Help & preferences → Privacy choices. Only selected details are sent to OrderWorth; reports work without either choice. Pro update activation sends a purchase key and site URL to the license service; later update checks use an installation token. Pro background FX lookup sends currency pairs and historical dates to Frankfurter; no order IDs or customer details are sent. Order and customer data remain in WordPress. Optional summary emails send aggregate financial figures through your own mail service. Interactive scans run while this page requests batches. Optional Pro summaries use bounded WP-Cron batches and can run late. Scan ID lists expire after one hour. Deactivation stops scheduled summaries and emails. Uninstall preserves settings, summaries, presets and order review records; remove them deliberately only after exporting your records.', 'orderworth') . '

' . '

' . esc_html__('Free versus Pro', 'orderworth') . '

' . '

' . esc_html__('Both use exactly the same engine and refund checks. Free includes recent and older date ranges, global expense defaults, manual order expenses, setup health checks and previewed CSV product-cost imports. Pro adds conditional method rules, period comparisons, profit-change drivers, personal report presets, scheduled summaries, bulk review of supported recorded payment fees, product/variation allocation and complete CSV report packs. Pro purchases, downloads and written support are managed on the OrderWorth website. Enable daily or weekly summaries in Scheduled summaries. Optional email uses your site’s mail service.', 'orderworth') . '

' . '
'); } }