This commit is contained in:
2026-04-19 22:43:02 +02:00
parent 10cba24727
commit fd1e23eb26
23 changed files with 2320 additions and 72 deletions

View File

@@ -0,0 +1,369 @@
<?php
declare(strict_types=1);
namespace App\Modules\Statistics;
use App\Core\Http\Request;
use App\Core\Http\Response;
use App\Core\Security\Csrf;
use App\Core\View\Template;
use App\Core\I18n\Translator;
use App\Modules\Auth\AuthService;
use DateInterval;
use DatePeriod;
use DateTimeImmutable;
final class OrdersStatisticsController
{
public function __construct(
private readonly Template $template,
private readonly Translator $translator,
private readonly AuthService $auth,
private readonly OrdersStatisticsRepository $repository
) {
}
public function index(Request $request): Response
{
[$dateFrom, $dateTo] = $this->resolveDateRange($request);
$statusGroups = $this->repository->listStatusGroups();
$statusGroupOptions = $this->mapStatusGroupOptions($statusGroups);
$selectedStatusGroups = $this->resolveSelectedStatusGroups($request, $statusGroupOptions);
$channelOptions = $this->mapChannelOptions($this->repository->listChannelOptions());
$selectedChannels = $this->resolveSelectedChannels($request, $channelOptions);
$statusCodes = $this->repository->statusCodesByGroupIds($selectedStatusGroups);
$aggregated = $this->repository->aggregateByDay($dateFrom, $dateTo, $selectedChannels, $statusCodes);
$debugEnabled = (string) $request->input('debug', '') === '1';
$diagnostics = $debugEnabled
? $this->repository->diagnostics($dateFrom, $dateTo, $selectedChannels, $statusCodes)
: [];
$table = $this->buildTable($dateFrom, $dateTo, $selectedChannels, $aggregated);
$html = $this->template->render('statistics/orders', [
'title' => $this->translator->get('statistics.orders.title'),
'activeMenu' => 'statistics',
'activeStatistics' => 'orders',
'user' => $this->auth->user(),
'csrfToken' => Csrf::token(),
'filters' => [
'date_from' => $dateFrom,
'date_to' => $dateTo,
'selected_channels' => $selectedChannels,
'selected_status_groups' => $selectedStatusGroups,
],
'channelOptions' => $channelOptions,
'statusGroupOptions' => $statusGroupOptions,
'table' => $table,
'debugEnabled' => $debugEnabled,
'diagnostics' => $diagnostics,
'debugMeta' => [
'selected_channels' => $selectedChannels,
'selected_status_groups' => $selectedStatusGroups,
'status_codes' => $statusCodes,
],
], 'layouts/app');
return Response::html($html);
}
/**
* @return array{0:string,1:string}
*/
private function resolveDateRange(Request $request): array
{
$now = new DateTimeImmutable('now');
$defaultFrom = $now->modify('first day of this month')->format('Y-m-d');
$defaultTo = $now->modify('last day of this month')->format('Y-m-d');
$dateFrom = trim((string) $request->input('date_from', $defaultFrom));
$dateTo = trim((string) $request->input('date_to', $defaultTo));
if (!$this->isValidDate($dateFrom)) {
$dateFrom = $defaultFrom;
}
if (!$this->isValidDate($dateTo)) {
$dateTo = $defaultTo;
}
if ($dateFrom > $dateTo) {
[$dateFrom, $dateTo] = [$dateTo, $dateFrom];
}
return [$dateFrom, $dateTo];
}
private function isValidDate(string $date): bool
{
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) !== 1) {
return false;
}
return DateTimeImmutable::createFromFormat('Y-m-d', $date) !== false;
}
/**
* @param array<int, array{id:int,name:string}> $groups
* @return array<int, array{id:int,name:string}>
*/
private function mapStatusGroupOptions(array $groups): array
{
$options = [];
foreach ($groups as $group) {
$groupId = (int) ($group['id'] ?? 0);
if ($groupId <= 0) {
continue;
}
$options[] = [
'id' => $groupId,
'name' => trim((string) ($group['name'] ?? '')),
];
}
return $options;
}
/**
* @param array<int, array{id:int,name:string}> $statusGroupOptions
* @return array<int, int>
*/
private function resolveSelectedStatusGroups(Request $request, array $statusGroupOptions): array
{
$allowed = [];
foreach ($statusGroupOptions as $option) {
$allowed[] = (int) $option['id'];
}
$allInput = $request->all();
$hasStatusGroupsParam = array_key_exists('status_groups', $allInput);
$selected = $this->toIntegerList($request->input('status_groups', []));
if (!$hasStatusGroupsParam) {
$selected = $this->defaultStatusGroupIds($statusGroupOptions);
}
$selected = array_values(array_intersect($selected, $allowed));
if ($selected !== []) {
return $selected;
}
return $hasStatusGroupsParam ? [] : $this->defaultStatusGroupIds($statusGroupOptions);
}
/**
* @param array<int, array{id:int,name:string}> $statusGroupOptions
* @return array<int, int>
*/
private function defaultStatusGroupIds(array $statusGroupOptions): array
{
$selected = [];
foreach ($statusGroupOptions as $group) {
$name = trim((string) ($group['name'] ?? ''));
if ($this->isCancelledGroup($name)) {
continue;
}
$selected[] = (int) ($group['id'] ?? 0);
}
return array_values(array_filter($selected, static fn (int $id): bool => $id > 0));
}
private function isCancelledGroup(string $name): bool
{
$normalized = strtr(mb_strtolower(trim($name)), [
'ą' => 'a',
'ć' => 'c',
'ę' => 'e',
'ł' => 'l',
'ń' => 'n',
'ó' => 'o',
'ś' => 's',
'ż' => 'z',
'ź' => 'z',
]);
return in_array($normalized, ['anulowane', 'anulowany', 'cancelled', 'canceled'], true);
}
/**
* @param array<int, array{key:string,label:string}> $channels
* @return array<int, array{key:string,label:string}>
*/
private function mapChannelOptions(array $channels): array
{
$options = [];
foreach ($channels as $channel) {
$key = trim((string) ($channel['key'] ?? ''));
if ($key === '') {
continue;
}
$options[] = [
'key' => $key,
'label' => trim((string) ($channel['label'] ?? $key)),
];
}
return $options;
}
/**
* @param array<int, array{key:string,label:string}> $channelOptions
* @return array<int, string>
*/
private function resolveSelectedChannels(Request $request, array $channelOptions): array
{
$allowed = [];
foreach ($channelOptions as $option) {
$allowed[] = (string) $option['key'];
}
$allInput = $request->all();
$hasChannelsParam = array_key_exists('channels', $allInput);
$selected = $this->toStringList($request->input('channels', []));
if (!$hasChannelsParam) {
return $allowed;
}
return array_values(array_intersect($selected, $allowed));
}
/**
* @param array<int, string> $selectedChannels
* @param array<int, array{day:string,channel_key:string,orders_count:int,total_net:float,total_gross:float}> $aggregated
* @return array{rows:array<int,array<string,mixed>>,totals:array<string,mixed>,hasData:bool}
*/
private function buildTable(string $dateFrom, string $dateTo, array $selectedChannels, array $aggregated): array
{
$rows = [];
foreach ($this->dateRange($dateFrom, $dateTo) as $day) {
$rows[$day] = [
'day' => $day,
'channels' => $this->emptyChannelsRow($selectedChannels),
'day_total_orders' => 0,
'day_total_net' => 0.0,
'day_total_gross' => 0.0,
];
}
foreach ($aggregated as $item) {
$day = (string) ($item['day'] ?? '');
$channelKey = (string) ($item['channel_key'] ?? '');
if ($day === '' || $channelKey === '' || !isset($rows[$day]['channels'][$channelKey])) {
continue;
}
$ordersCount = (int) ($item['orders_count'] ?? 0);
$totalNet = (float) ($item['total_net'] ?? 0);
$totalGross = (float) ($item['total_gross'] ?? 0);
$rows[$day]['channels'][$channelKey] = [
'orders_count' => $ordersCount,
'total_net' => $totalNet,
'total_gross' => $totalGross,
];
$rows[$day]['day_total_orders'] += $ordersCount;
$rows[$day]['day_total_net'] += $totalNet;
$rows[$day]['day_total_gross'] += $totalGross;
}
$totals = [
'channels' => $this->emptyChannelsRow($selectedChannels),
'orders_count' => 0,
'total_net' => 0.0,
'total_gross' => 0.0,
];
$hasData = false;
foreach ($rows as $row) {
foreach ($selectedChannels as $channelKey) {
$channelStats = $row['channels'][$channelKey];
$totals['channels'][$channelKey]['orders_count'] += (int) ($channelStats['orders_count'] ?? 0);
$totals['channels'][$channelKey]['total_net'] += (float) ($channelStats['total_net'] ?? 0);
$totals['channels'][$channelKey]['total_gross'] += (float) ($channelStats['total_gross'] ?? 0);
}
$totals['orders_count'] += (int) ($row['day_total_orders'] ?? 0);
$totals['total_net'] += (float) ($row['day_total_net'] ?? 0);
$totals['total_gross'] += (float) ($row['day_total_gross'] ?? 0);
if ((int) ($row['day_total_orders'] ?? 0) > 0) {
$hasData = true;
}
}
return [
'rows' => array_values($rows),
'totals' => $totals,
'hasData' => $hasData,
];
}
/**
* @param array<int, string> $channelKeys
* @return array<string, array{orders_count:int,total_net:float,total_gross:float}>
*/
private function emptyChannelsRow(array $channelKeys): array
{
$row = [];
foreach ($channelKeys as $channelKey) {
$row[$channelKey] = [
'orders_count' => 0,
'total_net' => 0.0,
'total_gross' => 0.0,
];
}
return $row;
}
/**
* @return array<int, string>
*/
private function dateRange(string $dateFrom, string $dateTo): array
{
$start = new DateTimeImmutable($dateFrom);
$end = (new DateTimeImmutable($dateTo))->add(new DateInterval('P1D'));
$period = new DatePeriod($start, new DateInterval('P1D'), $end);
$dates = [];
foreach ($period as $date) {
$dates[] = $date->format('Y-m-d');
}
return $dates;
}
/**
* @return array<int, string>
*/
private function toStringList(mixed $value): array
{
$items = is_array($value) ? $value : ($value !== null && $value !== '' ? [$value] : []);
$list = [];
foreach ($items as $item) {
$normalized = trim((string) $item);
if ($normalized !== '') {
$list[] = $normalized;
}
}
return array_values(array_unique($list));
}
/**
* @return array<int, int>
*/
private function toIntegerList(mixed $value): array
{
$items = is_array($value) ? $value : ($value !== null && $value !== '' ? [$value] : []);
$list = [];
foreach ($items as $item) {
$number = (int) $item;
if ($number > 0) {
$list[] = $number;
}
}
return array_values(array_unique($list));
}
}

