| @@ -75,8 +75,22 @@ | ||
| 75 | 75 | foreach ($reports as $reportKey => $report) { |
| 76 | 76 | $reports[$reportKey]['label'] = $inputLabels[$reportKey]; |
| 77 | 77 | $reports[$reportKey]['element'] = Arr::get($inputs, $reportKey, []); |
| 78 | 78 | $reports[$reportKey]['options'] = $formInputs[$reportKey]['options']; |
| 79 | + | |
| 80 | + // Per-element override hook. Safe with no listener (default | |
| 81 | + // payload is returned unchanged). Pro hooks the ranking | |
| 82 | + // variant; third-party fields can extend the same pattern. | |
| 83 | + $element = $reports[$reportKey]['element']; | |
| 84 | + if (is_string($element) && $element !== '') { | |
| 85 | + $reports[$reportKey] = apply_filters( | |
| 86 | + 'fluentform/reports/format_field_' . $element, | |
| 87 | + $reports[$reportKey], | |
| 88 | + array_merge($formInputs[$reportKey], ['name' => $reportKey]), | |
| 89 | + $form->id, | |
| 90 | + $statuses | |
| 91 | + ); | |
| 92 | + } | |
| 79 | 93 | } |
| 80 | 94 | |
| 81 | 95 | return [ |
| 82 | 96 | 'report_items' => $reports, |
| @@ -1225,76 +1239,130 @@ | ||
| 1225 | 1239 | |
| 1226 | 1240 | return ['dates' => $dates, 'labels' => $labels]; |
| 1227 | 1241 | } |
| 1228 | 1242 | |
| 1229 | - public static function getPaymentsByType($startDate, $endDate, $type, $formId = 0) | |
| 1243 | + /** | |
| 1244 | + * All payment figures in ONE transaction scan, keyed 'onetime' / 'subscription' | |
| 1245 | + * / 'all' (disabled module → each empty []). 'all' counts every transaction type | |
| 1246 | + * with no filter, preserving getPaymentsByType()'s legacy no-type-filter contract. | |
| 1247 | + * Each block keeps getPaymentsByType()'s shape — blended per-status totals for the | |
| 1248 | + * admin consumer — plus a by_currency split for callers that must not blend | |
| 1249 | + * currencies (MCP). One scan so consumers needing both types don't pay for two. | |
| 1250 | + */ | |
| 1251 | + public static function getPaymentBreakdown($startDate, $endDate, $formId = 0) | |
| 1230 | 1252 | { |
| 1231 | 1253 | $paymentSettings = get_option('__fluentform_payment_module_settings'); |
| 1232 | 1254 | if (!$paymentSettings || !Arr::isTrue($paymentSettings, 'status')) { |
| 1233 | - return []; // Return empty if payment module is disabled | |
| 1255 | + return ['onetime' => [], 'subscription' => [], 'all' => []]; | |
| 1234 | 1256 | } |
| 1235 | 1257 | list($startDate, $endDate) = self::processDateRange($startDate, $endDate); |
| 1236 | - | |
| 1237 | - // Base query for transactions | |
| 1258 | + | |
| 1238 | 1259 | $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 | 1260 | self::scopeQueryToAllowedForms($query, $formId); |
| 1248 | - | |
| 1249 | - // Get payments grouped by status | |
| 1250 | - $payments = $query->select('status') | |
| 1261 | + $payments = $query->select('transaction_type', 'status', 'currency') | |
| 1251 | 1262 | ->selectRaw('SUM(payment_total) as total_amount') |
| 1252 | 1263 | ->selectRaw('COUNT(*) as count') |
| 1253 | - ->groupBy('status') | |
| 1264 | + ->groupBy('transaction_type', 'status', 'currency') | |
| 1254 | 1265 | ->get(); |
| 1255 | - | |
| 1256 | - // Get the total payment amount | |
| 1257 | - $totalAmount = 0; | |
| 1266 | + | |
| 1267 | + $daysInRange = self::getDateDifference($startDate, $endDate); | |
| 1268 | + $weeksInRange = max(1, round($daysInRange / 7, 1)); | |
| 1269 | + $currencySign = Arr::get(PaymentHelper::getCurrencyConfig($formId), 'currency_sign', '$'); | |
| 1270 | + $defaultCurrency = strtoupper((string) Arr::get(PaymentHelper::getCurrencyConfig($formId), 'currency', 'USD')); | |
| 1271 | + | |
| 1272 | + $blended = ['onetime' => [], 'subscription' => [], 'all' => []]; | |
| 1273 | + $perCurrency = ['onetime' => [], 'subscription' => [], 'all' => []]; | |
| 1258 | 1274 | foreach ($payments as $payment) { |
| 1259 | - $totalAmount += $payment->total_amount; | |
| 1275 | + $status = strtolower((string) $payment->status); | |
| 1276 | + $cents = (int) $payment->total_amount; | |
| 1277 | + $count = (int) $payment->count; | |
| 1278 | + $currency = strtoupper(trim((string) $payment->currency)); | |
| 1279 | + if ('' === $currency) { | |
| 1280 | + $currency = $defaultCurrency; | |
| 1281 | + } | |
| 1282 | + | |
| 1283 | + self::addPaymentRow($blended['all'], $perCurrency['all'], $status, $currency, $cents, $count); | |
| 1284 | + | |
| 1285 | + if ('onetime' === $payment->transaction_type) { | |
| 1286 | + $type = 'onetime'; | |
| 1287 | + } elseif (in_array($payment->transaction_type, ['subscription', 'subscription_signup_fee'], true)) { | |
| 1288 | + $type = 'subscription'; | |
| 1289 | + } else { | |
| 1290 | + continue; | |
| 1291 | + } | |
| 1292 | + self::addPaymentRow($blended[$type], $perCurrency[$type], $status, $currency, $cents, $count); | |
| 1260 | 1293 | } |
| 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 | - ]; | |
| 1294 | + | |
| 1295 | + $out = []; | |
| 1296 | + foreach (['onetime', 'subscription', 'all'] as $type) { | |
| 1297 | + $byCurrency = []; | |
| 1298 | + foreach ($perCurrency[$type] as $currency => $statuses) { | |
| 1299 | + $symbol = html_entity_decode(PaymentHelper::getCurrencySymbol($currency), ENT_QUOTES); | |
| 1300 | + $byCurrency[$currency] = self::formatPaymentStatuses($statuses, $weeksInRange, $symbol); | |
| 1301 | + } | |
| 1302 | + $block = self::formatPaymentStatuses($blended[$type], $weeksInRange, $currencySign); | |
| 1303 | + $block['by_currency'] = $byCurrency; | |
| 1304 | + $out[$type] = $block; | |
| 1273 | 1305 | } |
| 1274 | - | |
| 1275 | - // Calculate weekly average paid amount | |
| 1276 | - $daysInRange = self::getDateDifference($startDate, $endDate); | |
| 1277 | - $weeksInRange = max(1, round($daysInRange / 7, 1)); | |
| 1278 | - | |
| 1306 | + | |
| 1307 | + return $out; | |
| 1308 | + } | |
| 1309 | + | |
| 1310 | + private static function addPaymentRow(array &$blended, array &$perCurrency, $status, $currency, $cents, $count) | |
| 1311 | + { | |
| 1312 | + if (!isset($blended[$status])) { | |
| 1313 | + $blended[$status] = ['amount' => 0, 'count' => 0]; | |
| 1314 | + } | |
| 1315 | + $blended[$status]['amount'] += $cents; | |
| 1316 | + $blended[$status]['count'] += $count; | |
| 1317 | + | |
| 1318 | + if (!isset($perCurrency[$currency][$status])) { | |
| 1319 | + $perCurrency[$currency][$status] = ['amount' => 0, 'count' => 0]; | |
| 1320 | + } | |
| 1321 | + $perCurrency[$currency][$status]['amount'] += $cents; | |
| 1322 | + $perCurrency[$currency][$status]['count'] += $count; | |
| 1323 | + } | |
| 1324 | + | |
| 1325 | + public static function getPaymentsByType($startDate, $endDate, $type, $formId = 0) | |
| 1326 | + { | |
| 1327 | + return Arr::get(self::getPaymentBreakdown($startDate, $endDate, $formId), $type, []); | |
| 1328 | + } | |
| 1329 | + | |
| 1330 | + /** | |
| 1331 | + * Shape a status => ['amount' => cents, 'count' => int] map into the payment | |
| 1332 | + * block used by both the admin Reports payment overview and MCP: whole-unit | |
| 1333 | + * amounts (cents ÷ 100), per-status share of the block total, integer counts, | |
| 1334 | + * and the paid weekly average. One formatter so the two callers can never drift. | |
| 1335 | + */ | |
| 1336 | + private static function formatPaymentStatuses(array $statuses, $weeksInRange, $currencySymbol) | |
| 1337 | + { | |
| 1338 | + $totalCents = 0; | |
| 1339 | + foreach ($statuses as $bucket) { | |
| 1340 | + $totalCents += $bucket['amount']; | |
| 1341 | + } | |
| 1342 | + | |
| 1343 | + $formatted = []; | |
| 1279 | 1344 | $paidAmount = 0; |
| 1280 | - foreach ($formattedData as $status => $data) { | |
| 1281 | - if ($status === 'paid') { | |
| 1282 | - $paidAmount = $data['amount']; | |
| 1283 | - break; | |
| 1345 | + foreach ($statuses as $status => $bucket) { | |
| 1346 | + $amount = $bucket['amount'] / 100; // Convert from cents to dollars | |
| 1347 | + $formatted[$status] = [ | |
| 1348 | + 'amount' => $amount, | |
| 1349 | + 'percentage' => $totalCents > 0 ? round(($bucket['amount'] / $totalCents) * 100, 2) : 0, | |
| 1350 | + 'count' => (int) $bucket['count'], | |
| 1351 | + ]; | |
| 1352 | + if ('paid' === $status) { | |
| 1353 | + $paidAmount = $amount; | |
| 1284 | 1354 | } |
| 1285 | 1355 | } |
| 1286 | - | |
| 1287 | - $weeklyAverage = $paidAmount / $weeksInRange; | |
| 1288 | - | |
| 1356 | + | |
| 1289 | 1357 | 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) | |
| 1358 | + 'currency_symbol' => $currencySymbol, | |
| 1359 | + 'payment_statuses' => $formatted, | |
| 1360 | + 'total_amount' => $totalCents / 100, // Convert from cents to dollars | |
| 1361 | + 'weekly_average' => round($paidAmount / $weeksInRange, 2), | |
| 1294 | 1362 | ]; |
| 1295 | 1363 | } |
| 1296 | - | |
| 1364 | + | |
| 1297 | 1365 | /** |
| 1298 | 1366 | * Format payment method name for display |
| 1299 | 1367 | */ |
| 1300 | 1368 | protected static function formatPaymentMethodName($paymentMethod) |