PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 1.11.2
Subscriptions for WooCommerce with Stripe Recurring Payments v1.11.2
2.0.0 1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 All 61 releases
subscription / includes / Illuminate / Stats.php

Stats.php in Subscriptions for WooCommerce with Stripe Recurring Payments 1.11.2, at includes/Illuminate/Stats.php

209 lines 5.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SpringDevs\Subscription\Illuminate;
4
5 use SpringDevs\Subscription\Installer;
6
7 /**
8 * Collects subscription statistics over time.
9 *
10 * Computes monthly recurring revenue (MRR) and status counts, and writes one
11 * snapshot per calendar day into {prefix}subscrpt_stats_snapshot so reports
12 * (free, pro) and the recovery add-on can chart MRR/subscriptions over time.
13 *
14 * The snapshot runs at most once per day, triggered by the hourly cron and,
15 * as a low-traffic-site safety net, on admin page loads. The heavy aggregation
16 * therefore happens only once daily regardless of trigger.
17 *
18 * @package SpringDevs\Subscription\Illuminate
19 */
20 class Stats {
21
22 /**
23 * Option key holding the last snapshot date (Y-m-d, UTC).
24 *
25 * @var string
26 */
27 const LAST_SNAPSHOT_OPTION = 'subscrpt_stats_last_snapshot';
28
29 /**
30 * Months represented by one unit of each billing period (for MRR).
31 *
32 * @var array<string,float>
33 */
34 const MONTHS_PER_UNIT = array(
35 'day' => 0.03333333333,
36 'week' => 0.23333333333,
37 'month' => 1.0,
38 'year' => 12.0,
39 );
40
41 /**
42 * Initialize the class.
43 */
44 public function __construct() {
45 // Self-heal the snapshot table for installs that updated without reactivating.
46 Installer::maybe_upgrade();
47
48 add_action( 'subscrpt_hourly_cron', array( $this, 'maybe_take_daily_snapshot' ) );
49 add_action( 'admin_init', array( $this, 'maybe_take_daily_snapshot' ) );
50 }
51
52 /**
53 * Normalize a recurring amount to a monthly figure (MRR).
54 *
55 * mrr = amount / (interval * months_per_unit[period]). Unknown periods are
56 * treated as monthly; a zero cycle returns 0 to avoid division by zero.
57 *
58 * @param float $amount Raw recurring price.
59 * @param string $period Billing period: day|week|month|year.
60 * @param int $interval Billing interval (the "every N").
61 * @return float Monthly-normalized amount.
62 */
63 public static function normalize_mrr( $amount, $period, $interval ) {
64 $months_per_unit = isset( self::MONTHS_PER_UNIT[ $period ] ) ? self::MONTHS_PER_UNIT[ $period ] : 1.0;
65 $cycle_in_months = $interval * $months_per_unit;
66
67 if ( $cycle_in_months <= 0 ) {
68 return 0.0;
69 }
70
71 return round( $amount / $cycle_in_months, 2 );
72 }
73
74 /**
75 * Total monthly recurring revenue across all active subscriptions.
76 *
77 * @return float
78 */
79 public static function calculate_active_mrr() {
80 $ids = Helper::get_subscriptions(
81 array(
82 'status' => 'active',
83 'user_id' => -1,
84 'return' => 'ids',
85 'posts_per_page' => -1,
86 )
87 );
88
89 $total = 0.0;
90
91 foreach ( (array) $ids as $id ) {
92 $amount = (float) get_post_meta( $id, '_subscrpt_price', true );
93
94 $product_id = (int) get_post_meta( $id, '_subscrpt_product_id', true );
95 $variation_id = (int) get_post_meta( $id, '_subscrpt_variation_id', true );
96 $fallback_id = $variation_id ? $variation_id : $product_id;
97
98 $period = get_post_meta( $id, '_subscrpt_timing_option', true );
99 $period = $period ? (string) $period : get_post_meta( $fallback_id, '_subscrpt_timing_option', true );
100 $period = $period ? $period : 'month';
101
102 $interval = get_post_meta( $id, '_subscrpt_timing_per', true );
103 $interval = ! empty( $interval ) ? $interval : get_post_meta( $fallback_id, '_subscrpt_timing_per', true );
104 $interval = max( 1, (int) $interval );
105
106 $total += self::normalize_mrr( $amount, $period, $interval );
107 }
108
109 return round( $total, 2 );
110 }
111
112 /**
113 * Count subscriptions per status.
114 *
115 * @return array<string,int> Keyed by status (active, pending, ...).
116 */
117 public static function get_status_counts() {
118 $counts = wp_count_posts( 'subscrpt_order' );
119 $statuses = array( 'active', 'pending', 'on_hold', 'cancelled', 'expired', 'pe_cancelled' );
120 $out = array();
121
122 foreach ( $statuses as $status ) {
123 $out[ $status ] = isset( $counts->$status ) ? (int) $counts->$status : 0;
124 }
125
126 return $out;
127 }
128
129 /**
130 * Take today's snapshot unless one already exists for today.
131 *
132 * @return void
133 */
134 public function maybe_take_daily_snapshot() {
135 $today = gmdate( 'Y-m-d' );
136
137 if ( get_option( self::LAST_SNAPSHOT_OPTION ) === $today ) {
138 return;
139 }
140
141 $this->take_snapshot( $today );
142 update_option( self::LAST_SNAPSHOT_OPTION, $today );
143 }
144
145 /**
146 * Compute and persist a snapshot row for the given date.
147 *
148 * @param string $date Snapshot date (Y-m-d, UTC). Defaults to today.
149 * @return void
150 */
151 public function take_snapshot( $date = '' ) {
152 global $wpdb;
153
154 $date = $date ? $date : gmdate( 'Y-m-d' );
155 $counts = self::get_status_counts();
156
157 $wpdb->replace(
158 $wpdb->prefix . 'subscrpt_stats_snapshot',
159 array(
160 'snapshot_date' => $date,
161 'active_count' => $counts['active'],
162 'pending_count' => $counts['pending'],
163 'on_hold_count' => $counts['on_hold'],
164 'cancelled_count' => $counts['cancelled'],
165 'expired_count' => $counts['expired'],
166 'pe_cancelled_count' => $counts['pe_cancelled'],
167 'active_mrr' => self::calculate_active_mrr(),
168 'created_at' => current_time( 'mysql', true ),
169 ),
170 array( '%s', '%d', '%d', '%d', '%d', '%d', '%d', '%f', '%s' )
171 );
172 }
173
174 /**
175 * Read snapshot rows within a date range (inclusive), oldest first.
176 *
177 * @param string $from Start date (Y-m-d). Empty for no lower bound.
178 * @param string $to End date (Y-m-d). Empty for no upper bound.
179 * @return array<int,object> Snapshot rows.
180 */
181 public static function get_snapshots( $from = '', $to = '' ) {
182 global $wpdb;
183
184 $table = $wpdb->prefix . 'subscrpt_stats_snapshot';
185 $where = '1=1';
186 $args = array();
187
188 if ( $from ) {
189 $where .= ' AND snapshot_date >= %s';
190 $args[] = $from;
191 }
192
193 if ( $to ) {
194 $where .= ' AND snapshot_date <= %s';
195 $args[] = $to;
196 }
197
198 $sql = "SELECT * FROM {$table} WHERE {$where} ORDER BY snapshot_date ASC";
199
200 if ( $args ) {
201 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
202 $sql = $wpdb->prepare( $sql, $args );
203 }
204
205 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
206 return $wpdb->get_results( $sql );
207 }
208 }
209