feat: Implement Allegro Order Sync and Status Management
- Added AllegroOrderSyncStateRepository for managing sync state with Allegro orders. - Introduced AllegroOrdersSyncService to handle the synchronization of orders from Allegro. - Created AllegroStatusDiscoveryService to discover and store order statuses from Allegro. - Developed AllegroStatusMappingRepository for managing status mappings between Allegro and OrderPro. - Implemented AllegroStatusSyncService to facilitate status synchronization. - Added CronSettingsController for managing cron job settings related to Allegro integration.
This commit is contained in:
421
src/Modules/Orders/OrderImportRepository.php
Normal file
421
src/Modules/Orders/OrderImportRepository.php
Normal file
@@ -0,0 +1,421 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Orders;
|
||||
|
||||
use PDO;
|
||||
use Throwable;
|
||||
|
||||
final class OrderImportRepository
|
||||
{
|
||||
public function __construct(private readonly PDO $pdo)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $orderData
|
||||
* @param array<int, array<string, mixed>> $addresses
|
||||
* @param array<int, array<string, mixed>> $items
|
||||
* @param array<int, array<string, mixed>> $payments
|
||||
* @param array<int, array<string, mixed>> $shipments
|
||||
* @param array<int, array<string, mixed>> $notes
|
||||
* @param array<int, array<string, mixed>> $statusHistory
|
||||
* @return array{order_id:int, created:bool}
|
||||
*/
|
||||
public function upsertOrderAggregate(
|
||||
array $orderData,
|
||||
array $addresses,
|
||||
array $items,
|
||||
array $payments,
|
||||
array $shipments,
|
||||
array $notes,
|
||||
array $statusHistory
|
||||
): array {
|
||||
$this->pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
$source = trim((string) ($orderData['source'] ?? 'allegro'));
|
||||
$sourceOrderId = trim((string) ($orderData['source_order_id'] ?? ''));
|
||||
$existingOrderId = $this->findOrderIdBySource($source, $sourceOrderId);
|
||||
$created = $existingOrderId === null;
|
||||
$orderId = $created
|
||||
? $this->insertOrder($orderData)
|
||||
: $this->updateOrder($existingOrderId, $orderData);
|
||||
|
||||
$this->replaceAddresses($orderId, $addresses);
|
||||
$this->replaceItems($orderId, $items);
|
||||
$this->replacePayments($orderId, $payments);
|
||||
$this->replaceShipments($orderId, $shipments);
|
||||
$this->replaceNotes($orderId, $notes);
|
||||
$this->replaceStatusHistory($orderId, $statusHistory);
|
||||
|
||||
$this->pdo->commit();
|
||||
} catch (Throwable $exception) {
|
||||
if ($this->pdo->inTransaction()) {
|
||||
$this->pdo->rollBack();
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
return [
|
||||
'order_id' => $orderId,
|
||||
'created' => $created,
|
||||
];
|
||||
}
|
||||
|
||||
private function findOrderIdBySource(string $source, string $sourceOrderId): ?int
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id
|
||||
FROM orders
|
||||
WHERE source = :source AND source_order_id = :source_order_id
|
||||
LIMIT 1'
|
||||
);
|
||||
$statement->execute([
|
||||
'source' => $source,
|
||||
'source_order_id' => $sourceOrderId,
|
||||
]);
|
||||
$value = $statement->fetchColumn();
|
||||
if ($value === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$id = (int) $value;
|
||||
return $id > 0 ? $id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $orderData
|
||||
*/
|
||||
private function insertOrder(array $orderData): int
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO orders (
|
||||
integration_id, source, source_order_id, external_order_id, external_platform_id, external_platform_account_id,
|
||||
external_status_id, external_payment_type_id, payment_status, external_carrier_id, external_carrier_account_id,
|
||||
customer_login, is_invoice, is_encrypted, is_canceled_by_buyer, currency,
|
||||
total_without_tax, total_with_tax, total_paid, send_date_min, send_date_max, ordered_at,
|
||||
source_created_at, source_updated_at, preferences_json, payload_json, fetched_at
|
||||
) VALUES (
|
||||
:integration_id, :source, :source_order_id, :external_order_id, :external_platform_id, :external_platform_account_id,
|
||||
:external_status_id, :external_payment_type_id, :payment_status, :external_carrier_id, :external_carrier_account_id,
|
||||
:customer_login, :is_invoice, :is_encrypted, :is_canceled_by_buyer, :currency,
|
||||
:total_without_tax, :total_with_tax, :total_paid, :send_date_min, :send_date_max, :ordered_at,
|
||||
:source_created_at, :source_updated_at, :preferences_json, :payload_json, :fetched_at
|
||||
)'
|
||||
);
|
||||
$statement->execute($this->orderParams($orderData));
|
||||
|
||||
return (int) $this->pdo->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $orderData
|
||||
*/
|
||||
private function updateOrder(int $orderId, array $orderData): int
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE orders
|
||||
SET integration_id = :integration_id,
|
||||
source = :source,
|
||||
source_order_id = :source_order_id,
|
||||
external_order_id = :external_order_id,
|
||||
external_platform_id = :external_platform_id,
|
||||
external_platform_account_id = :external_platform_account_id,
|
||||
external_status_id = :external_status_id,
|
||||
external_payment_type_id = :external_payment_type_id,
|
||||
payment_status = :payment_status,
|
||||
external_carrier_id = :external_carrier_id,
|
||||
external_carrier_account_id = :external_carrier_account_id,
|
||||
customer_login = :customer_login,
|
||||
is_invoice = :is_invoice,
|
||||
is_encrypted = :is_encrypted,
|
||||
is_canceled_by_buyer = :is_canceled_by_buyer,
|
||||
currency = :currency,
|
||||
total_without_tax = :total_without_tax,
|
||||
total_with_tax = :total_with_tax,
|
||||
total_paid = :total_paid,
|
||||
send_date_min = :send_date_min,
|
||||
send_date_max = :send_date_max,
|
||||
ordered_at = :ordered_at,
|
||||
source_created_at = :source_created_at,
|
||||
source_updated_at = :source_updated_at,
|
||||
preferences_json = :preferences_json,
|
||||
payload_json = :payload_json,
|
||||
fetched_at = :fetched_at,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id'
|
||||
);
|
||||
|
||||
$params = $this->orderParams($orderData);
|
||||
$params['id'] = $orderId;
|
||||
$statement->execute($params);
|
||||
|
||||
return $orderId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $orderData
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function orderParams(array $orderData): array
|
||||
{
|
||||
return [
|
||||
'integration_id' => $orderData['integration_id'] ?? null,
|
||||
'source' => (string) ($orderData['source'] ?? 'allegro'),
|
||||
'source_order_id' => (string) ($orderData['source_order_id'] ?? ''),
|
||||
'external_order_id' => $orderData['external_order_id'] ?? null,
|
||||
'external_platform_id' => $orderData['external_platform_id'] ?? null,
|
||||
'external_platform_account_id' => $orderData['external_platform_account_id'] ?? null,
|
||||
'external_status_id' => $orderData['external_status_id'] ?? null,
|
||||
'external_payment_type_id' => $orderData['external_payment_type_id'] ?? null,
|
||||
'payment_status' => $orderData['payment_status'] ?? null,
|
||||
'external_carrier_id' => $orderData['external_carrier_id'] ?? null,
|
||||
'external_carrier_account_id' => $orderData['external_carrier_account_id'] ?? null,
|
||||
'customer_login' => $orderData['customer_login'] ?? null,
|
||||
'is_invoice' => !empty($orderData['is_invoice']) ? 1 : 0,
|
||||
'is_encrypted' => !empty($orderData['is_encrypted']) ? 1 : 0,
|
||||
'is_canceled_by_buyer' => !empty($orderData['is_canceled_by_buyer']) ? 1 : 0,
|
||||
'currency' => (string) ($orderData['currency'] ?? 'PLN'),
|
||||
'total_without_tax' => $orderData['total_without_tax'] ?? null,
|
||||
'total_with_tax' => $orderData['total_with_tax'] ?? null,
|
||||
'total_paid' => $orderData['total_paid'] ?? null,
|
||||
'send_date_min' => $orderData['send_date_min'] ?? null,
|
||||
'send_date_max' => $orderData['send_date_max'] ?? null,
|
||||
'ordered_at' => $orderData['ordered_at'] ?? null,
|
||||
'source_created_at' => $orderData['source_created_at'] ?? null,
|
||||
'source_updated_at' => $orderData['source_updated_at'] ?? null,
|
||||
'preferences_json' => $this->encodeJson($orderData['preferences_json'] ?? null),
|
||||
'payload_json' => $this->encodeJson($orderData['payload_json'] ?? null),
|
||||
'fetched_at' => $orderData['fetched_at'] ?? date('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
*/
|
||||
private function replaceAddresses(int $orderId, array $rows): void
|
||||
{
|
||||
$this->pdo->prepare('DELETE FROM order_addresses WHERE order_id = :order_id')->execute(['order_id' => $orderId]);
|
||||
if ($rows === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO order_addresses (
|
||||
order_id, address_type, name, phone, email, street_name, street_number, city, zip_code, country,
|
||||
department, parcel_external_id, parcel_name, address_class, company_tax_number, company_name, payload_json
|
||||
) VALUES (
|
||||
:order_id, :address_type, :name, :phone, :email, :street_name, :street_number, :city, :zip_code, :country,
|
||||
:department, :parcel_external_id, :parcel_name, :address_class, :company_tax_number, :company_name, :payload_json
|
||||
)'
|
||||
);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$statement->execute([
|
||||
'order_id' => $orderId,
|
||||
'address_type' => (string) ($row['address_type'] ?? 'customer'),
|
||||
'name' => (string) ($row['name'] ?? ''),
|
||||
'phone' => $row['phone'] ?? null,
|
||||
'email' => $row['email'] ?? null,
|
||||
'street_name' => $row['street_name'] ?? null,
|
||||
'street_number' => $row['street_number'] ?? null,
|
||||
'city' => $row['city'] ?? null,
|
||||
'zip_code' => $row['zip_code'] ?? null,
|
||||
'country' => $row['country'] ?? null,
|
||||
'department' => $row['department'] ?? null,
|
||||
'parcel_external_id' => $row['parcel_external_id'] ?? null,
|
||||
'parcel_name' => $row['parcel_name'] ?? null,
|
||||
'address_class' => $row['address_class'] ?? null,
|
||||
'company_tax_number' => $row['company_tax_number'] ?? null,
|
||||
'company_name' => $row['company_name'] ?? null,
|
||||
'payload_json' => $this->encodeJson($row['payload_json'] ?? null),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
*/
|
||||
private function replaceItems(int $orderId, array $rows): void
|
||||
{
|
||||
$this->pdo->prepare('DELETE FROM order_items WHERE order_id = :order_id')->execute(['order_id' => $orderId]);
|
||||
if ($rows === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO order_items (
|
||||
order_id, source_item_id, external_item_id, ean, sku, original_name, original_code,
|
||||
original_price_with_tax, original_price_without_tax, media_url, quantity, tax_rate, item_status,
|
||||
unit, item_type, source_product_id, source_product_set_id, sort_order, payload_json
|
||||
) VALUES (
|
||||
:order_id, :source_item_id, :external_item_id, :ean, :sku, :original_name, :original_code,
|
||||
:original_price_with_tax, :original_price_without_tax, :media_url, :quantity, :tax_rate, :item_status,
|
||||
:unit, :item_type, :source_product_id, :source_product_set_id, :sort_order, :payload_json
|
||||
)'
|
||||
);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$statement->execute([
|
||||
'order_id' => $orderId,
|
||||
'source_item_id' => $row['source_item_id'] ?? null,
|
||||
'external_item_id' => $row['external_item_id'] ?? null,
|
||||
'ean' => $row['ean'] ?? null,
|
||||
'sku' => $row['sku'] ?? null,
|
||||
'original_name' => (string) ($row['original_name'] ?? ''),
|
||||
'original_code' => $row['original_code'] ?? null,
|
||||
'original_price_with_tax' => $row['original_price_with_tax'] ?? null,
|
||||
'original_price_without_tax' => $row['original_price_without_tax'] ?? null,
|
||||
'media_url' => $row['media_url'] ?? null,
|
||||
'quantity' => $row['quantity'] ?? 1,
|
||||
'tax_rate' => $row['tax_rate'] ?? null,
|
||||
'item_status' => $row['item_status'] ?? null,
|
||||
'unit' => $row['unit'] ?? null,
|
||||
'item_type' => (string) ($row['item_type'] ?? 'product'),
|
||||
'source_product_id' => $row['source_product_id'] ?? null,
|
||||
'source_product_set_id' => $row['source_product_set_id'] ?? null,
|
||||
'sort_order' => (int) ($row['sort_order'] ?? 0),
|
||||
'payload_json' => $this->encodeJson($row['payload_json'] ?? null),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
*/
|
||||
private function replacePayments(int $orderId, array $rows): void
|
||||
{
|
||||
$this->pdo->prepare('DELETE FROM order_payments WHERE order_id = :order_id')->execute(['order_id' => $orderId]);
|
||||
if ($rows === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO order_payments (
|
||||
order_id, source_payment_id, external_payment_id, payment_type_id, payment_date, amount, currency, comment, payload_json
|
||||
) VALUES (
|
||||
:order_id, :source_payment_id, :external_payment_id, :payment_type_id, :payment_date, :amount, :currency, :comment, :payload_json
|
||||
)'
|
||||
);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$statement->execute([
|
||||
'order_id' => $orderId,
|
||||
'source_payment_id' => $row['source_payment_id'] ?? null,
|
||||
'external_payment_id' => $row['external_payment_id'] ?? null,
|
||||
'payment_type_id' => (string) ($row['payment_type_id'] ?? 'unknown'),
|
||||
'payment_date' => $row['payment_date'] ?? null,
|
||||
'amount' => $row['amount'] ?? null,
|
||||
'currency' => $row['currency'] ?? null,
|
||||
'comment' => $row['comment'] ?? null,
|
||||
'payload_json' => $this->encodeJson($row['payload_json'] ?? null),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
*/
|
||||
private function replaceShipments(int $orderId, array $rows): void
|
||||
{
|
||||
$this->pdo->prepare('DELETE FROM order_shipments WHERE order_id = :order_id')->execute(['order_id' => $orderId]);
|
||||
if ($rows === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO order_shipments (
|
||||
order_id, source_shipment_id, external_shipment_id, tracking_number, carrier_provider_id, posted_at, media_uuid, payload_json
|
||||
) VALUES (
|
||||
:order_id, :source_shipment_id, :external_shipment_id, :tracking_number, :carrier_provider_id, :posted_at, :media_uuid, :payload_json
|
||||
)'
|
||||
);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$statement->execute([
|
||||
'order_id' => $orderId,
|
||||
'source_shipment_id' => $row['source_shipment_id'] ?? null,
|
||||
'external_shipment_id' => $row['external_shipment_id'] ?? null,
|
||||
'tracking_number' => (string) ($row['tracking_number'] ?? ''),
|
||||
'carrier_provider_id' => (string) ($row['carrier_provider_id'] ?? 'unknown'),
|
||||
'posted_at' => $row['posted_at'] ?? null,
|
||||
'media_uuid' => $row['media_uuid'] ?? null,
|
||||
'payload_json' => $this->encodeJson($row['payload_json'] ?? null),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
*/
|
||||
private function replaceNotes(int $orderId, array $rows): void
|
||||
{
|
||||
$this->pdo->prepare('DELETE FROM order_notes WHERE order_id = :order_id')->execute(['order_id' => $orderId]);
|
||||
if ($rows === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO order_notes (
|
||||
order_id, source_note_id, note_type, created_at_external, comment, payload_json
|
||||
) VALUES (
|
||||
:order_id, :source_note_id, :note_type, :created_at_external, :comment, :payload_json
|
||||
)'
|
||||
);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$statement->execute([
|
||||
'order_id' => $orderId,
|
||||
'source_note_id' => $row['source_note_id'] ?? null,
|
||||
'note_type' => (string) ($row['note_type'] ?? 'message'),
|
||||
'created_at_external' => $row['created_at_external'] ?? null,
|
||||
'comment' => (string) ($row['comment'] ?? ''),
|
||||
'payload_json' => $this->encodeJson($row['payload_json'] ?? null),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
*/
|
||||
private function replaceStatusHistory(int $orderId, array $rows): void
|
||||
{
|
||||
$this->pdo->prepare('DELETE FROM order_status_history WHERE order_id = :order_id')->execute(['order_id' => $orderId]);
|
||||
if ($rows === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO order_status_history (
|
||||
order_id, from_status_id, to_status_id, changed_at, change_source, comment, payload_json
|
||||
) VALUES (
|
||||
:order_id, :from_status_id, :to_status_id, :changed_at, :change_source, :comment, :payload_json
|
||||
)'
|
||||
);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$statement->execute([
|
||||
'order_id' => $orderId,
|
||||
'from_status_id' => $row['from_status_id'] ?? null,
|
||||
'to_status_id' => (string) ($row['to_status_id'] ?? ''),
|
||||
'changed_at' => $row['changed_at'] ?? date('Y-m-d H:i:s'),
|
||||
'change_source' => (string) ($row['change_source'] ?? 'import'),
|
||||
'comment' => $row['comment'] ?? null,
|
||||
'payload_json' => $this->encodeJson($row['payload_json'] ?? null),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function encodeJson(mixed $value): ?string
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
if (!is_array($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: null;
|
||||
}
|
||||
}
|
||||
@@ -39,11 +39,11 @@ final class OrdersController
|
||||
$result = $this->orders->paginate($filters);
|
||||
$totalPages = max(1, (int) ceil(((int) $result['total']) / max(1, (int) $result['per_page'])));
|
||||
$sourceOptions = $this->orders->sourceOptions();
|
||||
$statusOptions = $this->orders->statusOptions();
|
||||
$stats = $this->orders->quickStats();
|
||||
$statusCounts = $this->orders->statusCounts();
|
||||
$statusConfig = $this->orders->statusPanelConfig();
|
||||
$statusLabelMap = $this->statusLabelMap($statusConfig);
|
||||
$statusOptions = $this->buildStatusFilterOptions($this->orders->statusOptions(), $statusLabelMap);
|
||||
$statusPanel = $this->buildStatusPanel($statusConfig, $statusCounts, $filters['status'], $filters);
|
||||
|
||||
$tableRows = array_map(fn (array $row): array => $this->toTableRow($row, $statusLabelMap), (array) ($result['items'] ?? []));
|
||||
@@ -144,7 +144,7 @@ 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'] : [];
|
||||
$statusCode = (string) ($order['external_status_id'] ?? '');
|
||||
$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);
|
||||
@@ -183,7 +183,7 @@ final class OrdersController
|
||||
$buyerName = trim((string) ($row['buyer_name'] ?? ''));
|
||||
$buyerEmail = trim((string) ($row['buyer_email'] ?? ''));
|
||||
$buyerCity = trim((string) ($row['buyer_city'] ?? ''));
|
||||
$status = trim((string) ($row['external_status_id'] ?? ''));
|
||||
$status = trim((string) (($row['effective_status_id'] ?? '') !== '' ? $row['effective_status_id'] : ($row['external_status_id'] ?? '')));
|
||||
$currency = trim((string) ($row['currency'] ?? ''));
|
||||
$totalWithTax = $row['total_with_tax'] !== null ? number_format((float) $row['total_with_tax'], 2, '.', ' ') : '-';
|
||||
$totalPaid = $row['total_paid'] !== null ? number_format((float) $row['total_paid'], 2, '.', ' ') : '-';
|
||||
@@ -211,7 +211,7 @@ final class OrdersController
|
||||
. '</div>'
|
||||
. '</div>',
|
||||
'status_badges' => '<div class="orders-status-wrap">'
|
||||
. $this->statusBadge($this->statusLabel($status, $statusLabelMap))
|
||||
. $this->statusBadge($status, $this->statusLabel($status, $statusLabelMap))
|
||||
. '</div>',
|
||||
'products' => $this->productsHtml($itemsPreview, $itemsCount, $itemsQty),
|
||||
'totals' => '<div class="orders-money">'
|
||||
@@ -227,17 +227,18 @@ final class OrdersController
|
||||
];
|
||||
}
|
||||
|
||||
private function statusBadge(string $status): string
|
||||
private function statusBadge(string $statusCode, string $statusLabel): string
|
||||
{
|
||||
$label = $status !== '' ? $status : '-';
|
||||
$label = $statusLabel !== '' ? $statusLabel : '-';
|
||||
$code = strtolower(trim($statusCode));
|
||||
$class = 'is-neutral';
|
||||
if (in_array($status, ['shipped', 'delivered'], true)) {
|
||||
if (in_array($code, ['shipped', 'delivered'], true)) {
|
||||
$class = 'is-success';
|
||||
} elseif (in_array($status, ['cancelled', 'returned'], true)) {
|
||||
} elseif (in_array($code, ['cancelled', 'returned'], true)) {
|
||||
$class = 'is-danger';
|
||||
} elseif (in_array($status, ['new', 'confirmed'], true)) {
|
||||
} elseif (in_array($code, ['new', 'confirmed'], true)) {
|
||||
$class = 'is-info';
|
||||
} elseif (in_array($status, ['processing', 'packed', 'paid'], true)) {
|
||||
} elseif (in_array($code, ['processing', 'packed', 'paid'], true)) {
|
||||
$class = 'is-warn';
|
||||
}
|
||||
|
||||
@@ -255,7 +256,8 @@ final class OrdersController
|
||||
return (string) $statusLabelMap[$key];
|
||||
}
|
||||
|
||||
return ucfirst($statusCode);
|
||||
$normalized = str_replace(['_', '-'], ' ', $key);
|
||||
return ucfirst($normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -415,6 +417,26 @@ final class OrdersController
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $statusCodes
|
||||
* @param array<string, string> $statusLabelMap
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function buildStatusFilterOptions(array $statusCodes, array $statusLabelMap): array
|
||||
{
|
||||
$options = [];
|
||||
foreach ($statusCodes as $code => $value) {
|
||||
$rawCode = trim((string) ($code !== '' ? $code : $value));
|
||||
if ($rawCode === '') {
|
||||
continue;
|
||||
}
|
||||
$normalizedCode = strtolower($rawCode);
|
||||
$options[$normalizedCode] = $this->statusLabel($normalizedCode, $statusLabelMap);
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $itemsPreview
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,8 @@ use Throwable;
|
||||
|
||||
final class OrdersRepository
|
||||
{
|
||||
private ?bool $supportsMappedMedia = null;
|
||||
|
||||
public function __construct(private readonly PDO $pdo)
|
||||
{
|
||||
}
|
||||
@@ -24,6 +26,7 @@ final class OrdersRepository
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
$effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
|
||||
|
||||
$search = trim((string) ($filters['search'] ?? ''));
|
||||
if ($search !== '') {
|
||||
@@ -45,7 +48,7 @@ final class OrdersRepository
|
||||
|
||||
$status = trim((string) ($filters['status'] ?? ''));
|
||||
if ($status !== '') {
|
||||
$where[] = 'o.external_status_id = :status';
|
||||
$where[] = $effectiveStatusSql . ' = :status';
|
||||
$params['status'] = $status;
|
||||
}
|
||||
|
||||
@@ -86,7 +89,8 @@ final class OrdersRepository
|
||||
|
||||
try {
|
||||
$countSql = 'SELECT COUNT(*) FROM orders o '
|
||||
. 'LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer"'
|
||||
. 'LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer" '
|
||||
. 'LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code'
|
||||
. $whereSql;
|
||||
$countStmt = $this->pdo->prepare($countSql);
|
||||
$countStmt->execute($params);
|
||||
@@ -98,6 +102,7 @@ final class OrdersRepository
|
||||
o.source_order_id,
|
||||
o.external_order_id,
|
||||
o.external_status_id,
|
||||
' . $effectiveStatusSql . ' AS effective_status_id,
|
||||
o.payment_status,
|
||||
o.currency,
|
||||
o.total_with_tax,
|
||||
@@ -115,7 +120,8 @@ final class OrdersRepository
|
||||
(SELECT COUNT(*) FROM order_shipments sh WHERE sh.order_id = o.id) AS shipments_count,
|
||||
(SELECT COUNT(*) FROM order_documents od WHERE od.order_id = o.id) AS documents_count
|
||||
FROM orders o
|
||||
LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer"'
|
||||
LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer"
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code'
|
||||
. $whereSql
|
||||
. ' ORDER BY ' . $sortColumn . ' ' . $sortDir
|
||||
. ' LIMIT :limit OFFSET :offset';
|
||||
@@ -150,6 +156,7 @@ final class OrdersRepository
|
||||
'source_order_id' => (string) ($row['source_order_id'] ?? ''),
|
||||
'external_order_id' => (string) ($row['external_order_id'] ?? ''),
|
||||
'external_status_id' => (string) ($row['external_status_id'] ?? ''),
|
||||
'effective_status_id' => (string) ($row['effective_status_id'] ?? ''),
|
||||
'payment_status' => isset($row['payment_status']) ? (int) $row['payment_status'] : null,
|
||||
'currency' => (string) ($row['currency'] ?? ''),
|
||||
'total_with_tax' => $row['total_with_tax'] !== null ? (float) $row['total_with_tax'] : null,
|
||||
@@ -191,7 +198,15 @@ final class OrdersRepository
|
||||
public function statusOptions(): array
|
||||
{
|
||||
try {
|
||||
$rows = $this->pdo->query('SELECT DISTINCT external_status_id FROM orders WHERE external_status_id IS NOT NULL AND external_status_id <> "" ORDER BY external_status_id ASC')->fetchAll(PDO::FETCH_COLUMN);
|
||||
$effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
|
||||
$rows = $this->pdo->query(
|
||||
'SELECT DISTINCT ' . $effectiveStatusSql . ' AS effective_status_id
|
||||
FROM orders o
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code
|
||||
WHERE ' . $effectiveStatusSql . ' IS NOT NULL
|
||||
AND ' . $effectiveStatusSql . ' <> ""
|
||||
ORDER BY effective_status_id ASC'
|
||||
)->fetchAll(PDO::FETCH_COLUMN);
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
@@ -245,11 +260,13 @@ final class OrdersRepository
|
||||
public function quickStats(): array
|
||||
{
|
||||
try {
|
||||
$effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
|
||||
$row = $this->pdo->query('SELECT
|
||||
COUNT(*) AS all_count,
|
||||
SUM(CASE WHEN payment_status = 2 THEN 1 ELSE 0 END) AS paid_count,
|
||||
SUM(CASE WHEN external_status_id IN ("shipped", "delivered", "returned") THEN 1 ELSE 0 END) AS shipped_count
|
||||
FROM orders')->fetch(PDO::FETCH_ASSOC);
|
||||
SUM(CASE WHEN ' . $effectiveStatusSql . ' IN ("shipped", "delivered", "returned") THEN 1 ELSE 0 END) AS shipped_count
|
||||
FROM orders o
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code')->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable) {
|
||||
return [
|
||||
'all' => 0,
|
||||
@@ -279,7 +296,13 @@ final class OrdersRepository
|
||||
public function statusCounts(): array
|
||||
{
|
||||
try {
|
||||
$rows = $this->pdo->query('SELECT external_status_id, COUNT(*) AS cnt FROM orders GROUP BY external_status_id')->fetchAll(PDO::FETCH_ASSOC);
|
||||
$effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
|
||||
$rows = $this->pdo->query(
|
||||
'SELECT ' . $effectiveStatusSql . ' AS effective_status_id, COUNT(*) AS cnt
|
||||
FROM orders o
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code
|
||||
GROUP BY effective_status_id'
|
||||
)->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
@@ -290,7 +313,7 @@ final class OrdersRepository
|
||||
|
||||
$result = [];
|
||||
foreach ($rows as $row) {
|
||||
$key = trim((string) ($row['external_status_id'] ?? ''));
|
||||
$key = trim((string) ($row['effective_status_id'] ?? ''));
|
||||
if ($key === '') {
|
||||
$key = '_empty';
|
||||
}
|
||||
@@ -366,7 +389,14 @@ final class OrdersRepository
|
||||
}
|
||||
|
||||
try {
|
||||
$orderStmt = $this->pdo->prepare('SELECT * FROM orders WHERE id = :id LIMIT 1');
|
||||
$effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
|
||||
$orderStmt = $this->pdo->prepare(
|
||||
'SELECT o.*, ' . $effectiveStatusSql . ' AS effective_status_id
|
||||
FROM orders o
|
||||
LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code
|
||||
WHERE o.id = :id
|
||||
LIMIT 1'
|
||||
);
|
||||
$orderStmt->execute(['id' => $orderId]);
|
||||
$order = $orderStmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!is_array($order)) {
|
||||
@@ -380,12 +410,25 @@ final class OrdersRepository
|
||||
$addresses = [];
|
||||
}
|
||||
|
||||
$itemsStmt = $this->pdo->prepare('SELECT * FROM order_items WHERE order_id = :order_id ORDER BY sort_order ASC, id ASC');
|
||||
$itemsMediaSql = $this->resolvedMediaUrlSql('oi');
|
||||
$itemsStmt = $this->pdo->prepare('SELECT oi.*, ' . $itemsMediaSql . ' AS resolved_media_url
|
||||
FROM order_items oi
|
||||
WHERE oi.order_id = :order_id
|
||||
ORDER BY oi.sort_order ASC, oi.id ASC');
|
||||
$itemsStmt->execute(['order_id' => $orderId]);
|
||||
$items = $itemsStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (!is_array($items)) {
|
||||
$items = [];
|
||||
}
|
||||
$items = array_map(static function (array $row): array {
|
||||
$resolvedMediaUrl = trim((string) ($row['resolved_media_url'] ?? ''));
|
||||
if ($resolvedMediaUrl !== '') {
|
||||
$row['media_url'] = $resolvedMediaUrl;
|
||||
}
|
||||
unset($row['resolved_media_url']);
|
||||
|
||||
return $row;
|
||||
}, $items);
|
||||
|
||||
$paymentsStmt = $this->pdo->prepare('SELECT * FROM order_payments WHERE order_id = :order_id ORDER BY payment_date ASC, id ASC');
|
||||
$paymentsStmt->execute(['order_id' => $orderId]);
|
||||
@@ -457,10 +500,11 @@ final class OrdersRepository
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($cleanIds), '?'));
|
||||
try {
|
||||
$sql = 'SELECT order_id, original_name, quantity, COALESCE(media_url, "") AS media_url, sort_order, id
|
||||
FROM order_items
|
||||
WHERE order_id IN (' . $placeholders . ')
|
||||
ORDER BY order_id ASC, sort_order ASC, id ASC';
|
||||
$resolvedMediaSql = $this->resolvedMediaUrlSql('oi');
|
||||
$sql = 'SELECT oi.order_id, oi.original_name, oi.quantity, ' . $resolvedMediaSql . ' AS media_url, oi.sort_order, oi.id
|
||||
FROM order_items oi
|
||||
WHERE oi.order_id IN (' . $placeholders . ')
|
||||
ORDER BY oi.order_id ASC, oi.sort_order ASC, oi.id ASC';
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
foreach ($cleanIds as $index => $orderId) {
|
||||
$stmt->bindValue($index + 1, $orderId, PDO::PARAM_INT);
|
||||
@@ -496,6 +540,88 @@ final class OrdersRepository
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function effectiveStatusSql(string $orderAlias, string $mappingAlias): string
|
||||
{
|
||||
return 'CASE
|
||||
WHEN ' . $orderAlias . '.source = "allegro"
|
||||
AND ' . $mappingAlias . '.orderpro_status_code IS NOT NULL
|
||||
AND ' . $mappingAlias . '.orderpro_status_code <> ""
|
||||
THEN ' . $mappingAlias . '.orderpro_status_code
|
||||
ELSE ' . $orderAlias . '.external_status_id
|
||||
END';
|
||||
}
|
||||
|
||||
private function resolvedMediaUrlSql(string $itemAlias): string
|
||||
{
|
||||
if (!$this->canResolveMappedMedia()) {
|
||||
return 'COALESCE(NULLIF(TRIM(' . $itemAlias . '.media_url), ""), "")';
|
||||
}
|
||||
|
||||
return 'COALESCE(
|
||||
NULLIF(TRIM(' . $itemAlias . '.media_url), ""),
|
||||
(
|
||||
SELECT NULLIF(TRIM(pi.storage_path), "")
|
||||
FROM product_channel_map pcm
|
||||
INNER JOIN sales_channels sc ON sc.id = pcm.channel_id
|
||||
INNER JOIN product_images pi ON pi.product_id = pcm.product_id
|
||||
WHERE LOWER(sc.code) = "allegro"
|
||||
AND (
|
||||
pcm.external_product_id = ' . $itemAlias . '.external_item_id
|
||||
OR pcm.external_product_id = ' . $itemAlias . '.source_product_id
|
||||
)
|
||||
ORDER BY pi.is_main DESC, pi.sort_order ASC, pi.id ASC
|
||||
LIMIT 1
|
||||
),
|
||||
""
|
||||
)';
|
||||
}
|
||||
|
||||
private function canResolveMappedMedia(): bool
|
||||
{
|
||||
if ($this->supportsMappedMedia !== null) {
|
||||
return $this->supportsMappedMedia;
|
||||
}
|
||||
|
||||
try {
|
||||
$requiredColumns = [
|
||||
['table' => 'product_channel_map', 'column' => 'product_id'],
|
||||
['table' => 'product_channel_map', 'column' => 'channel_id'],
|
||||
['table' => 'product_channel_map', 'column' => 'external_product_id'],
|
||||
['table' => 'sales_channels', 'column' => 'id'],
|
||||
['table' => 'sales_channels', 'column' => 'code'],
|
||||
['table' => 'product_images', 'column' => 'id'],
|
||||
['table' => 'product_images', 'column' => 'product_id'],
|
||||
['table' => 'product_images', 'column' => 'storage_path'],
|
||||
['table' => 'product_images', 'column' => 'sort_order'],
|
||||
['table' => 'product_images', 'column' => 'is_main'],
|
||||
];
|
||||
|
||||
$pairsSql = [];
|
||||
$params = [];
|
||||
foreach ($requiredColumns as $index => $required) {
|
||||
$tableParam = ':table_' . $index;
|
||||
$columnParam = ':column_' . $index;
|
||||
$pairsSql[] = '(TABLE_NAME = ' . $tableParam . ' AND COLUMN_NAME = ' . $columnParam . ')';
|
||||
$params['table_' . $index] = $required['table'];
|
||||
$params['column_' . $index] = $required['column'];
|
||||
}
|
||||
|
||||
$sql = 'SELECT COUNT(*) AS cnt
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND (' . implode(' OR ', $pairsSql) . ')';
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$count = (int) $stmt->fetchColumn();
|
||||
|
||||
$this->supportsMappedMedia = ($count === count($requiredColumns));
|
||||
} catch (Throwable) {
|
||||
$this->supportsMappedMedia = false;
|
||||
}
|
||||
|
||||
return $this->supportsMappedMedia;
|
||||
}
|
||||
|
||||
private function normalizeColorHex(string $value): string
|
||||
{
|
||||
$trimmed = trim($value);
|
||||
|
||||
Reference in New Issue
Block a user