Files
orderPRO/src/Modules/Shipments/ShipmentController.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 !== '';
}
}