Files
orderPRO/tools/apaczka_probe_order.php
Jacek Pyziak 2b12fde248 feat(shipments): add ShipmentProviderInterface and ShipmentProviderRegistry
- 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.
2026-03-08 23:45:10 +01:00

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;
}