$counts) { if (!is_string($date) || !preg_match('/^\d{4}-\d{2}-\d{2}$/D', $date) || $date < $cutoff || $date > $today || !is_array($counts)) { continue; } foreach (self::events() as $event) { $count = $counts[$event] ?? 0; if (is_int($count) && $count > 0) { $days[$date][$event] = min(1000, $count); } } } ksort($days); return ['record'=>!empty($state['record']), 'dismissed'=>array_values(array_intersect(array_filter((array)($state['dismissed'] ?? []), 'is_string'), ['expenses','insights','fees','schedule','supplier'])), 'days'=>$days]; } public static function record(string $event): void { if (!current_user_can('manage_woocommerce') || !in_array($event, self::events(), true)) { return; } $previous = get_user_meta(get_current_user_id(), OWWC_User_State::key(self::META), true); $state = self::state(); if (!$state['record'] || !is_array($previous) || empty($previous['record'])) { return; } $cutoff = gmdate('Y-m-d', time() - 89 * DAY_IN_SECONDS); foreach ($state['days'] as $date=>$counts) { if (!preg_match('/^\d{4}-\d{2}-\d{2}$/D', (string)$date) || $date < $cutoff || !is_array($counts)) { unset($state['days'][$date]); } } $today = gmdate('Y-m-d'); $state['days'][$today][$event] = min(1000, 1 + (int)($state['days'][$today][$event] ?? 0)); // Compare the consent-bearing record: an overlapping opt-out must never // be overwritten by a late event. Advisory counts may lose a racing increment. update_user_meta(get_current_user_id(), OWWC_User_State::key(self::META), $state, $previous); } public static function assets(string $hook): void { if ($hook !== 'toplevel_page_orderworth' || !current_user_can('manage_woocommerce')) { return; } wp_enqueue_style('owwc-experience', plugins_url('assets/experience.css', OWWC_FILE), ['owwc-admin'], OWWC_VERSION); wp_enqueue_script('owwc-experience', plugins_url('assets/experience.js', OWWC_FILE), ['owwc-admin','wp-i18n'], OWWC_VERSION, true); wp_set_script_translations('owwc-experience', 'orderworth', dirname(OWWC_FILE) . '/languages'); $state = self::state(); wp_localize_script('owwc-experience', 'OWWCExperience', ['ajax'=>admin_url('admin-ajax.php'), 'nonce'=>wp_create_nonce('owwc_experience'), 'pro'=>OWWC_Pro_Journey::access_state() === 'active', 'hidden'=>!OWWC_Pro_Previews::visible(), 'record'=>$state['record'], 'dismissed'=>$state['dismissed'], 'sessionScope'=>wp_hash(get_current_blog_id() . ':' . get_current_user_id()), 'compare'=>OWWC_Admin::url(['view'=>'pro_compare']), 'previews'=>['expenses'=>OWWC_Pro_Previews::url('expenses'),'insights'=>OWWC_Pro_Previews::url('insights'),'schedule'=>OWWC_Pro_Previews::url('schedules'),'supplier'=>OWWC_Pro_Previews::url('supplier'),'fees'=>OWWC_Pro_Previews::url('fees')], 'progress'=>OWWC_Admin::url(['view'=>'progress'])]); } public static function ajax(): void { OWWC_Admin::authorize(); OWWC_Admin::validate_nonce_input(); check_ajax_referer('owwc_experience', 'nonce'); $op = isset($_POST['op']) && is_string($_POST['op']) ? sanitize_key(wp_unslash($_POST['op'])) : ''; $value = isset($_POST['value']) && is_string($_POST['value']) ? sanitize_key(wp_unslash($_POST['value'])) : ''; if ($op === 'event' && in_array($value, self::events(), true)) { self::record($value); } elseif ($op === 'dismiss' && in_array($value, ['expenses','insights','fees','schedule','supplier'], true)) { $previous = get_user_meta(get_current_user_id(), OWWC_User_State::key(self::META), true); $state = self::state(); if (!in_array($value, $state['dismissed'], true)) { $state['dismissed'][] = $value; $saved = is_array($previous) ? update_user_meta(get_current_user_id(), OWWC_User_State::key(self::META), $state, $previous) : add_user_meta(get_current_user_id(), OWWC_User_State::key(self::META), $state, true); if (!$saved) { wp_send_json_error(['message'=>__('Your preferences changed. Reload and retry.', 'orderworth')], 409); } } } else { wp_send_json_error(['message'=>__('Unknown local preference.', 'orderworth')], 400); } wp_send_json_success(['saved'=>true]); } public static function preferences(): void { OWWC_Admin::authorize(); OWWC_Admin::validate_nonce_input(); check_admin_referer('owwc_experience_preferences'); if (!isset($_POST['complete']) || $_POST['complete'] !== '1') { wp_die(esc_html__('The preference form was incomplete. Your preferences were not changed.', 'orderworth'), '', ['response'=>400]); } foreach (['record','clear','restore'] as $field) { if (array_key_exists($field, $_POST) && $_POST[$field] !== '1') { wp_die(esc_html__('Invalid preference value. Your preferences were not changed.', 'orderworth'), '', ['response'=>400]); } } $previous = get_user_meta(get_current_user_id(), OWWC_User_State::key(self::META), true); $state = self::state(); $state['record'] = isset($_POST['record']) && $_POST['record'] === '1'; if (!$state['record'] || isset($_POST['clear']) && $_POST['clear'] === '1') { $state['days'] = []; } if (isset($_POST['restore']) && $_POST['restore'] === '1') { $state['dismissed'] = []; } if ($previous !== $state) { $saved = is_array($previous) ? update_user_meta(get_current_user_id(), OWWC_User_State::key(self::META), $state, $previous) : add_user_meta(get_current_user_id(), OWWC_User_State::key(self::META), $state, true); if (!$saved) { wp_die(esc_html__('Your preferences changed while saving. Reload and retry.', 'orderworth'), '', ['response'=>409]); } } wp_safe_redirect(OWWC_Admin::url(['view'=>'progress','saved'=>'1'])); exit; } public static function progress(): void { OWWC_Admin::authorize(); $state = self::state(); echo '

' . esc_html__('Your local workflow progress', 'orderworth') . '

' . esc_html__('Optional counts help you review how you use OrderWorth. They stay in your WordPress user profile on this site. Nothing is sent to OrderWorth or a tracking service.', 'orderworth') . '

' . esc_html__('Counts cover reports, individual reviews and workflow links for up to 90 days. They contain no order IDs, costs, customer details or website identifiers. They are activity counts, not unique users, verified purchases or a conversion rate. Days without events may remain stored until your next event or until you clear them.', 'orderworth') . '

'; wp_nonce_field('owwc_experience_preferences'); echo '

' . esc_html__('Turning recording off clears your counts. Dismissal preferences remain separate. An export happens only when you request it below.', 'orderworth') . '

'; $totals = array_fill_keys(self::events(), 0); foreach ($state['days'] as $counts) { foreach ($totals as $key=>$count) { $totals[$key] += max(0, (int)($counts[$key] ?? 0)); } } $labels = [__('Completed reports', 'orderworth'),__('Saved individual reviews', 'orderworth'),__('Fee workflow opened', 'orderworth'),__('Scheduled report workflow opened', 'orderworth'),__('Supplier workflow opened', 'orderworth'),__('Plan link selected', 'orderworth'),__('Pro workflow hub opened', 'orderworth')]; echo '
'; $i = 0; foreach ($totals as $count) { echo '
' . esc_html($labels[$i++]) . '
' . esc_html((string)$count) . '
'; } echo '
'; wp_nonce_field('owwc_experience_export'); echo '

'; OWWC_Pro_Previews::preference(); OWWC_Pro_Journey::panel(); OWWC_Review_Request::preferences(); } public static function export(): void { OWWC_Admin::authorize(); OWWC_Admin::validate_nonce_input(); check_admin_referer('owwc_experience_export'); nocache_headers(); header('Content-Type: text/csv; charset=UTF-8'); header('Content-Disposition: attachment; filename="orderworth-local-workflow-counts.csv"'); header('X-Content-Type-Options: nosniff'); $out = fopen('php://output', 'w'); fputcsv($out, ['utc_date','event','count'], ',', '"', ''); foreach (self::state()['days'] as $date=>$counts) { if (!preg_match('/^\d{4}-\d{2}-\d{2}$/D', (string)$date) || !is_array($counts)) { continue; } foreach (self::events() as $event) { if (!empty($counts[$event])) { fputcsv($out, [$date,$event,max(0,min(1000,(int)$counts[$event]))], ',', '"', ''); } } } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closes the php://output response stream; no filesystem access or mutation. fclose($out); exit; } /** Navigation only: opening these pages never enables reporting or email. */ public static function report_links(): void { if (apply_filters('owwc_pro_enabled', false)) { if (has_action('owwc_admin_view_advanced')) { echo '' . esc_html__('Compare periods · Pro', 'orderworth') . ''; } if (has_action('owwc_admin_view_automations')) { echo '' . esc_html__('Schedule reports · Pro', 'orderworth') . '' . esc_html__('Set up future closed calendar periods. Nothing starts until you save an enabled schedule.', 'orderworth') . ''; } return; } if (!OWWC_Pro_Previews::visible()) { return; } echo '' . esc_html__('Compare periods · Pro', 'orderworth') . ''; if (!in_array('schedule', self::state()['dismissed'], true)) { echo '' . esc_html__('Schedule reports · Pro', 'orderworth') . ''; } } private static function example_url(string $workflow): string { return OWWC_Admin::url(['view'=>'pro_compare','workflow'=>$workflow]) . '#owwc-example-' . $workflow; } private static function example(string $workflow, bool $open): void { $titles = [ 'expenses'=>__('See an expense-rule example', 'orderworth'), 'analysis'=>__('See a period and product example', 'orderworth'), 'schedule'=>__('See a scheduled-report example', 'orderworth'), 'supplier'=>__('See a supplier-import example', 'orderworth'), 'fees'=>__('See a bulk fee-review example', 'orderworth'), ]; echo '
' . esc_html($titles[$workflow]) . '

' . esc_html__('Fictional example — not your store data', 'orderworth') . '

' . esc_html__('Opening this example does not run a report, import costs, change settings or send an email.', 'orderworth') . '

'; if ($workflow === 'expenses') { OWWC_Pro_Previews::expense_example(); } elseif ($workflow === 'analysis') { echo '
' . esc_html__('Two complete seven-day periods · USD', 'orderworth') . '
' . esc_html__('Period', 'orderworth') . '' . esc_html__('Net revenue', 'orderworth') . '' . esc_html__('Supplied costs', 'orderworth') . '' . esc_html__('Estimated profit', 'orderworth') . '
' . esc_html__('1–7 September', 'orderworth') . '1,000.00620.00380.00
' . esc_html__('8–14 September', 'orderworth') . '1,200.00740.00460.00

' . esc_html__('Estimated profit is USD 80.00 higher: 460.00 − 380.00.', 'orderworth') . ' ' . esc_html__('The current period includes USD 600.00 in product costs, 50.00 in payment expenses, 70.00 in shipping expenses and 20.00 in packaging expenses: 740.00 in total.', 'orderworth') . '

'; echo '
' . esc_html__('Current-period product allocation · USD', 'orderworth') . '
' . esc_html__('Product', 'orderworth') . '' . esc_html__('Allocated net revenue', 'orderworth') . '' . esc_html__('Allocated profit', 'orderworth') . '
' . esc_html__('Canvas bag', 'orderworth') . '720.00276.00
' . esc_html__('Travel pouch', 'orderworth') . '480.00184.00
' . esc_html__('Total', 'orderworth') . '1,200.00460.00

' . esc_html__('This example includes all 10 orders in each period, with no refunds or missing costs. Product figures allocate shared order expenses and reconcile to the included order estimates. Real reports show exclusions; incomplete cost coverage can withhold the profit comparison. Overhead and advertising are excluded.', 'orderworth') . '

'; } elseif ($workflow === 'schedule') { echo '
' . esc_html__('Weekly report · 7–13 September 2026', 'orderworth') . '

' . esc_html__('Finished report illustration · USD', 'orderworth') . '

' . esc_html__('Complete', 'orderworth') . '
' . esc_html__('Included net revenue', 'orderworth') . '
1,200.00
' . esc_html__('Included costs', 'orderworth') . '
740.00
' . esc_html__('Estimated profit', 'orderworth') . '
460.00

' . esc_html__('10 orders selected · 10 included · 0 excluded · no refunds', 'orderworth') . '

' . esc_html__('Costs behind this estimate', 'orderworth') . '
' . esc_html__('Product costs', 'orderworth') . '
600.00
' . esc_html__('Payment expenses · estimated', 'orderworth') . '
50.00
' . esc_html__('Shipping expenses', 'orderworth') . '
70.00
' . esc_html__('Packaging expenses', 'orderworth') . '
20.00
' . esc_html__('Report pack contents', 'orderworth') . '
  • ' . esc_html__('Order, product and category CSVs', 'orderworth') . '
  • ' . esc_html__('Weekly and monthly trend CSVs', 'orderworth') . '
  • ' . esc_html__('Coverage, exclusions and report metadata', 'orderworth') . '

' . esc_html__('Email: not requested. In Pro, a retained completed report can be downloaded from report history.', 'orderworth') . '

' . esc_html__('Illustration only — no report or download is created. Complete means collection finished, not that costs are verified. Overhead, advertising and other unsupplied costs are excluded.', 'orderworth') . '

'; echo '

' . esc_html__('Reuse saved filters for each completed calendar week, then return to the report pack without rebuilding the report manually.', 'orderworth') . '

' . esc_html__('You choose and save an enabled schedule for future closed calendar periods; the dates in a manual report are not copied into a schedule. Email stays off unless you explicitly enable and save it. WP-Cron or your host’s scheduler must run, so collection may be late. Missed periods are not backfilled. Completion and failure status stay visible.', 'orderworth') . '

'; } elseif ($workflow === 'supplier') { echo '
  1. ' . esc_html__('Save a supplier mapping: Item code → SKU and Unit cost → product cost.', 'orderworth') . '
  2. ' . esc_html__('Choose the next supplier CSV and preview the matching products and proposed costs.', 'orderworth') . '
  3. ' . esc_html__('Approve the changes before applying them. Background application requires your confirmation.', 'orderworth') . '
' . esc_html__('Previewed changes · USD', 'orderworth') . '
' . esc_html__('SKU', 'orderworth') . '' . esc_html__('Current cost', 'orderworth') . '' . esc_html__('Proposed cost', 'orderworth') . '
BAG-0112.0013.00
POUCH-025.005.50

' . esc_html__('Guarded undo and change history remain available. Product imports do not rewrite historical order costs. Free already includes manual CSV preview, application and guarded undo.', 'orderworth') . '

'; } else { echo '
  1. ' . esc_html__('Preview supported recorded payment fees or a merchant CSV against the selected orders.', 'orderworth') . '
  2. ' . esc_html__('Review the source and the proposed fee before approving supported matches.', 'orderworth') . '
  3. ' . esc_html__('Keep unsupported or ambiguous records for individual review. Nothing is corrected automatically.', 'orderworth') . '
' . esc_html__('Fee review preview · USD', 'orderworth') . '
' . esc_html__('Sample order', 'orderworth') . '' . esc_html__('Estimated fee', 'orderworth') . '' . esc_html__('Supported recorded fee', 'orderworth') . '
A-1013.002.80
A-1024.003.90

' . esc_html__('These two fictional records have USD 0.30 less payment expense than the estimates. That changes the estimate; it does not recover money or prove a saving. Apply only after review, with guarded undo. Free keeps individual order review available.', 'orderworth') . '

'; } echo '
'; } public static function comparison(): void { OWWC_Admin::authorize(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only choice of locally bundled explanatory content. $workflow = isset($_GET['workflow']) && is_string($_GET['workflow']) ? sanitize_key(wp_unslash($_GET['workflow'])) : ''; $items = [ 'expenses'=>[ 'title'=>__('Expense rules by method', 'orderworth'), 'benefit'=>__('Reuse separate expense assumptions for different payment and shipping methods.', 'orderworth'), 'workflow'=>__('Expense assumptions', 'orderworth'), 'free'=>__('Set global expense defaults and adjust individual orders.', 'orderworth'), 'pro'=>__('Add reusable rules for payment methods and shipping methods.', 'orderworth'), 'details'=>__('You enter and approve the assumptions. Unmatched methods use defaults; saving rules does not rewrite existing order snapshots.', 'orderworth'), 'anchor'=>'reviewed-expenses', ], 'analysis'=>[ 'title'=>__('Period comparisons and product reports', 'orderworth'), 'benefit'=>__('Compare completed periods, then inspect product and variation allocations to understand the included profit estimate.', 'orderworth'), 'workflow'=>__('Profit analysis', 'orderworth'), 'free'=>__('Run recent or historical order-profit reports and review individual orders.', 'orderworth'), 'pro'=>__('Add period comparisons, profit-change drivers and product or variation allocations.', 'orderworth'), 'details'=>__('Both editions use the same calculation and refund checks. Missing costs and allocation exclusions remain visible; Pro does not make incomplete data complete.', 'orderworth'), 'anchor'=>'compare', ], 'fees'=>[ 'title'=>__('Bulk payment-fee review', 'orderworth'), 'benefit'=>__('Review supported payment fees in bulk to reduce repeated order-by-order work.', 'orderworth'), 'workflow'=>__('Payment-fee review', 'orderworth'), 'free'=>__('View expense sources and correct individual orders.', 'orderworth'), 'pro'=>__('Add bulk review for supported records and merchant CSVs.', 'orderworth'), 'details'=>__('Preview records, approve changes and retain guarded undo. Unsupported or ambiguous records still need individual review.', 'orderworth'), 'anchor'=>'reviewed-expenses', ], 'schedule'=>[ 'title'=>__('Scheduled reports', 'orderworth'), 'benefit'=>__('Prepare recurring report packs with saved filters and alerts you configure.', 'orderworth'), 'workflow'=>__('Reporting', 'orderworth'), 'free'=>__('Run reports manually, with continuation for larger reports.', 'orderworth'), 'pro'=>__('Add scheduled report packs, saved filters and configurable alerts.', 'orderworth'), 'details'=>__('A working WordPress scheduler is required. Setup, completion and failure status remain visible.', 'orderworth'), 'anchor'=>'scheduled-reports', ], 'supplier'=>[ 'title'=>__('Reusable supplier imports', 'orderworth'), 'benefit'=>__('Reuse supplier mappings to spend less time preparing recurring cost imports.', 'orderworth'), 'workflow'=>__('Supplier cost imports', 'orderworth'), 'free'=>__('Preview and apply manual CSV imports with guarded undo.', 'orderworth'), 'pro'=>__('Add reusable mappings, confirmed background processing and change history.', 'orderworth'), 'details'=>__('Pro also supports future landed costs. Background application requires confirmation. Product imports do not rewrite historical order costs.', 'orderworth'), 'anchor'=>'supplier-workflows', ], ]; $groups = [ ['title'=>__('Match expenses to the method', 'orderworth'),'benefit'=>__('Keep global defaults while using your own specific assumptions where methods differ.', 'orderworth'),'items'=>['expenses']], ['title'=>__('Understand what changed', 'orderworth'),'benefit'=>__('See how estimated profit changed between periods and how products contribute to it.', 'orderworth'),'items'=>['analysis']], ['title'=>__('Keep reports ready', 'orderworth'),'benefit'=>__('Prepare recurring report packs with the filters and optional alerts you choose.', 'orderworth'),'items'=>['schedule']], ['title'=>__('Spend less time on repeat work', 'orderworth'),'benefit'=>__('Reuse supplier mappings and review supported payment fees in bulk, with approval before changes.', 'orderworth'),'items'=>['supplier','fees']], ]; OWWC_Pro_Journey::panel(); echo '

' . esc_html__('Compare Free and Pro', 'orderworth') . '

' . esc_html__('Free includes recent and historical manual reports, individual order reviews and CSV cost imports. Pro is a separate add-on when you need method-specific expenses, deeper analysis or recurring and bulk workflows.', 'orderworth') . '

' . esc_html__('Already have Pro?', 'orderworth') . '

' . esc_html__('Keep Free active. Download Pro from My purchase, install and activate the Pro ZIP, then enter your licence. An administrator with plugin activation permission must complete activation. Activating Pro does not save rules, start imports or enable schedules.', 'orderworth') . '

'; OWWC_Pro_Previews::main_action('expenses', false); echo '' . esc_html__('My purchase', 'orderworth') . '
'; foreach ($groups as $group) { echo '

' . esc_html($group['title']) . '

' . esc_html($group['benefit']) . '

'; foreach ($group['items'] as $key) { $item = $items[$key]; echo '
' . esc_html__('Pro workflow', 'orderworth') . '

' . esc_html($item['title']) . '

' . esc_html($item['benefit']) . '

'; self::example($key, $workflow === $key); $task = $key === 'analysis' ? 'insights' : ($key === 'schedule' ? 'schedules' : $key); echo '

' . esc_html($item['details']) . '

'; OWWC_Pro_Previews::main_action($task); if (in_array($task, ['expenses','insights','schedules'], true)) { echo '' . esc_html__('See the Pro screen and setup steps', 'orderworth') . ''; } echo '
'; OWWC_Pro_Previews::continuation($task); echo '
'; } echo '
'; } echo '
' . esc_html__('Detailed Free and Pro comparison', 'orderworth') . ''; foreach ($items as $item) { echo ''; } echo '
' . esc_html__('Five workflows at a glance', 'orderworth') . '
' . esc_html__('Workflow', 'orderworth') . '' . esc_html__('Free', 'orderworth') . '' . esc_html__('Pro adds', 'orderworth') . '
' . esc_html($item['workflow']) . '' . esc_html($item['free']) . '' . esc_html($item['pro']) . '
' . esc_html__('Plans, renewal and compatibility', 'orderworth') . '

' . esc_html__('All Pro plans include the same plugin features; choose access for 1, 5 or 25 sites. New annual purchases renew automatically each year until cancelled. Cancel future renewals in My purchase → Manage billing; paid update, download and support access continues through the paid period. Historical purchases keep their agreed terms. Initial eligible online licence activation unlocks local Pro features on this site; earned local access continues after update access expires.', 'orderworth') . '

' . esc_html__('There are no automatic financial corrections, guaranteed savings or universal gateway compatibility. Review the current website terms before purchasing.', 'orderworth') . '

' . esc_html__('Continue with order reports', 'orderworth') . '
'; OWWC_Pro_Previews::preference(); } }