in_array($saved['status'] ?? '', ['active','later','complete'], true) ? $saved['status'] : 'new', 'step'=>in_array($saved['step'] ?? '', $ids, true) ? $saved['step'] : $ids[0], // Keep Pro progress when Pro is temporarily inactive, but only count available lessons in the UI. 'seen'=>is_array($saved['seen'] ?? null) ? array_values(array_filter($saved['seen'], 'is_string')) : [], ]; } /** Reading completion belongs to this user and the currently available lessons. */ public static function completed(): bool { $state = self::state(); return 'complete' === $state['status'] && !array_diff(array_column(self::lessons(), 'id'), $state['seen']); } public static function change(string $operation, string $lesson = ''): array { if (!current_user_can('manage_woocommerce')) { // 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(__('Store-management access required.', 'orderworth')); } $state = self::state(); $ids = array_column(self::lessons(), 'id'); if (!in_array($operation, ['start','later','resume','next','replay'], true)) { // 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(__('Unknown guide action.', 'orderworth')); } if ($lesson !== '' && !in_array($lesson, $ids, true)) { // 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(__('This lesson is unavailable in the active edition.', 'orderworth')); } if ($operation === 'replay') { $state = ['status'=>'active','step'=>$ids[0],'seen'=>[]]; } elseif ($operation === 'later') { $state['status']='later'; if ($lesson !== '') { $state['step']=$lesson; } } elseif ($operation === 'next') { if ($lesson === '') { // 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(__('Choose a lesson first.', 'orderworth')); } $state['seen']=array_values(array_unique(array_merge($state['seen'], [$lesson]))); $index=array_search($lesson, $ids, true); $remaining=array_values(array_diff($ids,$state['seen'])); $state['step']=$index+1 < count($ids) ? $ids[$index+1] : ($remaining[0] ?? $lesson); $state['status']=!$remaining ? 'complete' : 'active'; } else { $state['status']='active'; if ($lesson !== '') { $state['step']=$lesson; } } update_user_meta(get_current_user_id(), OWWC_User_State::key(self::META), $state); if (get_user_meta(get_current_user_id(), OWWC_User_State::key(self::META), true) !== $state) { // 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 RuntimeException(__('Guide progress could not be saved. Retry after checking the database.', 'orderworth')); } return $state; } public static function save(): void { OWWC_Admin::authorize(); OWWC_Admin::validate_nonce_input(); check_admin_referer('owwc_guide'); $operation=isset($_POST['guide_action']) && is_string($_POST['guide_action']) ? sanitize_key(wp_unslash($_POST['guide_action'])) : ''; $lesson=isset($_POST['lesson']) && is_string($_POST['lesson']) ? sanitize_key(wp_unslash($_POST['lesson'])) : ''; try { $state=self::change($operation,$lesson); } catch (Throwable $e) { wp_die(esc_html(OWWC_Settings::translate_error($e->getMessage())), 'OrderWorth', ['response'=>400]); return; } $destination = $operation === 'later' ? OWWC_Admin::url() : OWWC_Admin::url(['view'=>'tutorial','lesson'=>$state['step']]); if ($operation === 'next' && self::completed()) { $destination = OWWC_Admin::url(['view'=>'tutorial']); } wp_safe_redirect($destination); exit; } private static function button(string $operation, string $label, string $lesson = '', bool $primary = false): void { echo '
' . '' . ''; // Several forms share the nonce name, but DOM IDs must remain unique. echo ''; echo '
'; } public static function banner(): void { $state=self::state(); if (in_array($state['status'], ['later','complete'], true)) { return; } echo ('
' . '
' . '

' . ($state['status'] === 'new' ? esc_html__('Your first useful report', 'orderworth') : esc_html__('Continue your quickstart', 'orderworth')) . '

' . '

' . esc_html__('Optional lessons: add costs, confirm expenses, run a report and review missing data. Progress belongs to your WordPress account. Reopen anytime from Quickstart.', 'orderworth') . '

