373 lines
16 KiB
PHP
373 lines
16 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Shipments;
|
|
|
|
use App\Core\Http\Request;
|
|
use App\Core\Http\Response;
|
|
use App\Core\I18n\Translator;
|
|
use App\Core\Security\Csrf;
|
|
use App\Core\Support\Flash;
|
|
use App\Core\View\Template;
|
|
use App\Modules\Auth\AuthService;
|
|
use App\Modules\Orders\OrdersRepository;
|
|
use App\Modules\Settings\CarrierDeliveryMethodMappingRepository;
|
|
use App\Modules\Settings\CompanySettingsRepository;
|
|
use RuntimeException;
|
|
use Throwable;
|
|
|
|
final class ShipmentController
|
|
{
|
|
public function __construct(
|
|
private readonly Template $template,
|
|
private readonly Translator $translator,
|
|
private readonly AuthService $auth,
|
|
private readonly OrdersRepository $ordersRepository,
|
|
private readonly CompanySettingsRepository $companySettings,
|
|
private readonly ShipmentProviderRegistry $providerRegistry,
|
|
private readonly ShipmentPackageRepository $packageRepository,
|
|
private readonly string $storagePath,
|
|
private readonly ?CarrierDeliveryMethodMappingRepository $deliveryMappings = null
|
|
) {
|
|
}
|
|
|
|
public function prepare(Request $request): Response
|
|
{
|
|
$orderId = max(0, (int) $request->input('id', 0));
|
|
$details = $this->ordersRepository->findDetails($orderId);
|
|
if ($details === null) {
|
|
return Response::html('Not found', 404);
|
|
}
|
|
|
|
$order = is_array($details['order'] ?? null) ? $details['order'] : [];
|
|
$addresses = is_array($details['addresses'] ?? null) ? $details['addresses'] : [];
|
|
$items = is_array($details['items'] ?? null) ? $details['items'] : [];
|
|
$company = $this->companySettings->getSettings();
|
|
$existingPackages = $this->packageRepository->findByOrderId($orderId);
|
|
|
|
$deliveryAddr = null;
|
|
$customerAddr = null;
|
|
foreach ($addresses as $addr) {
|
|
$type = (string) ($addr['address_type'] ?? '');
|
|
if ($type === 'delivery') {
|
|
$deliveryAddr = $addr;
|
|
}
|
|
if ($type === 'customer') {
|
|
$customerAddr = $addr;
|
|
}
|
|
}
|
|
|
|
$receiverAddr = $this->buildReceiverAddress($deliveryAddr, $customerAddr);
|
|
$preferences = is_array($order['preferences_json'] ?? null)
|
|
? $order['preferences_json']
|
|
: (is_string($order['preferences_json'] ?? null) ? (json_decode($order['preferences_json'], true) ?: []) : []);
|
|
|
|
$deliveryServices = [];
|
|
$apaczkaServices = [];
|
|
$deliveryServicesError = '';
|
|
|
|
$allegroProvider = $this->providerRegistry->get('allegro_wza');
|
|
if ($allegroProvider !== null) {
|
|
try {
|
|
$deliveryServices = $allegroProvider->getDeliveryServices();
|
|
} catch (Throwable $exception) {
|
|
$deliveryServicesError = $exception->getMessage();
|
|
}
|
|
}
|
|
|
|
$apaczkaProvider = $this->providerRegistry->get('apaczka');
|
|
if ($apaczkaProvider !== null) {
|
|
try {
|
|
$apaczkaServices = $apaczkaProvider->getDeliveryServices();
|
|
} catch (Throwable $exception) {
|
|
if ($deliveryServicesError === '') {
|
|
$deliveryServicesError = $exception->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
$inpostServices = array_values(array_filter(
|
|
$deliveryServices,
|
|
static fn(array $svc) => stripos((string) ($svc['carrierId'] ?? ''), 'inpost') !== false
|
|
));
|
|
|
|
$flashSuccess = (string) Flash::get('shipment.success', '');
|
|
$flashError = (string) Flash::get('shipment.error', '');
|
|
|
|
$deliveryMapping = null;
|
|
$deliveryMappingDiagnostic = '';
|
|
$orderCarrierName = trim((string) ($order['external_carrier_id'] ?? ''));
|
|
$source = strtolower(trim((string) ($order['source'] ?? '')));
|
|
$sourceIntegrationId = $source === 'shoppro'
|
|
? max(0, (int) ($order['integration_id'] ?? 0))
|
|
: 0;
|
|
if ($orderCarrierName !== '' && $this->deliveryMappings !== null && in_array($source, ['allegro', 'shoppro'], true)) {
|
|
$deliveryMapping = $this->deliveryMappings->findByOrderMethod($source, $sourceIntegrationId, $orderCarrierName);
|
|
if ($deliveryMapping === null) {
|
|
$hasMappingsForSource = $this->deliveryMappings->hasMappingsForSource($source, $sourceIntegrationId);
|
|
if (!$hasMappingsForSource && $source === 'shoppro' && $sourceIntegrationId > 0) {
|
|
$deliveryMappingDiagnostic = 'Brak mapowan form dostawy dla tej instancji shopPRO (ID integracji: '
|
|
. $sourceIntegrationId
|
|
. ').';
|
|
} elseif (!$hasMappingsForSource) {
|
|
$deliveryMappingDiagnostic = 'Brak skonfigurowanych mapowan form dostawy dla tego zrodla zamowienia.';
|
|
} else {
|
|
$deliveryMappingDiagnostic = 'Brak mapowania dla metody dostawy: ' . $orderCarrierName . '.';
|
|
}
|
|
}
|
|
}
|
|
|
|
$html = $this->template->render('shipments/prepare', [
|
|
'title' => $this->translator->get('shipments.prepare.title') . ' #' . $orderId,
|
|
'activeMenu' => 'orders',
|
|
'activeOrders' => 'list',
|
|
'user' => $this->auth->user(),
|
|
'csrfToken' => Csrf::token(),
|
|
'orderId' => $orderId,
|
|
'order' => $order,
|
|
'items' => $items,
|
|
'receiverAddr' => $receiverAddr,
|
|
'preferences' => $preferences,
|
|
'company' => $company,
|
|
'deliveryServices' => $deliveryServices,
|
|
'apaczkaServices' => $apaczkaServices,
|
|
'deliveryServicesError' => $deliveryServicesError,
|
|
'existingPackages' => $existingPackages,
|
|
'flashSuccess' => $flashSuccess,
|
|
'flashError' => $flashError,
|
|
'deliveryMapping' => $deliveryMapping,
|
|
'deliveryMappingDiagnostic' => $deliveryMappingDiagnostic,
|
|
'inpostServices' => $inpostServices,
|
|
], 'layouts/app');
|
|
|
|
return Response::html($html);
|
|
}
|
|
|
|
public function create(Request $request): Response
|
|
{
|
|
$orderId = max(0, (int) $request->input('id', 0));
|
|
if ($orderId <= 0) {
|
|
return Response::html('Not found', 404);
|
|
}
|
|
|
|
$csrfToken = (string) $request->input('_token', '');
|
|
if (!Csrf::validate($csrfToken)) {
|
|
Flash::set('shipment.error', $this->translator->get('auth.errors.csrf_expired'));
|
|
return Response::redirect('/orders/' . $orderId . '/shipment/prepare');
|
|
}
|
|
|
|
$user = $this->auth->user();
|
|
$actorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : null;
|
|
$actorName = ($actorName !== null && $actorName !== '') ? $actorName : null;
|
|
|
|
try {
|
|
$providerCode = strtolower(trim((string) $request->input('provider_code', 'allegro_wza')));
|
|
if ($providerCode === 'inpost') {
|
|
$providerCode = 'allegro_wza';
|
|
}
|
|
$provider = $this->providerRegistry->get($providerCode);
|
|
if ($provider === null) {
|
|
throw new RuntimeException('Nieznany provider przesylek: ' . $providerCode);
|
|
}
|
|
|
|
$result = $provider->createShipment($orderId, [
|
|
'provider_code' => $providerCode,
|
|
'delivery_method_id' => (string) $request->input('delivery_method_id', ''),
|
|
'credentials_id' => (string) $request->input('credentials_id', ''),
|
|
'carrier_id' => (string) $request->input('carrier_id', ''),
|
|
'package_type' => (string) $request->input('package_type', 'PACKAGE'),
|
|
'length_cm' => (string) $request->input('length_cm', ''),
|
|
'width_cm' => (string) $request->input('width_cm', ''),
|
|
'height_cm' => (string) $request->input('height_cm', ''),
|
|
'weight_kg' => (string) $request->input('weight_kg', ''),
|
|
'insurance_amount' => (string) $request->input('insurance_amount', '0'),
|
|
'insurance_currency' => (string) $request->input('insurance_currency', 'PLN'),
|
|
'cod_amount' => (string) $request->input('cod_amount', '0'),
|
|
'cod_currency' => (string) $request->input('cod_currency', 'PLN'),
|
|
'label_format' => (string) $request->input('label_format', 'PDF'),
|
|
'receiver_name' => (string) $request->input('receiver_name', ''),
|
|
'receiver_company' => (string) $request->input('receiver_company', ''),
|
|
'receiver_street' => (string) $request->input('receiver_street', ''),
|
|
'receiver_city' => (string) $request->input('receiver_city', ''),
|
|
'receiver_postal_code' => (string) $request->input('receiver_postal_code', ''),
|
|
'receiver_country_code' => (string) $request->input('receiver_country_code', 'PL'),
|
|
'receiver_phone' => (string) $request->input('receiver_phone', ''),
|
|
'receiver_email' => (string) $request->input('receiver_email', ''),
|
|
'receiver_point_id' => (string) $request->input('receiver_point_id', ''),
|
|
'sender_point_id' => (string) $request->input('sender_point_id', ''),
|
|
]);
|
|
|
|
$packageId = (int) ($result['package_id'] ?? 0);
|
|
$this->ordersRepository->recordActivity(
|
|
$orderId,
|
|
'shipment_created',
|
|
'Zlecono utworzenie przesylki (' . $providerCode . ', ID paczki: ' . $packageId . ')',
|
|
['package_id' => $packageId, 'command_id' => $result['command_id'] ?? null, 'provider' => $providerCode],
|
|
'user',
|
|
$actorName
|
|
);
|
|
Flash::set('shipment.success', 'Komenda tworzenia przesylki wyslana. Sprawdz status.');
|
|
return Response::redirect('/orders/' . $orderId . '/shipment/prepare?check=' . $packageId);
|
|
} catch (Throwable $exception) {
|
|
$this->ordersRepository->recordActivity(
|
|
$orderId,
|
|
'shipment_error',
|
|
'Blad tworzenia przesylki: ' . $exception->getMessage(),
|
|
null,
|
|
'user',
|
|
$actorName
|
|
);
|
|
Flash::set('shipment.error', 'Blad tworzenia przesylki: ' . $exception->getMessage());
|
|
return Response::redirect('/orders/' . $orderId . '/shipment/prepare');
|
|
}
|
|
}
|
|
|
|
public function checkStatus(Request $request): Response
|
|
{
|
|
$orderId = max(0, (int) $request->input('id', 0));
|
|
$packageId = max(0, (int) $request->input('packageId', 0));
|
|
if ($orderId <= 0 || $packageId <= 0) {
|
|
return Response::json(['error' => 'Not found'], 404);
|
|
}
|
|
|
|
try {
|
|
$package = $this->packageRepository->findById($packageId);
|
|
if ($package === null) {
|
|
return Response::json(['error' => 'Not found'], 404);
|
|
}
|
|
|
|
$providerCode = strtolower(trim((string) ($package['provider'] ?? 'allegro_wza')));
|
|
$provider = $this->providerRegistry->get($providerCode);
|
|
if ($provider === null) {
|
|
return Response::json(['status' => 'error', 'error' => 'Brak providera: ' . $providerCode]);
|
|
}
|
|
|
|
$result = $provider->checkCreationStatus($packageId);
|
|
|
|
if (($result['status'] ?? '') === 'created') {
|
|
try {
|
|
$provider->downloadLabel($packageId, $this->storagePath);
|
|
$result['status'] = 'label_ready';
|
|
} catch (Throwable) {
|
|
// label generation failed, user can retry manually
|
|
}
|
|
}
|
|
|
|
return Response::json($result);
|
|
} catch (Throwable $exception) {
|
|
return Response::json(['status' => 'error', 'error' => $exception->getMessage()]);
|
|
}
|
|
}
|
|
|
|
public function label(Request $request): Response
|
|
{
|
|
$orderId = max(0, (int) $request->input('id', 0));
|
|
$packageId = max(0, (int) $request->input('packageId', 0));
|
|
if ($orderId <= 0 || $packageId <= 0) {
|
|
return Response::html('Not found', 404);
|
|
}
|
|
|
|
$csrfToken = (string) $request->input('_token', '');
|
|
if (!Csrf::validate($csrfToken)) {
|
|
Flash::set('shipment.error', $this->translator->get('auth.errors.csrf_expired'));
|
|
return Response::redirect('/orders/' . $orderId . '/shipment/prepare');
|
|
}
|
|
|
|
$user = $this->auth->user();
|
|
$actorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : null;
|
|
$actorName = ($actorName !== null && $actorName !== '') ? $actorName : null;
|
|
|
|
try {
|
|
$package = $this->packageRepository->findById($packageId);
|
|
if ($package === null) {
|
|
throw new RuntimeException('Paczka nie znaleziona.');
|
|
}
|
|
|
|
$providerCode = strtolower(trim((string) ($package['provider'] ?? 'allegro_wza')));
|
|
$provider = $this->providerRegistry->get($providerCode);
|
|
if ($provider === null) {
|
|
throw new RuntimeException('Brak providera: ' . $providerCode);
|
|
}
|
|
|
|
$result = $provider->downloadLabel($packageId, $this->storagePath);
|
|
$fullPath = (string) ($result['full_path'] ?? '');
|
|
if ($fullPath !== '' && file_exists($fullPath)) {
|
|
$labelFormat = strtoupper(trim((string) ($package['label_format'] ?? 'PDF')));
|
|
$contentType = $labelFormat === 'ZPL' ? 'application/octet-stream' : 'application/pdf';
|
|
$filename = basename($fullPath);
|
|
|
|
$this->ordersRepository->recordActivity(
|
|
$orderId,
|
|
'shipment_label_downloaded',
|
|
'Pobrano etykiete dla przesylki #' . $packageId,
|
|
['package_id' => $packageId, 'filename' => $filename, 'provider' => $providerCode],
|
|
'user',
|
|
$actorName
|
|
);
|
|
|
|
return new Response(
|
|
(string) file_get_contents($fullPath),
|
|
200,
|
|
[
|
|
'Content-Type' => $contentType,
|
|
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
|
]
|
|
);
|
|
}
|
|
|
|
Flash::set('shipment.success', 'Etykieta pobrana.');
|
|
} catch (Throwable $exception) {
|
|
$this->ordersRepository->recordActivity(
|
|
$orderId,
|
|
'shipment_error',
|
|
'Blad pobierania etykiety (paczka #' . $packageId . '): ' . $exception->getMessage(),
|
|
null,
|
|
'user',
|
|
$actorName
|
|
);
|
|
Flash::set('shipment.error', 'Blad pobierania etykiety: ' . $exception->getMessage());
|
|
}
|
|
|
|
return Response::redirect('/orders/' . $orderId . '/shipment/prepare');
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed>|null $deliveryAddr
|
|
* @param array<string, mixed>|null $customerAddr
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function buildReceiverAddress(?array $deliveryAddr, ?array $customerAddr): array
|
|
{
|
|
$delivery = is_array($deliveryAddr) ? $deliveryAddr : [];
|
|
$customer = is_array($customerAddr) ? $customerAddr : [];
|
|
if ($delivery === []) {
|
|
return $customer;
|
|
}
|
|
|
|
$result = $delivery;
|
|
$deliveryName = trim((string) ($delivery['name'] ?? ''));
|
|
$customerName = trim((string) ($customer['name'] ?? ''));
|
|
if (($this->isPickupPointDelivery($delivery) || $deliveryName === '') && $customerName !== '') {
|
|
$result['name'] = $customerName;
|
|
}
|
|
if (trim((string) ($result['phone'] ?? '')) === '' && trim((string) ($customer['phone'] ?? '')) !== '') {
|
|
$result['phone'] = $customer['phone'];
|
|
}
|
|
if (trim((string) ($result['email'] ?? '')) === '' && trim((string) ($customer['email'] ?? '')) !== '') {
|
|
$result['email'] = $customer['email'];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $deliveryAddr
|
|
*/
|
|
private function isPickupPointDelivery(array $deliveryAddr): bool
|
|
{
|
|
$parcelId = trim((string) ($deliveryAddr['parcel_external_id'] ?? ''));
|
|
$parcelName = trim((string) ($deliveryAddr['parcel_name'] ?? ''));
|
|
return $parcelId !== '' || $parcelName !== '';
|
|
}
|
|
}
|