Files
shopPRO/autoload/Domain/Order/OrderAdminService.php
Jacek Pyziak 69e78ca248 ver. 0.294: Remove all 12 legacy autoload/shop/ classes (~2363 lines)
Complete Domain-Driven Architecture migration:
- Phase 1-4: Transport, ProductSet, Coupon, Shop, Search, Basket,
  ProductCustomField, Category, ProductAttribute, Promotion
- Phase 5: Order (~562 lines) + Product (~952 lines)
- ~20 Product methods migrated to ProductRepository
- Apilo sync migrated to OrderAdminService
- Production hotfixes: stale Redis cache (prices 0.00), unqualified
  Product:: refs in LayoutEngine, object->array template conversion
- AttributeRepository::getAttributeValueById() Redis cache added

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 02:05:39 +01:00

581 lines
21 KiB
PHP

<?php
namespace Domain\Order;
class OrderAdminService
{
private OrderRepository $orders;
public function __construct(OrderRepository $orders)
{
$this->orders = $orders;
}
public function details(int $orderId): array
{
return $this->orders->findForAdmin($orderId);
}
public function statuses(): array
{
return $this->orders->orderStatuses();
}
/**
* @return array{items: array<int, array<string, mixed>>, total: int}
*/
public function listForAdmin(
array $filters,
string $sortColumn = 'date_order',
string $sortDir = 'DESC',
int $page = 1,
int $perPage = 15
): array {
return $this->orders->listForAdmin($filters, $sortColumn, $sortDir, $page, $perPage);
}
public function nextOrderId(int $orderId): ?int
{
return $this->orders->nextOrderId($orderId);
}
public function prevOrderId(int $orderId): ?int
{
return $this->orders->prevOrderId($orderId);
}
public function saveNotes(int $orderId, string $notes): bool
{
return $this->orders->saveNotes($orderId, $notes);
}
public function saveOrderByAdmin(array $input): bool
{
$saved = $this->orders->saveOrderByAdmin(
(int)($input['order_id'] ?? 0),
(string)($input['client_name'] ?? ''),
(string)($input['client_surname'] ?? ''),
(string)($input['client_street'] ?? ''),
(string)($input['client_postal_code'] ?? ''),
(string)($input['client_city'] ?? ''),
(string)($input['client_email'] ?? ''),
(string)($input['firm_name'] ?? ''),
(string)($input['firm_street'] ?? ''),
(string)($input['firm_postal_code'] ?? ''),
(string)($input['firm_city'] ?? ''),
(string)($input['firm_nip'] ?? ''),
(int)($input['transport_id'] ?? 0),
(string)($input['inpost_paczkomat'] ?? ''),
(int)($input['payment_method_id'] ?? 0)
);
return $saved;
}
public function changeStatus(int $orderId, int $status, bool $sendEmail): array
{
$order = $this->orders->findRawById($orderId);
if (!$order || (int)$order['status'] === $status) {
return ['result' => false];
}
$db = $this->orders->getDb();
if ($this->orders->updateOrderStatus($orderId, $status))
{
$this->orders->insertStatusHistory($orderId, $status, $sendEmail ? 1 : 0);
$response = ['result' => true];
if ($sendEmail)
{
$order['status'] = $status;
$response['email'] = $this->sendStatusChangeEmail($order);
}
// Apilo status sync
$this->syncApiloStatusIfNeeded($order, $status);
return $response;
}
return ['result' => false];
}
public function resendConfirmationEmail(int $orderId): bool
{
global $settings;
$db = $this->orders->getDb();
$order = $this->orders->orderDetailsFrontend($orderId);
if (!$order || !$order['id']) {
return false;
}
$coupon = (int)$order['coupon_id'] ? (new \Domain\Coupon\CouponRepository($db))->find((int)$order['coupon_id']) : null;
$mail_order = \Shared\Tpl\Tpl::view('shop-order/mail-summary', [
'settings' => $settings,
'order' => $order,
'coupon' => $coupon,
]);
$settings['ssl'] ? $base = 'https' : $base = 'http';
$regex = "-(<img[^>]+src\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i";
$mail_order = preg_replace($regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $mail_order);
$regex = "-(<a[^>]+href\s*=\s*['\"])(((?!'|\"|https?://).)*)(['\"][^>]*>)-i";
$mail_order = preg_replace($regex, "$1" . $base . "://" . $_SERVER['SERVER_NAME'] . "$2$4", $mail_order);
\Shared\Helpers\Helpers::send_email($order['client_email'], \Shared\Helpers\Helpers::lang('potwierdzenie-zamowienia-ze-sklepu') . ' ' . $settings['firm_name'], $mail_order);
\Shared\Helpers\Helpers::send_email($settings['contact_email'], 'Nowe zamówienie / ' . $settings['firm_name'] . ' / ' . $order['number'] . ' - ' . $order['client_surname'] . ' ' . $order['client_name'], $mail_order);
return true;
}
public function setOrderAsUnpaid(int $orderId): bool
{
$this->orders->setAsUnpaid($orderId);
return true;
}
public function setOrderAsPaid(int $orderId, bool $sendMail): bool
{
$order = $this->orders->findRawById($orderId);
if (!$order) {
return false;
}
// Apilo payment sync
$this->syncApiloPaymentIfNeeded($order);
// Mark as paid
$this->orders->setAsPaid($orderId);
// Set status to 1 (opłacone) without email
$this->changeStatus($orderId, 1, false);
// Set status to 4 (przyjęte do realizacji) with email
$this->changeStatus($orderId, 4, $sendMail);
return true;
}
public function sendOrderToApilo(int $orderId): bool
{
global $mdb;
if ($orderId <= 0) {
return false;
}
$order = $this->orders->findForAdmin($orderId);
if (empty($order) || empty($order['apilo_order_id'])) {
return false;
}
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb );
$accessToken = $integrationsRepository -> apiloGetAccessToken();
if (!$accessToken) {
return false;
}
$newStatus = 8;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://projectpro.apilo.com/rest/api/orders/' . $order['apilo_order_id'] . '/status/');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'id' => (int)$order['apilo_order_id'],
'status' => (int)( new \Domain\ShopStatus\ShopStatusRepository($mdb) )->getApiloStatusId( (int)$newStatus ),
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $accessToken,
'Accept: application/json',
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$apiloResultRaw = curl_exec($ch);
$apiloResult = json_decode((string)$apiloResultRaw, true);
if (!is_array($apiloResult) || (int)($apiloResult['updates'] ?? 0) !== 1) {
curl_close($ch);
return false;
}
$query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'pp_shop_orders' AND COLUMN_NAME != 'id'";
$columns = $mdb->query($query)->fetchAll(\PDO::FETCH_COLUMN);
$columnsList = implode(', ', $columns);
$mdb->query('INSERT INTO pp_shop_orders (' . $columnsList . ') SELECT ' . $columnsList . ' FROM pp_shop_orders pso WHERE pso.id = ' . $orderId);
$newOrderId = (int)$mdb->id();
$query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'pp_shop_order_products' AND COLUMN_NAME != 'id' AND COLUMN_NAME != 'order_id'";
$columns = $mdb->query($query)->fetchAll(\PDO::FETCH_COLUMN);
$columnsList = implode(', ', $columns);
$mdb->query('INSERT INTO pp_shop_order_products (order_id, ' . $columnsList . ') SELECT ' . $newOrderId . ', ' . $columnsList . ' FROM pp_shop_order_products psop WHERE psop.order_id = ' . $orderId);
$query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'pp_shop_order_statuses' AND COLUMN_NAME != 'id' AND COLUMN_NAME != 'order_id'";
$columns = $mdb->query($query)->fetchAll(\PDO::FETCH_COLUMN);
$columnsList = implode(', ', $columns);
$mdb->query('INSERT INTO pp_shop_order_statuses (order_id, ' . $columnsList . ') SELECT ' . $newOrderId . ', ' . $columnsList . ' FROM pp_shop_order_statuses psos WHERE psos.order_id = ' . $orderId);
$mdb->delete('pp_shop_orders', ['id' => $orderId]);
$mdb->delete('pp_shop_order_products', ['order_id' => $orderId]);
$mdb->delete('pp_shop_order_statuses', ['order_id' => $orderId]);
$mdb->update('pp_shop_orders', ['apilo_order_id' => null], ['id' => $newOrderId]);
curl_close($ch);
return true;
}
public function toggleTrustmateSend(int $orderId): array
{
$newValue = $this->orders->toggleTrustmateSend($orderId);
if ($newValue === null) {
return [
'result' => false,
];
}
return [
'result' => true,
'trustmate_send' => $newValue,
];
}
public function deleteOrder(int $orderId): bool
{
return $this->orders->deleteOrder($orderId);
}
// =========================================================================
// Apilo sync queue (migrated from \shop\Order)
// =========================================================================
private const APILO_SYNC_QUEUE_FILE = '/temp/apilo-sync-queue.json';
public function processApiloSyncQueue(int $limit = 10): int
{
$queue = self::loadApiloSyncQueue();
if (!\Shared\Helpers\Helpers::is_array_fix($queue)) {
return 0;
}
$processed = 0;
foreach ($queue as $key => $task)
{
if ($processed >= $limit) {
break;
}
$order_id = (int)($task['order_id'] ?? 0);
if ($order_id <= 0) {
unset($queue[$key]);
continue;
}
$order = $this->orders->findRawById($order_id);
if (!$order) {
unset($queue[$key]);
continue;
}
$error = '';
$sync_failed = false;
$payment_pending = !empty($task['payment']) && (int)$order['paid'] === 1;
if ($payment_pending && (int)$order['apilo_order_id']) {
if (!$this->syncApiloPayment($order)) {
$sync_failed = true;
$error = 'payment_sync_failed';
}
}
$status_pending = isset($task['status']) && $task['status'] !== null && $task['status'] !== '';
if (!$sync_failed && $status_pending && (int)$order['apilo_order_id']) {
if (!$this->syncApiloStatus($order, (int)$task['status'])) {
$sync_failed = true;
$error = 'status_sync_failed';
}
}
if ($sync_failed) {
$task['attempts'] = (int)($task['attempts'] ?? 0) + 1;
$task['last_error'] = $error;
$task['updated_at'] = date('Y-m-d H:i:s');
$queue[$key] = $task;
} else {
unset($queue[$key]);
}
$processed++;
}
self::saveApiloSyncQueue($queue);
return $processed;
}
// =========================================================================
// Private: email
// =========================================================================
private function sendStatusChangeEmail(array $order): bool
{
if (!$order['client_email']) {
return false;
}
$db = $this->orders->getDb();
$order_statuses = $this->orders->orderStatuses();
$firm_name = (new \Domain\Settings\SettingsRepository($db))->getSingleValue('firm_name');
$status = (int)$order['status'];
$number = $order['number'];
$subjects = [
0 => $firm_name . ' - zamówienie [NUMER] zostało złożone',
1 => $firm_name . ' - zamówienie [NUMER] zostało opłacone',
2 => $firm_name . ' - płatność za zamówienie [NUMER] została odrzucona',
3 => $firm_name . ' - płatność za zamówienie [NUMER] jest sprawdzania ręcznie',
4 => $firm_name . ' - zamówienie [NUMER] zostało przyjęte do realizacji',
5 => $firm_name . ' - zamówienie [NUMER] zostało wysłane',
6 => $firm_name . ' - zamówienie [NUMER] zostało zrealizowane',
7 => $firm_name . ' - zamówienie [NUMER] zostało przygotowane go wysłania',
8 => $firm_name . ' - zamówienie [NUMER] zostało anulowane',
];
$subject = isset($subjects[$status]) ? str_replace('[NUMER]', $number, $subjects[$status]) : '';
if (!$subject) {
return false;
}
$email = new \Email(0);
$email->load_by_name('#sklep-zmiana-statusu-zamowienia');
$email->text = str_replace('[NUMER_ZAMOWIENIA]', $number, $email->text);
$email->text = str_replace('[DATA_ZAMOWIENIA]', date('Y/m/d', strtotime($order['date_order'])), $email->text);
$email->text = str_replace('[STATUS]', isset($order_statuses[$status]) ? $order_statuses[$status] : '', $email->text);
return $email->send($order['client_email'], $subject, true);
}
// =========================================================================
// Private: Apilo sync
// =========================================================================
private function syncApiloPaymentIfNeeded(array $order): void
{
global $config;
$db = $this->orders->getDb();
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository($db);
$apilo_settings = $integrationsRepository->getSettings('apilo');
if (!$apilo_settings['enabled'] || !$apilo_settings['access-token'] || !$apilo_settings['sync_orders']) {
return;
}
if (isset($config['debug']['apilo']) && $config['debug']['apilo']) {
self::appendApiloLog("SET AS PAID\n" . print_r($order, true));
}
if ($order['apilo_order_id'] && !$this->syncApiloPayment($order)) {
self::queueApiloSync((int)$order['id'], true, null, 'payment_sync_failed');
}
}
private function syncApiloStatusIfNeeded(array $order, int $status): void
{
global $config;
$db = $this->orders->getDb();
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository($db);
$apilo_settings = $integrationsRepository->getSettings('apilo');
if (!$apilo_settings['enabled'] || !$apilo_settings['access-token'] || !$apilo_settings['sync_orders']) {
return;
}
if (isset($config['debug']['apilo']) && $config['debug']['apilo']) {
self::appendApiloLog("UPDATE STATUS\n" . print_r($order, true));
}
if ($order['apilo_order_id'] && !$this->syncApiloStatus($order, $status)) {
self::queueApiloSync((int)$order['id'], false, $status, 'status_sync_failed');
}
}
private function syncApiloPayment(array $order): bool
{
global $config;
$db = $this->orders->getDb();
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository($db);
if (!(int)$order['apilo_order_id']) {
return true;
}
$payment_type = (int)(new \Domain\PaymentMethod\PaymentMethodRepository($db))->getApiloPaymentTypeId((int)$order['payment_method_id']);
if ($payment_type <= 0) {
$payment_type = 1;
}
$payment_date = new \DateTime($order['date_order']);
$access_token = $integrationsRepository->apiloGetAccessToken();
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://projectpro.apilo.com/rest/api/orders/" . $order['apilo_order_id'] . '/payment/');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'amount' => str_replace(',', '.', $order['summary']),
'paymentDate' => $payment_date->format('Y-m-d\TH:i:s\Z'),
'type' => $payment_type,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $access_token,
"Accept: application/json",
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$apilo_response = curl_exec($ch);
$http_code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_errno($ch) ? curl_error($ch) : '';
curl_close($ch);
if (isset($config['debug']['apilo']) && $config['debug']['apilo']) {
self::appendApiloLog("PAYMENT RESPONSE\nHTTP: " . $http_code . "\nCURL: " . $curl_error . "\n" . print_r($apilo_response, true));
}
if ($curl_error !== '') return false;
if ($http_code < 200 || $http_code >= 300) return false;
return true;
}
private function syncApiloStatus(array $order, int $status): bool
{
global $config;
$db = $this->orders->getDb();
$integrationsRepository = new \Domain\Integrations\IntegrationsRepository($db);
if (!(int)$order['apilo_order_id']) {
return true;
}
$access_token = $integrationsRepository->apiloGetAccessToken();
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://projectpro.apilo.com/rest/api/orders/" . $order['apilo_order_id'] . '/status/');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'id' => $order['apilo_order_id'],
'status' => (int)(new \Domain\ShopStatus\ShopStatusRepository($db))->getApiloStatusId($status),
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $access_token,
"Accept: application/json",
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$apilo_result = curl_exec($ch);
$http_code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_errno($ch) ? curl_error($ch) : '';
curl_close($ch);
if (isset($config['debug']['apilo']) && $config['debug']['apilo']) {
self::appendApiloLog("STATUS RESPONSE\nHTTP: " . $http_code . "\nCURL: " . $curl_error . "\n" . print_r($apilo_result, true));
}
if ($curl_error !== '') return false;
if ($http_code < 200 || $http_code >= 300) return false;
return true;
}
// =========================================================================
// Private: Apilo sync queue file helpers
// =========================================================================
private static function queueApiloSync(int $order_id, bool $payment, ?int $status, string $error): void
{
if ($order_id <= 0) return;
$queue = self::loadApiloSyncQueue();
$key = (string)$order_id;
$row = is_array($queue[$key] ?? null) ? $queue[$key] : [];
$row['order_id'] = $order_id;
$row['payment'] = !empty($row['payment']) || $payment ? 1 : 0;
if ($status !== null) {
$row['status'] = $status;
}
$row['attempts'] = (int)($row['attempts'] ?? 0) + 1;
$row['last_error'] = $error;
$row['updated_at'] = date('Y-m-d H:i:s');
$queue[$key] = $row;
self::saveApiloSyncQueue($queue);
}
private static function apiloSyncQueuePath(): string
{
return dirname(__DIR__, 2) . self::APILO_SYNC_QUEUE_FILE;
}
private static function loadApiloSyncQueue(): array
{
$path = self::apiloSyncQueuePath();
if (!file_exists($path)) return [];
$content = file_get_contents($path);
if (!$content) return [];
$decoded = json_decode($content, true);
if (!is_array($decoded)) return [];
return $decoded;
}
private static function saveApiloSyncQueue(array $queue): void
{
$path = self::apiloSyncQueuePath();
$dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
file_put_contents($path, json_encode($queue, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), LOCK_EX);
}
private static function appendApiloLog(string $message): void
{
$base = isset($_SERVER['DOCUMENT_ROOT']) && $_SERVER['DOCUMENT_ROOT']
? rtrim($_SERVER['DOCUMENT_ROOT'], '/\\')
: dirname(__DIR__, 2);
$dir = $base . '/logs';
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
file_put_contents(
$dir . '/apilo.txt',
date('Y-m-d H:i:s') . ' --- ' . $message . "\n\n",
FILE_APPEND
);
}
}