Add Orders and Order Status repositories with pagination and management features
- Implemented OrdersRepository for handling order data with pagination, filtering, and sorting capabilities. - Added methods for retrieving order status options, quick stats, and detailed order information. - Created OrderStatusRepository for managing order status groups and statuses, including CRUD operations and sorting. - Introduced a bootstrap file for test environment setup and autoloading.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use PDO;
|
||||
|
||||
final class AppSettingsRepository
|
||||
{
|
||||
public function __construct(private readonly PDO $pdo)
|
||||
{
|
||||
}
|
||||
|
||||
public function get(string $key, ?string $default = null): ?string
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT setting_value
|
||||
FROM app_settings
|
||||
WHERE setting_key = :setting_key
|
||||
LIMIT 1'
|
||||
);
|
||||
$statement->execute(['setting_key' => trim($key)]);
|
||||
|
||||
$value = $statement->fetchColumn();
|
||||
if ($value === false || $value === null) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$text = trim((string) $value);
|
||||
return $text === '' ? $default : $text;
|
||||
}
|
||||
|
||||
public function getBool(string $key, bool $default = false): bool
|
||||
{
|
||||
$value = $this->get($key);
|
||||
if ($value === null) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return in_array(strtolower(trim($value)), ['1', 'true', 'yes', 'on'], true);
|
||||
}
|
||||
|
||||
public function getInt(string $key, int $default = 0): int
|
||||
{
|
||||
$value = $this->get($key);
|
||||
if ($value === null || !is_numeric($value)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
public function set(string $key, string $value): void
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO app_settings (setting_key, setting_value, created_at, updated_at)
|
||||
VALUES (:setting_key, :setting_value, :created_at, :updated_at)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
setting_value = VALUES(setting_value),
|
||||
updated_at = VALUES(updated_at)'
|
||||
);
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$statement->execute([
|
||||
'setting_key' => trim($key),
|
||||
'setting_value' => trim($value),
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,679 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use PDO;
|
||||
use RuntimeException;
|
||||
|
||||
final class IntegrationRepository
|
||||
{
|
||||
private ?bool $ordersFetchColumnsAvailable = null;
|
||||
private ?bool $orderStatusSyncDirectionColumnAvailable = null;
|
||||
private const ORDER_STATUS_SYNC_DIRECTION_DEFAULT = 'shoppro_to_orderpro';
|
||||
|
||||
public function __construct(
|
||||
private readonly PDO $pdo,
|
||||
private readonly string $secret
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function listByType(string $type): array
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id, type, name, base_url, timeout_seconds, is_active'
|
||||
. $this->ordersFetchSelectFragment()
|
||||
. $this->orderStatusSyncDirectionSelectFragment() . ',
|
||||
last_test_status, last_test_http_code, last_test_message, last_test_at,
|
||||
created_at, updated_at,
|
||||
CASE WHEN api_key_encrypted IS NULL OR api_key_encrypted = "" THEN 0 ELSE 1 END AS has_api_key
|
||||
FROM integrations
|
||||
WHERE type = :type
|
||||
ORDER BY id DESC'
|
||||
);
|
||||
$statement->execute(['type' => $type]);
|
||||
|
||||
$rows = $statement->fetchAll();
|
||||
if (!is_array($rows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map([$this, 'mapRow'], $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function findById(int $id): ?array
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id, type, name, base_url, timeout_seconds, is_active'
|
||||
. $this->ordersFetchSelectFragment()
|
||||
. $this->orderStatusSyncDirectionSelectFragment() . ',
|
||||
last_test_status, last_test_http_code, last_test_message, last_test_at,
|
||||
created_at, updated_at,
|
||||
CASE WHEN api_key_encrypted IS NULL OR api_key_encrypted = "" THEN 0 ELSE 1 END AS has_api_key
|
||||
FROM integrations
|
||||
WHERE id = :id
|
||||
LIMIT 1'
|
||||
);
|
||||
$statement->execute(['id' => $id]);
|
||||
|
||||
$row = $statement->fetch();
|
||||
if (!is_array($row)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->mapRow($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function findApiCredentials(int $id): ?array
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id, name, base_url, timeout_seconds, api_key_encrypted'
|
||||
. $this->ordersFetchSelectFragment()
|
||||
. $this->orderStatusSyncDirectionSelectFragment() . '
|
||||
FROM integrations
|
||||
WHERE id = :id
|
||||
LIMIT 1'
|
||||
);
|
||||
$statement->execute(['id' => $id]);
|
||||
|
||||
$row = $statement->fetch();
|
||||
if (!is_array($row)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'name' => (string) ($row['name'] ?? ''),
|
||||
'base_url' => (string) ($row['base_url'] ?? ''),
|
||||
'timeout_seconds' => (int) ($row['timeout_seconds'] ?? 10),
|
||||
'api_key' => $this->decryptApiKey((string) ($row['api_key_encrypted'] ?? '')),
|
||||
'orders_fetch_enabled' => (int) ($row['orders_fetch_enabled'] ?? 0) === 1,
|
||||
'orders_fetch_start_date' => $row['orders_fetch_start_date'] === null ? null : (string) $row['orders_fetch_start_date'],
|
||||
'order_status_sync_direction' => $this->normalizeOrderStatusSyncDirection((string) ($row['order_status_sync_direction'] ?? self::ORDER_STATUS_SYNC_DIRECTION_DEFAULT)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function findActiveApiCredentialsByType(string $type): ?array
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id, name, base_url, timeout_seconds, api_key_encrypted'
|
||||
. $this->ordersFetchSelectFragment()
|
||||
. $this->orderStatusSyncDirectionSelectFragment() . '
|
||||
FROM integrations
|
||||
WHERE type = :type AND is_active = 1
|
||||
ORDER BY id DESC
|
||||
LIMIT 1'
|
||||
);
|
||||
$statement->execute(['type' => $type]);
|
||||
|
||||
$row = $statement->fetch();
|
||||
if (!is_array($row)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'name' => (string) ($row['name'] ?? ''),
|
||||
'base_url' => (string) ($row['base_url'] ?? ''),
|
||||
'timeout_seconds' => (int) ($row['timeout_seconds'] ?? 10),
|
||||
'api_key' => $this->decryptApiKey((string) ($row['api_key_encrypted'] ?? '')),
|
||||
'orders_fetch_enabled' => (int) ($row['orders_fetch_enabled'] ?? 0) === 1,
|
||||
'orders_fetch_start_date' => $row['orders_fetch_start_date'] === null ? null : (string) $row['orders_fetch_start_date'],
|
||||
'order_status_sync_direction' => $this->normalizeOrderStatusSyncDirection((string) ($row['order_status_sync_direction'] ?? self::ORDER_STATUS_SYNC_DIRECTION_DEFAULT)),
|
||||
];
|
||||
}
|
||||
|
||||
public function create(
|
||||
string $type,
|
||||
string $name,
|
||||
string $baseUrl,
|
||||
int $timeoutSeconds,
|
||||
bool $isActive,
|
||||
string $apiKey,
|
||||
bool $ordersFetchEnabled = false,
|
||||
?string $ordersFetchStartDate = null,
|
||||
string $orderStatusSyncDirection = self::ORDER_STATUS_SYNC_DIRECTION_DEFAULT
|
||||
): int {
|
||||
$normalizedSyncDirection = $this->normalizeOrderStatusSyncDirection($orderStatusSyncDirection);
|
||||
if ($this->hasOrdersFetchColumns() && $this->hasOrderStatusSyncDirectionColumn()) {
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO integrations (
|
||||
type, name, base_url, api_key_encrypted, timeout_seconds, is_active,
|
||||
orders_fetch_enabled, orders_fetch_start_date, order_status_sync_direction,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
:type, :name, :base_url, :api_key_encrypted, :timeout_seconds, :is_active,
|
||||
:orders_fetch_enabled, :orders_fetch_start_date, :order_status_sync_direction,
|
||||
:created_at, :updated_at
|
||||
)'
|
||||
);
|
||||
$statement->execute([
|
||||
'type' => $type,
|
||||
'name' => $name,
|
||||
'base_url' => $baseUrl,
|
||||
'api_key_encrypted' => $this->encryptApiKey($apiKey),
|
||||
'timeout_seconds' => $timeoutSeconds,
|
||||
'is_active' => $isActive ? 1 : 0,
|
||||
'orders_fetch_enabled' => $ordersFetchEnabled ? 1 : 0,
|
||||
'orders_fetch_start_date' => $ordersFetchStartDate,
|
||||
'order_status_sync_direction' => $normalizedSyncDirection,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
} elseif ($this->hasOrdersFetchColumns()) {
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO integrations (
|
||||
type, name, base_url, api_key_encrypted, timeout_seconds, is_active,
|
||||
orders_fetch_enabled, orders_fetch_start_date,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
:type, :name, :base_url, :api_key_encrypted, :timeout_seconds, :is_active,
|
||||
:orders_fetch_enabled, :orders_fetch_start_date,
|
||||
:created_at, :updated_at
|
||||
)'
|
||||
);
|
||||
$statement->execute([
|
||||
'type' => $type,
|
||||
'name' => $name,
|
||||
'base_url' => $baseUrl,
|
||||
'api_key_encrypted' => $this->encryptApiKey($apiKey),
|
||||
'timeout_seconds' => $timeoutSeconds,
|
||||
'is_active' => $isActive ? 1 : 0,
|
||||
'orders_fetch_enabled' => $ordersFetchEnabled ? 1 : 0,
|
||||
'orders_fetch_start_date' => $ordersFetchStartDate,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
} else {
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO integrations (
|
||||
type, name, base_url, api_key_encrypted, timeout_seconds, is_active, created_at, updated_at
|
||||
) VALUES (
|
||||
:type, :name, :base_url, :api_key_encrypted, :timeout_seconds, :is_active, :created_at, :updated_at
|
||||
)'
|
||||
);
|
||||
$statement->execute([
|
||||
'type' => $type,
|
||||
'name' => $name,
|
||||
'base_url' => $baseUrl,
|
||||
'api_key_encrypted' => $this->encryptApiKey($apiKey),
|
||||
'timeout_seconds' => $timeoutSeconds,
|
||||
'is_active' => $isActive ? 1 : 0,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
return (int) $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
public function update(
|
||||
int $id,
|
||||
string $name,
|
||||
string $baseUrl,
|
||||
int $timeoutSeconds,
|
||||
bool $isActive,
|
||||
?string $apiKey,
|
||||
bool $ordersFetchEnabled = false,
|
||||
?string $ordersFetchStartDate = null,
|
||||
string $orderStatusSyncDirection = self::ORDER_STATUS_SYNC_DIRECTION_DEFAULT
|
||||
): void {
|
||||
$normalizedSyncDirection = $this->normalizeOrderStatusSyncDirection($orderStatusSyncDirection);
|
||||
$params = [
|
||||
'id' => $id,
|
||||
'name' => $name,
|
||||
'base_url' => $baseUrl,
|
||||
'timeout_seconds' => $timeoutSeconds,
|
||||
'is_active' => $isActive ? 1 : 0,
|
||||
'orders_fetch_enabled' => $ordersFetchEnabled ? 1 : 0,
|
||||
'orders_fetch_start_date' => $ordersFetchStartDate,
|
||||
'order_status_sync_direction' => $normalizedSyncDirection,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$sql = 'UPDATE integrations SET
|
||||
name = :name,
|
||||
base_url = :base_url,
|
||||
timeout_seconds = :timeout_seconds,
|
||||
is_active = :is_active,
|
||||
updated_at = :updated_at';
|
||||
if ($this->hasOrdersFetchColumns() && $this->hasOrderStatusSyncDirectionColumn()) {
|
||||
$sql = 'UPDATE integrations SET
|
||||
name = :name,
|
||||
base_url = :base_url,
|
||||
timeout_seconds = :timeout_seconds,
|
||||
is_active = :is_active,
|
||||
orders_fetch_enabled = :orders_fetch_enabled,
|
||||
orders_fetch_start_date = :orders_fetch_start_date,
|
||||
order_status_sync_direction = :order_status_sync_direction,
|
||||
updated_at = :updated_at';
|
||||
} elseif ($this->hasOrdersFetchColumns()) {
|
||||
$sql = 'UPDATE integrations SET
|
||||
name = :name,
|
||||
base_url = :base_url,
|
||||
timeout_seconds = :timeout_seconds,
|
||||
is_active = :is_active,
|
||||
orders_fetch_enabled = :orders_fetch_enabled,
|
||||
orders_fetch_start_date = :orders_fetch_start_date,
|
||||
updated_at = :updated_at';
|
||||
unset($params['order_status_sync_direction']);
|
||||
} else {
|
||||
unset($params['orders_fetch_enabled'], $params['orders_fetch_start_date'], $params['order_status_sync_direction']);
|
||||
}
|
||||
|
||||
if ($apiKey !== null && trim($apiKey) !== '') {
|
||||
$sql .= ', api_key_encrypted = :api_key_encrypted';
|
||||
$params['api_key_encrypted'] = $this->encryptApiKey($apiKey);
|
||||
}
|
||||
|
||||
$sql .= ' WHERE id = :id';
|
||||
|
||||
$statement = $this->pdo->prepare($sql);
|
||||
$statement->execute($params);
|
||||
}
|
||||
|
||||
public function setTestResult(
|
||||
int $id,
|
||||
string $status,
|
||||
?int $httpCode,
|
||||
string $message,
|
||||
string $testedAt
|
||||
): void {
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE integrations SET
|
||||
last_test_status = :status,
|
||||
last_test_http_code = :http_code,
|
||||
last_test_message = :message,
|
||||
last_test_at = :tested_at,
|
||||
updated_at = :updated_at
|
||||
WHERE id = :id'
|
||||
);
|
||||
$statement->execute([
|
||||
'id' => $id,
|
||||
'status' => $status,
|
||||
'http_code' => $httpCode,
|
||||
'message' => mb_substr($message, 0, 255),
|
||||
'tested_at' => $testedAt,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function logTest(
|
||||
int $integrationId,
|
||||
string $status,
|
||||
?int $httpCode,
|
||||
string $message,
|
||||
string $endpointUrl,
|
||||
string $testedAt
|
||||
): void {
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO integration_test_logs (
|
||||
integration_id, status, http_code, message, endpoint_url, tested_at
|
||||
) VALUES (
|
||||
:integration_id, :status, :http_code, :message, :endpoint_url, :tested_at
|
||||
)'
|
||||
);
|
||||
$statement->execute([
|
||||
'integration_id' => $integrationId,
|
||||
'status' => $status,
|
||||
'http_code' => $httpCode,
|
||||
'message' => mb_substr($message, 0, 255),
|
||||
'endpoint_url' => mb_substr($endpointUrl, 0, 255),
|
||||
'tested_at' => $testedAt,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function recentTests(int $integrationId, int $limit = 5): array
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id, integration_id, status, http_code, message, endpoint_url, tested_at
|
||||
FROM integration_test_logs
|
||||
WHERE integration_id = :integration_id
|
||||
ORDER BY tested_at DESC, id DESC
|
||||
LIMIT :limit'
|
||||
);
|
||||
$statement->bindValue(':integration_id', $integrationId, PDO::PARAM_INT);
|
||||
$statement->bindValue(':limit', max(1, $limit), PDO::PARAM_INT);
|
||||
$statement->execute();
|
||||
|
||||
$rows = $statement->fetchAll();
|
||||
if (!is_array($rows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(
|
||||
static fn (array $row): array => [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'integration_id' => (int) ($row['integration_id'] ?? 0),
|
||||
'status' => (string) ($row['status'] ?? ''),
|
||||
'http_code' => $row['http_code'] === null ? null : (int) $row['http_code'],
|
||||
'message' => (string) ($row['message'] ?? ''),
|
||||
'endpoint_url' => (string) ($row['endpoint_url'] ?? ''),
|
||||
'tested_at' => (string) ($row['tested_at'] ?? ''),
|
||||
],
|
||||
$rows
|
||||
);
|
||||
}
|
||||
|
||||
public function nameExists(string $type, string $name, ?int $excludeId = null): bool
|
||||
{
|
||||
$sql = 'SELECT 1 FROM integrations WHERE type = :type AND name = :name';
|
||||
$params = [
|
||||
'type' => $type,
|
||||
'name' => $name,
|
||||
];
|
||||
|
||||
if ($excludeId !== null) {
|
||||
$sql .= ' AND id <> :exclude_id';
|
||||
$params['exclude_id'] = $excludeId;
|
||||
}
|
||||
|
||||
$sql .= ' LIMIT 1';
|
||||
|
||||
$statement = $this->pdo->prepare($sql);
|
||||
$statement->execute($params);
|
||||
|
||||
return $statement->fetchColumn() !== false;
|
||||
}
|
||||
|
||||
public function ensureSalesChannelsSeeded(): void
|
||||
{
|
||||
$rows = [
|
||||
['code' => 'shoppro', 'name' => 'shopPRO', 'type' => 'shop_instance'],
|
||||
['code' => 'allegro', 'name' => 'Allegro', 'type' => 'marketplace'],
|
||||
['code' => 'erli', 'name' => 'Erli', 'type' => 'marketplace'],
|
||||
];
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO sales_channels (code, name, type, status, created_at, updated_at)
|
||||
VALUES (:code, :name, :type, 1, :created_at, :updated_at)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
type = VALUES(type),
|
||||
updated_at = VALUES(updated_at)'
|
||||
);
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
foreach ($rows as $row) {
|
||||
$statement->execute([
|
||||
'code' => $row['code'],
|
||||
'name' => $row['name'],
|
||||
'type' => $row['type'],
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function findMappedProductId(string $channelCode, string $externalProductId, ?int $integrationId = null): ?int
|
||||
{
|
||||
$sql = 'SELECT pcm.product_id
|
||||
FROM product_channel_map pcm
|
||||
INNER JOIN sales_channels sc ON sc.id = pcm.channel_id
|
||||
WHERE sc.code = :channel_code
|
||||
AND pcm.external_product_id = :external_product_id';
|
||||
$params = [
|
||||
'channel_code' => $channelCode,
|
||||
'external_product_id' => $externalProductId,
|
||||
];
|
||||
if ($integrationId !== null && $integrationId > 0) {
|
||||
$sql .= ' AND pcm.integration_id = :integration_id';
|
||||
$params['integration_id'] = $integrationId;
|
||||
}
|
||||
$sql .= ' LIMIT 1';
|
||||
|
||||
$statement = $this->pdo->prepare($sql);
|
||||
$statement->execute($params);
|
||||
|
||||
$value = $statement->fetchColumn();
|
||||
if ($value === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
public function upsertProductChannelMap(
|
||||
int $productId,
|
||||
string $channelCode,
|
||||
string $syncState,
|
||||
string $externalProductId = '',
|
||||
string $externalVariantId = '',
|
||||
?int $integrationId = null
|
||||
): void {
|
||||
$channelId = $this->findChannelIdByCode($channelCode);
|
||||
if ($channelId === null) {
|
||||
throw new RuntimeException('Brak kanalu sprzedazy: ' . $channelCode);
|
||||
}
|
||||
|
||||
$externalProductId = trim($externalProductId);
|
||||
$externalVariantId = trim($externalVariantId);
|
||||
$normalizedIntegrationId = $integrationId !== null && $integrationId > 0
|
||||
? $integrationId
|
||||
: null;
|
||||
$linkType = 'manual';
|
||||
$linkStatus = $externalProductId !== '' ? 'active' : 'unverified';
|
||||
$linkedAt = $externalProductId !== '' ? date('Y-m-d H:i:s') : null;
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO product_channel_map (
|
||||
product_id, channel_id, integration_id, external_product_id, external_variant_id,
|
||||
sync_state, link_type, link_status, linked_at, last_sync_at, created_at, updated_at
|
||||
) VALUES (
|
||||
:product_id, :channel_id, :integration_id, :external_product_id, :external_variant_id,
|
||||
:sync_state, :link_type, :link_status, :linked_at, :last_sync_at, :created_at, :updated_at
|
||||
) ON DUPLICATE KEY UPDATE
|
||||
integration_id = VALUES(integration_id),
|
||||
sync_state = VALUES(sync_state),
|
||||
last_sync_at = VALUES(last_sync_at),
|
||||
external_product_id = VALUES(external_product_id),
|
||||
external_variant_id = VALUES(external_variant_id),
|
||||
link_type = VALUES(link_type),
|
||||
link_status = VALUES(link_status),
|
||||
linked_at = VALUES(linked_at),
|
||||
updated_at = VALUES(updated_at)'
|
||||
);
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$statement->execute([
|
||||
'product_id' => $productId,
|
||||
'channel_id' => $channelId,
|
||||
'integration_id' => $normalizedIntegrationId,
|
||||
'external_product_id' => $externalProductId,
|
||||
'external_variant_id' => $externalVariantId !== '' ? $externalVariantId : null,
|
||||
'sync_state' => $syncState,
|
||||
'link_type' => $linkType,
|
||||
'link_status' => $linkStatus,
|
||||
'linked_at' => $linkedAt,
|
||||
'last_sync_at' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function mapRow(array $row): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'type' => (string) ($row['type'] ?? ''),
|
||||
'name' => (string) ($row['name'] ?? ''),
|
||||
'base_url' => (string) ($row['base_url'] ?? ''),
|
||||
'timeout_seconds' => (int) ($row['timeout_seconds'] ?? 10),
|
||||
'is_active' => (int) ($row['is_active'] ?? 0) === 1,
|
||||
'orders_fetch_enabled' => (int) ($row['orders_fetch_enabled'] ?? 0) === 1,
|
||||
'orders_fetch_start_date' => $row['orders_fetch_start_date'] === null ? null : (string) $row['orders_fetch_start_date'],
|
||||
'order_status_sync_direction' => $this->normalizeOrderStatusSyncDirection((string) ($row['order_status_sync_direction'] ?? self::ORDER_STATUS_SYNC_DIRECTION_DEFAULT)),
|
||||
'last_test_status' => (string) ($row['last_test_status'] ?? ''),
|
||||
'last_test_http_code' => $row['last_test_http_code'] === null ? null : (int) $row['last_test_http_code'],
|
||||
'last_test_message' => (string) ($row['last_test_message'] ?? ''),
|
||||
'last_test_at' => (string) ($row['last_test_at'] ?? ''),
|
||||
'has_api_key' => (int) ($row['has_api_key'] ?? 0) === 1,
|
||||
'created_at' => (string) ($row['created_at'] ?? ''),
|
||||
'updated_at' => (string) ($row['updated_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
private function encryptApiKey(string $apiKey): string
|
||||
{
|
||||
$plain = trim($apiKey);
|
||||
if ($plain === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$secret = trim($this->secret);
|
||||
if ($secret === '') {
|
||||
throw new RuntimeException('Brak INTEGRATIONS_SECRET w konfiguracji .env.');
|
||||
}
|
||||
|
||||
$iv = random_bytes(16);
|
||||
$cipher = openssl_encrypt(
|
||||
$plain,
|
||||
'AES-256-CBC',
|
||||
hash('sha256', $secret, true),
|
||||
OPENSSL_RAW_DATA,
|
||||
$iv
|
||||
);
|
||||
if ($cipher === false) {
|
||||
throw new RuntimeException('Nie mozna zaszyfrowac klucza API.');
|
||||
}
|
||||
|
||||
return base64_encode($iv) . ':' . base64_encode($cipher);
|
||||
}
|
||||
|
||||
private function decryptApiKey(string $payload): string
|
||||
{
|
||||
$serialized = trim($payload);
|
||||
if ($serialized === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$secret = trim($this->secret);
|
||||
if ($secret === '') {
|
||||
throw new RuntimeException('Brak INTEGRATIONS_SECRET w konfiguracji .env.');
|
||||
}
|
||||
|
||||
$parts = explode(':', $serialized, 2);
|
||||
if (count($parts) !== 2) {
|
||||
throw new RuntimeException('Niepoprawny format zapisanego klucza API.');
|
||||
}
|
||||
|
||||
$iv = base64_decode($parts[0], true);
|
||||
$cipher = base64_decode($parts[1], true);
|
||||
if ($iv === false || $cipher === false || strlen($iv) !== 16) {
|
||||
throw new RuntimeException('Nie mozna odczytac zapisanego klucza API.');
|
||||
}
|
||||
|
||||
$plain = openssl_decrypt(
|
||||
$cipher,
|
||||
'AES-256-CBC',
|
||||
hash('sha256', $secret, true),
|
||||
OPENSSL_RAW_DATA,
|
||||
$iv
|
||||
);
|
||||
if ($plain === false) {
|
||||
throw new RuntimeException('Nie mozna odszyfrowac zapisanego klucza API.');
|
||||
}
|
||||
|
||||
return $plain;
|
||||
}
|
||||
|
||||
private function findChannelIdByCode(string $code): ?int
|
||||
{
|
||||
$statement = $this->pdo->prepare('SELECT id FROM sales_channels WHERE code = :code LIMIT 1');
|
||||
$statement->execute(['code' => $code]);
|
||||
$value = $statement->fetchColumn();
|
||||
|
||||
if ($value === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
private function ordersFetchSelectFragment(): string
|
||||
{
|
||||
if ($this->hasOrdersFetchColumns()) {
|
||||
return ', orders_fetch_enabled, orders_fetch_start_date';
|
||||
}
|
||||
|
||||
return ', 0 AS orders_fetch_enabled, NULL AS orders_fetch_start_date';
|
||||
}
|
||||
|
||||
private function orderStatusSyncDirectionSelectFragment(): string
|
||||
{
|
||||
if ($this->hasOrderStatusSyncDirectionColumn()) {
|
||||
return ', order_status_sync_direction';
|
||||
}
|
||||
|
||||
return ", '" . self::ORDER_STATUS_SYNC_DIRECTION_DEFAULT . "' AS order_status_sync_direction";
|
||||
}
|
||||
|
||||
private function hasOrdersFetchColumns(): bool
|
||||
{
|
||||
if ($this->ordersFetchColumnsAvailable !== null) {
|
||||
return $this->ordersFetchColumnsAvailable;
|
||||
}
|
||||
|
||||
try {
|
||||
$enabledStmt = $this->pdo->query("SHOW COLUMNS FROM integrations LIKE 'orders_fetch_enabled'");
|
||||
$startDateStmt = $this->pdo->query("SHOW COLUMNS FROM integrations LIKE 'orders_fetch_start_date'");
|
||||
|
||||
$this->ordersFetchColumnsAvailable =
|
||||
$enabledStmt !== false
|
||||
&& $enabledStmt->fetch() !== false
|
||||
&& $startDateStmt !== false
|
||||
&& $startDateStmt->fetch() !== false;
|
||||
} catch (\Throwable) {
|
||||
$this->ordersFetchColumnsAvailable = false;
|
||||
}
|
||||
|
||||
return $this->ordersFetchColumnsAvailable;
|
||||
}
|
||||
|
||||
private function hasOrderStatusSyncDirectionColumn(): bool
|
||||
{
|
||||
if ($this->orderStatusSyncDirectionColumnAvailable !== null) {
|
||||
return $this->orderStatusSyncDirectionColumnAvailable;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $this->pdo->query("SHOW COLUMNS FROM integrations LIKE 'order_status_sync_direction'");
|
||||
$this->orderStatusSyncDirectionColumnAvailable =
|
||||
$stmt !== false
|
||||
&& $stmt->fetch() !== false;
|
||||
} catch (\Throwable) {
|
||||
$this->orderStatusSyncDirectionColumnAvailable = false;
|
||||
}
|
||||
|
||||
return $this->orderStatusSyncDirectionColumnAvailable;
|
||||
}
|
||||
|
||||
private function normalizeOrderStatusSyncDirection(string $value): string
|
||||
{
|
||||
$normalized = trim(mb_strtolower($value));
|
||||
if ($normalized === 'orderpro_to_shoppro') {
|
||||
return 'orderpro_to_shoppro';
|
||||
}
|
||||
|
||||
return self::ORDER_STATUS_SYNC_DIRECTION_DEFAULT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use PDO;
|
||||
|
||||
final class OrderStatusMappingRepository
|
||||
{
|
||||
public function __construct(private readonly PDO $pdo)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{orderpro_status_code:string, shoppro_status_name:string|null}>
|
||||
*/
|
||||
public function listByIntegration(int $integrationId): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT shoppro_status_code, shoppro_status_name, orderpro_status_code
|
||||
FROM order_status_mappings
|
||||
WHERE integration_id = :integration_id
|
||||
ORDER BY shoppro_status_code ASC'
|
||||
);
|
||||
$stmt->execute(['integration_id' => $integrationId]);
|
||||
|
||||
$rows = $stmt->fetchAll();
|
||||
if (!is_array($rows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$code = trim((string) ($row['shoppro_status_code'] ?? ''));
|
||||
if ($code === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[$code] = [
|
||||
'orderpro_status_code' => trim((string) ($row['orderpro_status_code'] ?? '')),
|
||||
'shoppro_status_name' => isset($row['shoppro_status_name']) ? trim((string) $row['shoppro_status_name']) : null,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{shoppro_status_code:string,shoppro_status_name:string|null,orderpro_status_code:string}> $mappings
|
||||
*/
|
||||
public function replaceForIntegration(int $integrationId, array $mappings): void
|
||||
{
|
||||
$deleteStmt = $this->pdo->prepare('DELETE FROM order_status_mappings WHERE integration_id = :integration_id');
|
||||
$deleteStmt->execute(['integration_id' => $integrationId]);
|
||||
|
||||
if ($mappings === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$insertStmt = $this->pdo->prepare(
|
||||
'INSERT INTO order_status_mappings (
|
||||
integration_id, shoppro_status_code, shoppro_status_name, orderpro_status_code, created_at, updated_at
|
||||
) VALUES (
|
||||
:integration_id, :shoppro_status_code, :shoppro_status_name, :orderpro_status_code, :created_at, :updated_at
|
||||
)'
|
||||
);
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
foreach ($mappings as $mapping) {
|
||||
$shopCode = trim((string) ($mapping['shoppro_status_code'] ?? ''));
|
||||
$orderCode = trim((string) ($mapping['orderpro_status_code'] ?? ''));
|
||||
if ($shopCode === '' || $orderCode === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$shopNameRaw = isset($mapping['shoppro_status_name']) ? trim((string) $mapping['shoppro_status_name']) : '';
|
||||
$shopName = $shopNameRaw === '' ? null : $shopNameRaw;
|
||||
|
||||
$insertStmt->execute([
|
||||
'integration_id' => $integrationId,
|
||||
'shoppro_status_code' => $shopCode,
|
||||
'shoppro_status_name' => $shopName,
|
||||
'orderpro_status_code' => $orderCode,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function listOrderProToShopProMap(int $integrationId): array
|
||||
{
|
||||
$rows = $this->listByIntegration($integrationId);
|
||||
if ($rows === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($rows as $shopCode => $mapping) {
|
||||
$orderProCode = trim((string) ($mapping['orderpro_status_code'] ?? ''));
|
||||
$normalizedOrderProCode = $this->normalizeCode($orderProCode);
|
||||
$normalizedShopCode = $this->normalizeCode((string) $shopCode);
|
||||
if ($normalizedOrderProCode === '' || $normalizedShopCode === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($result[$normalizedOrderProCode])) {
|
||||
$result[$normalizedOrderProCode] = $normalizedShopCode;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function normalizeCode(string $value): string
|
||||
{
|
||||
return trim(mb_strtolower($value));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user