View File

@@ -0,0 +1,590 @@
<?php
declare(strict_types=1);
namespace App\Modules\Statistics;
use PDO;
use Throwable;
final class OrdersStatisticsRepository
{
private static ?bool $hasOrdersTotalWithoutTax = null;
private static ?bool $hasOrdersTotalNet = null;
private static ?bool $hasOrdersTotalWithTax = null;
private static ?bool $hasOrdersTotalGross = null;
private static ?bool $hasOrdersIntegrationId = null;
private static ?bool $hasOrdersStatusCode = null;
private static ?bool $hasOrdersExternalStatusId = null;
public function __construct(private readonly PDO $pdo)
{
}
/**
* @return array<int, array{id:int,name:string}>
*/
public function listStatusGroups(): array
{
try {
$stmt = $this->pdo->query(
'SELECT id, name
FROM order_status_groups
WHERE is_active = 1
ORDER BY sort_order ASC, id ASC'
);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable) {
return [];
}
if (!is_array($rows)) {
return [];
}
$groups = [];
foreach ($rows as $row) {
$groupId = (int) ($row['id'] ?? 0);
if ($groupId <= 0) {
continue;
}
$groups[] = [
'id' => $groupId,
'name' => trim((string) ($row['name'] ?? '')),
];
}
return $groups;
}
/**
* @return array<int, array{key:string,label:string}>
*/
public function listChannelOptions(): array
{
$hasIntegrationId = $this->hasOrdersColumn('integration_id');
try {
if ($hasIntegrationId) {
$rows = $this->pdo->query(
'SELECT DISTINCT COALESCE(o.integration_id, 0) AS integration_id
FROM orders o
WHERE LOWER(COALESCE(o.source, "")) = "shoppro"
ORDER BY integration_id ASC'
)->fetchAll(PDO::FETCH_ASSOC);
} else {
$rows = $this->pdo->query(
'SELECT 0 AS integration_id
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE LOWER(COALESCE(o.source, "")) = "shoppro"
)'
)->fetchAll(PDO::FETCH_ASSOC);
}
} catch (Throwable) {
$rows = [];
}
$channels = [
['key' => 'allegro', 'label' => 'Allegro'],
];
if (!is_array($rows)) {
return $channels;
}
foreach ($rows as $row) {
$integrationId = (int) ($row['integration_id'] ?? 0);
$channels[] = [
'key' => 'shoppro:' . $integrationId,
'label' => $this->shopproChannelLabel($integrationId),
];
}
return $channels;
}
/**
* @param array<int, int> $groupIds
* @return array<int, string>
*/
public function statusCodesByGroupIds(array $groupIds): array
{
$groupIds = array_values(array_unique(array_filter($groupIds, static fn (int $id): bool => $id > 0)));
if ($groupIds === []) {
return [];
}
[$inSql, $params] = $this->buildIntegerInClause('gid', $groupIds);
try {
$stmt = $this->pdo->prepare(
'SELECT code
FROM order_statuses
WHERE is_active = 1
AND group_id IN (' . $inSql . ')
ORDER BY sort_order ASC, id ASC'
);
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_COLUMN);
} catch (Throwable) {
return [];
}
if (!is_array($rows)) {
return [];
}
$codes = [];
foreach ($rows as $code) {
$normalized = strtolower(trim((string) $code));
if ($normalized !== '') {
$codes[] = $normalized;
}
}
return array_values(array_unique($codes));
}
/**
* @param array<int, string> $channels
* @param array<int, string> $statusCodes
* @return array<int, array{day:string,channel_key:string,orders_count:int,total_net:float,total_gross:float}>
*/
public function aggregateByDay(string $dateFrom, string $dateTo, array $channels, array $statusCodes): array
{
$channels = array_values(array_unique(array_filter(
array_map(static fn (string $item): string => trim($item), $channels),
static fn (string $item): bool => $item !== ''
)));
$statusCodes = array_values(array_unique(array_filter(
array_map(static fn (string $item): string => strtolower(trim($item)), $statusCodes),
static fn (string $item): bool => $item !== ''
)));
if ($channels === []) {
return [];
}
$effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
$effectiveDateSql = $this->effectiveDateSql('o');
$channelSql = $this->channelSql('o');
$netAmountSql = $this->netAmountSql('o');
$grossAmountSql = $this->grossAmountSql('o');
$rawStatusSql = $this->rawStatusSql('o');
[$channelInSql, $channelParams] = $this->buildStringInClause('ch', $channels);
$params = array_merge($channelParams, [
'date_from' => $dateFrom . ' 00:00:00',
'date_to' => $dateTo . ' 23:59:59',
]);
$statusFilterSql = '';
if ($statusCodes !== []) {
[$statusInSql, $statusParams] = $this->buildStringInClause('st', $statusCodes);
$statusFilterSql = ' AND ' . $effectiveStatusSql . ' IN (' . $statusInSql . ')';
$params = array_merge($params, $statusParams);
}
$sql = 'SELECT
DATE(' . $effectiveDateSql . ') AS day,
' . $channelSql . ' AS channel_key,
COUNT(*) AS orders_count,
SUM(' . $netAmountSql . ') AS total_net,
SUM(' . $grossAmountSql . ') AS total_gross
FROM orders o
LEFT JOIN allegro_order_status_mappings asm
ON o.source = "allegro"
AND LOWER(' . $rawStatusSql . ') = asm.allegro_status_code
WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
AND ' . $effectiveDateSql . ' IS NOT NULL
AND ' . $effectiveDateSql . ' >= :date_from
AND ' . $effectiveDateSql . ' <= :date_to
AND ' . $channelSql . ' IN (' . $channelInSql . ')
' . $statusFilterSql . '
GROUP BY DATE(' . $effectiveDateSql . '), ' . $channelSql . '
ORDER BY DATE(' . $effectiveDateSql . ') ASC';
try {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable) {
return [];
}
if (!is_array($rows)) {
return [];
}
$result = [];
foreach ($rows as $row) {
$day = trim((string) ($row['day'] ?? ''));
$channelKey = trim((string) ($row['channel_key'] ?? ''));
if ($day === '' || $channelKey === '') {
continue;
}
$result[] = [
'day' => $day,
'channel_key' => $channelKey,
'orders_count' => (int) ($row['orders_count'] ?? 0),
'total_net' => (float) ($row['total_net'] ?? 0),
'total_gross' => (float) ($row['total_gross'] ?? 0),
];
}
return $result;
}
/**
* @param array<int, string> $channels
* @param array<int, string> $statusCodes
* @return array<string, mixed>
*/
public function diagnostics(string $dateFrom, string $dateTo, array $channels, array $statusCodes): array
{
$effectiveDateSql = $this->effectiveDateSql('o');
$channelSql = $this->channelSql('o');
$effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
$rawStatusSql = $this->rawStatusSql('o');
$data = [
'columns' => [
'integration_id' => $this->hasOrdersColumn('integration_id'),
'total_with_tax' => $this->hasOrdersColumn('total_with_tax'),
'total_gross' => $this->hasOrdersColumn('total_gross'),
'total_without_tax' => $this->hasOrdersColumn('total_without_tax'),
'total_net' => $this->hasOrdersColumn('total_net'),
'status_code' => $this->hasOrdersColumn('status_code'),
'external_status_id' => $this->hasOrdersColumn('external_status_id'),
],
'counts' => [],
'errors' => [],
];
$data['counts']['in_date_and_source'] = $this->safeCount(
'SELECT COUNT(*)
FROM orders o
WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
AND ' . $effectiveDateSql . ' IS NOT NULL
AND ' . $effectiveDateSql . ' >= :date_from
AND ' . $effectiveDateSql . ' <= :date_to',
[
'date_from' => $dateFrom . ' 00:00:00',
'date_to' => $dateTo . ' 23:59:59',
],
$data['errors'],
'in_date_and_source'
);
if ($channels !== []) {
[$channelInSql, $channelParams] = $this->buildStringInClause('dbg_ch', $channels);
$data['counts']['after_channel_filter'] = $this->safeCount(
'SELECT COUNT(*)
FROM orders o
WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
AND ' . $effectiveDateSql . ' IS NOT NULL
AND ' . $effectiveDateSql . ' >= :date_from
AND ' . $effectiveDateSql . ' <= :date_to
AND ' . $channelSql . ' IN (' . $channelInSql . ')',
array_merge(
[
'date_from' => $dateFrom . ' 00:00:00',
'date_to' => $dateTo . ' 23:59:59',
],
$channelParams
),
$data['errors'],
'after_channel_filter'
);
} else {
$data['counts']['after_channel_filter'] = 0;
}
if ($statusCodes !== []) {
[$statusInSql, $statusParams] = $this->buildStringInClause('dbg_st', $statusCodes);
$data['counts']['after_status_filter'] = $this->safeCount(
'SELECT COUNT(*)
FROM orders o
LEFT JOIN allegro_order_status_mappings asm
ON o.source = "allegro"
AND LOWER(' . $rawStatusSql . ') = asm.allegro_status_code
WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
AND ' . $effectiveDateSql . ' IS NOT NULL
AND ' . $effectiveDateSql . ' >= :date_from
AND ' . $effectiveDateSql . ' <= :date_to
AND ' . $effectiveStatusSql . ' IN (' . $statusInSql . ')',
array_merge(
[
'date_from' => $dateFrom . ' 00:00:00',
'date_to' => $dateTo . ' 23:59:59',
],
$statusParams
),
$data['errors'],
'after_status_filter'
);
} else {
$data['counts']['after_status_filter'] = $data['counts']['after_channel_filter'] ?? 0;
}
return $data;
}
/**
* @param array<int, int> $values
* @return array{0:string,1:array<string,int>}
*/
private function buildIntegerInClause(string $prefix, array $values): array
{
$placeholders = [];
$params = [];
foreach (array_values($values) as $index => $value) {
$key = $prefix . '_' . $index;
$placeholders[] = ':' . $key;
$params[$key] = (int) $value;
}
return [implode(', ', $placeholders), $params];
}
/**
* @param array<int, string> $values
* @return array{0:string,1:array<string,string>}
*/
private function buildStringInClause(string $prefix, array $values): array
{
$placeholders = [];
$params = [];
foreach (array_values($values) as $index => $value) {
$key = $prefix . '_' . $index;
$placeholders[] = ':' . $key;
$params[$key] = (string) $value;
}
return [implode(', ', $placeholders), $params];
}
/**
* @param array<string, mixed> $params
* @param array<int, string> $errors
*/
private function safeCount(string $sql, array $params, array &$errors, string $label): int
{
try {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return (int) $stmt->fetchColumn();
} catch (Throwable $exception) {
$errors[] = $label . ': ' . $exception->getMessage();
return -1;
}
}
private function effectiveStatusSql(string $orderAlias, string $mappingAlias): string
{
$rawStatusSql = $this->rawStatusSql($orderAlias);
return 'LOWER(
CASE
WHEN ' . $orderAlias . '.source = "allegro"
AND ' . $mappingAlias . '.orderpro_status_code IS NOT NULL
AND ' . $mappingAlias . '.orderpro_status_code <> ""
THEN ' . $mappingAlias . '.orderpro_status_code
ELSE ' . $rawStatusSql . '
END
)';
}
private function effectiveDateSql(string $orderAlias): string
{
return 'COALESCE(
' . $orderAlias . '.ordered_at,
' . $orderAlias . '.source_created_at,
' . $orderAlias . '.source_updated_at,
' . $orderAlias . '.fetched_at
)';
}
private function channelSql(string $orderAlias): string
{
if ($this->hasOrdersColumn('integration_id')) {
return '(CASE
WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "allegro" THEN "allegro"
WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "shoppro" THEN CONCAT("shoppro:", COALESCE(CAST(' . $orderAlias . '.integration_id AS CHAR) COLLATE utf8mb4_unicode_ci, "0"))
ELSE LOWER(COALESCE(' . $orderAlias . '.source, ""))
END) COLLATE utf8mb4_unicode_ci';
}
return '(CASE
WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "allegro" THEN "allegro"
WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "shoppro" THEN "shoppro:0"
ELSE LOWER(COALESCE(' . $orderAlias . '.source, ""))
END) COLLATE utf8mb4_unicode_ci';
}
private function shopproChannelLabel(int $integrationId): string
{
if ($integrationId <= 0) {
return 'shopPRO (brak integracji)';
}
try {
$stmt = $this->pdo->prepare(
'SELECT name
FROM integrations
WHERE id = :id
LIMIT 1'
);
$stmt->execute(['id' => $integrationId]);
$name = $stmt->fetchColumn();
} catch (Throwable) {
$name = false;
}
if (is_string($name) && trim($name) !== '') {
return trim($name);
}
return 'shopPRO #' . $integrationId;
}
private function netAmountSql(string $orderAlias): string
{
$netColumn = null;
if ($this->hasOrdersColumn('total_without_tax')) {
$netColumn = $orderAlias . '.total_without_tax';
} elseif ($this->hasOrdersColumn('total_net')) {
$netColumn = $orderAlias . '.total_net';
}
$grossColumn = null;
if ($this->hasOrdersColumn('total_with_tax')) {
$grossColumn = $orderAlias . '.total_with_tax';
} elseif ($this->hasOrdersColumn('total_gross')) {
$grossColumn = $orderAlias . '.total_gross';
}
// Fallback: gdy netto z zrodla jest puste (shopPRO nie wysyla netto), wyliczamy z brutto/1.23.
// TODO(STAT-NET): docelowo pobierac netto z shopPRO na poziomie zamowienia lub liczyc z order_items po faktycznym tax_rate.
if ($netColumn !== null && $grossColumn !== null) {
return 'CASE
WHEN ' . $netColumn . ' IS NOT NULL AND ' . $netColumn . ' > 0 THEN ' . $netColumn . '
WHEN ' . $grossColumn . ' IS NOT NULL AND ' . $grossColumn . ' > 0 THEN ROUND(' . $grossColumn . ' / 1.23, 2)
ELSE 0
END';
}
if ($netColumn !== null) {
return 'COALESCE(' . $netColumn . ', 0)';
}
if ($grossColumn !== null) {
return 'ROUND(COALESCE(' . $grossColumn . ', 0) / 1.23, 2)';
}
return '0';
}
private function grossAmountSql(string $orderAlias): string
{
if ($this->hasOrdersColumn('total_with_tax')) {
return 'COALESCE(' . $orderAlias . '.total_with_tax, 0)';
}
if ($this->hasOrdersColumn('total_gross')) {
return 'COALESCE(' . $orderAlias . '.total_gross, 0)';
}
if ($this->hasOrdersColumn('total_without_tax')) {
return 'COALESCE(' . $orderAlias . '.total_without_tax, 0)';
}
if ($this->hasOrdersColumn('total_net')) {
return 'COALESCE(' . $orderAlias . '.total_net, 0)';
}
return '0';
}
private function rawStatusSql(string $orderAlias): string
{
if ($this->hasOrdersColumn('status_code')) {
return 'COALESCE(' . $orderAlias . '.status_code, "")';
}
if ($this->hasOrdersColumn('external_status_id')) {
return 'COALESCE(' . $orderAlias . '.external_status_id, "")';
}
return '""';
}
private function hasOrdersColumn(string $column): bool
{
if ($column === 'integration_id' && self::$hasOrdersIntegrationId !== null) {
return self::$hasOrdersIntegrationId;
}
if ($column === 'total_without_tax' && self::$hasOrdersTotalWithoutTax !== null) {
return self::$hasOrdersTotalWithoutTax;
}
if ($column === 'total_net' && self::$hasOrdersTotalNet !== null) {
return self::$hasOrdersTotalNet;
}
if ($column === 'total_with_tax' && self::$hasOrdersTotalWithTax !== null) {
return self::$hasOrdersTotalWithTax;
}
if ($column === 'total_gross' && self::$hasOrdersTotalGross !== null) {
return self::$hasOrdersTotalGross;
}
if ($column === 'status_code' && self::$hasOrdersStatusCode !== null) {
return self::$hasOrdersStatusCode;
}
if ($column === 'external_status_id' && self::$hasOrdersExternalStatusId !== null) {
return self::$hasOrdersExternalStatusId;
}
try {
$stmt = $this->pdo->prepare(
'SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = :table_name
AND COLUMN_NAME = :column_name'
);
$stmt->execute([
'table_name' => 'orders',
'column_name' => $column,
]);
$exists = ((int) $stmt->fetchColumn()) > 0;
} catch (Throwable) {
$exists = false;
}
if ($column === 'total_without_tax') {
self::$hasOrdersTotalWithoutTax = $exists;
}
if ($column === 'total_net') {
self::$hasOrdersTotalNet = $exists;
}
if ($column === 'integration_id') {
self::$hasOrdersIntegrationId = $exists;
}
if ($column === 'total_with_tax') {
self::$hasOrdersTotalWithTax = $exists;
}
if ($column === 'total_gross') {
self::$hasOrdersTotalGross = $exists;
}
if ($column === 'status_code') {
self::$hasOrdersStatusCode = $exists;
}
if ($column === 'external_status_id') {
self::$hasOrdersExternalStatusId = $exists;
}
return $exists;
}
}