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:
2026-03-08 23:45:10 +01:00
parent af052e1ff5
commit 2b12fde248
34 changed files with 3285 additions and 233 deletions

View File

@@ -5,20 +5,31 @@ $receiver = is_array($receiverAddr ?? null) ? $receiverAddr : [];
$prefs = is_array($preferences ?? null) ? $preferences : [];
$comp = is_array($company ?? null) ? $company : [];
$services = is_array($deliveryServices ?? null) ? $deliveryServices : [];
$apaczkaSvcList = is_array($apaczkaServices ?? null) ? $apaczkaServices : [];
$packages = is_array($existingPackages ?? null) ? $existingPackages : [];
$servicesError = (string) ($deliveryServicesError ?? '');
$flashSuccessMsg = (string) ($flashSuccess ?? '');
$flashErrorMsg = (string) ($flashError ?? '');
$deliveryMappingDiagnostic = trim((string) ($deliveryMappingDiagnostic ?? ''));
$mapping = is_array($deliveryMapping ?? null) ? $deliveryMapping : [];
$mappedMethodId = trim((string) ($mapping['allegro_delivery_method_id'] ?? ''));
$mappedCredentialsId = trim((string) ($mapping['allegro_credentials_id'] ?? ''));
$mappedCarrierId = trim((string) ($mapping['allegro_carrier_id'] ?? ''));
$mappedCarrier = trim((string) ($mapping['carrier'] ?? ''));
$mappedServiceName = trim((string) ($mapping['allegro_service_name'] ?? ''));
$deliveryMethodId = $mappedCarrier === 'allegro' && $mappedMethodId !== ''
? $mappedMethodId
: ($mappedCarrier !== 'inpost' ? trim((string) ($prefs['delivery_method_id'] ?? ($orderRow['external_carrier_account_id'] ?? ''))) : '');
$mappedMethodId = trim((string) ($mapping['provider_service_id'] ?? ''));
$mappedCredentialsId = trim((string) ($mapping['provider_account_id'] ?? ''));
$mappedCarrierId = trim((string) ($mapping['provider_carrier_id'] ?? ''));
$mappedProvider = trim((string) ($mapping['provider'] ?? ''));
$mappedServiceName = trim((string) ($mapping['provider_service_name'] ?? ''));
$mappedCarrier = $mappedProvider === 'apaczka' ? 'apaczka' : 'allegro';
if ($mappedCarrier !== 'apaczka' && stripos($mappedCarrierId, 'inpost') !== false) {
$mappedCarrier = 'inpost';
}
$deliveryMethodId = '';
if ($mappedCarrier === 'apaczka' && $mappedMethodId !== '') {
$deliveryMethodId = $mappedMethodId;
} elseif (($mappedCarrier === 'allegro' || $mappedCarrier === 'inpost') && $mappedMethodId !== '') {
$deliveryMethodId = $mappedMethodId;
} elseif ($mappedCarrier !== 'inpost') {
$deliveryMethodId = trim((string) ($prefs['delivery_method_id'] ?? ($orderRow['external_carrier_account_id'] ?? '')));
}
$deliveryMethodName = trim((string) ($orderRow['external_carrier_id'] ?? ''));
$inpostSvcList = is_array($inpostServices ?? null) ? $inpostServices : [];
$preselectedCarrier = $mappedCarrier !== '' ? $mappedCarrier : ($mappedMethodId !== '' ? 'allegro' : '');
@@ -90,23 +101,32 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
<td><?= $e($pkgCarrier !== '' ? $pkgCarrier : '-') ?></td>
<td>
<?php if ($pkgLabelPath !== ''): ?>
<?php if ($pkgStatus === 'error'): ?>
-
<?php else: ?>
<form method="post" action="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/<?= $e((string) $pkgId) ?>/label" style="display:inline">
<input type="hidden" name="_csrf_token" value="<?= $e($csrfToken ?? '') ?>">
<button type="submit" class="btn btn--sm btn--secondary">Pobierz</button>
</form>
<?php endif; ?>
<?php elseif ($pkgShipmentId !== '' && $pkgStatus === 'created'): ?>
<form method="post" action="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/<?= $e((string) $pkgId) ?>/label" style="display:inline">
<input type="hidden" name="_csrf_token" value="<?= $e($csrfToken ?? '') ?>">
<button type="submit" class="btn btn--sm btn--primary">Generuj etykiete</button>
</form>
<span class="muted">Generowanie etykiety...</span>
<?php else: ?>
-
<?php endif; ?>
</td>
<td class="text-nowrap"><?= $e((string) ($pkg['created_at'] ?? '')) ?></td>
<td>
<?php if ($pkgStatus === 'pending'): ?>
<button type="button" class="btn btn--sm btn--secondary" data-check-status="<?= $e((string) $pkgId) ?>" data-order-id="<?= $e((string) ($orderId ?? 0)) ?>">Sprawdz status</button>
<?php
$shouldCheckStatus = $pkgStatus === 'pending' || ($pkgStatus === 'created' && $pkgLabelPath === '');
?>
<?php if ($shouldCheckStatus): ?>
<button type="button"
class="btn btn--sm btn--secondary"
data-check-status="<?= $e((string) $pkgId) ?>"
data-order-id="<?= $e((string) ($orderId ?? 0)) ?>"
data-package-status="<?= $e($pkgStatus) ?>"
data-auto-check="1">Sprawdz status</button>
<?php endif; ?>
</td>
</tr>
@@ -134,9 +154,13 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
<option value="">-- Wybierz --</option>
<option value="allegro"<?= $preselectedCarrier === 'allegro' ? ' selected' : '' ?>>Allegro</option>
<option value="inpost"<?= $preselectedCarrier === 'inpost' ? ' selected' : '' ?>>InPost</option>
<option value="apaczka"<?= $preselectedCarrier === 'apaczka' ? ' selected' : '' ?>>Apaczka</option>
</select>
<?php if ($deliveryMethodName !== ''): ?>
<div class="muted mt-4" style="font-size:12px">Metoda z zamowienia: <strong><?= $e($deliveryMethodName) ?></strong><?php if ($mappedServiceName !== ''): ?> &rarr; <?= $e($mappedCarrier === 'inpost' ? 'InPost' : 'Allegro') ?>: <?= $e($mappedServiceName) ?><?php endif; ?></div>
<div class="muted mt-4" style="font-size:12px">Metoda z zamowienia: <strong><?= $e($deliveryMethodName) ?></strong><?php if ($mappedServiceName !== ''): ?> &rarr; <?= $e($mappedCarrier === 'inpost' ? 'InPost' : ($mappedCarrier === 'apaczka' ? 'Apaczka' : 'Allegro')) ?>: <?= $e($mappedServiceName) ?><?php endif; ?></div>
<?php endif; ?>
<?php if ($deliveryMappingDiagnostic !== ''): ?>
<div class="flash flash--error mt-8"><?= $e($deliveryMappingDiagnostic) ?></div>
<?php endif; ?>
</div>
@@ -199,11 +223,39 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
<?php endif; ?>
</div>
<div id="shipment-apaczka-panel" style="<?= $preselectedCarrier !== 'apaczka' ? 'display:none' : '' ?>">
<?php if ($apaczkaSvcList === []): ?>
<div class="muted">Brak uslug Apaczka (sprawdz konfiguracje App ID/App Secret).</div>
<?php else: ?>
<select class="form-control" id="shipment-apaczka-select">
<option value="">-- Wybierz usluge Apaczka --</option>
<?php foreach ($apaczkaSvcList as $aSvc): ?>
<?php
if (!is_array($aSvc)) {
continue;
}
$aSvcId = trim((string) ($aSvc['service_id'] ?? $aSvc['id'] ?? ''));
$aSvcName = trim((string) ($aSvc['name'] ?? ''));
$aSvcCarrierCode = trim((string) ($aSvc['carrier_code'] ?? ''));
$aSvcSelected = $mappedCarrier === 'apaczka' && $mappedMethodId === $aSvcId;
?>
<option
value="<?= $e($aSvcId) ?>"
data-carrier-id="<?= $e($aSvcCarrierCode) ?>"
<?= $aSvcSelected ? 'selected' : '' ?>>
<?= $e($aSvcName !== '' ? $aSvcName : ('ID ' . $aSvcId)) ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
</div>
<div id="shipment-empty-panel" class="muted" style="<?= $preselectedCarrier !== '' ? 'display:none' : '' ?>">Wybierz przewoznika</div>
</div>
<input type="hidden" name="credentials_id" id="shipment-credentials-id" value="">
<input type="hidden" name="carrier_id" id="shipment-carrier-id" value="">
<input type="hidden" name="provider_code" id="shipment-provider-code" value="<?= $e($preselectedCarrier === 'apaczka' ? 'apaczka' : 'allegro_wza') ?>">
<label class="form-field">
<span class="field-label">Typ paczki</span>
@@ -349,7 +401,6 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
<script>
(function () {
// ── Generic searchable select enhancer ──
function enhanceSelect(selectEl) {
if (!selectEl || selectEl.dataset.enhanced) return;
selectEl.dataset.enhanced = '1';
@@ -458,17 +509,17 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
selectEl._syncTrigger = syncTrigger;
}
// ── Enhance all native selects on the page ──
var carrierSelect = document.getElementById('shipment-carrier-select');
var inpostSelect = document.getElementById('shipment-inpost-select');
var apaczkaSelect = document.getElementById('shipment-apaczka-select');
document.querySelectorAll('form select.form-control').forEach(function (sel) {
enhanceSelect(sel);
});
// ── Carrier / service panel logic ──
var allegroPanel = document.getElementById('shipment-allegro-panel');
var inpostPanel = document.getElementById('shipment-inpost-panel');
var apaczkaPanel = document.getElementById('shipment-apaczka-panel');
var emptyPanel = document.getElementById('shipment-empty-panel');
var wrapper = document.getElementById('shipment-service-wrapper');
@@ -477,6 +528,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
var dropdown = document.getElementById('shipment-service-dropdown');
var credentialsInput = document.getElementById('shipment-credentials-id');
var carrierInput = document.getElementById('shipment-carrier-id');
var providerInput = document.getElementById('shipment-provider-code');
if (!carrierSelect || !hiddenInput) return;
@@ -489,12 +541,13 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
}
function showPanel(carrier) {
allegroPanel.style.display = carrier === 'allegro' ? '' : 'none';
inpostPanel.style.display = carrier === 'inpost' ? '' : 'none';
emptyPanel.style.display = carrier === '' ? '' : 'none';
if (allegroPanel) allegroPanel.style.display = carrier === 'allegro' ? '' : 'none';
if (inpostPanel) inpostPanel.style.display = carrier === 'inpost' ? '' : 'none';
if (apaczkaPanel) apaczkaPanel.style.display = carrier === 'apaczka' ? '' : 'none';
if (emptyPanel) emptyPanel.style.display = carrier === '' ? '' : 'none';
if (providerInput) providerInput.value = carrier === 'apaczka' ? 'apaczka' : 'allegro_wza';
}
// --- Carrier select ---
carrierSelect.addEventListener('change', function () {
clearHiddenFields();
if (searchInput) searchInput.value = '';
@@ -502,17 +555,21 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
inpostSelect.selectedIndex = 0;
if (inpostSelect._syncTrigger) inpostSelect._syncTrigger();
}
if (apaczkaSelect) {
apaczkaSelect.selectedIndex = 0;
if (apaczkaSelect._syncTrigger) apaczkaSelect._syncTrigger();
}
allegroOpts.forEach(function (o) { o.classList.remove('is-selected'); });
showPanel(carrierSelect.value);
});
// --- InPost select ---
if (inpostSelect) {
function syncInpostFields() {
var opt = inpostSelect.options[inpostSelect.selectedIndex];
hiddenInput.value = inpostSelect.value;
credentialsInput.value = opt ? (opt.getAttribute('data-credentials-id') || '') : '';
carrierInput.value = opt ? (opt.getAttribute('data-carrier-id') || '') : '';
if (providerInput) providerInput.value = 'allegro_wza';
}
inpostSelect.addEventListener('change', syncInpostFields);
if (carrierSelect.value === 'inpost' && inpostSelect.value !== '') {
@@ -520,7 +577,20 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
}
}
// --- Allegro searchable select ---
if (apaczkaSelect) {
function syncApaczkaFields() {
var opt = apaczkaSelect.options[apaczkaSelect.selectedIndex];
hiddenInput.value = apaczkaSelect.value;
credentialsInput.value = '';
carrierInput.value = opt ? (opt.getAttribute('data-carrier-id') || '') : '';
if (providerInput) providerInput.value = 'apaczka';
}
apaczkaSelect.addEventListener('change', syncApaczkaFields);
if (carrierSelect.value === 'apaczka' && apaczkaSelect.value !== '') {
syncApaczkaFields();
}
}
if (wrapper && searchInput && dropdown) {
var isAllegroOpen = false;
@@ -528,6 +598,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
hiddenInput.value = opt.getAttribute('data-value') || '';
credentialsInput.value = opt.getAttribute('data-credentials-id') || '';
carrierInput.value = opt.getAttribute('data-carrier-id') || '';
if (providerInput) providerInput.value = 'allegro_wza';
searchInput.value = opt.getAttribute('data-label') || '';
closeAllegro();
allegroOpts.forEach(function (o) { o.classList.remove('is-selected'); });
@@ -592,7 +663,6 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
}
}
// --- Check status ---
function checkPackageStatus(pkgId, oId, btn, attempt) {
if (btn) {
btn.disabled = true;
@@ -601,19 +671,24 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
fetch('/orders/' + oId + '/shipment/' + pkgId + '/status')
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.status === 'created' || data.status === 'label_ready') {
if (data.status === 'label_ready') {
window.location.reload();
} else if (data.status === 'created' || data.status === 'in_progress' || data.status === 'pending') {
if (attempt < 10) {
var delayCreated = Math.min(2000 * Math.pow(1.5, attempt), 15000);
setTimeout(function () {
checkPackageStatus(pkgId, oId, btn, attempt + 1);
}, delayCreated);
if (btn) btn.textContent = 'Generuje etykiete... (' + (attempt + 1) + ')';
} else if (btn) {
btn.textContent = 'W toku... Odswiez status';
btn.disabled = false;
}
} else if (data.status === 'error') {
if (btn) {
btn.textContent = 'Blad: ' + (data.error || '');
btn.disabled = false;
}
} else if (attempt < 10) {
var delay = Math.min(2000 * Math.pow(1.5, attempt), 15000);
setTimeout(function () {
checkPackageStatus(pkgId, oId, btn, attempt + 1);
}, delay);
if (btn) btn.textContent = 'Sprawdzam... (' + (attempt + 1) + ')';
} else {
if (btn) {
btn.textContent = 'W toku... Sprobuj ponownie';
@@ -629,7 +704,6 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
});
}
// Manual check buttons
document.querySelectorAll('[data-check-status]').forEach(function (btn) {
btn.addEventListener('click', function () {
checkPackageStatus(
@@ -641,15 +715,25 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
});
});
// Auto-poll pending packages on page load
var params = new URLSearchParams(window.location.search);
var autoCheckId = params.get('check');
if (autoCheckId) {
var autoBtn = document.querySelector('[data-check-status="' + autoCheckId + '"]');
var autoOrderId = autoBtn ? autoBtn.getAttribute('data-order-id') : params.get('id');
if (autoOrderId) {
checkPackageStatus(autoCheckId, autoOrderId, autoBtn, 0);
document.querySelectorAll('[data-check-status][data-auto-check=\"1\"]').forEach(function (btn, idx) {
var pkgId = btn.getAttribute('data-check-status');
var oId = btn.getAttribute('data-order-id');
var pkgStatus = (btn.getAttribute('data-package-status') || '').toLowerCase();
if (!pkgId || !oId) return;
if (autoCheckId) {
if (pkgId !== autoCheckId) return;
} else {
if (pkgStatus !== 'pending') return;
}
}
setTimeout(function () {
checkPackageStatus(pkgId, oId, btn, 0);
}, idx * 400);
});
})();
</script>