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 '
';
}
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') . '
';
}
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__('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__('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') . '