PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.2
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.2
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Services / Report / ReportHelper.php

ReportHelper.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.2, at app/Services/Report/ReportHelper.php

1,554 lines 63.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\App\Services\Report;
4
5 use FluentForm\App\Helpers\Helper;
6 use FluentForm\App\Models\EntryDetails;
7 use FluentForm\App\Models\Form;
8 use FluentForm\App\Models\FormAnalytics;
9 use FluentForm\App\Models\Log;
10 use FluentForm\App\Models\Submission;
11 use FluentForm\App\Modules\Form\FormFieldsParser;
12 use FluentForm\App\Modules\Payments\PaymentHelper;
13 use FluentForm\App\Services\Manager\FormManagerService;
14 use FluentForm\App\Services\Submission\SubmissionService;
15 use FluentForm\Framework\Helpers\ArrayHelper as Arr;
16
17 class ReportHelper
18 {
19 private static function scopeQueryToAllowedForms($query, $formId = null, $column = 'form_id')
20 {
21 if ($formId) {
22 return $query->where($column, $formId);
23 }
24
25 if (false !== ($allowedFormIds = FormManagerService::getUserAllowedFormsScope())) {
26 return $query->whereIn($column, $allowedFormIds ?: [0]);
27 }
28
29 return $query;
30 }
31
32 public static function generateReport($form, $statuses = ['read', 'unread', 'unapproved', 'approved', 'declined', 'unconfirmed', 'confirmed'])
33 {
34 $formInputs = FormFieldsParser::getEntryInputs($form, ['admin_label', 'element', 'options']);
35 $inputLabels = FormFieldsParser::getAdminLabels($form, $formInputs);
36
37 $elements = [];
38 foreach ($formInputs as $inputName => $input) {
39 $elements[$inputName] = $input['element'];
40 if ('select_country' == $input['element']) {
41 $formInputs[$inputName]['options'] = getFluentFormCountryList();
42 }
43 }
44
45 $reportableInputs = Helper::getReportableInputs();
46 $formReportableInputs = array_intersect($reportableInputs, array_values($elements));
47 $reportableInputs = Helper::getSubFieldReportableInputs();
48 $formSubFieldInputs = array_intersect($reportableInputs, array_values($elements));
49
50
51 if (!$formReportableInputs && !$formSubFieldInputs) {
52 return [
53 'report_items' => (object)[],
54 'total_entries' => static::getEntryCounts($form->id, $statuses),
55 'browsers' => static::getBrowserCounts($form->id, $statuses),
56 'devices' => static::getDeviceCounts($form->id, $statuses),
57 ];
58 }
59
60 $inputs = [];
61 $subfieldInputs = [];
62 foreach ($elements as $elementKey => $element) {
63 if (in_array($element, $formReportableInputs)) {
64 $inputs[$elementKey] = $element;
65 }
66 if (in_array($element, $formSubFieldInputs)) {
67 $subfieldInputs[$elementKey] = $element;
68 }
69 }
70
71 $reports = static::getInputReport($form->id, array_keys($inputs), $statuses);
72
73 $subFieldReports = static::getSubFieldInputReport($form->id, array_keys($subfieldInputs), $statuses);
74 $reports = array_merge($reports, $subFieldReports);
75 foreach ($reports as $reportKey => $report) {
76 $reports[$reportKey]['label'] = $inputLabels[$reportKey];
77 $reports[$reportKey]['element'] = Arr::get($inputs, $reportKey, []);
78 $reports[$reportKey]['options'] = $formInputs[$reportKey]['options'];
79 }
80
81 return [
82 'report_items' => $reports,
83 'total_entries' => static::getEntryCounts($form->id, $statuses),
84 'browsers' => static::getBrowserCounts($form->id, $statuses),
85 'devices' => static::getDeviceCounts($form->id, $statuses),
86 ];
87 }
88
89 public static function getInputReport(
90 $formId,
91 $fieldNames,
92 $statuses = ['read', 'unread', 'unapproved', 'approved', 'declined', 'unconfirmed', 'confirmed']
93 ) {
94 if (!$fieldNames) {
95 return [];
96 }
97
98 $reports = EntryDetails::select(['field_name', 'sub_field_name', 'field_value'])
99 ->where('form_id', $formId)
100 ->whereIn('field_name', $fieldNames)
101 ->when(
102 is_array($statuses) && (count($statuses) > 0),
103 function ($q) use ($statuses) {
104 return $q->whereHas('submission', function ($q) use ($statuses) {
105 return $q->whereIn('status', $statuses);
106 });
107 })
108 ->selectRaw('COUNT(field_name) AS total_count')
109 ->groupBy(['field_name', 'field_value'])
110 ->get();
111
112 $formattedReports = [];
113 foreach ($reports as $report) {
114 $formattedReports[$report->field_name]['reports'][] = [
115 'value' => Helper::safeUnserialize($report->field_value),
116 'count' => $report->total_count,
117 'sub_field' => $report->sub_field_name,
118 ];
119
120 $formattedReports[$report->field_name]['total_entry'] = static::getEntryTotal($report->field_name, $formId,
121 $statuses);
122 }
123 if ($formattedReports) {
124 //sync with form field order
125 $formattedReports = array_replace(array_intersect_key(array_flip($fieldNames), $formattedReports),
126 $formattedReports);
127 }
128 return $formattedReports;
129 }
130
131 public static function getSubFieldInputReport($formId, $fieldNames, $statuses)
132 {
133 if (!$fieldNames) {
134 return [];
135 }
136
137 $reports = EntryDetails::select(['field_name', 'sub_field_name', 'field_value'])
138 ->selectRaw('COUNT(field_name) AS total_count')
139 ->where('form_id', $formId)
140 ->whereIn('field_name', $fieldNames)
141 ->when(
142 is_array($statuses) && (count($statuses) > 0),
143 function ($q) use ($statuses) {
144 return $q->whereHas('submission', function ($q) use ($statuses) {
145 return $q->whereIn('status', $statuses);
146 });
147 })
148 ->groupBy(['field_name', 'field_value', 'sub_field_name'])
149 ->get()->toArray();
150 return static::getFormattedReportsForSubInputs($reports, $formId, $statuses);
151 }
152
153 protected static function getFormattedReportsForSubInputs($reports, $formId, $statuses)
154 {
155 if (!count($reports)) {
156 return [];
157 }
158 $formattedReports = [];
159 foreach ($reports as $report) {
160 static::setReportForSubInput((array)$report, $formattedReports);
161 }
162 foreach ($formattedReports as $fieldName => $val) {
163 $formattedReports[$fieldName]['total_entry'] = static::getEntryTotal(
164 Arr::get($report, 'field_name'),
165 $formId,
166 $statuses
167 );
168 $formattedReports[$fieldName]['reports'] = array_values(
169 $formattedReports[$fieldName]['reports']
170 );
171 }
172 return $formattedReports;
173 }
174
175 protected static function setReportForSubInput($report, &$formattedReports)
176 {
177 $filedValue = Helper::safeUnserialize(Arr::get($report, 'field_value'));
178
179 if (is_array($filedValue)) {
180 foreach ($filedValue as $fVal) {
181 static::setReportForSubInput(
182 array_merge($report, ['field_value' => $fVal]),
183 $formattedReports
184 );
185 }
186 } else {
187 $value = Arr::get($report, 'sub_field_name') . ' : ' . $filedValue;
188 $count = Arr::get($formattedReports, $report['field_name'] . '.reports.' . $value . '.count');
189 $count = $count ? $count + Arr::get($report, 'total_count') : Arr::get($report, 'total_count');
190
191 $formattedReports[$report['field_name']]['reports'][$value] = [
192 'value' => $value,
193 'count' => $count,
194 'sub_field' => $report['sub_field_name'],
195 ];
196 }
197 }
198
199 public static function getEntryTotal($fieldName, $formId, $statuses = false)
200 {
201 return EntryDetails::select('id')->where('form_id', $formId)
202 ->where('field_name', $fieldName)
203 ->when(
204 is_array($statuses) && (count($statuses) > 0),
205 function ($q) use ($statuses) {
206 return $q->whereHas('submission', function ($q) use ($statuses) {
207 return $q->whereIn('status', $statuses);
208 });
209 }
210 )
211 ->distinct(['field_name', 'submission_id'])
212 ->count();
213 }
214
215 private static function getEntryCounts($formId, $statuses = false)
216 {
217 return Submission::where('form_id', $formId)
218 ->when(
219 is_array($statuses) && (count($statuses) > 0),
220 function ($q) use ($statuses) {
221 return $q->whereIn('status', $statuses);
222 })
223 ->when(!$statuses, function ($q) {
224 return $q->where('status', '!=', 'trashed');
225 })->count();
226 }
227
228 public static function getBrowserCounts($formId, $statuses = false)
229 {
230 return static::getCounts($formId, 'browser', $statuses);
231 }
232
233 public static function getDeviceCounts($formId, $statuses = false)
234 {
235 return static::getCounts($formId, 'device', $statuses);
236 }
237
238 private static function getCounts($formId, $for, $statuses)
239 {
240 $deviceCounts = Submission::select([
241 "$for",
242 ])
243 ->selectRaw('COUNT(id) as total_count')
244 ->where('form_id', $formId)
245 ->when(
246 is_array($statuses) && (count($statuses) > 0),
247 function ($q) use ($statuses) {
248 return $q->whereIn('status', $statuses);
249 })
250 ->when(!$statuses, function ($q) {
251 return $q->where('status', '!=', 'trashed');
252 })
253 ->groupBy("$for")->get();
254
255 $formattedData = [];
256 foreach ($deviceCounts as $deviceCount) {
257 $formattedData[$deviceCount->{$for}] = $deviceCount->total_count;
258 }
259 return $formattedData;
260 }
261
262 public static function maybeMigrateData($formId)
263 {
264 if ('yes' == Helper::getFormMeta($formId, 'report_data_migrated')) {
265 return true;
266 }
267
268 // Check if migration is already in progress (with 5-minute staleness timeout)
269 $migrationStatus = Helper::getFormMeta($formId, 'report_data_migration_status');
270 if ($migrationStatus && strpos($migrationStatus, 'processing:') === 0) {
271 $startedAt = (int) substr($migrationStatus, 11);
272 if ((time() - $startedAt) < 300) {
273 return true;
274 }
275 }
276
277 Helper::setFormMeta($formId, 'report_data_migration_status', 'processing:' . time());
278
279 // Dispatch a non-blocking background request
280 $args = [
281 'timeout' => 0.1,
282 'blocking' => false,
283 'body' => [
284 'action' => 'fluentform_report_data_migrate',
285 'form_id' => $formId,
286 'nonce' => wp_create_nonce('fluentform_report_data_migrate'),
287 ],
288 'cookies' => is_user_logged_in() ? wp_unslash($_COOKIE) : [],
289 'sslverify' => apply_filters('fluentform/https_local_ssl_verify', false),
290 ];
291
292 wp_remote_post(esc_url_raw(Helper::getAjaxUrl()), $args);
293
294 return true;
295 }
296
297 /**
298 * Ensure submission_id index exists on entry_details table for efficient anti-join queries.
299 */
300 private static $indexEnsured = false;
301
302 private static function ensureEntryDetailsIndex()
303 {
304 if (static::$indexEnsured) {
305 return;
306 }
307
308 global $wpdb;
309 $table = $wpdb->prefix . 'fluentform_entry_details';
310
311 // Check if index already exists
312 $indexExists = $wpdb->get_var($wpdb->prepare(
313 "SELECT COUNT(1) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND INDEX_NAME = 'idx_submission_id'",
314 $table
315 ));
316
317 if (!$indexExists) {
318 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $table is hardcoded prefix + table name, not user input
319 $wpdb->query("ALTER TABLE `{$table}` ADD INDEX `idx_submission_id` (`submission_id`)");
320 }
321
322 static::$indexEnsured = true;
323 }
324
325 public static function runMigrationBatch($formId)
326 {
327 if ('yes' == Helper::getFormMeta($formId, 'report_data_migrated')) {
328 return;
329 }
330
331 // Acquire a MySQL advisory lock to prevent concurrent batch execution per form.
332 // GET_LOCK returns 1 if acquired, 0 if timed out, NULL on error.
333 global $wpdb;
334 $lockName = 'ff_report_migrate_' . intval($formId);
335 $gotLock = $wpdb->get_var($wpdb->prepare("SELECT GET_LOCK(%s, 0)", $lockName));
336
337 if (!$gotLock) {
338 return; // Another worker holds the lock for this form
339 }
340
341 try {
342 // Ensure index exists for efficient doesntHave('entryDetails') queries
343 static::ensureEntryDetailsIndex();
344
345 $batchSize = apply_filters('fluentform/report_migration_batch_size', 200);
346 $maxBatches = apply_filters('fluentform/report_migration_max_batches', 50);
347 $submissionService = new SubmissionService();
348 $lastId = (int) Helper::getFormMeta($formId, 'report_data_migration_last_id');
349
350 for ($batch = 0; $batch < $maxBatches; $batch++) {
351 $unmigratedData = Submission::select(['id', 'response'])
352 ->where('form_id', $formId)
353 ->where('id', '>', $lastId)
354 ->doesntHave('entryDetails')
355 ->orderBy('id', 'ASC')
356 ->limit($batchSize)
357 ->get();
358
359 if (!$unmigratedData || count($unmigratedData) === 0) {
360 break;
361 }
362
363 foreach ($unmigratedData as $datum) {
364 $lastId = $datum->id;
365 $value = json_decode($datum->response, true);
366 if (is_array($value) && !empty($value)) {
367 $submissionService->recordEntryDetails($datum->id, $formId, $value);
368 }
369
370 // If no entry_details were created (empty/invalid response or all values filtered),
371 // insert a sentinel so doesntHave('entryDetails') excludes this row
372 $hasDetails = EntryDetails::where('submission_id', $datum->id)->exists();
373 if (!$hasDetails) {
374 EntryDetails::insert([
375 'form_id' => $formId,
376 'submission_id' => $datum->id,
377 'field_name' => '_migration_skipped',
378 'sub_field_name' => '',
379 'field_value' => '',
380 ]);
381 }
382 }
383
384 Helper::setFormMeta($formId, 'report_data_migration_last_id', $lastId);
385
386 if ($unmigratedData->count() < $batchSize) {
387 break;
388 }
389 }
390
391 // Only mark complete if no unmigrated rows remain
392 $remaining = Submission::where('form_id', $formId)
393 ->doesntHave('entryDetails')
394 ->count();
395
396 if ($remaining === 0) {
397 Helper::setFormMeta($formId, 'report_data_migrated', 'yes');
398 Helper::setFormMeta($formId, 'report_data_migration_last_id', 0);
399 }
400
401 Helper::setFormMeta($formId, 'report_data_migration_status', 'completed');
402 } finally {
403 $wpdb->query($wpdb->prepare("SELECT RELEASE_LOCK(%s)", $lockName));
404 }
405 }
406
407 /**
408 * Get overview chart data
409 */
410 public static function getOverviewChartData($startDate, $endDate, $formId, $view)
411 {
412 // Process and fix date ranges if needed
413 list($startDate, $endDate) = self::processDateRange($startDate, $endDate);
414
415 // Calculate date difference to determine grouping
416 $startDateTime = new \DateTime($startDate);
417 $endDateTime = new \DateTime($endDate);
418 $interval = $startDateTime->diff($endDateTime);
419 $daysInterval = $interval->days + 1;
420
421 // Determine grouping mode based on date range
422 $groupingMode = self::getGroupingMode($daysInterval);
423 $data = self::getAggregatedData($startDate, $endDate, $groupingMode, $view, $formId);
424 // Get date labels based on grouping mode
425 $dateLabels = self::getDateLabels($startDateTime, $endDateTime, $groupingMode);
426 // Format the data for the chart
427 $chartData = self::formatDataForChart($dateLabels, $data, $formId);
428 // Get views data based on the ip
429 $views = self::getFormViews($startDate, $endDate, $groupingMode, $formId);
430 if ($views) {
431 $chartData['values']['views'] = array_values(self::fillMissingData($dateLabels['dates'], $views));
432 }
433 return $chartData;
434 }
435
436
437
438
439
440 public static function getFormStats($startDate, $endDate, $formId)
441 {
442 // Process and fix date ranges if needed
443 list($startDate, $endDate) = self::processDateRange($startDate, $endDate);
444
445 // Calculate the date range duration to determine previous period
446 $startDateTime = new \DateTime($startDate);
447 $endDateTime = new \DateTime($endDate);
448 $interval = $startDateTime->diff($endDateTime);
449 $daysDifference = $interval->days;
450
451 // Calculate previous period dates (same duration, shifted back)
452 $previousEndDateTime = clone $startDateTime;
453 $previousEndDateTime->modify('-1 day');
454 $previousStartDateTime = clone $previousEndDateTime;
455 $previousStartDateTime->modify("-{$daysDifference} days");
456
457 $previousStartDate = $previousStartDateTime->format('Y-m-d H:i:s');
458 $previousEndDate = $previousEndDateTime->format('Y-m-d H:i:s');
459
460 // Get submission counts
461 $periodSubmissionsQuery = Submission::whereBetween('created_at', [$startDate, $endDate]);
462 self::scopeQueryToAllowedForms($periodSubmissionsQuery, $formId);
463 $periodSubmissions = $periodSubmissionsQuery->count();
464
465 $previousPeriodSubmissionsQuery = Submission::whereBetween('created_at', [$previousStartDate, $previousEndDate]);
466 self::scopeQueryToAllowedForms($previousPeriodSubmissionsQuery, $formId);
467 $previousPeriodSubmissions = $previousPeriodSubmissionsQuery->count();
468
469 // Get submission status counts (grouped)
470 $statusCountsQuery = Submission::whereBetween('created_at', [$startDate, $endDate])
471 ->selectRaw('status, COUNT(*) as count')
472 ->groupBy('status');
473 self::scopeQueryToAllowedForms($statusCountsQuery, $formId);
474 $statusCounts = $statusCountsQuery->pluck('count', 'status');
475
476 $unreadSubmissions = intval(Arr::get($statusCounts, 'unread', 0));
477 $readSubmissions = intval(Arr::get($statusCounts, 'read', 0));
478 $periodSpamSubmissions = intval(Arr::get($statusCounts, 'spam', 0));
479
480
481 $previousStatusCountsQuery = Submission::whereBetween('created_at', [$previousStartDate, $previousEndDate])
482 ->selectRaw('status, COUNT(*) as count')
483 ->groupBy('status');
484 self::scopeQueryToAllowedForms($previousStatusCountsQuery, $formId);
485 $previousStatusCounts = $previousStatusCountsQuery->pluck('count', 'status');
486
487 $previousSpamSubmissions = intval(Arr::get($previousStatusCounts, 'spam', 0));
488
489 // Get active integrations count from wp_options
490 $modulesStatus = get_option('fluentform_global_modules_status');
491 $activeIntegrations = count(array_filter($modulesStatus, function ($status) {
492 return $status === 'yes' || $status == 1 || $status == 'true';
493 }));
494
495 // Calculate period growth percentage
496 $growthPercentage = 0;
497 if ($previousPeriodSubmissions > 0) {
498 $growthPercentage = round((($periodSubmissions - $previousPeriodSubmissions) / $previousPeriodSubmissions) * 100,
499 1);
500 } elseif ($periodSubmissions > 0) {
501 $growthPercentage = 100;
502 }
503
504 $growthText = $growthPercentage > 0 ? '+' . $growthPercentage . '%' : $growthPercentage . '%';
505 $growthType = $growthPercentage > 0 ? 'up' : ($growthPercentage < 0 ? 'down' : 'neutral');
506
507 // calculate spam percentage
508 $spamPercentage = 0;
509 if ($previousSpamSubmissions > 0) {
510 $spamPercentage = round((($periodSpamSubmissions - $previousSpamSubmissions) / $previousSpamSubmissions) * 100,
511 1);
512 } elseif ($periodSpamSubmissions > 0) {
513 $spamPercentage = 100;
514 }
515 $spamText = $spamPercentage > 0 ? '+' . $spamPercentage . '%' : $spamPercentage . '%';
516 $spamType = $spamPercentage > 0 ? 'down' : ($spamPercentage < 0 ? 'up' : 'neutral'); // Refunds going up is bad
517
518 // Active forms
519 $periodActiveFormsQuery = Form::where('status', 'published')->whereBetween('created_at', [$startDate, $endDate]);
520 self::scopeQueryToAllowedForms($periodActiveFormsQuery, $formId, 'id');
521 $periodActiveFormsCount = $periodActiveFormsQuery->count();
522
523 $previousActiveFormsQuery = Form::where('status', 'published')->whereBetween('created_at', [$previousStartDate, $previousEndDate]);
524 self::scopeQueryToAllowedForms($previousActiveFormsQuery, $formId, 'id');
525 $previousActiveFormsCount = $previousActiveFormsQuery->count();
526 $activeFormsPercentage = 0;
527 if ($previousActiveFormsCount > 0) {
528 $activeFormsPercentage = round((($periodActiveFormsCount - $previousActiveFormsCount) / $previousActiveFormsCount) * 100,
529 1);
530 } elseif ($periodActiveFormsCount > 0) {
531 $activeFormsPercentage = 100;
532 }
533 $activeFormsText = $activeFormsPercentage > 0 ? '+' . $activeFormsPercentage . '%' : $activeFormsPercentage . '%';
534 $activeFormsType = $activeFormsPercentage > 0 ? 'up' : ($activeFormsPercentage < 0 ? 'down' : 'neutral');
535
536 $readRate = $periodSubmissions > 0 ? round(($readSubmissions / $periodSubmissions) * 100, 1) : 0;
537
538 $stats = [
539 'period' => $daysDifference . ' days',
540 'total_submissions' => [
541 'value' => $periodSubmissions,
542 'period_value' => $periodSubmissions,
543 'change' => $growthText,
544 'change_type' => $growthType
545 ],
546 'spam_submissions' => [
547 'value' => $periodSpamSubmissions,
548 'period_value' => $previousSpamSubmissions,
549 'change' => $spamText,
550 'change_type' => $spamType
551 ],
552 'active_integrations' => [
553 'value' => $activeIntegrations,
554 ],
555 'unread_submissions' => [
556 'value' => $unreadSubmissions,
557 ],
558 'read_submissions' => [
559 'value' => $readSubmissions,
560 ],
561 'active_forms' => [
562 'value' => $periodActiveFormsCount,
563 'change' => $activeFormsText,
564 'change_type' => $activeFormsType
565 ],
566 'read_submission_rate' => [
567 'value' => $readRate,
568 ]
569 ];
570
571 // Add payment statistics if payment module is enabled
572 $paymentSettings = get_option('__fluentform_payment_module_settings');
573 if ($paymentSettings && Arr::get($paymentSettings, 'status') === 'yes') {
574 // Get payment statistics
575 $paymentStats = self::getPaymentStats($startDate, $endDate, $previousStartDate, $previousEndDate, $formId);
576 $stats = array_merge($stats, $paymentStats);
577 }
578
579 return $stats;
580 }
581
582
583 /**
584 * Initialize heatmap data structure based on aggregation type
585 */
586 protected static function initializeHeatmapData($aggregationType)
587 {
588 if ($aggregationType === 'day_of_week') {
589 $dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
590 $heatmapData = [];
591 foreach ($dayNames as $day) {
592 $heatmapData[$day] = array_fill(0, 24, 0); // 24 time slots (0-23 hours), all initialized to 0
593 }
594 return $heatmapData;
595 }
596
597 return [];
598 }
599
600 /**
601 * Get submission data for heatmap with appropriate grouping
602 * Optimized version with better query performance
603 */
604 protected static function getHeatmapSubmissionData($startDate, $endDate, $formId, $aggregationType)
605 {
606 if ($aggregationType === 'day_of_week') {
607 $query = Submission::selectRaw('
608 DAYOFWEEK(created_at) as day_of_week,
609 HOUR(created_at) as submission_hour,
610 COUNT(*) as count
611 ')
612 ->whereBetween('created_at', [$startDate, $endDate])
613 ->whereNotIn('status', ['trashed', 'spam'])
614 ->groupBy('day_of_week', 'submission_hour')
615 ->orderBy('day_of_week')
616 ->orderBy('submission_hour');
617
618 self::scopeQueryToAllowedForms($query, $formId);
619
620 return $query->get();
621 }
622
623 return collect([]);
624 }
625
626
627 public static function getApiLogs($startDate, $endDate, $formId = null)
628 {
629 // Process date range
630 list($startDate, $endDate) = self::processDateRange($startDate, $endDate);
631
632 // Calculate date difference to determine grouping
633 $startDateTime = new \DateTime($startDate);
634 $endDateTime = new \DateTime($endDate);
635 $interval = $startDateTime->diff($endDateTime);
636 $daysInterval = $interval->days + 1;
637
638 // Determine grouping mode based on date range
639 $groupingMode = self::getGroupingMode($daysInterval);
640
641 // Define the date format based on grouping mode
642 if ($groupingMode === 'day') {
643 $dateFormat = "DATE(created_at)";
644 } elseif ($groupingMode === '3days') {
645 $dateFormat = "DATE(created_at)";
646 } elseif ($groupingMode === 'week') {
647 $dateFormat = "DATE(DATE_ADD(created_at, INTERVAL(-WEEKDAY(created_at)) DAY))";
648 } else { // month
649 $dateFormat = "DATE_FORMAT(created_at, '%Y-%m-01')";
650 }
651
652 // Components to exclude
653 $excludedComponents = [
654 'postFeeds',
655 'AdminApproval',
656 'Payment',
657 'EntryEditor',
658 'DoubleOptin',
659 'Subscription',
660 'UserRegistration',
661 'Akismet Integration',
662 'CleanTalk API Integration'
663 ];
664
665 // Get logs grouped by date and status using Eloquent, excluding specific components
666 $logsQuery = Log::whereBetween('created_at', [$startDate, $endDate]);
667
668 // Exclude components - handle both NULL and specific values
669 $logsQuery->where(function ($query) use ($excludedComponents) {
670 $query->whereNull('component')
671 ->orWhereNotIn('component', $excludedComponents);
672 });
673
674 self::scopeQueryToAllowedForms($logsQuery, $formId, 'parent_source_id');
675
676 $results = $logsQuery->selectRaw($dateFormat . ' as log_date')
677 ->selectRaw('status')
678 ->selectRaw('COUNT(*) as count')
679 ->groupBy('log_date', 'status')
680 ->orderBy('log_date')
681 ->get();
682
683 // Get total counts by status (also excluding the specific components)
684 $totalsQuery = Log::whereBetween('created_at', [$startDate, $endDate]);
685
686 $totalsQuery->where(function ($query) use ($excludedComponents) {
687 $query->whereNull('component')
688 ->orWhereNotIn('component', $excludedComponents);
689 });
690
691 self::scopeQueryToAllowedForms($totalsQuery, $formId, 'parent_source_id');
692
693 $totalsResults = $totalsQuery->selectRaw('status')
694 ->selectRaw('COUNT(*) as count')
695 ->groupBy('status')
696 ->get();
697
698 $totals = [
699 'success' => 0,
700 'pending' => 0,
701 'failed' => 0
702 ];
703
704 foreach ($totalsResults as $total) {
705 $status = strtolower($total->status);
706 if (isset($totals[$status])) {
707 $totals[$status] = (int)$total->count;
708 }
709 }
710
711 // Get date labels and prepare data
712 $dateLabels = self::getDateLabels($startDateTime, $endDateTime, $groupingMode);
713 $dates = $dateLabels['dates'];
714 $formattedLabels = $dateLabels['labels'];
715
716 // Initialize data structure - always with all dates, even if no data exists
717 $seriesData = [
718 'success' => array_fill_keys($dates, 0),
719 'pending' => array_fill_keys($dates, 0),
720 'failed' => array_fill_keys($dates, 0)
721 ];
722
723 // Fill in data from results when available
724 foreach ($results as $row) {
725 $date = $row->log_date;
726 $status = strtolower($row->status);
727 $count = (int)$row->count;
728
729 // Map status to our categories
730 if ($status === 'success' || $status === 'pending' || $status === 'failed') {
731 if (isset($seriesData[$status][$date])) {
732 $seriesData[$status][$date] = $count;
733 }
734 }
735 }
736
737 return [
738 'logs_data' => [
739 'categories' => $formattedLabels,
740 'series' => $seriesData
741 ],
742 'totals' => $totals,
743 'start_date' => $startDate,
744 'end_date' => $endDate
745 ];
746 }
747
748 /**
749 * Get top performing forms by entries, views, or payments
750 */
751 public static function getTopPerformingForms($startDate, $endDate, $metric = 'entries', $formIds = false)
752 {
753 list($startDate, $endDate) = self::processDateRange($startDate, $endDate);
754 global $wpdb;
755 $prefix = $wpdb->prefix;
756 $formResults = [];
757 $disableMessage = '';
758 $hasFormScope = false !== $formIds;
759 $formIds = $hasFormScope
760 ? array_values(array_filter(array_map('intval', (array) $formIds)))
761 : [];
762
763 switch ($metric) {
764 case 'entries':
765 // Use Form model with Submission relationship
766 $results = Form::select(['id', 'title'])
767 ->withCount([
768 'submissions' => function ($q) use ($startDate, $endDate) {
769 $q->whereBetween('created_at', [$startDate, $endDate]);
770 $q->whereNotIn('status', ['trashed', 'spam']);
771 }
772 ]);
773
774 if ($hasFormScope) {
775 $results->whereIn('id', $formIds ?: [0]);
776 }
777
778 $results = $results
779 ->orderBy('submissions_count', 'DESC')
780 ->limit(5)
781 ->get();
782
783 // Map the results to standard format
784 foreach ($results as $form) {
785 $formResults[] = (object)[
786 'id' => $form->id,
787 'title' => $form->title,
788 'value' => $form->submissions_count
789 ];
790 }
791 break;
792
793 case 'payments':
794 // Check if payment module is enabled
795 $paymentSettings = get_option('__fluentform_payment_module_settings');
796 if ($paymentSettings && Arr::get($paymentSettings, 'status')) {
797 $results = wpFluent()->table('fluentform_forms')
798 ->select([
799 'fluentform_forms.id',
800 'fluentform_forms.title',
801 wpFluent()->raw("COALESCE(SUM({$prefix}fluentform_transactions.payment_total), 0) as raw_value")
802 ])
803 ->leftJoin('fluentform_transactions', 'fluentform_forms.id', '=',
804 'fluentform_transactions.form_id')
805 ->whereBetween('fluentform_transactions.created_at', [$startDate, $endDate])
806 ->where('fluentform_transactions.status', 'paid');
807
808 if ($hasFormScope) {
809 $results->whereIn('fluentform_forms.id', $formIds ?: [0]);
810 }
811
812 $results = $results
813 ->groupBy('fluentform_forms.id')
814 ->orderBy('raw_value', 'DESC')
815 ->limit(5)
816 ->get();
817
818 // Convert cents to dollars in PHP for better precision
819 foreach ($results as $form) {
820 $form->value = round((float)$form->raw_value / 100, 2);
821 }
822
823 $formResults = $results;
824 } else {
825 $disableMessage = __('Payment module is disabled. Please enable it to view top performing form by payments.', 'fluentform');
826 }
827 break;
828
829 case 'views':
830 // Count unique views by IP from analytics table if analytics enabled
831 if (!apply_filters('fluentform/disabled_analytics', true)) {
832 $results = wpFluent()->table('fluentform_forms')
833 ->select([
834 'fluentform_forms.id',
835 'fluentform_forms.title',
836 wpFluent()->raw("COUNT(DISTINCT {$prefix}fluentform_form_analytics.ip) as value")
837 ])
838 ->leftJoin('fluentform_form_analytics', 'fluentform_forms.id', '=',
839 'fluentform_form_analytics.form_id')
840 ->whereBetween('fluentform_form_analytics.created_at', [$startDate, $endDate]);
841
842 if ($hasFormScope) {
843 $results->whereIn('fluentform_forms.id', $formIds ?: [0]);
844 }
845
846 $results = $results
847 ->groupBy('fluentform_forms.id')
848 ->orderBy('value', 'DESC')
849 ->limit(5)
850 ->get();
851
852 $formResults = $results;
853 } else {
854 $disableMessage = __('Analytics is disabled. Please enable it to view top performing form by views.', 'fluentform');
855 }
856 break;
857 }
858
859 // Common formatting for all results
860 $topForms = [];
861 foreach ($formResults as $form) {
862 if ((float)$form->value > 0) {
863 $topForms[] = [
864 'id' => $form->id,
865 'title' => $form->title ?: 'Untitled Form',
866 'value' => (float)$form->value
867 ];
868 }
869 }
870 return [
871 'disable_message' => $disableMessage,
872 'data' => array_reverse($topForms)
873 ];
874 }
875
876 /**
877 * Get form views date chunks
878 */
879 private static function getFormViews($startDate, $endDate, $groupingMode, $formId)
880 {
881 if (apply_filters('fluentform/disabled_analytics', true)) {
882 return [];
883 }
884
885 // 1. Get UNIQUE VIEWS by IP address
886 $viewsQuery = FormAnalytics::whereBetween('created_at', [$startDate, $endDate])
887 ->whereNotNull('ip');
888 self::scopeQueryToAllowedForms($viewsQuery, $formId);
889
890 // Group by date and IP to count unique visitors
891 if ($groupingMode === 'day') {
892 $viewsQuery->selectRaw('DATE(created_at) as date_group, COUNT(DISTINCT ip) as unique_count');
893 } elseif ($groupingMode === '3days') {
894 // Get min date for reference
895 $minDateQuery = FormAnalytics::whereBetween('created_at', [$startDate, $endDate])
896 ->selectRaw('MIN(DATE(created_at)) as min_date');
897 self::scopeQueryToAllowedForms($minDateQuery, $formId);
898 $minDateRecord = $minDateQuery->first();
899
900 if ($minDateRecord && $minDateRecord->min_date) {
901 $minDate = $minDateRecord->min_date;
902 $viewsQuery->selectRaw("FLOOR(DATEDIFF(DATE(created_at), ?) / 3) as group_num", [$minDate])
903 ->selectRaw('MIN(DATE(created_at)) as date_group')
904 ->selectRaw('COUNT(DISTINCT ip) as unique_count')
905 ->groupBy('group_num');
906 } else {
907 $viewsQuery->selectRaw('DATE(created_at) as date_group, COUNT(DISTINCT ip) as unique_count')
908 ->groupBy('date_group');
909 }
910 } elseif ($groupingMode === 'week') {
911 $viewsQuery->selectRaw("DATE(DATE_ADD(created_at, INTERVAL(-WEEKDAY(created_at)) DAY)) as date_group, COUNT(DISTINCT ip) as unique_count");
912 } else { // month
913 $viewsQuery->selectRaw("DATE_FORMAT(created_at, '%Y-%m-01') as date_group, COUNT(DISTINCT ip) as unique_count");
914 }
915
916 if ($groupingMode !== '3days' || !(isset($minDateRecord) && $minDateRecord->min_date)) {
917 $viewsQuery->groupBy('date_group');
918 }
919
920 $results = $viewsQuery->orderBy('date_group')->get();
921 $views = [];
922 foreach ($results as $result) {
923 $views[$result->date_group] = $result->unique_count;
924 }
925 return $views;
926 }
927
928 /**
929 * Process date range
930 */
931 public static function processDateRange($startDate, $endDate)
932 {
933 // Validate date formats
934 if (!strtotime($startDate) || !strtotime($endDate)) {
935 return [];
936 }
937
938 // Sanity check - ensure start date is before end date
939 $startDateTime = new \DateTime($startDate);
940 $endDateTime = new \DateTime($endDate);
941
942 // If start date is after end date, swap them
943 if ($startDateTime > $endDateTime) {
944 $temp = $startDate;
945 $startDate = $endDate;
946 $endDate = $temp;
947 }
948
949 return [$startDate, $endDate];
950 }
951
952 /**
953 * Determine grouping mode based on date range
954 */
955 private static function getGroupingMode($daysInterval)
956 {
957 if ($daysInterval <= 7) {
958 return 'day'; // 1-7 days: group by day
959 } elseif ($daysInterval <= 31) {
960 return '3days'; // 8-31 days: group by 3 days
961 } elseif ($daysInterval <= 92) {
962 return 'week'; // Group by week for 1-3 months
963 } else {
964 return 'month'; // 3+ months: group by month
965 }
966 }
967
968 /**
969 * Get aggregated data based on grouping mode
970 */
971 private static function getAggregatedData($startDate, $endDate, $groupingMode, $view, $formId)
972 {
973 $baseQuery = Submission::whereBetween('created_at', [$startDate, $endDate]);
974 self::scopeQueryToAllowedForms($baseQuery, $formId);
975
976 if ($view === 'revenue') {
977 // Clone the base query for each payment status
978 $paidQuery = clone $baseQuery;
979 $pendingQuery = clone $baseQuery;
980 $refundedQuery = clone $baseQuery;
981
982 // Get paid payments
983 $paidQuery->whereNotNull('payment_total')
984 ->where(function ($query) {
985 $query->where('payment_status', 'paid');
986 })
987 ->selectRaw('ROUND(SUM(payment_total) / 100, 2) as count');
988
989 // Get pending payments
990 $pendingQuery->whereNotNull('payment_total')
991 ->where('payment_status', 'pending')
992 ->selectRaw('ROUND(SUM(payment_total) / 100, 2) as count');
993
994 // Get refunded payments
995 $refundedQuery->whereNotNull('payment_total')
996 ->where('payment_status', 'refunded')
997 ->selectRaw('ROUND(SUM(payment_total) / 100, 2) as count');
998
999 // Apply grouping based on mode to all three queries
1000 if ($groupingMode === 'day') {
1001 $paidQuery->selectRaw('DATE(created_at) as date_group')->groupBy('date_group');
1002 $pendingQuery->selectRaw('DATE(created_at) as date_group')->groupBy('date_group');
1003 $refundedQuery->selectRaw('DATE(created_at) as date_group')->groupBy('date_group');
1004 } elseif ($groupingMode === '3days') {
1005 // Get minimum date for reference
1006 $minDateQuery = Submission::whereBetween('created_at', [$startDate, $endDate])
1007 ->selectRaw('MIN(DATE(created_at)) as min_date');
1008 self::scopeQueryToAllowedForms($minDateQuery, $formId);
1009 $minDateRecord = $minDateQuery->first();
1010
1011 if ($minDateRecord && $minDateRecord->min_date) {
1012 $minDate = $minDateRecord->min_date;
1013
1014 $paidQuery->selectRaw("MIN(DATE(created_at)) as date_group")
1015 ->selectRaw("FLOOR(DATEDIFF(DATE(created_at), ?) / 3) as group_num", [$minDate])
1016 ->groupBy('group_num');
1017
1018 $pendingQuery->selectRaw("MIN(DATE(created_at)) as date_group")
1019 ->selectRaw("FLOOR(DATEDIFF(DATE(created_at), ?) / 3) as group_num", [$minDate])
1020 ->groupBy('group_num');
1021
1022 $refundedQuery->selectRaw("MIN(DATE(created_at)) as date_group")
1023 ->selectRaw("FLOOR(DATEDIFF(DATE(created_at), ?) / 3) as group_num", [$minDate])
1024 ->groupBy('group_num');
1025 } else {
1026 $paidQuery->selectRaw('DATE(created_at) as date_group')->groupBy('date_group');
1027 $pendingQuery->selectRaw('DATE(created_at) as date_group')->groupBy('date_group');
1028 $refundedQuery->selectRaw('DATE(created_at) as date_group')->groupBy('date_group');
1029 }
1030 } elseif ($groupingMode === 'week') {
1031 $paidQuery->selectRaw("DATE(DATE_ADD(created_at, INTERVAL(-WEEKDAY(created_at)) DAY)) as date_group")->groupBy('date_group');
1032 $pendingQuery->selectRaw("DATE(DATE_ADD(created_at, INTERVAL(-WEEKDAY(created_at)) DAY)) as date_group")->groupBy('date_group');
1033 $refundedQuery->selectRaw("DATE(DATE_ADD(created_at, INTERVAL(-WEEKDAY(created_at)) DAY)) as date_group")->groupBy('date_group');
1034 } else {
1035 $paidQuery->selectRaw("DATE_FORMAT(created_at, '%Y-%m-01') as date_group")->groupBy('date_group');
1036 $pendingQuery->selectRaw("DATE_FORMAT(created_at, '%Y-%m-01') as date_group")->groupBy('date_group');
1037 $refundedQuery->selectRaw("DATE_FORMAT(created_at, '%Y-%m-01') as date_group")->groupBy('date_group');
1038 }
1039
1040 // Execute the queries
1041 $paidResults = $paidQuery->orderBy('date_group')->get();
1042 $pendingResults = $pendingQuery->orderBy('date_group')->get();
1043 $refundedResults = $refundedQuery->orderBy('date_group')->get();
1044
1045
1046 // Format the data
1047 $paidData = $revenuePayments = [];
1048 foreach ($paidResults as $result) {
1049 $paidData[$result->date_group] = $result->count;
1050 $revenuePayments[$result->date_group] = $result->count;
1051 }
1052
1053 $pendingData = [];
1054 foreach ($pendingResults as $result) {
1055 $pendingData[$result->date_group] = $result->count;
1056 }
1057
1058 $refundedData = [];
1059 foreach ($refundedResults as $result) {
1060 $refundedData[$result->date_group] = $result->count;
1061 if (isset($revenuePayments[$result->date_group])) {
1062 $revenuePayments[$result->date_group] -= $result->count;
1063 }
1064 }
1065
1066 // Return all three datasets
1067 return [
1068 'paid' => $paidData,
1069 'pending' => $pendingData,
1070 'refunded' => $refundedData,
1071 'payments' => $revenuePayments
1072 ];
1073 } else {
1074 $query = $baseQuery->selectRaw('COUNT(*) as count')->selectRaw('status');
1075 $query->groupBy('status');
1076
1077 // Apply grouping based on mode
1078 if ($groupingMode === 'day') {
1079 $query->selectRaw('DATE(created_at) as date_group')
1080 ->groupBy('date_group');
1081 } elseif ($groupingMode === '3days') {
1082 $minDateQuery = Submission::whereBetween('created_at', [$startDate, $endDate])
1083 ->selectRaw('MIN(DATE(created_at)) as min_date');
1084 self::scopeQueryToAllowedForms($minDateQuery, $formId);
1085 $minDateRecord = $minDateQuery->first();
1086
1087 if ($minDateRecord && $minDateRecord->min_date) {
1088 $query->selectRaw("MIN(DATE(created_at)) as date_group")
1089 ->selectRaw("FLOOR(DATEDIFF(DATE(created_at), ?) / 3) as group_num", [$minDateRecord->min_date])
1090 ->groupBy('group_num');
1091 } else {
1092 $query->selectRaw('DATE(created_at) as date_group')
1093 ->groupBy('date_group');
1094 }
1095 } elseif ($groupingMode === 'week') {
1096 $query->selectRaw("DATE(DATE_ADD(created_at, INTERVAL(-WEEKDAY(created_at)) DAY)) as date_group")
1097 ->groupBy('date_group');
1098 } else {
1099 $query->selectRaw("DATE_FORMAT(created_at, '%Y-%m-01') as date_group")
1100 ->groupBy('date_group');
1101 }
1102
1103 $results = $query->orderBy('date_group')->get();
1104 $total = $read = $unread = $spam = $trashed = [];
1105 foreach ($results as $result) {
1106 if ($result->status === 'read') {
1107 $read[$result->date_group] = $result->count;
1108 }
1109 if ($result->status === 'unread') {
1110 $unread[$result->date_group] = $result->count;
1111 }
1112 if ($result->status === 'spam') {
1113 $spam[$result->date_group] = $result->count;
1114 }
1115 if ($result->status === 'trashed') {
1116 $trashed[$result->date_group] = $result->count;
1117 }
1118 $total[$result->date_group] = isset($total[$result->date_group]) ? $total[$result->date_group] + $result->count : $result->count;
1119 }
1120 // Return all four datasets
1121 return [
1122 'submissions' => $total,
1123 'read' => $read,
1124 'unread' => $unread,
1125 'spam' => $spam,
1126 'trashed' => $trashed
1127 ];
1128 }
1129 }
1130
1131 /**
1132 * Generate date labels based on grouping mode
1133 */
1134 private static function getDateLabels(\DateTime $startDate, \DateTime $endDate, $groupingMode)
1135 {
1136 $dates = [];
1137 $labels = [];
1138 $current = clone $startDate;
1139
1140 if ($groupingMode === 'day') {
1141 // Generate daily labels
1142 while ($current <= $endDate) {
1143 $dateKey = $current->format('Y-m-d');
1144 $dates[] = $dateKey;
1145 $labels[] = $current->format('M d');
1146 $current->modify('+1 day');
1147 }
1148 } elseif ($groupingMode === '3days') {
1149 // Generate labels for every 3 days
1150 $dayIndex = 0;
1151 $groupStartDate = clone $current;
1152
1153 while ($current <= $endDate) {
1154 if ($dayIndex % 3 === 0 && $dayIndex > 0) {
1155 $previousDate = clone $current;
1156 $previousDate->modify('-1 day');
1157
1158 $dateKey = $groupStartDate->format('Y-m-d');
1159 $dates[] = $dateKey;
1160 $labels[] = $groupStartDate->format('M d');
1161
1162 $groupStartDate = clone $current;
1163 }
1164
1165 $current->modify('+1 day');
1166 $dayIndex++;
1167 }
1168
1169 // Add the last group if needed
1170 if ($groupStartDate <= $endDate) {
1171 $dateKey = $groupStartDate->format('Y-m-d');
1172 $dates[] = $dateKey;
1173 $labels[] = $groupStartDate->format('M d');
1174 }
1175 } elseif ($groupingMode === 'week') {
1176 // Generate weekly labels
1177 while ($current <= $endDate) {
1178 // Use simple approach to get Monday (start of week)
1179 $dayOfWeek = (int)$current->format('N'); // 1 (Monday) through 7 (Sunday)
1180 $daysToSubtract = $dayOfWeek - 1;
1181
1182 $weekStart = clone $current;
1183 if ($daysToSubtract > 0) {
1184 $weekStart->modify("-{$daysToSubtract} days");
1185 }
1186
1187 // Calculate end of week (Sunday)
1188 $weekEnd = clone $weekStart;
1189 $weekEnd->modify('+6 days');
1190
1191 // If weekend exceeds the range end, cap it
1192 if ($weekEnd > $endDate) {
1193 $weekEnd = clone $endDate;
1194 }
1195
1196 $dateKey = $weekStart->format('Y-m-d');
1197 $dates[] = $dateKey;
1198 $labels[] = $weekStart->format('M d');
1199
1200 // Move to next week
1201 $current->modify('+7 days');
1202 }
1203 } else {
1204 // Generate monthly labels
1205 while ($current <= $endDate) {
1206 $dateKey = $current->format('Y-m-01');
1207 $dates[] = $dateKey;
1208 $labels[] = $current->format('M Y');
1209
1210 // Manually move to first day of next month
1211 $year = (int)$current->format('Y');
1212 $month = (int)$current->format('m');
1213
1214 // Move to next month
1215 $month++;
1216 if ($month > 12) {
1217 $month = 1;
1218 $year++;
1219 }
1220
1221 // Set to first day of next month
1222 $current = new \DateTime("$year-$month-01");
1223 }
1224 }
1225
1226 return ['dates' => $dates, 'labels' => $labels];
1227 }
1228
1229 public static function getPaymentsByType($startDate, $endDate, $type, $formId = 0)
1230 {
1231 $paymentSettings = get_option('__fluentform_payment_module_settings');
1232 if (!$paymentSettings || !Arr::isTrue($paymentSettings, 'status')) {
1233 return []; // Return empty if payment module is disabled
1234 }
1235 list($startDate, $endDate) = self::processDateRange($startDate, $endDate);
1236
1237 // Base query for transactions
1238 $query = \FluentForm\App\Models\Transaction::whereBetween('created_at', [$startDate, $endDate]);
1239
1240 // Filter by transaction type if specified
1241 if ($type === 'subscription') {
1242 $query->whereIn('transaction_type', ['subscription', 'subscription_signup_fee']);
1243 } elseif ($type === 'onetime') {
1244 $query->where('transaction_type', 'onetime');
1245 }
1246
1247 self::scopeQueryToAllowedForms($query, $formId);
1248
1249 // Get payments grouped by status
1250 $payments = $query->select('status')
1251 ->selectRaw('SUM(payment_total) as total_amount')
1252 ->selectRaw('COUNT(*) as count')
1253 ->groupBy('status')
1254 ->get();
1255
1256 // Get the total payment amount
1257 $totalAmount = 0;
1258 foreach ($payments as $payment) {
1259 $totalAmount += $payment->total_amount;
1260 }
1261
1262 $formattedData = [];
1263 foreach ($payments as $payment) {
1264 $status = strtolower($payment->status);
1265 $amount = $payment->total_amount / 100; // Convert from cents to dollars
1266 $percentage = $totalAmount > 0 ? round(($payment->total_amount / $totalAmount) * 100, 2) : 0;
1267
1268 $formattedData[$status] = [
1269 'amount' => $amount,
1270 'percentage' => $percentage,
1271 'count' => $payment->count
1272 ];
1273 }
1274
1275 // Calculate weekly average paid amount
1276 $daysInRange = self::getDateDifference($startDate, $endDate);
1277 $weeksInRange = max(1, round($daysInRange / 7, 1));
1278
1279 $paidAmount = 0;
1280 foreach ($formattedData as $status => $data) {
1281 if ($status === 'paid') {
1282 $paidAmount = $data['amount'];
1283 break;
1284 }
1285 }
1286
1287 $weeklyAverage = $paidAmount / $weeksInRange;
1288
1289 return [
1290 'currency_symbol' => Arr::get(PaymentHelper::getCurrencyConfig($formId), 'currency_sign', '$'),
1291 'payment_statuses' => $formattedData,
1292 'total_amount' => $totalAmount / 100, // Convert from cents to dollars
1293 'weekly_average' => round($weeklyAverage, 2)
1294 ];
1295 }
1296
1297 /**
1298 * Format payment method name for display
1299 */
1300 protected static function formatPaymentMethodName($paymentMethod)
1301 {
1302 $methodNames = [
1303 'stripe' => 'Stripe',
1304 'paypal' => 'PayPal',
1305 'razorpay' => 'Razorpay',
1306 'paystack' => 'Paystack',
1307 'mollie' => 'Mollie',
1308 'square' => 'Square',
1309 'paddle' => 'Paddle',
1310 'test' => 'Offline/Test',
1311 'offline' => 'Offline'
1312 ];
1313
1314 return $methodNames[$paymentMethod] ?? ucfirst($paymentMethod);
1315 }
1316
1317 /**
1318 * Format data for the chart
1319 */
1320 private static function formatDataForChart($dateLabels, $data, $formId)
1321 {
1322 $dates = $dateLabels['dates'];
1323 $labels = $dateLabels['labels'];
1324
1325 if (is_array($data) && isset($data['paid'])) {
1326 $paidValues = self::fillMissingData($dates, $data['paid']);
1327 $pendingValues = self::fillMissingData($dates, $data['pending']);
1328 $refundedValues = self::fillMissingData($dates, $data['refunded']);
1329 $paymentsValues = self::fillMissingData($dates, $data['payments']);
1330 $currencyConfig = PaymentHelper::getCurrencyConfig($formId);
1331
1332 return [
1333 'dates' => $labels,
1334 'currency_sign' => Arr::get($currencyConfig, 'currency_sign', '$'),
1335 'currency' => Arr::get($currencyConfig, 'currency', 'USD'),
1336 'values' => [
1337 'paid' => array_values($paidValues),
1338 'pending' => array_values($pendingValues),
1339 'refunded' => array_values($refundedValues),
1340 'payments' => array_values($paymentsValues)
1341 ]
1342 ];
1343 } else {
1344 return [
1345 'dates' => $labels,
1346 'values' => [
1347 'submissions' => array_values(self::fillMissingData($dates, $data['submissions'])),
1348 'read' => array_values(self::fillMissingData($dates, $data['read'])),
1349 'unread' => array_values(self::fillMissingData($dates, $data['unread'])),
1350 'spam' => array_values(self::fillMissingData($dates, $data['spam'])),
1351 'trashed' => array_values(self::fillMissingData($dates, $data['trashed']))
1352 ]
1353 ];
1354 }
1355 }
1356
1357 /**
1358 * Fill in missing data based on date intervals
1359 *
1360 * @param array $allDates Array of interval start dates
1361 * @param array $data Associative array of date => value pairs
1362 *
1363 * @return array Result with interval start dates mapped to summed values
1364 */
1365 private static function fillMissingData($allDates, $data)
1366 {
1367 $result = [];
1368
1369 // Pre-convert dates to timestamps for faster comparison
1370 $dataTimestamps = [];
1371 foreach ($data as $date => $value) {
1372 $dataTimestamps[strtotime($date)] = $value;
1373 }
1374
1375 $allDatesCount = count($allDates);
1376 for ($i = 0; $i < $allDatesCount; $i++) {
1377 $startTimestamp = strtotime($allDates[$i]);
1378
1379 // Calculate end timestamp of interval (exclusive)
1380 $endTimestamp = isset($allDates[$i + 1])
1381 ? strtotime($allDates[$i + 1])
1382 : $startTimestamp + (3 * 24 * 60 * 60); // 3 days in seconds
1383
1384 $sum = 0;
1385 $hasData = false;
1386
1387 // Check each timestamp in the data array
1388 foreach ($dataTimestamps as $timestamp => $value) {
1389 if ($timestamp >= $startTimestamp && $timestamp < $endTimestamp) {
1390 $sum += $value;
1391 $hasData = true;
1392 }
1393 }
1394
1395 $result[$allDates[$i]] = $hasData ? $sum : 0;
1396 }
1397
1398 return $result;
1399 }
1400
1401 private static function getPaymentStats($startDate, $endDate, $previousStartDate, $previousEndDate, $formId)
1402 {
1403 // Get total payments (paid status) for current period
1404 $currentPaymentsQuery = wpFluent()
1405 ->table('fluentform_transactions')
1406 ->whereBetween('created_at', [$startDate, $endDate])
1407 ->where('status', 'paid');
1408 self::scopeQueryToAllowedForms($currentPaymentsQuery, $formId);
1409 $currentPayments = $currentPaymentsQuery->sum('payment_total');
1410
1411 // Get total payments for previous period
1412 $previousPaymentsQuery = wpFluent()
1413 ->table('fluentform_transactions')
1414 ->whereBetween('created_at', [$previousStartDate, $previousEndDate])
1415 ->where('status', 'paid');
1416 self::scopeQueryToAllowedForms($previousPaymentsQuery, $formId);
1417 $previousPayments = $previousPaymentsQuery->sum('payment_total');
1418
1419 // Get pending payments for current period
1420 $currentPendingQuery = wpFluent()
1421 ->table('fluentform_transactions')
1422 ->whereBetween('created_at', [$startDate, $endDate])
1423 ->where('status', 'pending');
1424 self::scopeQueryToAllowedForms($currentPendingQuery, $formId);
1425 $currentPending = $currentPendingQuery->sum('payment_total');
1426
1427 // Get pending payments for previous period
1428 $previousPendingQuery = wpFluent()
1429 ->table('fluentform_transactions')
1430 ->whereBetween('created_at', [$previousStartDate, $previousEndDate])
1431 ->where('status', 'pending');
1432 self::scopeQueryToAllowedForms($previousPendingQuery, $formId);
1433 $previousPending = $previousPendingQuery->sum('payment_total');
1434
1435 // Get total refunds for current period
1436 $currentRefundsQuery = wpFluent()
1437 ->table('fluentform_transactions')
1438 ->whereBetween('created_at', [$startDate, $endDate])
1439 ->where('status', 'refunded');
1440 self::scopeQueryToAllowedForms($currentRefundsQuery, $formId);
1441 $currentRefunds = $currentRefundsQuery->sum('payment_total');
1442
1443 // Get total refunds for previous period
1444 $previousRefundsQuery = wpFluent()
1445 ->table('fluentform_transactions')
1446 ->whereBetween('created_at', [$previousStartDate, $previousEndDate])
1447 ->where('status', 'refunded');
1448 self::scopeQueryToAllowedForms($previousRefundsQuery, $formId);
1449 $previousRefunds = $previousRefundsQuery->sum('payment_total');
1450
1451 // Convert from cents to dollars
1452 $currentPayments = $currentPayments ? $currentPayments / 100 : 0;
1453 $previousPayments = $previousPayments ? $previousPayments / 100 : 0;
1454 $currentPending = $currentPending ? $currentPending / 100 : 0;
1455 $previousPending = $previousPending ? $previousPending / 100 : 0;
1456 $currentRefunds = $currentRefunds ? $currentRefunds / 100 : 0;
1457 $previousRefunds = $previousRefunds ? $previousRefunds / 100 : 0;
1458
1459 // Calculate payment growth percentage
1460 $paymentGrowthPercentage = 0;
1461 if ($previousPayments > 0) {
1462 $paymentGrowthPercentage = round((($currentPayments - $previousPayments) / $previousPayments) * 100, 1);
1463 } elseif ($currentPayments > 0) {
1464 $paymentGrowthPercentage = 100;
1465 }
1466
1467 $paymentGrowthText = $paymentGrowthPercentage > 0 ? '+' . $paymentGrowthPercentage . '%' : $paymentGrowthPercentage . '%';
1468 $paymentGrowthType = $paymentGrowthPercentage > 0 ? 'up' : ($paymentGrowthPercentage < 0 ? 'down' : 'neutral');
1469
1470 // Calculate refund growth percentage
1471 $refundGrowthPercentage = 0;
1472 if ($previousRefunds > 0) {
1473 $refundGrowthPercentage = round((($currentRefunds - $previousRefunds) / $previousRefunds) * 100, 1);
1474 } elseif ($currentRefunds > 0) {
1475 $refundGrowthPercentage = 100;
1476 }
1477
1478 $refundGrowthText = $refundGrowthPercentage > 0 ? '+' . $refundGrowthPercentage . '%' : $refundGrowthPercentage . '%';
1479 $refundGrowthType = $refundGrowthPercentage > 0 ? 'down' : ($refundGrowthPercentage < 0 ? 'up' : 'neutral'); // Refunds going up is bad
1480
1481 // Calculate pending growth percentage
1482 $pendingGrowthPercentage = 0;
1483 if ($previousPending > 0) {
1484 $pendingGrowthPercentage = round((($currentPending - $previousPending) / $previousPending) * 100, 1);
1485 } elseif ($currentPending > 0) {
1486 $pendingGrowthPercentage = 100;
1487 }
1488
1489 $pendingGrowthText = $pendingGrowthPercentage > 0 ? '+' . $pendingGrowthPercentage . '%' : $pendingGrowthPercentage . '%';
1490 $pendingGrowthType = $pendingGrowthPercentage > 0 ? 'up' : ($pendingGrowthPercentage < 0 ? 'down' : 'neutral');
1491
1492 // Calculate revenue percentage
1493 $totalRevenue = $currentPayments - $currentRefunds;
1494 $previousRevenue = $previousPayments - $previousRefunds;
1495 $revenuePercentage = 0;
1496 if ($previousRevenue > 0) {
1497 $revenuePercentage = round((($totalRevenue - $previousRevenue) / $previousRevenue) * 100, 1);
1498 } elseif ($totalRevenue > 0) {
1499 $revenuePercentage = 100;
1500 }
1501 $revenueText = $revenuePercentage > 0 ? '+' . $revenuePercentage . '%' : $revenuePercentage . '%';
1502 $revenueType = $revenuePercentage > 0 ? 'up' : ($revenuePercentage < 0 ? 'down' : 'neutral');
1503
1504 // Get default currency from payment settings
1505 $paymentSettings = PaymentHelper::getPaymentSettings();
1506 $currency = Arr::get($paymentSettings, 'currency', 'USD');
1507 $currencySymbol = PaymentHelper::getCurrencySymbol($currency);
1508
1509 return [
1510 'total_payments' => [
1511 'value' => number_format($currentPayments, 2),
1512 'raw_value' => $currentPayments,
1513 'currency' => $currency,
1514 'currency_symbol' => $currencySymbol,
1515 'change' => $paymentGrowthText,
1516 'change_type' => $paymentGrowthType
1517 ],
1518 'pending_payments' => [
1519 'value' => number_format($currentPending, 2),
1520 'raw_value' => $currentPending,
1521 'currency' => $currency,
1522 'currency_symbol' => $currencySymbol,
1523 'change' => $pendingGrowthText,
1524 'change_type' => $pendingGrowthType
1525 ],
1526 'total_refunds' => [
1527 'value' => number_format($currentRefunds, 2),
1528 'raw_value' => $currentRefunds,
1529 'currency' => $currency,
1530 'currency_symbol' => $currencySymbol,
1531 'change' => $refundGrowthText,
1532 'change_type' => $refundGrowthType
1533 ],
1534 'total_revenue' => [
1535 'value' => number_format($totalRevenue, 2),
1536 'raw_value' => $totalRevenue,
1537 'change' => $revenueText,
1538 'change_type' => $revenueType,
1539 'currency' => $currency,
1540 'currency_symbol' => $currencySymbol
1541 ]
1542 ];
1543 }
1544
1545
1546 protected static function getDateDifference($startDate, $endDate)
1547 {
1548 $start = new \DateTime($startDate);
1549 $end = new \DateTime($endDate);
1550 $interval = $start->diff($end);
1551 return $interval->days + 1;
1552 }
1553 }
1554