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.
This commit is contained in:
474
tools/apaczka_probe_order.php
Normal file
474
tools/apaczka_probe_order.php
Normal file
@@ -0,0 +1,474 @@
|
||||
<?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;
|
||||
}
|
||||
Reference in New Issue
Block a user