- Introduced ShipmentProviderInterface to define the contract for shipment providers. - Implemented ShipmentProviderRegistry to manage and retrieve shipment providers. - Added a new tool for probing Apaczka order_send payload variants, enhancing debugging capabilities.
475 lines
16 KiB
PHP
475 lines
16 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Debug helper for probing Apaczka order_send payload variants for a single order.
|
|
*
|
|
* Usage examples:
|
|
* php tools/apaczka_probe_order.php --order-id=21 --use-remote
|
|
* php tools/apaczka_probe_order.php --order-id=21 --service-id=41 --use-remote
|
|
*/
|
|
|
|
const DEFAULT_ORDER_ID = 0;
|
|
const DEFAULT_TIMEOUT_SECONDS = 30;
|
|
|
|
$options = parseOptions($argv);
|
|
if (($options['order_id'] ?? 0) <= 0) {
|
|
fwrite(STDERR, "Missing --order-id=<int>\n");
|
|
exit(1);
|
|
}
|
|
|
|
$basePath = dirname(__DIR__);
|
|
registerAutoloader($basePath);
|
|
$env = parse_ini_file($basePath . '/.env');
|
|
if (!is_array($env)) {
|
|
fwrite(STDERR, "Cannot read .env\n");
|
|
exit(1);
|
|
}
|
|
|
|
[$pdo, $hostUsed] = openDatabase($env, !empty($options['use_remote']));
|
|
$order = fetchOrder($pdo, (int) $options['order_id']);
|
|
if ($order === null) {
|
|
fwrite(STDERR, "Order not found: " . (int) $options['order_id'] . "\n");
|
|
exit(1);
|
|
}
|
|
|
|
$addresses = fetchAddresses($pdo, (int) $order['id']);
|
|
$sender = fetchSenderAddress($pdo);
|
|
$mapping = fetchDeliveryMapping($pdo, $order);
|
|
|
|
[$appId, $appSecret] = fetchApaczkaCredentials($pdo, $env);
|
|
$client = new App\Modules\Settings\ApaczkaApiClient();
|
|
$services = $client->getServiceStructure($appId, $appSecret);
|
|
$serviceIndex = buildServiceIndex($services);
|
|
|
|
$basePayload = buildBasePayload($order, $addresses, $sender, $mapping);
|
|
$candidates = buildCandidates($basePayload, $mapping, $serviceIndex, $options);
|
|
|
|
echo "DB host: {$hostUsed}\n";
|
|
echo "Order: #" . (int) $order['id'] . " source=" . (string) ($order['source'] ?? '') . " source_order_id=" . (string) ($order['source_order_id'] ?? '') . "\n";
|
|
echo "Mapped service: " . (string) ($mapping['provider_service_id'] ?? '(none)') . " (" . (string) ($mapping['provider_service_name'] ?? '') . ")\n";
|
|
echo "Receiver point: " . (string) ($basePayload['receiver_point_id'] ?? '(none)') . "\n";
|
|
echo "Candidates: " . count($candidates) . "\n\n";
|
|
|
|
$attempt = 0;
|
|
$success = false;
|
|
foreach ($candidates as $candidate) {
|
|
$attempt++;
|
|
$label = buildCandidateLabel($candidate, $serviceIndex);
|
|
echo "[{$attempt}] {$label}\n";
|
|
|
|
$apiPayload = buildApiPayloadFromCandidate($basePayload, $candidate);
|
|
try {
|
|
$response = $client->sendOrder($appId, $appSecret, $apiPayload);
|
|
$orderResponse = is_array($response['response']['order'] ?? null) ? $response['response']['order'] : [];
|
|
$apaczkaOrderId = (string) ($orderResponse['id'] ?? '');
|
|
$waybill = (string) ($orderResponse['waybill_number'] ?? '');
|
|
echo " OK: order_id=" . ($apaczkaOrderId !== '' ? $apaczkaOrderId : '-') . " waybill=" . ($waybill !== '' ? $waybill : '-') . "\n";
|
|
$success = true;
|
|
|
|
if (empty($options['continue_on_success'])) {
|
|
break;
|
|
}
|
|
} catch (Throwable $exception) {
|
|
echo " ERR: " . trim($exception->getMessage()) . "\n";
|
|
}
|
|
}
|
|
|
|
echo "\nResult: " . ($success ? "SUCCESS" : "NO SUCCESS") . "\n";
|
|
exit($success ? 0 : 2);
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
function parseOptions(array $argv): array
|
|
{
|
|
$options = [
|
|
'order_id' => DEFAULT_ORDER_ID,
|
|
'service_id' => '',
|
|
'use_remote' => false,
|
|
'continue_on_success' => false,
|
|
];
|
|
|
|
foreach (array_slice($argv, 1) as $arg) {
|
|
if (preg_match('/^--order-id=(\d+)$/', (string) $arg, $m) === 1) {
|
|
$options['order_id'] = (int) $m[1];
|
|
continue;
|
|
}
|
|
if (preg_match('/^--service-id=([A-Za-z0-9_-]+)$/', (string) $arg, $m) === 1) {
|
|
$options['service_id'] = (string) $m[1];
|
|
continue;
|
|
}
|
|
if ((string) $arg === '--use-remote') {
|
|
$options['use_remote'] = true;
|
|
continue;
|
|
}
|
|
if ((string) $arg === '--continue-on-success') {
|
|
$options['continue_on_success'] = true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
function registerAutoloader(string $basePath): void
|
|
{
|
|
spl_autoload_register(static function (string $class) use ($basePath): void {
|
|
$prefix = 'App\\';
|
|
if (!str_starts_with($class, $prefix)) {
|
|
return;
|
|
}
|
|
$relative = substr($class, strlen($prefix));
|
|
$file = $basePath . '/src/' . str_replace('\\', '/', $relative) . '.php';
|
|
if (is_file($file)) {
|
|
require $file;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @return array{0: PDO, 1: string}
|
|
*/
|
|
function openDatabase(array $env, bool $useRemote): array
|
|
{
|
|
$host = $useRemote
|
|
? (string) ($env['DB_HOST_REMOTE'] ?? ($env['DB_HOST'] ?? '127.0.0.1'))
|
|
: (string) ($env['DB_HOST'] ?? '127.0.0.1');
|
|
$port = (string) ($env['DB_PORT'] ?? '3306');
|
|
$db = (string) ($env['DB_DATABASE'] ?? '');
|
|
$user = (string) ($env['DB_USERNAME'] ?? '');
|
|
$pass = (string) ($env['DB_PASSWORD'] ?? '');
|
|
$dsn = 'mysql:host=' . $host . ';port=' . $port . ';dbname=' . $db . ';charset=utf8mb4';
|
|
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
return [$pdo, $host];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
function fetchOrder(PDO $pdo, int $orderId): ?array
|
|
{
|
|
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = :id LIMIT 1');
|
|
$stmt->execute(['id' => $orderId]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
return is_array($row) ? $row : null;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
function fetchAddresses(PDO $pdo, int $orderId): array
|
|
{
|
|
$stmt = $pdo->prepare('SELECT * FROM order_addresses WHERE order_id = :order_id ORDER BY id ASC');
|
|
$stmt->execute(['order_id' => $orderId]);
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
return is_array($rows) ? $rows : [];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
function fetchSenderAddress(PDO $pdo): array
|
|
{
|
|
$stmt = $pdo->prepare('SELECT * FROM company_settings WHERE id = 1 LIMIT 1');
|
|
$stmt->execute();
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
$data = is_array($row) ? $row : [];
|
|
|
|
return [
|
|
'name' => trim((string) (($data['person_name'] ?? '') !== '' ? $data['person_name'] : ($data['company_name'] ?? ''))),
|
|
'line1' => trim((string) ($data['street'] ?? '')),
|
|
'postal_code' => trim((string) ($data['postal_code'] ?? '')),
|
|
'city' => trim((string) ($data['city'] ?? '')),
|
|
'country_code' => strtoupper(trim((string) ($data['country_code'] ?? 'PL'))),
|
|
'phone' => trim((string) ($data['phone'] ?? '')),
|
|
'email' => trim((string) ($data['email'] ?? '')),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $order
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
function fetchDeliveryMapping(PDO $pdo, array $order): ?array
|
|
{
|
|
$source = strtolower(trim((string) ($order['source'] ?? '')));
|
|
$orderMethod = trim((string) ($order['external_carrier_id'] ?? ''));
|
|
if ($source === '' || $orderMethod === '') {
|
|
return null;
|
|
}
|
|
|
|
$sourceIntegrationId = $source === 'shoppro' ? max(0, (int) ($order['integration_id'] ?? 0)) : 0;
|
|
$stmt = $pdo->prepare(
|
|
'SELECT *
|
|
FROM carrier_delivery_method_mappings
|
|
WHERE source_system = :source_system
|
|
AND source_integration_id = :source_integration_id
|
|
AND order_delivery_method = :order_delivery_method
|
|
LIMIT 1'
|
|
);
|
|
$stmt->execute([
|
|
'source_system' => $source,
|
|
'source_integration_id' => $sourceIntegrationId,
|
|
'order_delivery_method' => $orderMethod,
|
|
]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
return is_array($row) ? $row : null;
|
|
}
|
|
|
|
/**
|
|
* @return array{0: string, 1: string}
|
|
*/
|
|
function fetchApaczkaCredentials(PDO $pdo, array $env): array
|
|
{
|
|
$stmt = $pdo->prepare(
|
|
'SELECT a.app_id,
|
|
COALESCE(NULLIF(i.api_key_encrypted, \'\'), a.app_secret_encrypted, a.api_key_encrypted) AS secret_encrypted
|
|
FROM apaczka_integration_settings a
|
|
LEFT JOIN integrations i ON i.id = a.integration_id
|
|
WHERE a.id = 1
|
|
LIMIT 1'
|
|
);
|
|
$stmt->execute();
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if (!is_array($row)) {
|
|
throw new RuntimeException('Missing apaczka_integration_settings row.');
|
|
}
|
|
|
|
$appId = trim((string) ($row['app_id'] ?? ''));
|
|
$encrypted = trim((string) ($row['secret_encrypted'] ?? ''));
|
|
if ($appId === '' || $encrypted === '') {
|
|
throw new RuntimeException('Missing Apaczka app_id or app_secret.');
|
|
}
|
|
|
|
$cipher = new App\Modules\Settings\IntegrationSecretCipher((string) ($env['INTEGRATIONS_SECRET'] ?? ''));
|
|
$appSecret = trim($cipher->decrypt($encrypted));
|
|
if ($appSecret === '') {
|
|
throw new RuntimeException('Cannot decrypt Apaczka app_secret.');
|
|
}
|
|
|
|
return [$appId, $appSecret];
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $services
|
|
* @return array<string, array<string, mixed>>
|
|
*/
|
|
function buildServiceIndex(array $services): array
|
|
{
|
|
$result = [];
|
|
foreach ($services as $service) {
|
|
if (!is_array($service)) {
|
|
continue;
|
|
}
|
|
$serviceId = trim((string) ($service['service_id'] ?? $service['id'] ?? ''));
|
|
if ($serviceId === '') {
|
|
continue;
|
|
}
|
|
$result[$serviceId] = $service;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $order
|
|
* @param array<int, array<string, mixed>> $addresses
|
|
* @param array<string, string> $sender
|
|
* @param array<string, mixed>|null $mapping
|
|
* @return array<string, mixed>
|
|
*/
|
|
function buildBasePayload(array $order, array $addresses, array $sender, ?array $mapping): array
|
|
{
|
|
$delivery = null;
|
|
$customer = null;
|
|
foreach ($addresses as $address) {
|
|
if (!is_array($address)) {
|
|
continue;
|
|
}
|
|
$type = trim((string) ($address['address_type'] ?? ''));
|
|
if ($type === 'delivery' && $delivery === null) {
|
|
$delivery = $address;
|
|
}
|
|
if ($type === 'customer' && $customer === null) {
|
|
$customer = $address;
|
|
}
|
|
}
|
|
|
|
$receiver = is_array($delivery) ? $delivery : (is_array($customer) ? $customer : []);
|
|
$receiverPointId = trim((string) ($receiver['parcel_external_id'] ?? ''));
|
|
$receiverName = trim((string) ($receiver['name'] ?? ''));
|
|
$customerName = trim((string) (($customer['name'] ?? '')));
|
|
if ($receiverPointId !== '' && $customerName !== '') {
|
|
$receiverName = $customerName;
|
|
}
|
|
if ($receiverName === '') {
|
|
$receiverName = 'Klient';
|
|
}
|
|
|
|
$totalWithTax = (float) ($order['total_with_tax'] ?? 0);
|
|
$serviceId = trim((string) ($mapping['provider_service_id'] ?? ''));
|
|
if ($serviceId === '') {
|
|
$serviceId = trim((string) ($order['external_carrier_account_id'] ?? ''));
|
|
}
|
|
|
|
return [
|
|
'service_id' => $serviceId,
|
|
'source_order_id' => trim((string) ($order['source_order_id'] ?? $order['id'] ?? '')),
|
|
'insurance_cents' => $totalWithTax > 0 ? (int) round($totalWithTax * 100) : 0,
|
|
'receiver_point_id' => $receiverPointId,
|
|
'sender_point_id' => '',
|
|
'receiver' => [
|
|
'name' => $receiverName,
|
|
'line1' => trim((string) ($receiver['street_name'] ?? '')),
|
|
'postal_code' => trim((string) ($receiver['zip_code'] ?? '')),
|
|
'city' => trim((string) ($receiver['city'] ?? '')),
|
|
'country_code' => strtoupper(trim((string) ($receiver['country'] ?? 'PL'))),
|
|
'phone' => trim((string) ($receiver['phone'] ?? '')),
|
|
'email' => trim((string) ($receiver['email'] ?? '')),
|
|
],
|
|
'sender' => $sender,
|
|
'shipment' => [
|
|
'shipment_type_code' => 'PACZKA',
|
|
'dimension1' => 25,
|
|
'dimension2' => 20,
|
|
'dimension3' => 8,
|
|
'weight' => 1.0,
|
|
'is_nstd' => 0,
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $basePayload
|
|
* @param array<string, mixed>|null $mapping
|
|
* @param array<string, array<string, mixed>> $serviceIndex
|
|
* @param array<string, mixed> $options
|
|
* @return array<int, array<string, string>>
|
|
*/
|
|
function buildCandidates(array $basePayload, ?array $mapping, array $serviceIndex, array $options): array
|
|
{
|
|
$serviceIds = [];
|
|
$forcedServiceId = trim((string) ($options['service_id'] ?? ''));
|
|
if ($forcedServiceId !== '') {
|
|
$serviceIds[] = $forcedServiceId;
|
|
} else {
|
|
$mapped = trim((string) ($basePayload['service_id'] ?? ''));
|
|
if ($mapped !== '') {
|
|
$serviceIds[] = $mapped;
|
|
}
|
|
foreach ($serviceIndex as $serviceId => $service) {
|
|
$supplier = strtoupper(trim((string) ($service['supplier'] ?? '')));
|
|
$doorToPoint = (int) ($service['door_to_point'] ?? 0) === 1;
|
|
$pointToPoint = (int) ($service['point_to_point'] ?? 0) === 1;
|
|
if ($supplier !== 'INPOST') {
|
|
continue;
|
|
}
|
|
if (!$doorToPoint && !$pointToPoint) {
|
|
continue;
|
|
}
|
|
$serviceIds[] = $serviceId;
|
|
}
|
|
}
|
|
|
|
$serviceIds = array_values(array_unique(array_filter($serviceIds, static fn(string $v): bool => $v !== '')));
|
|
if ($serviceIds === []) {
|
|
return [];
|
|
}
|
|
|
|
$pointModes = ['all_keys', 'point_only', 'foreign_only', 'point_id_only'];
|
|
$result = [];
|
|
foreach ($serviceIds as $serviceId) {
|
|
foreach ($pointModes as $mode) {
|
|
$result[] = [
|
|
'service_id' => $serviceId,
|
|
'point_mode' => $mode,
|
|
];
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $candidate
|
|
* @param array<string, array<string, mixed>> $serviceIndex
|
|
*/
|
|
function buildCandidateLabel(array $candidate, array $serviceIndex): string
|
|
{
|
|
$serviceId = (string) ($candidate['service_id'] ?? '');
|
|
$pointMode = (string) ($candidate['point_mode'] ?? '');
|
|
$service = $serviceIndex[$serviceId] ?? null;
|
|
$serviceName = is_array($service) ? trim((string) ($service['name'] ?? '')) : '';
|
|
return 'service=' . $serviceId
|
|
. ($serviceName !== '' ? ' (' . $serviceName . ')' : '')
|
|
. ' point_mode=' . $pointMode;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $basePayload
|
|
* @param array<string, string> $candidate
|
|
* @return array<string, mixed>
|
|
*/
|
|
function buildApiPayloadFromCandidate(array $basePayload, array $candidate): array
|
|
{
|
|
$serviceId = (string) ($candidate['service_id'] ?? '');
|
|
$pointMode = (string) ($candidate['point_mode'] ?? 'all_keys');
|
|
$receiverPointId = trim((string) ($basePayload['receiver_point_id'] ?? ''));
|
|
$senderPointId = trim((string) ($basePayload['sender_point_id'] ?? ''));
|
|
|
|
$receiver = (array) ($basePayload['receiver'] ?? []);
|
|
$sender = (array) ($basePayload['sender'] ?? []);
|
|
unset($receiver['point'], $receiver['foreign_address_id'], $receiver['point_id']);
|
|
unset($sender['point'], $sender['foreign_address_id'], $sender['point_id']);
|
|
|
|
applyPointMode($receiver, $receiverPointId, $pointMode);
|
|
if ($senderPointId !== '') {
|
|
applyPointMode($sender, $senderPointId, $pointMode);
|
|
}
|
|
|
|
$payload = [
|
|
'service_id' => ctype_digit($serviceId) ? (int) $serviceId : $serviceId,
|
|
'address' => [
|
|
'receiver' => $receiver,
|
|
'sender' => $sender,
|
|
],
|
|
'shipment' => [(array) ($basePayload['shipment'] ?? [])],
|
|
'content' => 'orderPRO ' . (string) ($basePayload['source_order_id'] ?? ''),
|
|
'comment' => 'orderPRO ' . (string) ($basePayload['source_order_id'] ?? ''),
|
|
];
|
|
$insuranceCents = (int) ($basePayload['insurance_cents'] ?? 0);
|
|
if ($insuranceCents > 0) {
|
|
$payload['shipment_value'] = $insuranceCents;
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $node
|
|
*/
|
|
function applyPointMode(array &$node, string $pointId, string $mode): void
|
|
{
|
|
if ($pointId === '') {
|
|
return;
|
|
}
|
|
|
|
if ($mode === 'point_only') {
|
|
$node['point'] = $pointId;
|
|
return;
|
|
}
|
|
if ($mode === 'foreign_only') {
|
|
$node['foreign_address_id'] = $pointId;
|
|
return;
|
|
}
|
|
if ($mode === 'point_id_only') {
|
|
$node['point_id'] = $pointId;
|
|
return;
|
|
}
|
|
|
|
$node['point'] = $pointId;
|
|
$node['foreign_address_id'] = $pointId;
|
|
$node['point_id'] = $pointId;
|
|
}
|