' . '
' . '
'); self::button($state['status'] === 'new' ? 'start' : 'resume', $state['status'] === 'new' ? __('Start quickstart', 'orderworth') : __('Resume quickstart', 'orderworth'), '', true); self::button('later',__('Not now', 'orderworth')); echo '
'; } public static function report_completed(array $scan): void { if (!current_user_can('manage_woocommerce') || (int) ($scan['owner'] ?? 0) !== get_current_user_id()) { return; } $groups=array_map(static function($g){return ['from'=>$g['from'],'to'=>$g['to'],'total'=>OWWC_Reports::group_total($g)];},$scan['groups']); update_user_meta(get_current_user_id(),OWWC_User_State::key('owwc_guide_report'),['at'=>time(),'groups'=>$groups,'currency'=>OWWC_Reports::unit()['currency'],'decimals'=>OWWC_Reports::unit()['decimals']]); } public static function review_saved($order): void { if (!current_user_can('manage_woocommerce')) { return; } update_user_meta(get_current_user_id(),OWWC_User_State::key('owwc_guide_review'),['id'=>$order->get_id(),'signature'=>OWWC_Profit_Calculator::review_signature($order)]); } public static function checks(): array { if (!current_user_can('manage_woocommerce')) { return []; } $native=OWWC_Setup::ready(); $settings=OWWC_Settings::get(); $sample=[]; if ($native) { foreach (['simple','variation'] as $type) { foreach (wc_get_products(['type'=>$type,'limit'=>5,'orderby'=>'ID','order'=>'DESC','return'=>'objects']) as $product) { $sample[]=$product->get_cogs_value(); } } } $found=count(array_filter($sample,static function($value){return $value !== null && $value !== '' && (float) $value > 0;})); $report=get_user_meta(get_current_user_id(),OWWC_User_State::key('owwc_guide_report'),true); $valid=is_array($report) && ($report['currency'] ?? '') === OWWC_Reports::unit()['currency'] && (int) ($report['decimals'] ?? -1) === (int) OWWC_Reports::unit()['decimals']; $review=get_user_meta(get_current_user_id(),OWWC_User_State::key('owwc_guide_review'),true); $review_ok=false; if (is_array($review) && ($order=wc_get_order((int) ($review['id'] ?? 0)))) { $meta=(array) $order->get_meta('_owwc_overrides',true); $review_ok=(int) ($meta['reviewed_by'] ?? 0) === get_current_user_id() && hash_equals(OWWC_Profit_Calculator::review_signature($order),(string) ($review['signature'] ?? '')) && !empty(OWWC_Profit_Calculator::calculate($order)['ready']); } $checks=[ 'costs'=>[$native && OWWC_Reports::unit()['decimals'] <= 4, $native ? (OWWC_Reports::unit()['decimals'] <= 4 ? __('Native costs enabled; currency precision supported.', 'orderworth') : __('Store currency precision exceeds four decimals.', 'orderworth')) : __('Enable native costs in WooCommerce first.', 'orderworth')], 'products'=>[$found > 0, $sample ? sprintf(/* translators: 1: report value or label; 2: report value or label. */ __('%1$s of %2$s recently added products/variations have a positive cost. This small sample is not a full catalog check.', 'orderworth'), $found, count($sample)) : __('No supported products sampled yet. Add a known product cost when ready.', 'orderworth')], 'expenses'=>[!empty($settings['confirmed']), !empty($settings['confirmed']) ? __('Expense assumptions are confirmed. Check them against your records.', 'orderworth') : __('Review your expense assumptions and save the confirmation.', 'orderworth')], 'report'=>[$valid, $valid ? sprintf(/* translators: 1: report value or label; 2: report value or label; 3: report value or label. */ __('Every batch was processed for your report on %1$s. %2$s orders across %3$s period(s). Run again for current results.', 'orderworth'), wp_date('M j, Y H:i', (int) $report['at']), array_sum(array_column($report['groups'],'total')), count($report['groups'])) : __('No completed report recorded for your account and current currency.', 'orderworth')], 'review'=>[$review_ok, $review_ok ? __('Your last saved review is unchanged and currently has enough data for an estimate.', 'orderworth') : __('No unchanged, complete review recorded for your account. A review is optional when no order needs one.', 'orderworth')], ]; return apply_filters('owwc_guide_checks', $checks, $report, $valid); } private static function destination(string $action): string { return apply_filters('owwc_guide_destination', OWWC_Admin::url($action === 'report' ? [] : ['view'=>$action]), $action); } private static function completion(): void { echo '

' . esc_html__('QUICKSTART FINISHED', 'orderworth') . '

' . esc_html__('Quickstart complete', 'orderworth') . '

' . esc_html__('Your lesson progress is saved. You can open your reports now and revisit any lesson later.', 'orderworth') . '

' . esc_html__('Reading the lessons does not confirm your costs or report accuracy. Review your store checks and any missing data before relying on an estimate.', 'orderworth') . '

' . esc_html__('Open profit overview', 'orderworth') . '

'; $invited = OWWC_Consent::invitation(); echo '

' . esc_html__('Keep learning at your own pace', 'orderworth') . '

' . esc_html__('You can replay the quickstart without changing your saved store settings or privacy choices.', 'orderworth') . '

'; self::button('replay', __('Replay quickstart', 'orderworth')); if (!$invited && OWWC_Consent::allowed()) { echo '' . esc_html__('Review privacy choices', 'orderworth') . ''; } echo '
'; } public static function page(): void { if (!current_user_can('manage_woocommerce')) { return; } $lessons=self::lessons(); $ids=array_column($lessons,'id'); $state=self::state(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only lesson navigation; save() independently checks capability and nonce. $requested=isset($_GET['lesson']) && is_string($_GET['lesson']) ? sanitize_key(wp_unslash($_GET['lesson'])) : $state['step']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only selection of the saved completion screen; no consent or progress is changed on GET. if (self::completed() && (!isset($_GET['lesson']) || !is_string($_GET['lesson']) || !in_array($requested, $ids, true))) { self::completion(); return; } $index=array_search($requested,$ids,true); $index=$index === false ? 0 : $index; $lesson=$lessons[$index]; $checks=self::checks(); echo ('
' . '

