*/ public const STATUSES = array( 'active', 'pending-cancel', 'on-hold', 'pending', 'cancelled', 'expired' ); public function name(): string { return 'get_subscription_state'; } public function validate( array $params ): array { return array( 'subscription_id' => Registry::int_param( $params, 'subscription_id', 0, 0, PHP_INT_MAX ) ); } public function execute( array $params, float $deadline ): array { if ( ! function_exists( 'wcs_get_subscription' ) ) { return array( 'error' => 'subscriptions_not_active' ); } if ( (int) $params['subscription_id'] === 0 ) { return $this->summary( $deadline ); } $sub = wcs_get_subscription( $params['subscription_id'] ); if ( ! $sub ) { return array( 'error' => 'subscription_not_found', 'id' => $params['subscription_id'] ); } $last = $sub->get_last_order( 'all', array( 'renewal' ) ); return array( 'id' => $sub->get_id(), 'status' => $sub->get_status(), 'payment_method' => (string) $sub->get_payment_method(), 'requires_manual_renewal' => (bool) $sub->get_requires_manual_renewal(), 'billing_period' => (string) $sub->get_billing_period(), 'billing_interval' => (int) $sub->get_billing_interval(), 'total' => (float) $sub->get_total(), 'currency' => $sub->get_currency(), 'start' => $sub->get_date( 'start' ) ?: null, 'next_payment' => $sub->get_date( 'next_payment' ) ?: null, 'last_payment' => $sub->get_date( 'last_order_date_created' ) ?: null, 'end' => $sub->get_date( 'end' ) ?: null, 'renewal_order_count' => count( $sub->get_related_orders( 'ids', 'renewal' ) ), 'failed_payment_count' => (int) $sub->get_failed_payment_count(), 'last_renewal_order' => $last instanceof \WC_Order ? array( 'id' => $last->get_id(), 'status' => $last->get_status(), 'total' => (float) $last->get_total() ) : null, ); } /** * Counts per status via id-only queries (cheap on big stores) and the recurring total of active subscriptions * normalised to 30 days, capped at MRR_CAP objects and by the time budget. * * @return array */ private function summary( float $deadline ): array { if ( ! function_exists( 'wc_get_orders' ) || ! function_exists( 'wcs_get_subscription' ) ) { return array( 'error' => 'woocommerce_not_active' ); } $counts = array(); $active_ids = array(); foreach ( self::STATUSES as $status ) { $found = wc_get_orders( array( 'type' => 'shop_subscription', 'status' => 'wc-' . $status, 'return' => 'ids', 'limit' => -1 ) ); $ids = array(); foreach ( is_array( $found ) ? $found : array() as $row ) { // 'return' => 'ids' yields ints; be tolerant of objects in case a filter changes the return shape. if ( is_numeric( $row ) ) { $ids[] = (int) $row; } elseif ( is_object( $row ) && method_exists( $row, 'get_id' ) ) { $ids[] = (int) $row->get_id(); } } $counts[ $status ] = count( $ids ); if ( 'active' === $status || 'pending-cancel' === $status ) { $active_ids = array_merge( $active_ids, $ids ); } $this->check_deadline( $deadline ); } $mrr = 0.0; $counted = 0; $currency = null; foreach ( array_slice( $active_ids, 0, self::MRR_CAP ) as $id ) { $sub = wcs_get_subscription( $id ); if ( ! $sub ) { continue; } $mrr += self::normalise_to_30_days( (float) $sub->get_total(), (string) $sub->get_billing_period(), (int) $sub->get_billing_interval() ); $counted++; $currency ??= (string) $sub->get_currency(); if ( 0 === $counted % 50 ) { $this->check_deadline( $deadline ); } } return array( 'summary' => true, 'counts_by_status' => $counts, 'active_count' => count( $active_ids ), 'mrr_30d' => $counted > 0 ? round( $mrr, 2 ) : null, 'mrr_counted' => $counted, 'mrr_partial' => count( $active_ids ) > $counted, 'currency' => $currency, ); } /** month → total/interval; week → total×30/7/interval; day → total×30/interval; year → total/(12×interval). */ public static function normalise_to_30_days( float $total, string $period, int $interval ): float { $iv = max( 1, $interval ); switch ( $period ) { case 'day': return $total * 30 / $iv; case 'week': return $total * ( 30 / 7 ) / $iv; case 'year': return $total / ( 12 * $iv ); default: return $total / $iv; } } }