*/ public function candidates_for_categories( array $category_ids ) { $global = $this->resolve_global()->to_array(); $candidates = array( array( 'term_id' => null, 'profile' => $global, 'source' => 'global', ), ); /** * Pro may replace/extend candidates with category overrides. * * @param array $candidates Global-only candidates. * @param int[] $category_ids Category term IDs. * @param array $global Global threshold profile array. */ $filtered = apply_filters( 'aksmb_threshold_candidates', $candidates, $category_ids, $global ); return is_array( $filtered ) && ! empty( $filtered ) ? $filtered : $candidates; } /** * Classify a line/product using global or Pro category precedence. * * @param \Kayvandra\MarginBeacon\Domain\Money\MoneyAmount|null $revenue Revenue. * @param \Kayvandra\MarginBeacon\Domain\Money\MoneyAmount|null $cogs COGS. * @param int[] $category_ids Category term IDs. * @param bool $complete Evaluation complete. * @param SeverityClassifier|null $classifier Optional classifier. * @return array */ public function classify_for_categories( $revenue, $cogs, array $category_ids, $complete, $classifier = null ) { $candidates = $this->candidates_for_categories( $category_ids ); $classifier = $classifier instanceof SeverityClassifier ? $classifier : new SeverityClassifier(); /** * Pro multi-category precedence. Free falls back to single-candidate classify. * * @param array|null $result Prior result. * @param mixed $revenue Revenue. * @param mixed $cogs COGS. * @param bool $complete Complete. * @param array $candidates Candidates. * @param SeverityClassifier $classifier Classifier. */ $filtered = apply_filters( 'aksmb_classify_threshold_candidates', null, $revenue, $cogs, $complete, $candidates, $classifier ); if ( is_array( $filtered ) ) { return $filtered; } // Free-only: classify against the first (global) candidate. $candidate = $candidates[0]; $profile = isset( $candidate['profile'] ) && is_array( $candidate['profile'] ) ? $candidate['profile'] : ThresholdProfile::defaults()->to_array(); $result = $classifier->classify( $revenue, $cogs, $profile, (bool) $complete ); return array( 'severity' => (string) $result['severity'], 'gross_profit' => $result['gross_profit'], 'severity_basis' => isset( $result['severity_basis'] ) && is_array( $result['severity_basis'] ) ? $result['severity_basis'] : array(), 'reason_code' => $result['reason_code'], 'thresholds' => array( 'critical_below' => (int) $profile['critical_below'], 'high_below' => (int) $profile['high_below'], 'warning_below' => (int) $profile['warning_below'], ), 'source' => isset( $candidate['source'] ) ? (string) $candidate['source'] : 'global', 'category_id' => array_key_exists( 'term_id', $candidate ) && null !== $candidate['term_id'] ? (int) $candidate['term_id'] : null, 'considered_category_ids' => array(), ); } /** * Category refs payload for persistence (bounded IDs + resolution meta). * * @param array $resolution Output of classify_for_categories / precedence. * @return array{considered:int[],resolved_category_id:?int,source:string} */ public static function category_refs_payload( array $resolution ) { return array( 'considered' => isset( $resolution['considered_category_ids'] ) && is_array( $resolution['considered_category_ids'] ) ? array_values( array_map( 'intval', $resolution['considered_category_ids'] ) ) : array(), 'resolved_category_id' => isset( $resolution['category_id'] ) ? $resolution['category_id'] : null, 'source' => isset( $resolution['source'] ) ? (string) $resolution['source'] : 'global', ); } }