135 lines
5.2 KiB
PHP
135 lines
5.2 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Esun\SeventeenTrack\Model;
|
|
|
|
use Magento\Framework\App\ResourceConnection;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
class ApiLogWriter
|
|
{
|
|
private const TABLE = 'esun_seventeen_track_api_log';
|
|
private const MAX_TEXT_LENGTH = 16777215;
|
|
private const SENSITIVE_KEYS = [
|
|
'17token', 'api_key', 'apikey', 'token', 'sign', 'signature',
|
|
'phone', 'telephone', 'phone_number', 'phone_number_last_4',
|
|
'email', 'address', 'address_line1', 'address_line2', 'street',
|
|
'postal_code', 'destination_postal_code', 'consignee', 'shipper',
|
|
'cpf_or_cnpj',
|
|
];
|
|
|
|
public function __construct(
|
|
private readonly ResourceConnection $resource,
|
|
private readonly Config $config,
|
|
private readonly LoggerInterface $logger
|
|
) {
|
|
}
|
|
|
|
/** Log without allowing a logging failure to interrupt synchronization or webhook acknowledgement. */
|
|
public function write(array $data): void
|
|
{
|
|
try {
|
|
foreach (['request_data', 'response_data'] as $field) {
|
|
if (array_key_exists($field, $data)) {
|
|
$data[$field] = $this->encode($data[$field]);
|
|
}
|
|
}
|
|
if (isset($data['message'])) {
|
|
$data['message'] = mb_substr((string)$data['message'], 0, 65535);
|
|
}
|
|
$data['created_at'] = gmdate('Y-m-d H:i:s');
|
|
$this->resource->getConnection('sales')->insert(
|
|
$this->resource->getTableName(self::TABLE),
|
|
$data
|
|
);
|
|
} catch (\Throwable $exception) {
|
|
$this->logger->error('Unable to write a 17TRACK API audit log.', [
|
|
'operation' => (string)($data['operation'] ?? ''),
|
|
'tracking_number' => (string)($data['tracking_number'] ?? ''),
|
|
'exception' => $exception,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/** @return array{order_id:?int,order_number:?string,tracking_number:?string,event:?string} */
|
|
public function getWebhookContext(string $rawBody): array
|
|
{
|
|
$context = ['order_id' => null, 'order_number' => null, 'tracking_number' => null, 'event' => null];
|
|
try {
|
|
$payload = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
|
|
if (!is_array($payload)) {
|
|
return $context;
|
|
}
|
|
$context['event'] = isset($payload['event']) ? mb_substr((string)$payload['event'], 0, 50) : null;
|
|
$number = trim((string)($payload['data']['number'] ?? ''));
|
|
$context['tracking_number'] = $number !== '' ? mb_substr($number, 0, 50) : null;
|
|
if ($number === '') {
|
|
return $context;
|
|
}
|
|
$connection = $this->resource->getConnection('sales');
|
|
$trackingTable = $this->resource->getTableName('esun_seventeen_track');
|
|
$orderTable = $this->resource->getTableName('sales_order');
|
|
$row = $connection->fetchRow(
|
|
$connection->select()
|
|
->from(['tracking' => $trackingTable], ['order_id'])
|
|
->joinLeft(['orders' => $orderTable], 'orders.entity_id = tracking.order_id', ['order_number' => 'increment_id'])
|
|
->where('tracking.tracking_number = ?', $number)
|
|
->limit(1)
|
|
);
|
|
if (is_array($row)) {
|
|
$context['order_id'] = isset($row['order_id']) ? (int)$row['order_id'] : null;
|
|
$context['order_number'] = isset($row['order_number']) ? (string)$row['order_number'] : null;
|
|
}
|
|
} catch (\Throwable) {
|
|
// Invalid JSON and unavailable order context are captured by the processing result itself.
|
|
}
|
|
return $context;
|
|
}
|
|
|
|
public function getWebhookPath(): string
|
|
{
|
|
return '/seventeentrack/webhook/index';
|
|
}
|
|
|
|
public function getRegistrationPath(): string
|
|
{
|
|
return rtrim($this->config->getApiUrl(), '/') . '/register';
|
|
}
|
|
|
|
private function encode(mixed $value): ?string
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
if (is_string($value)) {
|
|
try {
|
|
$decoded = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
|
|
$value = is_array($decoded) ? $decoded : $value;
|
|
} catch (\JsonException) {
|
|
return mb_substr($value, 0, self::MAX_TEXT_LENGTH);
|
|
}
|
|
}
|
|
$encoded = json_encode(
|
|
is_array($value) ? $this->sanitize($value) : $value,
|
|
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
|
|
);
|
|
return mb_substr($encoded, 0, self::MAX_TEXT_LENGTH);
|
|
}
|
|
|
|
private function sanitize(array $data): array
|
|
{
|
|
foreach ($data as $key => $value) {
|
|
$normalized = strtolower((string)$key);
|
|
if (in_array($normalized, self::SENSITIVE_KEYS, true)
|
|
|| str_contains($normalized, 'password')
|
|
|| str_contains($normalized, 'secret')
|
|
) {
|
|
$data[$key] = '[REDACTED]';
|
|
} elseif (is_array($value)) {
|
|
$data[$key] = $this->sanitize($value);
|
|
}
|
|
}
|
|
return $data;
|
|
}
|
|
}
|