' . esc_html__('Optional quickstart', 'orderworth') . '

' . '

' . esc_html__('Follow one lesson at a time, skip ahead or stop whenever you like. Reading a lesson does not change costs, orders or schedules.', 'orderworth') . '

' . '

' . '' . esc_html(sprintf(/* translators: 1: report value or label; 2: report value or label. */ __('%1$s of %2$s lessons marked read', 'orderworth'), count(array_intersect($ids,$state['seen'])), count($ids))) . '' . esc_html__(' · Saved for your account. Store checks below are separate from reading progress.', 'orderworth') . '

' . '
'); self::button($state['status'] === 'new' ? 'start' : 'resume',$state['status'] === 'new' ? __('Start quickstart', 'orderworth') : __('Resume quickstart', 'orderworth'),'',true); self::button('later',__('Not now', 'orderworth'),$lesson['id']); self::button('replay',__('Replay from start', 'orderworth')); echo ('
' . '
' . '
' . '

' . ($lesson['edition'] === 'pro' ? esc_html__('PRO LESSON', 'orderworth') : esc_html__('FREE + PRO', 'orderworth')) . ' · ' . (int) ($index+1) . ' / ' . count($ids) . '

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

' . esc_html($lesson['intro']) . '

    '; foreach ($lesson['steps'] as $step) { echo '
  1. ' . esc_html($step) . '
  2. '; } echo '

' . esc_html($lesson['note']) . '

'; if (isset($checks[$lesson['id']])) { $check=$checks[$lesson['id']]; echo '

' . ($check[0] ? esc_html__('Observed in your store: ', 'orderworth') : esc_html__('Check your setup: ', 'orderworth')) . '' . esc_html($check[1]) . '

'; } if (isset($lesson['example'])) { self::example($lesson['example']); } echo '

' . esc_html($lesson['actionLabel']) . '

'; if (isset($lesson['reference'])) { echo ('

' . '' . esc_html__('Official WooCommerce cost setup reference', 'orderworth') . '' . '

'); } echo '
'; if ($index > 0) { echo ('' . esc_html__('Previous lesson', 'orderworth') . ''); } $last_unread = !array_diff($ids, array_merge($state['seen'], [$lesson['id']])); self::button('next',$last_unread ? __('Finish quickstart', 'orderworth') : __('Mark read & continue', 'orderworth'),$lesson['id'],true); echo ('
' . '

' . esc_html__('Replay resets your lesson progress only. Your saved store data stays in place. ', 'orderworth') . '' . esc_html__('Calculation reference and troubleshooting', 'orderworth') . '' . '

' . '
' . '
'); } private static function example(string $name): void { echo ('
' . '' . esc_html__('See an annotated example · sample data', 'orderworth') . ''); if ($name === 'coverage') { echo ('

' . '' . esc_html__('18 of 20 orders included', 'orderworth') . '' . '

' . '
    ' . '
  1. ' . esc_html__('The displayed profit covers 18 orders with enough data.', 'orderworth') . '
  2. ' . '
  3. ' . esc_html__('The other 2 need review and are excluded.', 'orderworth') . '
  4. ' . '
  5. ' . esc_html__('Review missing data, then rerun the report. Do not assume the missing orders earned zero.', 'orderworth') . '
  6. ' . '
'); } elseif ($name === 'expenses') { echo ('

' . esc_html__('Example: customer pays 8 for shipping; you pay the carrier 6 and spend 1 on packaging.', 'orderworth') . '

' . '
    ' . '
  1. ' . esc_html__('The customer\'s 8 is already part of revenue.', 'orderworth') . '
  2. ' . '
  3. ' . esc_html__('Enter 6 as your shipping expense.', 'orderworth') . '
  4. ' . '
  5. ' . esc_html__('Enter 1 as your packaging expense.', 'orderworth') . '
  6. ' . '
'); } else { echo ('

' . esc_html__('Example: original product cost 40; returned stock recovers 20.', 'orderworth') . '

' . '
    ' . '
  1. ' . esc_html__('Confirm original product costs of 40.', 'orderworth') . '
  2. ' . '
  3. ' . esc_html__('Confirm recovered cost of 20 only if that stock is recoverable.', 'orderworth') . '
  4. ' . '
  5. ' . esc_html__('The net product-cost deduction is 20. Review retained payment and shipping expenses separately.', 'orderworth') . '
  6. ' . '
'); } echo ('

' . esc_html__('Illustrative values only; this example creates no products or orders.', 'orderworth') . '

' . '
'); } }