PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
← All changes | app/Modules/MCP/Tools/SubscriptionTools.php +40 -9 1.5.3 → 1.6.5 View file →
@@ -3,8 +3,9 @@
3 3 namespace FluentCart\App\Modules\MCP\Tools;
4 4
5 5 use FluentCart\App\Helpers\Helper;
6 6 use FluentCart\App\Models\Subscription;
7 +use FluentCart\App\Modules\MCP\Support\AdvancedSearch;
7 8 use FluentCart\App\Modules\MCP\Support\MCPHelper;
8 9 use FluentCart\App\Modules\MCP\Support\PermissionGate;
9 10 use FluentCart\App\Modules\MCP\Support\WriteGuard;
10 11
@@ -23,15 +24,16 @@
23 24 class SubscriptionTools
24 25 {
25 26 public static function definitions()
26 27 {
27 - $statuses = ContextTools::ENUMS['subscription_statuses'];
28 - $intervals = ContextTools::ENUMS['billing_intervals'];
28 + $enums = ContextTools::enums();
29 + $statuses = $enums['subscription_statuses'];
30 + $intervals = $enums['billing_intervals'];
29 31
30 32 return [
31 33 'fluent-cart/list-subscriptions' => [
32 34 'label' => __('List Subscriptions', 'fluent-cart'),
33 - 'description' => __('Find and filter subscriptions. Compact rows carry customer, plan, status, recurring_total, interval, next/created/canceled dates, and installment fields: is_installment, installments_paid, installments_remaining, total_contract_value = recurring_total x bill_times (the full committed price). Use plan_type to split fixed-term installment/split-pay from open-ended recurring plans; next_billing_before for upcoming renewals; created_*/canceled_* ranges for cohorts and churn — a completed installment is paid-in-full, not churn. summary_only=true returns just the aggregates (count by status, committed recurring total, remaining installments). min_recurring is in store currency, not cents.', 'fluent-cart'),
35 + 'description' => __('Find and filter subscriptions. Compact rows carry customer, plan, status, recurring_total, interval, next/created/canceled dates, and installment fields: is_installment, installments_paid, installments_remaining, total_contract_value = recurring_total x bill_times (the full committed price). Use plan_type to split fixed-term installment/split-pay from open-ended recurring plans; next_billing_before for upcoming renewals; created_*/canceled_* ranges for cohorts and churn — a completed installment is paid-in-full, not churn. summary_only=true returns just the aggregates (count by status, committed recurring total, remaining installments). min_recurring is in store currency, not cents. For conditions these flat filters cannot express (OR groups, vendor/transaction ids, payment method, license properties, labels) pass advanced_filters — call get-search-schema entity=subscriptions first (Pro).', 'fluent-cart'),
34 36 'input_schema' => [
35 37 'type' => 'object',
36 38 'properties' => [
37 39 'status' => ['type' => 'string', 'enum' => $statuses],
@@ -45,11 +47,12 @@
45 47 'created_before' => ['type' => 'string', 'description' => 'YYYY-MM-DD or ISO 8601, UTC. Subscriptions started on or before this date.'],
46 48 'canceled_after' => ['type' => 'string', 'description' => 'YYYY-MM-DD or ISO 8601, UTC. Subscriptions canceled on or after this date. Pair with status=canceled to measure churn in a window.'],
47 49 'canceled_before' => ['type' => 'string', 'description' => 'YYYY-MM-DD or ISO 8601, UTC. Subscriptions canceled on or before this date.'],
48 50 'min_recurring' => ['type' => 'number', 'description' => 'Minimum recurring total in store currency.'],
51 + 'advanced_filters' => ['type' => 'array', 'items' => ['type' => ['object', 'array']], 'description' => 'Pro: condition groups {property, operator, value} — outer array = OR groups, inner = AND. Call get-search-schema entity=subscriptions FIRST for properties/operators/format. AND-combines with the other filters here (summary_only included). An empty array means no advanced filter.'],
49 52 'sort_by' => ['type' => 'string', 'enum' => ['id', 'next_billing_date', 'created_at', 'canceled_at', 'recurring_total'], 'default' => 'id'],
50 53 'sort_type' => ['type' => 'string', 'enum' => ['ASC', 'DESC'], 'default' => 'DESC'],
51 - 'fields' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Optional: return only these row keys to shrink the payload (subscription_id is always kept). Available: status, item_name, customer, recurring_total, billing_interval, next_billing_date, created_at, canceled_at, bill_count, bill_times, is_installment, installments_paid, installments_remaining, total_contract_value, currency, label. Omit for the full row.'],
54 + 'fields' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Optional: return only these row keys to shrink the payload (subscription_id is always kept). Available: status, item_name, customer, recurring_total, billing_interval, next_billing_date, created_at, canceled_at, bill_count, bill_times, plan_type, is_installment, installments_paid, installments_remaining, total_contract_value, currency, label. Omit for the full row.'],
52 55 'summary_only' => ['type' => 'boolean', 'description' => 'When true, return ONLY aggregates across all matching subscriptions — count_by_status, summed recurring_total, and total remaining installments — with no per-record array. Answers "how many active subs and how much is committed" with a tiny payload. Honors all the filters above.'],
53 56 'page' => ['type' => 'integer', 'default' => 1],
54 57 'per_page' => ['type' => 'integer', 'default' => 15, 'description' => 'Max 200.'],
55 58 ],
@@ -110,9 +113,23 @@
110 113 }
111 114
112 115 public static function listSubscriptions($params = [])
113 116 {
114 - $query = Subscription::query();
117 + // advanced_filters routes through the admin filter engine (validated
118 + // first — a bad condition errors, never silently drops); the named
119 + // filters below then AND onto the same query either way, and the
120 + // summary_only branch aggregates over the same filtered set.
121 + $advWarnings = [];
122 + if (!empty($params['advanced_filters'])) {
123 + $built = AdvancedSearch::buildQuery('subscriptions', $params['advanced_filters']);
124 + if (is_wp_error($built)) {
125 + return $built;
126 + }
127 + $query = $built['query'];
128 + $advWarnings = $built['warnings'];
129 + } else {
130 + $query = Subscription::query();
131 + }
115 132
116 133 foreach (['status', 'billing_interval'] as $col) {
117 134 if (!empty($params[$col])) {
118 135 $query->where($col, sanitize_text_field($params[$col]));
@@ -155,9 +172,9 @@
155 172 // Bonus: aggregate-only mode — counts + sums across ALL matching
156 173 // subscriptions (not just one page), no per-record array. Respects every
157 174 // filter applied above.
158 175 if (!empty($params['summary_only'])) {
159 - return self::summaryResponse($query);
176 + return self::summaryResponse($query, $advWarnings);
160 177 }
161 178
162 179 $paging = MCPHelper::pagination($params, 15, 200);
163 180 $query->with('customer');
@@ -177,8 +194,13 @@
177 194 foreach (MCPHelper::paginatorItems($paginator) as $sub) {
178 195 $rows[] = MCPHelper::pickFields(self::formatRow($sub), $fields, ['subscription_id']);
179 196 }
180 197
198 + $meta = MCPHelper::pagingMeta($paginator);
199 + if ($advWarnings) {
200 + $meta['warnings'] = $advWarnings;
201 + }
202 +
181 203 return MCPHelper::envelope(
182 204 sprintf(
183 205 /* translators: %d: number of matching subscriptions */
184 206 _n('%d subscription found.', '%d subscriptions found.', $total, 'fluent-cart'),
@@ -184,9 +206,9 @@
184 206 _n('%d subscription found.', '%d subscriptions found.', $total, 'fluent-cart'),
185 207 $total
186 208 ),
187 209 ['subscriptions' => $rows],
188 - MCPHelper::pagingMeta($paginator)
210 + $meta
189 211 );
190 212 }
191 213
192 214 /**
@@ -194,9 +216,9 @@
194 216 * recurring_total and total remaining installments across the full filtered
195 217 * set. Two lightweight GROUP BY / SUM scans, no row hydration. Money is in the
196 218 * store currency (subscriptions are not currency-scoped), matching formatRow.
197 219 */
198 - private static function summaryResponse($query)
220 + private static function summaryResponse($query, array $advWarnings = [])
199 221 {
200 222 $byStatusRows = (clone $query)
201 223 ->selectRaw('status, COUNT(*) as cnt, COALESCE(SUM(recurring_total), 0) as recurring_sum')
202 224 ->groupBy('status')
@@ -228,8 +250,13 @@
228 250 $totalCount,
229 251 MCPHelper::displayAmount($recurringSum, MCPHelper::currencyCode())
230 252 );
231 253
254 + $meta = ['currency' => MCPHelper::currencyCode(), 'note' => 'Aggregates across all matching subscriptions; money is in the store currency, not currency-scoped.'];
255 + if ($advWarnings) {
256 + $meta['warnings'] = $advWarnings;
257 + }
258 +
232 259 return MCPHelper::envelope(
233 260 $summary,
234 261 [
235 262 'summary_only' => true,
@@ -237,9 +264,9 @@
237 264 'recurring_total_sum' => MCPHelper::moneyCompact($recurringSum),
238 265 'remaining_installments_total' => $remaining,
239 266 'count_by_status' => $byStatus,
240 267 ],
241 - ['currency' => MCPHelper::currencyCode(), 'note' => 'Aggregates across all matching subscriptions; money is in the store currency, not currency-scoped.']
268 + $meta
242 269 );
243 270 }
244 271
245 272 private static function formatRow($sub)
@@ -263,8 +290,11 @@
263 290 'bill_count' => (int) $sub->bill_count,
264 291 'bill_times' => (int) $sub->bill_times,
265 292 // Derived installment view (bill_times > 0). total_contract_value is
266 293 // null for open-ended plans, which have no fixed committed total.
294 + // plan_type names the same distinction using the plan_types enum from
295 + // get-store-context, so the enum, the filter and the row all agree.
296 + 'plan_type' => $isInstallment ? 'installment' : 'recurring',
267 297 'is_installment' => $isInstallment,
268 298 'installments_paid' => (int) $sub->bill_count,
269 299 'installments_remaining' => $sub->installmentsRemaining(),
270 300 'total_contract_value' => $isInstallment ? MCPHelper::money($sub->totalContractValue(), $currency) : null,
@@ -320,8 +350,9 @@
320 350 'recurring_total' => MCPHelper::money($sub->recurring_total, $currency),
321 351 'bill_times' => (int) $sub->bill_times,
322 352 'bill_count' => (int) $sub->bill_count,
323 353 'collection_method' => $sub->collection_method,
354 + 'plan_type' => $sub->isInstallment() ? 'installment' : 'recurring',
324 355 'is_installment' => $sub->isInstallment(),
325 356 'installments_paid' => (int) $sub->bill_count,
326 357 'installments_remaining' => $sub->installmentsRemaining(),
327 358 'total_contract_value' => $sub->isInstallment() ? MCPHelper::money($sub->totalContractValue(), $currency) : null,