537 lines
25 KiB
PHP
537 lines
25 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Plugins\AdvancedReporting;
|
|
|
|
use Core\Http\Response;
|
|
use Core\Services\ApiLayer;
|
|
use Core\Services\Auth;
|
|
use Core\Services\Database;
|
|
use Core\Services\Settings;
|
|
use Core\Views\View;
|
|
use PDO;
|
|
use Throwable;
|
|
|
|
class ReportsController
|
|
{
|
|
private View $view;
|
|
private array $releaseTrackCache = [];
|
|
private array $trackDownloadCountCache = [];
|
|
|
|
public function __construct()
|
|
{
|
|
$this->view = new View(__DIR__ . '/views');
|
|
}
|
|
|
|
public function adminIndex(): Response
|
|
{
|
|
if ($guard = $this->guard()) {
|
|
return $guard;
|
|
}
|
|
|
|
$db = Database::get();
|
|
if (!($db instanceof PDO)) {
|
|
return new Response($this->view->render('admin/index.php', [
|
|
'title' => 'Sales Reports',
|
|
'tab' => 'overview',
|
|
'filters' => $this->filters(),
|
|
'overview' => [],
|
|
'artist_rows' => [],
|
|
'track_rows' => [],
|
|
'artist_options' => [],
|
|
'tables_ready' => false,
|
|
'currency' => Settings::get('store_currency', 'GBP'),
|
|
]));
|
|
}
|
|
|
|
ApiLayer::ensureSchema($db);
|
|
$filters = $this->filters();
|
|
$tab = $filters['tab'];
|
|
|
|
return new Response($this->view->render('admin/index.php', [
|
|
'title' => 'Sales Reports',
|
|
'tab' => $tab,
|
|
'filters' => $filters,
|
|
'overview' => $this->overviewPayload($db, $filters),
|
|
'artist_rows' => $tab === 'artists' ? $this->artistRows($db, $filters) : [],
|
|
'track_rows' => $tab === 'tracks' ? $this->trackRows($db, $filters) : [],
|
|
'artist_options' => $this->artistOptions($db),
|
|
'tables_ready' => $this->tablesReady($db),
|
|
'currency' => strtoupper(trim((string)Settings::get('store_currency', 'GBP'))),
|
|
]));
|
|
}
|
|
|
|
public function adminExport(): Response
|
|
{
|
|
if ($guard = $this->guard()) {
|
|
return $guard;
|
|
}
|
|
|
|
$db = Database::get();
|
|
if (!($db instanceof PDO)) {
|
|
return new Response('Database unavailable', 500);
|
|
}
|
|
|
|
ApiLayer::ensureSchema($db);
|
|
$filters = $this->filters();
|
|
$type = strtolower(trim((string)($_GET['type'] ?? 'artists')));
|
|
if (!in_array($type, ['artists', 'tracks'], true)) {
|
|
$type = 'artists';
|
|
}
|
|
|
|
$rows = $type === 'tracks' ? $this->trackRows($db, $filters) : $this->artistRows($db, $filters);
|
|
$stream = fopen('php://temp', 'w+');
|
|
if ($stream === false) {
|
|
return new Response('Unable to create export', 500);
|
|
}
|
|
|
|
if ($type === 'tracks') {
|
|
fputcsv($stream, ['Artist', 'Track', 'Release', 'Catalog', 'Units Sold', 'Units Refunded', 'Gross Revenue', 'Refunded Revenue', 'Net Revenue', 'PayPal Fees', 'Net After Fees', 'Downloads']);
|
|
foreach ($rows as $row) {
|
|
fputcsv($stream, [
|
|
(string)($row['artist_name'] ?? ''),
|
|
(string)($row['track_display'] ?? ''),
|
|
(string)($row['release_title'] ?? ''),
|
|
(string)($row['catalog_no'] ?? ''),
|
|
(int)($row['units_sold'] ?? 0),
|
|
(int)($row['units_refunded'] ?? 0),
|
|
number_format((float)($row['gross_revenue'] ?? 0), 2, '.', ''),
|
|
number_format((float)($row['refunded_revenue'] ?? 0), 2, '.', ''),
|
|
number_format((float)($row['net_revenue'] ?? 0), 2, '.', ''),
|
|
number_format((float)($row['payment_fees'] ?? 0), 2, '.', ''),
|
|
number_format((float)($row['net_after_fees'] ?? 0), 2, '.', ''),
|
|
(int)($row['download_count'] ?? 0),
|
|
]);
|
|
}
|
|
} else {
|
|
fputcsv($stream, ['Artist', 'Paid Orders', 'Units Sold', 'Units Refunded', 'Gross Revenue', 'Refunded Revenue', 'Net Revenue', 'PayPal Fees', 'Net After Fees', 'Releases', 'Tracks']);
|
|
foreach ($rows as $row) {
|
|
fputcsv($stream, [
|
|
(string)($row['artist_name'] ?? ''),
|
|
(int)($row['paid_orders'] ?? 0),
|
|
(int)($row['units_sold'] ?? 0),
|
|
(int)($row['units_refunded'] ?? 0),
|
|
number_format((float)($row['gross_revenue'] ?? 0), 2, '.', ''),
|
|
number_format((float)($row['refunded_revenue'] ?? 0), 2, '.', ''),
|
|
number_format((float)($row['net_revenue'] ?? 0), 2, '.', ''),
|
|
number_format((float)($row['payment_fees'] ?? 0), 2, '.', ''),
|
|
number_format((float)($row['net_after_fees'] ?? 0), 2, '.', ''),
|
|
(int)($row['release_count'] ?? 0),
|
|
(int)($row['track_count'] ?? 0),
|
|
]);
|
|
}
|
|
}
|
|
|
|
rewind($stream);
|
|
$csv = stream_get_contents($stream);
|
|
fclose($stream);
|
|
|
|
return new Response((string)$csv, 200, [
|
|
'Content-Type' => 'text/csv; charset=UTF-8',
|
|
'Content-Disposition' => 'attachment; filename="sales-report-' . $type . '-' . gmdate('Ymd-His') . '.csv"',
|
|
]);
|
|
}
|
|
|
|
private function guard(): ?Response
|
|
{
|
|
if (!Auth::check()) {
|
|
return new Response('', 302, ['Location' => '/admin/login']);
|
|
}
|
|
if (!Auth::hasRole(['admin', 'manager'])) {
|
|
return new Response('', 302, ['Location' => '/admin']);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private function filters(): array
|
|
{
|
|
$tab = strtolower(trim((string)($_GET['tab'] ?? 'overview')));
|
|
if (!in_array($tab, ['overview', 'artists', 'tracks'], true)) {
|
|
$tab = 'overview';
|
|
}
|
|
|
|
return [
|
|
'tab' => $tab,
|
|
'from' => trim((string)($_GET['from'] ?? '')),
|
|
'to' => trim((string)($_GET['to'] ?? '')),
|
|
'q' => trim((string)($_GET['q'] ?? '')),
|
|
'artist_id' => max(0, (int)($_GET['artist_id'] ?? 0)),
|
|
];
|
|
}
|
|
|
|
private function tablesReady(PDO $db): bool
|
|
{
|
|
try {
|
|
$probe = $db->query("SHOW TABLES LIKE 'ac_store_order_item_allocations'");
|
|
return (bool)($probe && $probe->fetch(PDO::FETCH_NUM));
|
|
} catch (Throwable $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private function overviewPayload(PDO $db, array $filters): array
|
|
{
|
|
$stats = [
|
|
'gross_revenue' => 0.0,
|
|
'refunded_revenue' => 0.0,
|
|
'net_revenue' => 0.0,
|
|
'payment_fees' => 0.0,
|
|
'net_after_fees' => 0.0,
|
|
'paid_orders' => 0,
|
|
'refunded_orders' => 0,
|
|
'units_sold' => 0,
|
|
'units_refunded' => 0,
|
|
'top_artists' => [],
|
|
'top_tracks' => [],
|
|
];
|
|
|
|
try {
|
|
[$whereSql, $params] = $this->dateWhere($filters, 'o');
|
|
$stmt = $db->prepare("
|
|
SELECT
|
|
COALESCE(SUM(CASE WHEN o.status = 'paid' THEN o.total ELSE 0 END), 0) AS gross_revenue,
|
|
COALESCE(SUM(CASE WHEN o.status = 'refunded' THEN o.total ELSE 0 END), 0) AS refunded_revenue,
|
|
COALESCE(SUM(CASE WHEN o.status = 'paid' THEN COALESCE(o.payment_fee, 0) ELSE 0 END), 0) AS payment_fees,
|
|
COUNT(DISTINCT CASE WHEN o.status = 'paid' THEN o.id END) AS paid_orders,
|
|
COUNT(DISTINCT CASE WHEN o.status = 'refunded' THEN o.id END) AS refunded_orders
|
|
FROM ac_store_orders o
|
|
{$whereSql}
|
|
");
|
|
$stmt->execute($params);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
|
$stats['gross_revenue'] = (float)($row['gross_revenue'] ?? 0);
|
|
$stats['refunded_revenue'] = (float)($row['refunded_revenue'] ?? 0);
|
|
$stats['payment_fees'] = (float)($row['payment_fees'] ?? 0);
|
|
$stats['net_revenue'] = $stats['gross_revenue'] - $stats['refunded_revenue'];
|
|
$stats['net_after_fees'] = $stats['net_revenue'] - $stats['payment_fees'];
|
|
$stats['paid_orders'] = (int)($row['paid_orders'] ?? 0);
|
|
$stats['refunded_orders'] = (int)($row['refunded_orders'] ?? 0);
|
|
} catch (Throwable $e) {
|
|
}
|
|
|
|
try {
|
|
[$whereSql, $params] = $this->dateWhere($filters, 'o');
|
|
$stmt = $db->prepare("
|
|
SELECT
|
|
COALESCE(SUM(CASE WHEN o.status = 'paid' THEN a.qty ELSE 0 END), 0) AS units_sold,
|
|
COALESCE(SUM(CASE WHEN o.status = 'refunded' THEN a.qty ELSE 0 END), 0) AS units_refunded
|
|
FROM ac_store_order_item_allocations a
|
|
JOIN ac_store_orders o ON o.id = a.order_id
|
|
{$whereSql}
|
|
");
|
|
$stmt->execute($params);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
|
$stats['units_sold'] = (int)($row['units_sold'] ?? 0);
|
|
$stats['units_refunded'] = (int)($row['units_refunded'] ?? 0);
|
|
} catch (Throwable $e) {
|
|
}
|
|
|
|
$topFilters = $filters;
|
|
$topFilters['limit'] = 5;
|
|
$stats['top_artists'] = $this->artistRows($db, $topFilters);
|
|
$stats['top_tracks'] = $this->trackRows($db, $topFilters);
|
|
|
|
return $stats;
|
|
}
|
|
|
|
private function artistRows(PDO $db, array $filters): array
|
|
{
|
|
[$whereSql, $params] = $this->dateWhere($filters, 'o');
|
|
if (!empty($filters['artist_id'])) {
|
|
$whereSql .= ($whereSql === '' ? ' WHERE ' : ' AND ') . 'alloc.artist_id = :artist_id';
|
|
$params[':artist_id'] = (int)$filters['artist_id'];
|
|
}
|
|
$q = trim((string)($filters['q'] ?? ''));
|
|
if ($q !== '') {
|
|
$whereSql .= ($whereSql === '' ? ' WHERE ' : ' AND ') . " (ar.name LIKE :q OR r.artist_name LIKE :q OR alloc.title_snapshot LIKE :q)";
|
|
$params[':q'] = '%' . $q . '%';
|
|
}
|
|
$limitSql = !empty($filters['limit']) ? ' LIMIT ' . max(1, (int)$filters['limit']) : '';
|
|
|
|
try {
|
|
$stmt = $db->prepare("
|
|
SELECT
|
|
COALESCE(alloc.artist_id, 0) AS artist_id,
|
|
COALESCE(NULLIF(MAX(ar.name), ''), NULLIF(MAX(r.artist_name), ''), 'Unknown Artist') AS artist_name,
|
|
COUNT(DISTINCT CASE WHEN o.status = 'paid' THEN alloc.order_id END) AS paid_orders,
|
|
COALESCE(SUM(CASE WHEN o.status = 'paid' THEN alloc.qty ELSE 0 END), 0) AS units_sold,
|
|
COALESCE(SUM(CASE WHEN o.status = 'refunded' THEN alloc.qty ELSE 0 END), 0) AS units_refunded,
|
|
COALESCE(SUM(CASE WHEN o.status = 'paid' THEN alloc.gross_amount ELSE 0 END), 0) AS gross_revenue,
|
|
COALESCE(SUM(CASE WHEN o.status = 'refunded' THEN alloc.gross_amount ELSE 0 END), 0) AS refunded_revenue,
|
|
COALESCE(SUM(CASE WHEN o.status = 'paid' AND COALESCE(NULLIF(o.payment_gross, 0), NULLIF(o.total, 0), 0) > 0 THEN COALESCE(o.payment_fee, 0) * (alloc.gross_amount / COALESCE(NULLIF(o.payment_gross, 0), NULLIF(o.total, 0))) ELSE 0 END), 0) AS payment_fees,
|
|
COUNT(DISTINCT CASE WHEN o.status = 'paid' AND alloc.release_id IS NOT NULL THEN alloc.release_id END) AS release_count,
|
|
COUNT(DISTINCT CASE WHEN o.status = 'paid' AND alloc.track_id IS NOT NULL THEN alloc.track_id END) AS track_count
|
|
FROM ac_store_order_item_allocations alloc
|
|
JOIN ac_store_orders o ON o.id = alloc.order_id
|
|
LEFT JOIN ac_artists ar ON ar.id = alloc.artist_id
|
|
LEFT JOIN ac_releases r ON r.id = alloc.release_id
|
|
{$whereSql}
|
|
GROUP BY COALESCE(alloc.artist_id, 0)
|
|
ORDER BY
|
|
((COALESCE(SUM(CASE WHEN o.status = 'paid' THEN alloc.gross_amount ELSE 0 END), 0) - COALESCE(SUM(CASE WHEN o.status = 'refunded' THEN alloc.gross_amount ELSE 0 END), 0)) - COALESCE(SUM(CASE WHEN o.status = 'paid' AND COALESCE(NULLIF(o.payment_gross, 0), NULLIF(o.total, 0), 0) > 0 THEN COALESCE(o.payment_fee, 0) * (alloc.gross_amount / COALESCE(NULLIF(o.payment_gross, 0), NULLIF(o.total, 0))) ELSE 0 END), 0)) DESC,
|
|
COALESCE(SUM(CASE WHEN o.status = 'paid' THEN alloc.gross_amount ELSE 0 END), 0) DESC,
|
|
artist_name ASC
|
|
{$limitSql}
|
|
");
|
|
$stmt->execute($params);
|
|
$rows = array_values(array_filter($stmt->fetchAll(PDO::FETCH_ASSOC) ?: [], static function (array $row): bool {
|
|
return (float)($row['gross_revenue'] ?? 0) > 0 || (float)($row['refunded_revenue'] ?? 0) > 0;
|
|
}));
|
|
foreach ($rows as &$row) {
|
|
$row['payment_fees'] = (float)($row['payment_fees'] ?? 0);
|
|
$row['net_revenue'] = (float)($row['gross_revenue'] ?? 0) - (float)($row['refunded_revenue'] ?? 0);
|
|
$row['net_after_fees'] = $row['net_revenue'] - $row['payment_fees'];
|
|
}
|
|
unset($row);
|
|
return $rows;
|
|
} catch (Throwable $e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private function trackRows(PDO $db, array $filters): array
|
|
{
|
|
[$whereSql, $params] = $this->dateWhere($filters, 'o');
|
|
if (!empty($filters['artist_id'])) {
|
|
$whereSql .= ($whereSql === '' ? ' WHERE ' : ' AND ') . 'COALESCE(alloc.artist_id, r.artist_id) = :artist_id';
|
|
$params[':artist_id'] = (int)$filters['artist_id'];
|
|
}
|
|
$q = trim((string)($filters['q'] ?? ''));
|
|
if ($q !== '') {
|
|
$whereSql .= ($whereSql === '' ? ' WHERE ' : ' AND ') . "(t.title LIKE :q OR t.mix_name LIKE :q OR ar.name LIKE :q OR r.title LIKE :q OR r.catalog_no LIKE :q)";
|
|
$params[':q'] = '%' . $q . '%';
|
|
}
|
|
$limitSql = !empty($filters['limit']) ? ' LIMIT ' . max(1, (int)$filters['limit']) : '';
|
|
|
|
try {
|
|
$stmt = $db->prepare("
|
|
SELECT
|
|
alloc.id,
|
|
alloc.order_id,
|
|
alloc.track_id,
|
|
alloc.release_id,
|
|
alloc.artist_id,
|
|
alloc.qty,
|
|
alloc.gross_amount,
|
|
o.status,
|
|
COALESCE(o.payment_fee, 0) AS payment_fee,
|
|
COALESCE(o.payment_gross, 0) AS payment_gross,
|
|
COALESCE(o.total, 0) AS order_total,
|
|
COALESCE(NULLIF(MAX(t.title), ''), MAX(alloc.title_snapshot), 'Track') AS track_title,
|
|
COALESCE(NULLIF(MAX(t.mix_name), ''), '') AS mix_name,
|
|
COALESCE(NULLIF(MAX(ar.name), ''), NULLIF(MAX(r.artist_name), ''), 'Unknown Artist') AS artist_name,
|
|
COALESCE(NULLIF(MAX(r.title), ''), 'Release') AS release_title,
|
|
COALESCE(NULLIF(MAX(r.catalog_no), ''), '') AS catalog_no
|
|
FROM ac_store_order_item_allocations alloc
|
|
JOIN ac_store_orders o ON o.id = alloc.order_id
|
|
LEFT JOIN ac_release_tracks t ON t.id = alloc.track_id
|
|
LEFT JOIN ac_releases r ON r.id = COALESCE(alloc.release_id, t.release_id)
|
|
LEFT JOIN ac_artists ar ON ar.id = COALESCE(alloc.artist_id, r.artist_id)
|
|
{$whereSql}
|
|
GROUP BY alloc.id
|
|
ORDER BY alloc.id DESC
|
|
");
|
|
$stmt->execute($params);
|
|
$allocations = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
|
$rows = [];
|
|
foreach ($allocations as $alloc) {
|
|
$releaseId = (int)($alloc['release_id'] ?? 0);
|
|
$trackId = (int)($alloc['track_id'] ?? 0);
|
|
if ($trackId > 0) {
|
|
$targets = [[
|
|
'track_id' => $trackId,
|
|
'artist_name' => (string)($alloc['artist_name'] ?? 'Unknown Artist'),
|
|
'release_title' => (string)($alloc['release_title'] ?? 'Release'),
|
|
'catalog_no' => (string)($alloc['catalog_no'] ?? ''),
|
|
'track_title' => (string)($alloc['track_title'] ?? 'Track'),
|
|
'mix_name' => (string)($alloc['mix_name'] ?? ''),
|
|
'weight' => 1.0,
|
|
]];
|
|
} elseif ($releaseId > 0) {
|
|
$targets = $this->releaseTracks($db, $releaseId, (string)($alloc['artist_name'] ?? 'Unknown Artist'), (string)($alloc['release_title'] ?? 'Release'), (string)($alloc['catalog_no'] ?? ''));
|
|
if (!$targets) {
|
|
continue;
|
|
}
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
$totalWeight = 0.0;
|
|
foreach ($targets as $target) {
|
|
$totalWeight += max(0.0, (float)($target['weight'] ?? 0));
|
|
}
|
|
if ($totalWeight <= 0) {
|
|
$totalWeight = (float)count($targets);
|
|
foreach ($targets as &$target) {
|
|
$target['weight'] = 1.0;
|
|
}
|
|
unset($target);
|
|
}
|
|
|
|
foreach ($targets as $target) {
|
|
$key = (int)($target['track_id'] ?? 0);
|
|
if ($key <= 0) {
|
|
continue;
|
|
}
|
|
if (!isset($rows[$key])) {
|
|
$trackTitle = trim((string)($target['track_title'] ?? 'Track'));
|
|
$mixName = trim((string)($target['mix_name'] ?? ''));
|
|
$rows[$key] = [
|
|
'track_id' => $key,
|
|
'artist_name' => (string)($target['artist_name'] ?? 'Unknown Artist'),
|
|
'release_title' => (string)($target['release_title'] ?? 'Release'),
|
|
'catalog_no' => (string)($target['catalog_no'] ?? ''),
|
|
'track_title' => $trackTitle,
|
|
'mix_name' => $mixName,
|
|
'track_display' => $mixName !== '' ? $trackTitle . ' (' . $mixName . ')' : $trackTitle,
|
|
'units_sold' => 0,
|
|
'units_refunded' => 0,
|
|
'gross_revenue' => 0.0,
|
|
'refunded_revenue' => 0.0,
|
|
'download_count' => $this->trackDownloadCount($db, $key),
|
|
'payment_fees' => 0.0,
|
|
'net_after_fees' => 0.0,
|
|
];
|
|
}
|
|
$share = max(0.0, (float)($target['weight'] ?? 0)) / $totalWeight;
|
|
$amount = (float)($alloc['gross_amount'] ?? 0) * $share;
|
|
$qty = (int)($alloc['qty'] ?? 0);
|
|
$feeBase = (float)($alloc['payment_gross'] ?? 0);
|
|
if ($feeBase <= 0) {
|
|
$feeBase = (float)($alloc['order_total'] ?? 0);
|
|
}
|
|
$feeShare = ((string)($alloc['status'] ?? '') === 'paid' && $feeBase > 0)
|
|
? ((float)($alloc['payment_fee'] ?? 0) * (((float)($alloc['gross_amount'] ?? 0)) / $feeBase) * $share)
|
|
: 0.0;
|
|
if ((string)($alloc['status'] ?? '') === 'paid') {
|
|
$rows[$key]['units_sold'] += $qty;
|
|
$rows[$key]['gross_revenue'] += $amount;
|
|
$rows[$key]['payment_fees'] += $feeShare;
|
|
} elseif ((string)($alloc['status'] ?? '') === 'refunded') {
|
|
$rows[$key]['units_refunded'] += $qty;
|
|
$rows[$key]['refunded_revenue'] += $amount;
|
|
}
|
|
}
|
|
}
|
|
$rows = array_values(array_filter($rows, static function (array $row): bool {
|
|
return (float)($row['gross_revenue'] ?? 0) > 0 || (float)($row['refunded_revenue'] ?? 0) > 0;
|
|
}));
|
|
foreach ($rows as &$row) {
|
|
$row['payment_fees'] = (float)($row['payment_fees'] ?? 0);
|
|
$row['net_revenue'] = (float)($row['gross_revenue'] ?? 0) - (float)($row['refunded_revenue'] ?? 0);
|
|
$row['net_after_fees'] = $row['net_revenue'] - $row['payment_fees'];
|
|
}
|
|
unset($row);
|
|
usort($rows, static function (array $a, array $b): int {
|
|
$cmp = ((float)($b['net_revenue'] ?? 0)) <=> ((float)($a['net_revenue'] ?? 0));
|
|
if ($cmp !== 0) {
|
|
return $cmp;
|
|
}
|
|
$cmp = ((float)($b['net_after_fees'] ?? 0)) <=> ((float)($a['net_after_fees'] ?? 0));
|
|
if ($cmp !== 0) {
|
|
return $cmp;
|
|
}
|
|
return strcasecmp((string)($a['track_display'] ?? ''), (string)($b['track_display'] ?? ''));
|
|
});
|
|
if (!empty($filters['limit'])) {
|
|
$rows = array_slice($rows, 0, max(1, (int)$filters['limit']));
|
|
}
|
|
return $rows;
|
|
} catch (Throwable $e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private function releaseTracks(PDO $db, int $releaseId, string $artistName, string $releaseTitle, string $catalogNo): array
|
|
{
|
|
if ($releaseId <= 0) {
|
|
return [];
|
|
}
|
|
if (isset($this->releaseTrackCache[$releaseId])) {
|
|
return $this->releaseTrackCache[$releaseId];
|
|
}
|
|
try {
|
|
$stmt = $db->prepare("
|
|
SELECT
|
|
t.id,
|
|
t.title,
|
|
COALESCE(t.mix_name, '') AS mix_name,
|
|
COALESCE(sp.track_price, 0) AS track_price
|
|
FROM ac_release_tracks t
|
|
LEFT JOIN ac_store_track_products sp ON sp.release_track_id = t.id AND sp.is_enabled = 1
|
|
WHERE t.release_id = :release_id
|
|
ORDER BY t.track_no ASC, t.id ASC
|
|
");
|
|
$stmt->execute([':release_id' => $releaseId]);
|
|
$tracks = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
|
$targets = [];
|
|
foreach ($tracks as $track) {
|
|
$targets[] = [
|
|
'track_id' => (int)($track['id'] ?? 0),
|
|
'artist_name' => $artistName,
|
|
'release_title' => $releaseTitle,
|
|
'catalog_no' => $catalogNo,
|
|
'track_title' => (string)($track['title'] ?? 'Track'),
|
|
'mix_name' => (string)($track['mix_name'] ?? ''),
|
|
'weight' => max(0.0, (float)($track['track_price'] ?? 0)),
|
|
];
|
|
}
|
|
$this->releaseTrackCache[$releaseId] = $targets;
|
|
return $targets;
|
|
} catch (Throwable $e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private function trackDownloadCount(PDO $db, int $trackId): int
|
|
{
|
|
if ($trackId <= 0) {
|
|
return 0;
|
|
}
|
|
if (isset($this->trackDownloadCountCache[$trackId])) {
|
|
return $this->trackDownloadCountCache[$trackId];
|
|
}
|
|
try {
|
|
$stmt = $db->prepare("
|
|
SELECT COUNT(e.id) AS download_count
|
|
FROM ac_store_files f
|
|
LEFT JOIN ac_store_download_events e ON e.file_id = f.id
|
|
WHERE f.scope_type = 'track' AND f.scope_id = :track_id
|
|
");
|
|
$stmt->execute([':track_id' => $trackId]);
|
|
$count = (int)(($stmt->fetch(PDO::FETCH_ASSOC) ?: [])['download_count'] ?? 0);
|
|
$this->trackDownloadCountCache[$trackId] = $count;
|
|
return $count;
|
|
} catch (Throwable $e) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private function artistOptions(PDO $db): array
|
|
{
|
|
try {
|
|
$stmt = $db->query("SELECT id, name FROM ac_artists WHERE name IS NOT NULL AND name <> '' ORDER BY name ASC");
|
|
return $stmt ? ($stmt->fetchAll(PDO::FETCH_ASSOC) ?: []) : [];
|
|
} catch (Throwable $e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private function dateWhere(array $filters, string $alias): array
|
|
{
|
|
$where = [];
|
|
$params = [];
|
|
$from = trim((string)($filters['from'] ?? ''));
|
|
$to = trim((string)($filters['to'] ?? ''));
|
|
if ($from !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $from)) {
|
|
$where[] = $alias . '.created_at >= :from_date';
|
|
$params[':from_date'] = $from . ' 00:00:00';
|
|
}
|
|
if ($to !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) {
|
|
$where[] = $alias . '.created_at <= :to_date';
|
|
$params[':to_date'] = $to . ' 23:59:59';
|
|
}
|
|
return [$where ? ' WHERE ' . implode(' AND ', $where) : '', $params];
|
|
}
|
|
}
|