Add Allegro shipment service and related components
- Implement AllegroShipmentService for managing shipment creation and status checks. - Create ShipmentController to handle shipment preparation and label downloading. - Introduce ShipmentPackageRepository for database interactions related to shipment packages. - Add methods for retrieving delivery services, creating shipments, checking creation status, and downloading labels. - Implement address validation and token management for Allegro API integration.
This commit is contained in:
437
src/Modules/Shipments/AllegroShipmentService.php
Normal file
437
src/Modules/Shipments/AllegroShipmentService.php
Normal file
@@ -0,0 +1,437 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Shipments;
|
||||
|
||||
use App\Modules\Orders\OrdersRepository;
|
||||
use App\Modules\Settings\AllegroApiClient;
|
||||
use App\Modules\Settings\AllegroIntegrationRepository;
|
||||
use App\Modules\Settings\AllegroOAuthClient;
|
||||
use App\Modules\Settings\CompanySettingsRepository;
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
final class AllegroShipmentService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AllegroIntegrationRepository $integrationRepository,
|
||||
private readonly AllegroOAuthClient $oauthClient,
|
||||
private readonly AllegroApiClient $apiClient,
|
||||
private readonly ShipmentPackageRepository $packages,
|
||||
private readonly CompanySettingsRepository $companySettings,
|
||||
private readonly OrdersRepository $ordersRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function getDeliveryServices(): array
|
||||
{
|
||||
[$accessToken, $env] = $this->resolveToken();
|
||||
$response = $this->apiClient->getDeliveryServices($env, $accessToken);
|
||||
return is_array($response['services'] ?? null) ? $response['services'] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $formData
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function createShipment(int $orderId, array $formData): array
|
||||
{
|
||||
$order = $this->ordersRepository->findDetails($orderId);
|
||||
if ($order === null) {
|
||||
throw new RuntimeException('Zamowienie nie znalezione.');
|
||||
}
|
||||
|
||||
$company = $this->companySettings->getSettings();
|
||||
$sender = $this->companySettings->getSenderAddress();
|
||||
$this->validateSenderAddress($sender);
|
||||
|
||||
$deliveryMethodId = trim((string) ($formData['delivery_method_id'] ?? ''));
|
||||
if ($deliveryMethodId === '') {
|
||||
throw new RuntimeException('Nie podano metody dostawy.');
|
||||
}
|
||||
|
||||
$receiverAddress = $this->buildReceiverAddress($order, $formData);
|
||||
$senderAddress = $sender;
|
||||
if (trim((string) ($formData['sender_point_id'] ?? '')) !== '') {
|
||||
$senderAddress['point'] = trim((string) $formData['sender_point_id']);
|
||||
}
|
||||
|
||||
$packageType = strtoupper(trim((string) ($formData['package_type'] ?? 'PACKAGE')));
|
||||
$lengthCm = (float) ($formData['length_cm'] ?? $company['default_package_length_cm']);
|
||||
$widthCm = (float) ($formData['width_cm'] ?? $company['default_package_width_cm']);
|
||||
$heightCm = (float) ($formData['height_cm'] ?? $company['default_package_height_cm']);
|
||||
$weightKg = (float) ($formData['weight_kg'] ?? $company['default_package_weight_kg']);
|
||||
$labelFormat = strtoupper(trim((string) ($formData['label_format'] ?? $company['default_label_format'])));
|
||||
if (!in_array($labelFormat, ['PDF', 'ZPL'], true)) {
|
||||
$labelFormat = 'PDF';
|
||||
}
|
||||
|
||||
$orderData = is_array($order['order'] ?? null) ? $order['order'] : [];
|
||||
$sourceOrderId = trim((string) ($orderData['source_order_id'] ?? ''));
|
||||
|
||||
$commandId = $this->generateUuid();
|
||||
|
||||
$apiPayload = [
|
||||
'commandId' => $commandId,
|
||||
'input' => [
|
||||
'deliveryMethodId' => $deliveryMethodId,
|
||||
'sender' => $senderAddress,
|
||||
'receiver' => $receiverAddress,
|
||||
'referenceNumber' => $sourceOrderId !== '' ? $sourceOrderId : (string) $orderId,
|
||||
'packages' => [[
|
||||
'type' => $packageType,
|
||||
'length' => ['value' => $lengthCm, 'unit' => 'CENTIMETER'],
|
||||
'width' => ['value' => $widthCm, 'unit' => 'CENTIMETER'],
|
||||
'height' => ['value' => $heightCm, 'unit' => 'CENTIMETER'],
|
||||
'weight' => ['value' => $weightKg, 'unit' => 'KILOGRAMS'],
|
||||
]],
|
||||
'labelFormat' => $labelFormat,
|
||||
],
|
||||
];
|
||||
|
||||
$insuranceAmount = (float) ($formData['insurance_amount'] ?? 0);
|
||||
if ($insuranceAmount > 0) {
|
||||
$apiPayload['input']['insurance'] = [
|
||||
'amount' => number_format($insuranceAmount, 2, '.', ''),
|
||||
'currency' => strtoupper(trim((string) ($formData['insurance_currency'] ?? 'PLN'))),
|
||||
];
|
||||
}
|
||||
|
||||
$codAmount = (float) ($formData['cod_amount'] ?? 0);
|
||||
if ($codAmount > 0) {
|
||||
$cod = [
|
||||
'amount' => number_format($codAmount, 2, '.', ''),
|
||||
'currency' => strtoupper(trim((string) ($formData['cod_currency'] ?? 'PLN'))),
|
||||
];
|
||||
if (trim($company['bank_owner_name']) !== '') {
|
||||
$cod['ownerName'] = $company['bank_owner_name'];
|
||||
}
|
||||
if (trim($company['bank_account']) !== '') {
|
||||
$cod['iban'] = $company['bank_account'];
|
||||
}
|
||||
$apiPayload['input']['cashOnDelivery'] = $cod;
|
||||
}
|
||||
|
||||
$credentialsId = trim((string) ($formData['credentials_id'] ?? ''));
|
||||
if ($credentialsId !== '') {
|
||||
$apiPayload['input']['credentialsId'] = $credentialsId;
|
||||
}
|
||||
|
||||
$packageId = $this->packages->create([
|
||||
'order_id' => $orderId,
|
||||
'provider' => 'allegro_wza',
|
||||
'delivery_method_id' => $deliveryMethodId,
|
||||
'credentials_id' => $credentialsId !== '' ? $credentialsId : null,
|
||||
'command_id' => $commandId,
|
||||
'status' => 'pending',
|
||||
'carrier_id' => trim((string) ($formData['carrier_id'] ?? '')),
|
||||
'package_type' => $packageType,
|
||||
'weight_kg' => $weightKg,
|
||||
'length_cm' => $lengthCm,
|
||||
'width_cm' => $widthCm,
|
||||
'height_cm' => $heightCm,
|
||||
'insurance_amount' => $insuranceAmount > 0 ? $insuranceAmount : null,
|
||||
'insurance_currency' => $insuranceAmount > 0 ? strtoupper(trim((string) ($formData['insurance_currency'] ?? 'PLN'))) : null,
|
||||
'cod_amount' => $codAmount > 0 ? $codAmount : null,
|
||||
'cod_currency' => $codAmount > 0 ? strtoupper(trim((string) ($formData['cod_currency'] ?? 'PLN'))) : null,
|
||||
'label_format' => $labelFormat,
|
||||
'receiver_point_id' => trim((string) ($formData['receiver_point_id'] ?? '')),
|
||||
'sender_point_id' => trim((string) ($formData['sender_point_id'] ?? '')),
|
||||
'reference_number' => $sourceOrderId !== '' ? $sourceOrderId : (string) $orderId,
|
||||
'payload_json' => $apiPayload,
|
||||
]);
|
||||
|
||||
[$accessToken, $env] = $this->resolveToken();
|
||||
|
||||
try {
|
||||
$response = $this->apiClient->createShipment($env, $accessToken, $apiPayload);
|
||||
} catch (RuntimeException $exception) {
|
||||
if (trim($exception->getMessage()) === 'ALLEGRO_HTTP_401') {
|
||||
[$accessToken, $env] = $this->forceRefreshToken();
|
||||
$response = $this->apiClient->createShipment($env, $accessToken, $apiPayload);
|
||||
} else {
|
||||
$this->packages->update($packageId, [
|
||||
'status' => 'error',
|
||||
'error_message' => $exception->getMessage(),
|
||||
]);
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
$returnedCommandId = trim((string) ($response['commandId'] ?? $commandId));
|
||||
$this->packages->update($packageId, [
|
||||
'command_id' => $returnedCommandId,
|
||||
'payload_json' => $response,
|
||||
]);
|
||||
|
||||
return [
|
||||
'package_id' => $packageId,
|
||||
'command_id' => $returnedCommandId,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function checkCreationStatus(int $packageId): array
|
||||
{
|
||||
$package = $this->packages->findById($packageId);
|
||||
if ($package === null) {
|
||||
throw new RuntimeException('Paczka nie znaleziona.');
|
||||
}
|
||||
|
||||
$commandId = trim((string) ($package['command_id'] ?? ''));
|
||||
if ($commandId === '') {
|
||||
throw new RuntimeException('Brak command_id dla tej paczki.');
|
||||
}
|
||||
|
||||
[$accessToken, $env] = $this->resolveToken();
|
||||
$response = $this->apiClient->getShipmentCreationStatus($env, $accessToken, $commandId);
|
||||
|
||||
$status = strtoupper(trim((string) ($response['status'] ?? '')));
|
||||
$shipmentId = trim((string) ($response['shipmentId'] ?? ''));
|
||||
|
||||
if ($status === 'SUCCESS' && $shipmentId !== '') {
|
||||
$details = $this->apiClient->getShipmentDetails($env, $accessToken, $shipmentId);
|
||||
$trackingNumber = trim((string) ($details['waybill'] ?? ''));
|
||||
|
||||
$this->packages->update($packageId, [
|
||||
'status' => 'created',
|
||||
'shipment_id' => $shipmentId,
|
||||
'tracking_number' => $trackingNumber !== '' ? $trackingNumber : null,
|
||||
'payload_json' => $details,
|
||||
]);
|
||||
|
||||
return [
|
||||
'status' => 'created',
|
||||
'shipment_id' => $shipmentId,
|
||||
'tracking_number' => $trackingNumber,
|
||||
];
|
||||
}
|
||||
|
||||
if ($status === 'ERROR') {
|
||||
$errors = is_array($response['errors'] ?? null) ? $response['errors'] : [];
|
||||
$messages = [];
|
||||
foreach ($errors as $err) {
|
||||
if (is_array($err)) {
|
||||
$messages[] = trim((string) ($err['message'] ?? ($err['userMessage'] ?? '')));
|
||||
}
|
||||
}
|
||||
$errorMsg = implode('; ', array_filter($messages)) ?: 'Blad tworzenia przesylki.';
|
||||
|
||||
$this->packages->update($packageId, [
|
||||
'status' => 'error',
|
||||
'error_message' => $errorMsg,
|
||||
'payload_json' => $response,
|
||||
]);
|
||||
|
||||
return ['status' => 'error', 'error' => $errorMsg];
|
||||
}
|
||||
|
||||
return ['status' => 'in_progress'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function downloadLabel(int $packageId, string $storagePath): array
|
||||
{
|
||||
$package = $this->packages->findById($packageId);
|
||||
if ($package === null) {
|
||||
throw new RuntimeException('Paczka nie znaleziona.');
|
||||
}
|
||||
|
||||
$shipmentId = trim((string) ($package['shipment_id'] ?? ''));
|
||||
if ($shipmentId === '') {
|
||||
throw new RuntimeException('Przesylka nie zostala jeszcze utworzona.');
|
||||
}
|
||||
|
||||
[$accessToken, $env] = $this->resolveToken();
|
||||
$labelFormat = trim((string) ($package['label_format'] ?? 'PDF'));
|
||||
$pageSize = $labelFormat === 'ZPL' ? 'A6' : 'A6';
|
||||
$binary = $this->apiClient->getShipmentLabel($env, $accessToken, [$shipmentId], $pageSize);
|
||||
|
||||
$dir = rtrim($storagePath, '/\\') . '/labels';
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0775, true);
|
||||
}
|
||||
|
||||
$ext = $labelFormat === 'ZPL' ? 'zpl' : 'pdf';
|
||||
$filename = 'label_' . $packageId . '_' . $shipmentId . '.' . $ext;
|
||||
$filePath = $dir . '/' . $filename;
|
||||
file_put_contents($filePath, $binary);
|
||||
|
||||
$relativePath = 'labels/' . $filename;
|
||||
$this->packages->update($packageId, [
|
||||
'status' => 'label_ready',
|
||||
'label_path' => $relativePath,
|
||||
]);
|
||||
|
||||
return [
|
||||
'label_path' => $relativePath,
|
||||
'full_path' => $filePath,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $orderDetails
|
||||
* @param array<string, mixed> $formData
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildReceiverAddress(?array $orderDetails, array $formData): array
|
||||
{
|
||||
$addresses = is_array($orderDetails['addresses'] ?? null) ? $orderDetails['addresses'] : [];
|
||||
$deliveryAddr = null;
|
||||
$customerAddr = null;
|
||||
foreach ($addresses as $addr) {
|
||||
$type = (string) ($addr['address_type'] ?? '');
|
||||
if ($type === 'delivery') {
|
||||
$deliveryAddr = $addr;
|
||||
}
|
||||
if ($type === 'customer') {
|
||||
$customerAddr = $addr;
|
||||
}
|
||||
}
|
||||
|
||||
$addr = $deliveryAddr ?? $customerAddr ?? [];
|
||||
|
||||
$email = trim((string) ($formData['receiver_email'] ?? ($addr['email'] ?? '')));
|
||||
$receiver = [
|
||||
'name' => trim((string) ($formData['receiver_name'] ?? ($addr['name'] ?? ''))),
|
||||
'street' => trim((string) ($formData['receiver_street'] ?? ($addr['street_name'] ?? ''))),
|
||||
'city' => trim((string) ($formData['receiver_city'] ?? ($addr['city'] ?? ''))),
|
||||
'postalCode' => trim((string) ($formData['receiver_postal_code'] ?? ($addr['zip_code'] ?? ''))),
|
||||
'countryCode' => strtoupper(trim((string) ($formData['receiver_country_code'] ?? ($addr['country'] ?? 'PL')))),
|
||||
'phone' => trim((string) ($formData['receiver_phone'] ?? ($addr['phone'] ?? ''))),
|
||||
'email' => $email,
|
||||
];
|
||||
|
||||
$buyerEmail = trim((string) ($customerAddr['email'] ?? $email));
|
||||
if ($buyerEmail !== '') {
|
||||
$receiver['hashedMail'] = hash('sha256', strtolower($buyerEmail));
|
||||
}
|
||||
|
||||
$company = trim((string) ($formData['receiver_company'] ?? ($addr['company_name'] ?? '')));
|
||||
if ($company !== '') {
|
||||
$receiver['company'] = $company;
|
||||
}
|
||||
|
||||
$pointId = trim((string) ($formData['receiver_point_id'] ?? ($addr['parcel_external_id'] ?? '')));
|
||||
if ($pointId !== '') {
|
||||
$receiver['point'] = $pointId;
|
||||
}
|
||||
|
||||
return $receiver;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $sender
|
||||
*/
|
||||
private function validateSenderAddress(array $sender): void
|
||||
{
|
||||
$required = ['street', 'city', 'postalCode', 'phone', 'email'];
|
||||
foreach ($required as $field) {
|
||||
if (trim((string) ($sender[$field] ?? '')) === '') {
|
||||
throw new RuntimeException('Uzupelnij dane nadawcy w Ustawienia > Dane firmy (brak: ' . $field . ').');
|
||||
}
|
||||
}
|
||||
|
||||
$name = trim((string) ($sender['name'] ?? ''));
|
||||
$company = trim((string) ($sender['company'] ?? ''));
|
||||
if ($name === '' && $company === '') {
|
||||
throw new RuntimeException('Uzupelnij dane nadawcy w Ustawienia > Dane firmy (brak nazwy/firmy).');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: string, 1: string}
|
||||
*/
|
||||
private function resolveToken(): array
|
||||
{
|
||||
$oauth = $this->integrationRepository->getTokenCredentials();
|
||||
if ($oauth === null) {
|
||||
throw new RuntimeException('Brak polaczenia OAuth Allegro. Polacz konto w Ustawieniach.');
|
||||
}
|
||||
|
||||
$env = (string) ($oauth['environment'] ?? 'sandbox');
|
||||
$accessToken = trim((string) ($oauth['access_token'] ?? ''));
|
||||
$tokenExpiresAt = trim((string) ($oauth['token_expires_at'] ?? ''));
|
||||
|
||||
if ($accessToken === '') {
|
||||
return $this->forceRefreshToken();
|
||||
}
|
||||
|
||||
if ($tokenExpiresAt !== '') {
|
||||
try {
|
||||
$expiresAt = new DateTimeImmutable($tokenExpiresAt);
|
||||
if ($expiresAt <= (new DateTimeImmutable('now'))->add(new DateInterval('PT5M'))) {
|
||||
return $this->forceRefreshToken();
|
||||
}
|
||||
} catch (Throwable) {
|
||||
return $this->forceRefreshToken();
|
||||
}
|
||||
}
|
||||
|
||||
return [$accessToken, $env];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: string, 1: string}
|
||||
*/
|
||||
private function forceRefreshToken(): array
|
||||
{
|
||||
$oauth = $this->integrationRepository->getTokenCredentials();
|
||||
if ($oauth === null) {
|
||||
throw new RuntimeException('Brak danych OAuth Allegro.');
|
||||
}
|
||||
|
||||
$token = $this->oauthClient->refreshAccessToken(
|
||||
(string) ($oauth['environment'] ?? 'sandbox'),
|
||||
(string) ($oauth['client_id'] ?? ''),
|
||||
(string) ($oauth['client_secret'] ?? ''),
|
||||
(string) ($oauth['refresh_token'] ?? '')
|
||||
);
|
||||
|
||||
$expiresAt = null;
|
||||
$expiresIn = max(0, (int) ($token['expires_in'] ?? 0));
|
||||
if ($expiresIn > 0) {
|
||||
$expiresAt = (new DateTimeImmutable('now'))
|
||||
->add(new DateInterval('PT' . $expiresIn . 'S'))
|
||||
->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$refreshToken = trim((string) ($token['refresh_token'] ?? ''));
|
||||
if ($refreshToken === '') {
|
||||
$refreshToken = (string) ($oauth['refresh_token'] ?? '');
|
||||
}
|
||||
|
||||
$this->integrationRepository->saveTokens(
|
||||
(string) ($token['access_token'] ?? ''),
|
||||
$refreshToken,
|
||||
(string) ($token['token_type'] ?? ''),
|
||||
(string) ($token['scope'] ?? ''),
|
||||
$expiresAt
|
||||
);
|
||||
|
||||
$updated = $this->integrationRepository->getTokenCredentials();
|
||||
$newToken = trim((string) ($updated['access_token'] ?? ''));
|
||||
if ($newToken === '') {
|
||||
throw new RuntimeException('Nie udalo sie odswiezyc tokenu Allegro.');
|
||||
}
|
||||
|
||||
return [$newToken, (string) ($updated['environment'] ?? 'sandbox')];
|
||||
}
|
||||
|
||||
private function generateUuid(): string
|
||||
{
|
||||
$data = random_bytes(16);
|
||||
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user