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:
@@ -107,7 +107,6 @@ final class OrdersController
|
||||
['key' => 'totals', 'label' => $this->translator->get('orders.fields.totals'), 'sortable' => true, 'sort_key' => 'total_with_tax', 'raw' => true],
|
||||
['key' => 'shipping', 'label' => $this->translator->get('orders.fields.shipping'), 'raw' => true],
|
||||
['key' => 'ordered_at', 'label' => $this->translator->get('orders.fields.ordered_at'), 'sortable' => true, 'sort_key' => 'ordered_at'],
|
||||
['key' => 'source_updated_at', 'label' => $this->translator->get('orders.fields.source_updated_at'), 'sortable' => true, 'sort_key' => 'source_updated_at'],
|
||||
],
|
||||
'rows' => $tableRows,
|
||||
'pagination' => [
|
||||
@@ -144,11 +143,20 @@ final class OrdersController
|
||||
$documents = is_array($details['documents'] ?? null) ? $details['documents'] : [];
|
||||
$notes = is_array($details['notes'] ?? null) ? $details['notes'] : [];
|
||||
$history = is_array($details['status_history'] ?? null) ? $details['status_history'] : [];
|
||||
$activityLog = is_array($details['activity_log'] ?? null) ? $details['activity_log'] : [];
|
||||
$statusCode = (string) (($order['effective_status_id'] ?? '') !== '' ? $order['effective_status_id'] : ($order['external_status_id'] ?? ''));
|
||||
$statusCounts = $this->orders->statusCounts();
|
||||
$statusConfig = $this->orders->statusPanelConfig();
|
||||
$statusLabelMap = $this->statusLabelMap($statusConfig);
|
||||
|
||||
$resolvedHistory = $this->resolveHistoryLabels($history, $statusLabelMap);
|
||||
|
||||
$allStatuses = $this->buildAllStatusOptions($statusConfig);
|
||||
|
||||
$flashSuccess = (string) ($_SESSION['order_flash_success'] ?? '');
|
||||
$flashError = (string) ($_SESSION['order_flash_error'] ?? '');
|
||||
unset($_SESSION['order_flash_success'], $_SESSION['order_flash_error']);
|
||||
|
||||
$html = $this->template->render('orders/show', [
|
||||
'title' => $this->translator->get('orders.details.title') . ' #' . $orderId,
|
||||
'activeMenu' => 'orders',
|
||||
@@ -163,14 +171,51 @@ final class OrdersController
|
||||
'shipments' => $shipments,
|
||||
'documents' => $documents,
|
||||
'notes' => $notes,
|
||||
'history' => $history,
|
||||
'history' => $resolvedHistory,
|
||||
'activityLog' => $activityLog,
|
||||
'statusLabel' => $this->statusLabel($statusCode, $statusLabelMap),
|
||||
'statusPanel' => $this->buildStatusPanel($statusConfig, $statusCounts, $statusCode),
|
||||
'allStatuses' => $allStatuses,
|
||||
'currentStatusCode' => $statusCode,
|
||||
'flashSuccess' => $flashSuccess,
|
||||
'flashError' => $flashError,
|
||||
], 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
public function updateStatus(Request $request): Response
|
||||
{
|
||||
$orderId = max(0, (int) $request->input('id', 0));
|
||||
if ($orderId <= 0) {
|
||||
return Response::html('Not found', 404);
|
||||
}
|
||||
|
||||
$csrfToken = (string) $request->input('_csrf_token', '');
|
||||
if (!Csrf::validate($csrfToken)) {
|
||||
$_SESSION['order_flash_error'] = $this->translator->get('auth.errors.csrf_expired');
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
|
||||
$newStatus = trim((string) $request->input('new_status', ''));
|
||||
if ($newStatus === '') {
|
||||
$_SESSION['order_flash_error'] = $this->translator->get('orders.details.status_change.status_required');
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
|
||||
$user = $this->auth->user();
|
||||
$actorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : null;
|
||||
|
||||
$success = $this->orders->updateOrderStatus($orderId, $newStatus, 'user', $actorName !== '' ? $actorName : null);
|
||||
if ($success) {
|
||||
$_SESSION['order_flash_success'] = $this->translator->get('orders.details.status_change.success');
|
||||
} else {
|
||||
$_SESSION['order_flash_error'] = $this->translator->get('orders.details.status_change.failed');
|
||||
}
|
||||
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @return array<string, mixed>
|
||||
@@ -218,12 +263,12 @@ final class OrdersController
|
||||
. '<div class="orders-money__main">' . htmlspecialchars($totalWithTax . ' ' . $currency, ENT_QUOTES, 'UTF-8') . '</div>'
|
||||
. '<div class="orders-money__meta">oplacono: ' . htmlspecialchars($totalPaid . ' ' . $currency, ENT_QUOTES, 'UTF-8') . '</div>'
|
||||
. '</div>',
|
||||
'shipping' => '<div class="orders-mini">'
|
||||
. '<div>wys.: <strong>' . $shipments . '</strong></div>'
|
||||
. '<div>dok.: <strong>' . $documents . '</strong></div>'
|
||||
. '</div>',
|
||||
'shipping' => $this->shippingHtml(
|
||||
trim((string) ($row['external_carrier_id'] ?? '')),
|
||||
$shipments,
|
||||
$documents
|
||||
),
|
||||
'ordered_at' => (string) ($row['ordered_at'] ?? ''),
|
||||
'source_updated_at' => (string) ($row['source_updated_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -455,9 +500,10 @@ final class OrdersController
|
||||
$mediaUrl = trim((string) ($item['media_url'] ?? ''));
|
||||
|
||||
$thumb = $mediaUrl !== ''
|
||||
? '<button type="button" class="orders-image-trigger js-order-img-open" data-image-url="' . htmlspecialchars($mediaUrl, ENT_QUOTES, 'UTF-8') . '" aria-label="Podglad zdjecia produktu">'
|
||||
? '<span class="orders-image-hover-wrap">'
|
||||
. '<img class="orders-product__thumb" src="' . htmlspecialchars($mediaUrl, ENT_QUOTES, 'UTF-8') . '" alt="">'
|
||||
. '</button>'
|
||||
. '<img class="orders-image-hover-popup" src="' . htmlspecialchars($mediaUrl, ENT_QUOTES, 'UTF-8') . '" alt="">'
|
||||
. '</span>'
|
||||
: '<span class="orders-product__thumb orders-product__thumb--empty"></span>';
|
||||
|
||||
$html .= '<div class="orders-product">'
|
||||
@@ -478,6 +524,18 @@ final class OrdersController
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function shippingHtml(string $deliveryMethod, int $shipments, int $documents): string
|
||||
{
|
||||
$html = '<div class="orders-mini">';
|
||||
if ($deliveryMethod !== '' && !preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $deliveryMethod)) {
|
||||
$html .= '<div class="orders-mini__delivery">' . htmlspecialchars($deliveryMethod, ENT_QUOTES, 'UTF-8') . '</div>';
|
||||
}
|
||||
$html .= '<div>wys.: <strong>' . $shipments . '</strong> dok.: <strong>' . $documents . '</strong></div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function formatQuantity(float $value): string
|
||||
{
|
||||
$rounded = round($value, 3);
|
||||
@@ -512,4 +570,47 @@ final class OrdersController
|
||||
'3' => 'zwrocone',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{name:string,color_hex:string,items:array<int, array{code:string,name:string}>}> $config
|
||||
* @return array<int, array{code:string, name:string, group:string}>
|
||||
*/
|
||||
private function buildAllStatusOptions(array $config): array
|
||||
{
|
||||
$options = [];
|
||||
foreach ($config as $group) {
|
||||
$groupName = trim((string) ($group['name'] ?? ''));
|
||||
$items = is_array($group['items'] ?? null) ? $group['items'] : [];
|
||||
foreach ($items as $item) {
|
||||
$code = strtolower(trim((string) ($item['code'] ?? '')));
|
||||
if ($code === '') {
|
||||
continue;
|
||||
}
|
||||
$options[] = [
|
||||
'code' => $code,
|
||||
'name' => (string) ($item['name'] ?? $code),
|
||||
'group' => $groupName,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $history
|
||||
* @param array<string, string> $statusLabelMap
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function resolveHistoryLabels(array $history, array $statusLabelMap): array
|
||||
{
|
||||
return array_map(function (array $entry) use ($statusLabelMap): array {
|
||||
$fromCode = trim((string) ($entry['from_status_id'] ?? ''));
|
||||
$toCode = trim((string) ($entry['to_status_id'] ?? ''));
|
||||
$entry['from_label'] = $fromCode !== '' ? $this->statusLabel($fromCode, $statusLabelMap) : '-';
|
||||
$entry['to_label'] = $toCode !== '' ? $this->statusLabel($toCode, $statusLabelMap) : '-';
|
||||
|
||||
return $entry;
|
||||
}, $history);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ final class OrdersRepository
|
||||
a.name AS buyer_name,
|
||||
a.email AS buyer_email,
|
||||
a.city AS buyer_city,
|
||||
o.external_carrier_id,
|
||||
(SELECT COUNT(*) FROM order_items oi WHERE oi.order_id = o.id) AS items_count,
|
||||
(SELECT COALESCE(SUM(oi.quantity), 0) FROM order_items oi WHERE oi.order_id = o.id) AS items_qty,
|
||||
(SELECT COUNT(*) FROM order_shipments sh WHERE sh.order_id = o.id) AS shipments_count,
|
||||
@@ -170,6 +171,7 @@ final class OrdersRepository
|
||||
'fetched_at' => (string) ($row['fetched_at'] ?? ''),
|
||||
'is_invoice' => (int) ($row['is_invoice'] ?? 0) === 1,
|
||||
'is_canceled_by_buyer' => (int) ($row['is_canceled_by_buyer'] ?? 0) === 1,
|
||||
'external_carrier_id' => (string) ($row['external_carrier_id'] ?? ''),
|
||||
'buyer_name' => (string) ($row['buyer_name'] ?? ''),
|
||||
'buyer_email' => (string) ($row['buyer_email'] ?? ''),
|
||||
'buyer_city' => (string) ($row['buyer_city'] ?? ''),
|
||||
@@ -469,6 +471,8 @@ final class OrdersRepository
|
||||
$history = [];
|
||||
}
|
||||
|
||||
$activityLog = $this->loadActivityLog($orderId);
|
||||
|
||||
return [
|
||||
'order' => $order,
|
||||
'addresses' => $addresses,
|
||||
@@ -478,6 +482,7 @@ final class OrdersRepository
|
||||
'documents' => $documents,
|
||||
'notes' => $notes,
|
||||
'status_history' => $history,
|
||||
'activity_log' => $activityLog,
|
||||
];
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
@@ -636,6 +641,139 @@ final class OrdersRepository
|
||||
return $this->supportsMappedMedia;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function loadActivityLog(int $orderId): array
|
||||
{
|
||||
try {
|
||||
$stmt = $this->pdo->prepare(
|
||||
'SELECT * FROM order_activity_log
|
||||
WHERE order_id = :order_id
|
||||
ORDER BY created_at DESC, id DESC'
|
||||
);
|
||||
$stmt->execute(['order_id' => $orderId]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return is_array($rows) ? $rows : [];
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $details
|
||||
*/
|
||||
public function recordActivity(
|
||||
int $orderId,
|
||||
string $eventType,
|
||||
string $summary,
|
||||
?array $details = null,
|
||||
string $actorType = 'system',
|
||||
?string $actorName = null
|
||||
): void {
|
||||
$stmt = $this->pdo->prepare(
|
||||
'INSERT INTO order_activity_log
|
||||
(order_id, event_type, summary, details_json, actor_type, actor_name, created_at)
|
||||
VALUES
|
||||
(:order_id, :event_type, :summary, :details_json, :actor_type, :actor_name, NOW())'
|
||||
);
|
||||
$stmt->execute([
|
||||
'order_id' => $orderId,
|
||||
'event_type' => $eventType,
|
||||
'summary' => $summary,
|
||||
'details_json' => $details !== null ? json_encode($details, JSON_UNESCAPED_UNICODE) : null,
|
||||
'actor_type' => $actorType,
|
||||
'actor_name' => $actorName,
|
||||
]);
|
||||
}
|
||||
|
||||
public function recordStatusChange(
|
||||
int $orderId,
|
||||
?string $fromStatus,
|
||||
string $toStatus,
|
||||
string $changeSource = 'manual',
|
||||
?string $comment = null,
|
||||
string $actorType = 'system',
|
||||
?string $actorName = null
|
||||
): void {
|
||||
$stmt = $this->pdo->prepare(
|
||||
'INSERT INTO order_status_history
|
||||
(order_id, from_status_id, to_status_id, changed_at, change_source, comment)
|
||||
VALUES
|
||||
(:order_id, :from_status_id, :to_status_id, NOW(), :change_source, :comment)'
|
||||
);
|
||||
$stmt->execute([
|
||||
'order_id' => $orderId,
|
||||
'from_status_id' => $fromStatus,
|
||||
'to_status_id' => $toStatus,
|
||||
'change_source' => $changeSource,
|
||||
'comment' => $comment,
|
||||
]);
|
||||
|
||||
$fromLabel = $fromStatus !== null ? $this->resolveStatusName($fromStatus) : '-';
|
||||
$toLabel = $this->resolveStatusName($toStatus);
|
||||
$summary = 'Zmiana statusu: ' . $fromLabel . ' → ' . $toLabel;
|
||||
|
||||
$this->recordActivity($orderId, 'status_change', $summary, [
|
||||
'from_status' => $fromStatus,
|
||||
'to_status' => $toStatus,
|
||||
'change_source' => $changeSource,
|
||||
'comment' => $comment,
|
||||
], $actorType, $actorName);
|
||||
}
|
||||
|
||||
public function updateOrderStatus(int $orderId, string $newStatusCode, string $actorType = 'user', ?string $actorName = null): bool
|
||||
{
|
||||
try {
|
||||
$stmt = $this->pdo->prepare('SELECT external_status_id FROM orders WHERE id = :id LIMIT 1');
|
||||
$stmt->execute(['id' => $orderId]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!is_array($row)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldStatus = trim((string) ($row['external_status_id'] ?? ''));
|
||||
|
||||
$update = $this->pdo->prepare('UPDATE orders SET external_status_id = :status, updated_at = NOW() WHERE id = :id');
|
||||
$update->execute(['status' => $newStatusCode, 'id' => $orderId]);
|
||||
|
||||
$this->recordStatusChange(
|
||||
$orderId,
|
||||
$oldStatus !== '' ? $oldStatus : null,
|
||||
$newStatusCode,
|
||||
'manual',
|
||||
null,
|
||||
$actorType,
|
||||
$actorName
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveStatusName(string $code): string
|
||||
{
|
||||
$normalized = strtolower(trim($code));
|
||||
if ($normalized === '') {
|
||||
return $code;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $this->pdo->prepare('SELECT name FROM order_statuses WHERE LOWER(code) = :code LIMIT 1');
|
||||
$stmt->execute(['code' => $normalized]);
|
||||
$name = $stmt->fetchColumn();
|
||||
if (is_string($name) && trim($name) !== '') {
|
||||
return trim($name);
|
||||
}
|
||||
} catch (Throwable) {
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
private function normalizeColorHex(string $value): string
|
||||
{
|
||||
$trimmed = trim($value);
|
||||
|
||||
Reference in New Issue
Block a user