update
This commit is contained in:
29
src/Core/Http/RedirectPathResolver.php
Normal file
29
src/Core/Http/RedirectPathResolver.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core\Http;
|
||||
|
||||
final class RedirectPathResolver
|
||||
{
|
||||
/**
|
||||
* @param string $requestedPath Raw path from user input
|
||||
* @param array<int, string> $allowedPaths Prefixes that the path must start with
|
||||
* @param string $default Fallback path when validation fails
|
||||
*/
|
||||
public static function resolve(string $requestedPath, array $allowedPaths, string $default): string
|
||||
{
|
||||
$value = trim($requestedPath);
|
||||
if ($value === '') {
|
||||
return $default;
|
||||
}
|
||||
|
||||
foreach ($allowedPaths as $prefix) {
|
||||
if (str_starts_with($value, $prefix)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
30
src/Core/Http/SslCertificateResolver.php
Normal file
30
src/Core/Http/SslCertificateResolver.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core\Http;
|
||||
|
||||
final class SslCertificateResolver
|
||||
{
|
||||
public static function resolve(): ?string
|
||||
{
|
||||
$envPath = (string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? '');
|
||||
if ($envPath !== '' && is_file($envPath)) {
|
||||
return $envPath;
|
||||
}
|
||||
$iniPath = (string) ini_get('curl.cainfo');
|
||||
if ($iniPath !== '' && is_file($iniPath)) {
|
||||
return $iniPath;
|
||||
}
|
||||
$candidates = [
|
||||
'C:/xampp/apache/bin/curl-ca-bundle.crt',
|
||||
'C:/xampp/php/extras/ssl/cacert.pem',
|
||||
'/etc/ssl/certs/ca-certificates.crt',
|
||||
];
|
||||
foreach ($candidates as $path) {
|
||||
if (is_file($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
16
src/Core/Http/ToggleableRepositoryTrait.php
Normal file
16
src/Core/Http/ToggleableRepositoryTrait.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core\Http;
|
||||
|
||||
trait ToggleableRepositoryTrait
|
||||
{
|
||||
public function toggleActive(string $table, int $id, string $column = 'is_active'): bool
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE ' . $table . ' SET ' . $column . ' = NOT ' . $column . ' WHERE id = :id'
|
||||
);
|
||||
|
||||
return $statement->execute(['id' => $id]);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,8 @@ final class ReceiptController
|
||||
private readonly ReceiptConfigRepository $receiptConfigs,
|
||||
private readonly CompanySettingsRepository $companySettings,
|
||||
private readonly OrdersRepository $orders,
|
||||
private readonly AutomationService $automation
|
||||
private readonly AutomationService $automation,
|
||||
private readonly ReceiptService $receiptService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -48,14 +49,8 @@ final class ReceiptController
|
||||
|
||||
$order = is_array($details['order'] ?? null) ? $details['order'] : [];
|
||||
$items = is_array($details['items'] ?? null) ? $details['items'] : [];
|
||||
$seller = $this->companySettings->getSettings();
|
||||
|
||||
$totalGross = 0.0;
|
||||
foreach ($items as $item) {
|
||||
$qty = (float) ($item['quantity'] ?? 0);
|
||||
$price = $item['original_price_with_tax'] !== null ? (float) $item['original_price_with_tax'] : 0.0;
|
||||
$totalGross += $qty * $price;
|
||||
}
|
||||
$totalGross = $this->receiptService->calculateTotalGross($items);
|
||||
|
||||
$html = $this->template->render('orders/receipt-create', [
|
||||
'title' => $this->translator->get('receipts.create.title'),
|
||||
@@ -67,7 +62,7 @@ final class ReceiptController
|
||||
'order' => $order,
|
||||
'items' => $items,
|
||||
'configs' => array_values($configs),
|
||||
'seller' => $seller,
|
||||
'seller' => $this->companySettings->getSettings(),
|
||||
'totalGross' => $totalGross,
|
||||
'existingReceipts' => $existingReceipts,
|
||||
], 'layouts/app');
|
||||
@@ -90,98 +85,13 @@ final class ReceiptController
|
||||
return Response::redirect('/orders/' . $orderId . '/receipt/create');
|
||||
}
|
||||
|
||||
$config = $this->receiptConfigs->findById($configId);
|
||||
if ($config === null || (int) ($config['is_active'] ?? 0) !== 1) {
|
||||
Flash::set('order.error', $this->translator->get('receipts.create.invalid_config'));
|
||||
return Response::redirect('/orders/' . $orderId . '/receipt/create');
|
||||
}
|
||||
|
||||
$details = $this->orders->findDetails($orderId);
|
||||
if ($details === null) {
|
||||
return Response::html('Not found', 404);
|
||||
}
|
||||
|
||||
$order = is_array($details['order'] ?? null) ? $details['order'] : [];
|
||||
$items = is_array($details['items'] ?? null) ? $details['items'] : [];
|
||||
$addresses = is_array($details['addresses'] ?? null) ? $details['addresses'] : [];
|
||||
$payments = is_array($details['payments'] ?? null) ? $details['payments'] : [];
|
||||
|
||||
$seller = $this->companySettings->getSettings();
|
||||
$sellerSnapshot = [
|
||||
'company_name' => $seller['company_name'] ?? '',
|
||||
'tax_number' => $seller['tax_number'] ?? '',
|
||||
'street' => $seller['street'] ?? '',
|
||||
'city' => $seller['city'] ?? '',
|
||||
'postal_code' => $seller['postal_code'] ?? '',
|
||||
'phone' => $seller['phone'] ?? '',
|
||||
'email' => $seller['email'] ?? '',
|
||||
'bank_account' => $seller['bank_account'] ?? '',
|
||||
'bdo_number' => $seller['bdo_number'] ?? '',
|
||||
'regon' => $seller['regon'] ?? '',
|
||||
'court_register' => $seller['court_register'] ?? '',
|
||||
];
|
||||
|
||||
$buyerAddress = $this->resolveBuyerAddress($addresses);
|
||||
$buyerSnapshot = $buyerAddress !== null ? [
|
||||
'name' => $buyerAddress['name'] ?? '',
|
||||
'company_name' => $buyerAddress['company_name'] ?? '',
|
||||
'tax_number' => $buyerAddress['company_tax_number'] ?? '',
|
||||
'street' => trim(($buyerAddress['street_name'] ?? '') . ' ' . ($buyerAddress['street_number'] ?? '')),
|
||||
'city' => $buyerAddress['city'] ?? '',
|
||||
'postal_code' => $buyerAddress['zip_code'] ?? '',
|
||||
'phone' => $buyerAddress['phone'] ?? '',
|
||||
'email' => $buyerAddress['email'] ?? '',
|
||||
] : null;
|
||||
|
||||
$itemsSnapshot = [];
|
||||
$totalGross = 0.0;
|
||||
foreach ($items as $item) {
|
||||
$qty = (float) ($item['quantity'] ?? 0);
|
||||
$price = $item['original_price_with_tax'] !== null ? (float) $item['original_price_with_tax'] : 0.0;
|
||||
$lineTotal = $qty * $price;
|
||||
$totalGross += $lineTotal;
|
||||
$itemsSnapshot[] = [
|
||||
'name' => $item['original_name'] ?? '',
|
||||
'quantity' => $qty,
|
||||
'price' => $price,
|
||||
'total' => $lineTotal,
|
||||
'sku' => $item['sku'] ?? '',
|
||||
'ean' => $item['ean'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
$issueDate = trim((string) $request->input('issue_date', ''));
|
||||
if ($issueDate !== '' && strtotime($issueDate) !== false) {
|
||||
$issueDate = date('Y-m-d H:i:s', strtotime($issueDate));
|
||||
} else {
|
||||
$issueDate = date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$saleDate = $this->resolveSaleDate($config, $order, $payments, $issueDate);
|
||||
|
||||
$orderReference = $this->resolveOrderReference($config, $order);
|
||||
$user = $this->auth->user();
|
||||
|
||||
try {
|
||||
$receiptNumber = $this->receipts->getNextNumber(
|
||||
$configId,
|
||||
(string) ($config['number_format'] ?? 'PAR/%N/%M/%Y'),
|
||||
(string) ($config['numbering_type'] ?? 'monthly')
|
||||
);
|
||||
|
||||
$user = $this->auth->user();
|
||||
|
||||
$this->receipts->create([
|
||||
$result = $this->receiptService->issue([
|
||||
'order_id' => $orderId,
|
||||
'config_id' => $configId,
|
||||
'receipt_number' => $receiptNumber,
|
||||
'issue_date' => $issueDate,
|
||||
'sale_date' => $saleDate,
|
||||
'seller_data_json' => json_encode($sellerSnapshot, JSON_UNESCAPED_UNICODE),
|
||||
'buyer_data_json' => $buyerSnapshot !== null ? json_encode($buyerSnapshot, JSON_UNESCAPED_UNICODE) : null,
|
||||
'items_json' => json_encode($itemsSnapshot, JSON_UNESCAPED_UNICODE),
|
||||
'total_net' => number_format($totalGross, 2, '.', ''),
|
||||
'total_gross' => number_format($totalGross, 2, '.', ''),
|
||||
'order_reference_value' => $orderReference,
|
||||
'issue_date_override' => (string) $request->input('issue_date', ''),
|
||||
'created_by' => is_array($user) ? ($user['id'] ?? null) : null,
|
||||
]);
|
||||
|
||||
@@ -189,19 +99,20 @@ final class ReceiptController
|
||||
$this->orders->recordActivity(
|
||||
$orderId,
|
||||
'receipt_issued',
|
||||
'Wystawiono paragon: ' . $receiptNumber,
|
||||
['receipt_number' => $receiptNumber, 'config_id' => $configId, 'total_gross' => number_format($totalGross, 2, '.', '')],
|
||||
'Wystawiono paragon: ' . $result['receipt_number'],
|
||||
['receipt_number' => $result['receipt_number'], 'config_id' => $configId, 'total_gross' => $result['total_gross']],
|
||||
'user',
|
||||
$userName !== '' ? $userName : null
|
||||
);
|
||||
|
||||
Flash::set('order.success', 'Paragon wystawiony: ' . $receiptNumber);
|
||||
Flash::set('order.success', 'Paragon wystawiony: ' . $result['receipt_number']);
|
||||
|
||||
try {
|
||||
$this->automation->trigger('receipt.created', $orderId);
|
||||
} catch (Throwable) {
|
||||
// Blad automatyzacji nie blokuje sukcesu paragonu
|
||||
}
|
||||
} catch (ReceiptIssueException $e) {
|
||||
Flash::set('order.error', $e->getMessage());
|
||||
} catch (Throwable) {
|
||||
Flash::set('order.error', 'Blad wystawiania paragonu');
|
||||
}
|
||||
@@ -209,71 +120,6 @@ final class ReceiptController
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $addresses
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function resolveBuyerAddress(array $addresses): ?array
|
||||
{
|
||||
$byType = [];
|
||||
foreach ($addresses as $addr) {
|
||||
$type = (string) ($addr['address_type'] ?? '');
|
||||
if ($type !== '' && !isset($byType[$type])) {
|
||||
$byType[$type] = $addr;
|
||||
}
|
||||
}
|
||||
|
||||
return $byType['invoice'] ?? $byType['customer'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
* @param array<string, mixed> $order
|
||||
* @param list<array<string, mixed>> $payments
|
||||
*/
|
||||
private function resolveSaleDate(array $config, array $order, array $payments, string $issueDate): string
|
||||
{
|
||||
$source = (string) ($config['sale_date_source'] ?? 'issue_date');
|
||||
|
||||
if ($source === 'order_date') {
|
||||
$ordered = (string) ($order['ordered_at'] ?? '');
|
||||
if ($ordered !== '') {
|
||||
$ts = strtotime($ordered);
|
||||
return $ts !== false ? date('Y-m-d', $ts) : $issueDate;
|
||||
}
|
||||
}
|
||||
|
||||
if ($source === 'payment_date' && $payments !== []) {
|
||||
$lastPayment = $payments[0] ?? [];
|
||||
$payDate = (string) ($lastPayment['payment_date'] ?? '');
|
||||
if ($payDate !== '') {
|
||||
$ts = strtotime($payDate);
|
||||
return $ts !== false ? date('Y-m-d', $ts) : $issueDate;
|
||||
}
|
||||
}
|
||||
|
||||
return $issueDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
* @param array<string, mixed> $order
|
||||
*/
|
||||
private function resolveOrderReference(array $config, array $order): ?string
|
||||
{
|
||||
$ref = (string) ($config['order_reference'] ?? 'none');
|
||||
|
||||
if ($ref === 'orderpro') {
|
||||
return (string) ($order['internal_order_number'] ?? '');
|
||||
}
|
||||
|
||||
if ($ref === 'integration') {
|
||||
return (string) ($order['external_order_id'] ?? '');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function show(Request $request): Response
|
||||
{
|
||||
$orderId = max(0, (int) $request->input('id', 0));
|
||||
|
||||
10
src/Modules/Accounting/ReceiptIssueException.php
Normal file
10
src/Modules/Accounting/ReceiptIssueException.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Accounting;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class ReceiptIssueException extends RuntimeException
|
||||
{
|
||||
}
|
||||
275
src/Modules/Accounting/ReceiptService.php
Normal file
275
src/Modules/Accounting/ReceiptService.php
Normal file
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Accounting;
|
||||
|
||||
use App\Modules\Orders\OrdersRepository;
|
||||
use App\Modules\Settings\CompanySettingsRepository;
|
||||
use App\Modules\Settings\ReceiptConfigRepository;
|
||||
use Throwable;
|
||||
|
||||
final class ReceiptService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ReceiptRepository $receipts,
|
||||
private readonly ReceiptConfigRepository $receiptConfigs,
|
||||
private readonly CompanySettingsRepository $companySettings,
|
||||
private readonly OrdersRepository $orders
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* order_id: int,
|
||||
* config_id: int,
|
||||
* issue_date_mode?: string,
|
||||
* issue_date_override?: string,
|
||||
* created_by?: int|null,
|
||||
* } $params
|
||||
* @return array{receipt_number: string, total_gross: string}
|
||||
* @throws ReceiptIssueException
|
||||
*/
|
||||
public function issue(array $params): array
|
||||
{
|
||||
$orderId = (int) $params['order_id'];
|
||||
$configId = (int) $params['config_id'];
|
||||
|
||||
$config = $this->receiptConfigs->findById($configId);
|
||||
if ($config === null || (int) ($config['is_active'] ?? 0) !== 1) {
|
||||
throw new ReceiptIssueException('Nieprawidlowa lub nieaktywna konfiguracja paragonu');
|
||||
}
|
||||
|
||||
$details = $this->orders->findDetails($orderId);
|
||||
if ($details === null) {
|
||||
throw new ReceiptIssueException('Zamowienie nie istnieje');
|
||||
}
|
||||
|
||||
$order = is_array($details['order'] ?? null) ? $details['order'] : [];
|
||||
$items = is_array($details['items'] ?? null) ? $details['items'] : [];
|
||||
$addresses = is_array($details['addresses'] ?? null) ? $details['addresses'] : [];
|
||||
$payments = is_array($details['payments'] ?? null) ? $details['payments'] : [];
|
||||
|
||||
$issueDateOverride = trim((string) ($params['issue_date_override'] ?? ''));
|
||||
if ($issueDateOverride !== '' && strtotime($issueDateOverride) !== false) {
|
||||
$issueDate = date('Y-m-d H:i:s', strtotime($issueDateOverride));
|
||||
} else {
|
||||
$issueDateMode = (string) ($params['issue_date_mode'] ?? 'today');
|
||||
$issueDate = $this->resolveIssueDate($issueDateMode, $order, $payments);
|
||||
}
|
||||
|
||||
$saleDate = $this->resolveSaleDate($config, $order, $payments, $issueDate);
|
||||
$orderReference = $this->resolveOrderReference($config, $order);
|
||||
$sellerSnapshot = $this->buildSellerSnapshot();
|
||||
$buyerSnapshot = $this->buildBuyerSnapshot($addresses);
|
||||
['items' => $itemsSnapshot, 'total_gross' => $totalGross] = $this->buildItemsSnapshot($items);
|
||||
|
||||
$receiptNumber = $this->receipts->getNextNumber(
|
||||
$configId,
|
||||
(string) ($config['number_format'] ?? 'PAR/%N/%M/%Y'),
|
||||
(string) ($config['numbering_type'] ?? 'monthly')
|
||||
);
|
||||
|
||||
$this->receipts->create([
|
||||
'order_id' => $orderId,
|
||||
'config_id' => $configId,
|
||||
'receipt_number' => $receiptNumber,
|
||||
'issue_date' => $issueDate,
|
||||
'sale_date' => $saleDate,
|
||||
'seller_data_json' => json_encode($sellerSnapshot, JSON_UNESCAPED_UNICODE),
|
||||
'buyer_data_json' => $buyerSnapshot !== null ? json_encode($buyerSnapshot, JSON_UNESCAPED_UNICODE) : null,
|
||||
'items_json' => json_encode($itemsSnapshot, JSON_UNESCAPED_UNICODE),
|
||||
'total_net' => number_format($totalGross, 2, '.', ''),
|
||||
'total_gross' => number_format($totalGross, 2, '.', ''),
|
||||
'order_reference_value' => $orderReference,
|
||||
'created_by' => $params['created_by'] ?? null,
|
||||
]);
|
||||
|
||||
return [
|
||||
'receipt_number' => $receiptNumber,
|
||||
'total_gross' => number_format($totalGross, 2, '.', ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $items
|
||||
*/
|
||||
public function calculateTotalGross(array $items): float
|
||||
{
|
||||
$total = 0.0;
|
||||
foreach ($items as $item) {
|
||||
$qty = (float) ($item['quantity'] ?? 0);
|
||||
$price = $item['original_price_with_tax'] !== null ? (float) $item['original_price_with_tax'] : 0.0;
|
||||
$total += $qty * $price;
|
||||
}
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $order
|
||||
* @param list<array<string, mixed>> $payments
|
||||
*/
|
||||
private function resolveIssueDate(string $mode, array $order, array $payments): string
|
||||
{
|
||||
if ($mode === 'order_date') {
|
||||
$orderedAt = trim((string) ($order['ordered_at'] ?? ''));
|
||||
if ($orderedAt !== '') {
|
||||
$timestamp = strtotime($orderedAt);
|
||||
if ($timestamp !== false) {
|
||||
return date('Y-m-d H:i:s', $timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($mode === 'payment_date') {
|
||||
$firstPayment = $payments[0] ?? [];
|
||||
$paymentDate = trim((string) ($firstPayment['payment_date'] ?? ''));
|
||||
if ($paymentDate !== '') {
|
||||
$timestamp = strtotime($paymentDate);
|
||||
if ($timestamp !== false) {
|
||||
return date('Y-m-d H:i:s', $timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
* @param array<string, mixed> $order
|
||||
* @param list<array<string, mixed>> $payments
|
||||
*/
|
||||
private function resolveSaleDate(array $config, array $order, array $payments, string $issueDate): string
|
||||
{
|
||||
$source = (string) ($config['sale_date_source'] ?? 'issue_date');
|
||||
|
||||
if ($source === 'order_date') {
|
||||
$ordered = (string) ($order['ordered_at'] ?? '');
|
||||
if ($ordered !== '') {
|
||||
$ts = strtotime($ordered);
|
||||
return $ts !== false ? date('Y-m-d H:i:s', $ts) : $issueDate;
|
||||
}
|
||||
}
|
||||
|
||||
if ($source === 'payment_date' && $payments !== []) {
|
||||
$lastPayment = $payments[0] ?? [];
|
||||
$payDate = (string) ($lastPayment['payment_date'] ?? '');
|
||||
if ($payDate !== '') {
|
||||
$ts = strtotime($payDate);
|
||||
return $ts !== false ? date('Y-m-d H:i:s', $ts) : $issueDate;
|
||||
}
|
||||
}
|
||||
|
||||
return $issueDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
* @param array<string, mixed> $order
|
||||
*/
|
||||
private function resolveOrderReference(array $config, array $order): ?string
|
||||
{
|
||||
$ref = (string) ($config['order_reference'] ?? 'none');
|
||||
|
||||
if ($ref === 'orderpro') {
|
||||
return (string) ($order['internal_order_number'] ?? '');
|
||||
}
|
||||
|
||||
if ($ref === 'integration') {
|
||||
return (string) ($order['external_order_id'] ?? '');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildSellerSnapshot(): array
|
||||
{
|
||||
$seller = $this->companySettings->getSettings();
|
||||
|
||||
return [
|
||||
'company_name' => $seller['company_name'] ?? '',
|
||||
'tax_number' => $seller['tax_number'] ?? '',
|
||||
'street' => $seller['street'] ?? '',
|
||||
'city' => $seller['city'] ?? '',
|
||||
'postal_code' => $seller['postal_code'] ?? '',
|
||||
'phone' => $seller['phone'] ?? '',
|
||||
'email' => $seller['email'] ?? '',
|
||||
'bank_account' => $seller['bank_account'] ?? '',
|
||||
'bdo_number' => $seller['bdo_number'] ?? '',
|
||||
'regon' => $seller['regon'] ?? '',
|
||||
'court_register' => $seller['court_register'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $addresses
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function buildBuyerSnapshot(array $addresses): ?array
|
||||
{
|
||||
$buyerAddress = $this->resolveBuyerAddress($addresses);
|
||||
if ($buyerAddress === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $buyerAddress['name'] ?? '',
|
||||
'company_name' => $buyerAddress['company_name'] ?? '',
|
||||
'tax_number' => $buyerAddress['company_tax_number'] ?? '',
|
||||
'street' => trim(($buyerAddress['street_name'] ?? '') . ' ' . ($buyerAddress['street_number'] ?? '')),
|
||||
'city' => $buyerAddress['city'] ?? '',
|
||||
'postal_code' => $buyerAddress['zip_code'] ?? '',
|
||||
'phone' => $buyerAddress['phone'] ?? '',
|
||||
'email' => $buyerAddress['email'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $addresses
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function resolveBuyerAddress(array $addresses): ?array
|
||||
{
|
||||
$byType = [];
|
||||
foreach ($addresses as $addr) {
|
||||
$type = (string) ($addr['address_type'] ?? '');
|
||||
if ($type !== '' && !isset($byType[$type])) {
|
||||
$byType[$type] = $addr;
|
||||
}
|
||||
}
|
||||
|
||||
return $byType['invoice'] ?? $byType['customer'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $items
|
||||
* @return array{items: list<array<string, mixed>>, total_gross: float}
|
||||
*/
|
||||
private function buildItemsSnapshot(array $items): array
|
||||
{
|
||||
$itemsSnapshot = [];
|
||||
$totalGross = 0.0;
|
||||
foreach ($items as $item) {
|
||||
$qty = (float) ($item['quantity'] ?? 0);
|
||||
$price = $item['original_price_with_tax'] !== null ? (float) $item['original_price_with_tax'] : 0.0;
|
||||
$lineTotal = $qty * $price;
|
||||
$totalGross += $lineTotal;
|
||||
$itemsSnapshot[] = [
|
||||
'name' => $item['original_name'] ?? '',
|
||||
'quantity' => $qty,
|
||||
'price' => $price,
|
||||
'total' => $lineTotal,
|
||||
'sku' => $item['sku'] ?? '',
|
||||
'ean' => $item['ean'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $itemsSnapshot,
|
||||
'total_gross' => $totalGross,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Automation;
|
||||
|
||||
use App\Core\Http\ToggleableRepositoryTrait;
|
||||
use PDO;
|
||||
use Throwable;
|
||||
|
||||
final class AutomationRepository
|
||||
{
|
||||
use ToggleableRepositoryTrait {
|
||||
toggleActive as private traitToggleActive;
|
||||
}
|
||||
public function __construct(
|
||||
private readonly PDO $pdo
|
||||
) {
|
||||
@@ -150,10 +154,7 @@ final class AutomationRepository
|
||||
|
||||
public function toggleActive(int $id): void
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE automation_rules SET is_active = NOT is_active WHERE id = :id'
|
||||
);
|
||||
$statement->execute(['id' => $id]);
|
||||
$this->traitToggleActive('automation_rules', $id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Automation;
|
||||
|
||||
use App\Modules\Accounting\ReceiptIssueException;
|
||||
use App\Modules\Accounting\ReceiptRepository;
|
||||
use App\Modules\Accounting\ReceiptService;
|
||||
use App\Modules\Email\EmailSendingService;
|
||||
use App\Modules\Orders\OrdersRepository;
|
||||
use App\Modules\Settings\CompanySettingsRepository;
|
||||
@@ -36,7 +38,8 @@ final class AutomationService
|
||||
private readonly CompanySettingsRepository $companySettings,
|
||||
private readonly ReceiptRepository $receipts,
|
||||
private readonly ReceiptConfigRepository $receiptConfigs,
|
||||
private readonly ShipmentPackageRepository $shipmentPackages
|
||||
private readonly ShipmentPackageRepository $shipmentPackages,
|
||||
private readonly ReceiptService $receiptService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -347,22 +350,8 @@ final class AutomationService
|
||||
if ($configId <= 0) {
|
||||
return;
|
||||
}
|
||||
$issueDateMode = (string) ($config['issue_date_mode'] ?? 'today');
|
||||
$duplicatePolicy = (string) ($config['duplicate_policy'] ?? 'skip_if_exists');
|
||||
|
||||
$receiptConfig = $this->receiptConfigs->findById($configId);
|
||||
if ($receiptConfig === null || (int) ($receiptConfig['is_active'] ?? 0) !== 1) {
|
||||
$this->orders->recordActivity(
|
||||
$orderId,
|
||||
'automation_receipt_failed',
|
||||
$actorName . ' - nieprawidlowa lub nieaktywna konfiguracja paragonu',
|
||||
['receipt_config_id' => $configId],
|
||||
'system',
|
||||
$actorName
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$existingReceipts = $this->receipts->findByOrderId($orderId);
|
||||
if ($duplicatePolicy === 'skip_if_exists' && $existingReceipts !== []) {
|
||||
$this->orders->recordActivity(
|
||||
@@ -376,55 +365,23 @@ final class AutomationService
|
||||
return;
|
||||
}
|
||||
|
||||
$details = $this->orders->findDetails($orderId);
|
||||
if ($details === null) {
|
||||
return;
|
||||
}
|
||||
$order = is_array($details['order'] ?? null) ? $details['order'] : [];
|
||||
$items = is_array($details['items'] ?? null) ? $details['items'] : [];
|
||||
$addresses = is_array($details['addresses'] ?? null) ? $details['addresses'] : [];
|
||||
$payments = is_array($details['payments'] ?? null) ? $details['payments'] : [];
|
||||
|
||||
$issueDate = $this->resolveIssueDate($issueDateMode, $order, $payments);
|
||||
$saleDate = $this->resolveSaleDate($receiptConfig, $order, $payments, $issueDate);
|
||||
$orderReference = $this->resolveOrderReference($receiptConfig, $order);
|
||||
$sellerSnapshot = $this->buildSellerSnapshot();
|
||||
$buyerSnapshot = $this->buildBuyerSnapshot($addresses);
|
||||
['items' => $itemsSnapshot, 'total_gross' => $totalGross] = $this->buildItemsSnapshot($items);
|
||||
|
||||
try {
|
||||
$receiptNumber = $this->receipts->getNextNumber(
|
||||
$configId,
|
||||
(string) ($receiptConfig['number_format'] ?? 'PAR/%N/%M/%Y'),
|
||||
(string) ($receiptConfig['numbering_type'] ?? 'monthly')
|
||||
);
|
||||
$this->receipts->create([
|
||||
$result = $this->receiptService->issue([
|
||||
'order_id' => $orderId,
|
||||
'config_id' => $configId,
|
||||
'receipt_number' => $receiptNumber,
|
||||
'issue_date' => $issueDate,
|
||||
'sale_date' => $saleDate,
|
||||
'seller_data_json' => json_encode($sellerSnapshot, JSON_UNESCAPED_UNICODE),
|
||||
'buyer_data_json' => $buyerSnapshot !== null ? json_encode($buyerSnapshot, JSON_UNESCAPED_UNICODE) : null,
|
||||
'items_json' => json_encode($itemsSnapshot, JSON_UNESCAPED_UNICODE),
|
||||
'total_net' => number_format($totalGross, 2, '.', ''),
|
||||
'total_gross' => number_format($totalGross, 2, '.', ''),
|
||||
'order_reference_value' => $orderReference,
|
||||
'issue_date_mode' => (string) ($config['issue_date_mode'] ?? 'today'),
|
||||
'created_by' => null,
|
||||
]);
|
||||
|
||||
$this->orders->recordActivity(
|
||||
$orderId,
|
||||
'receipt_issued',
|
||||
'Wystawiono paragon: ' . $receiptNumber,
|
||||
['receipt_number' => $receiptNumber, 'config_id' => $configId, 'total_gross' => number_format($totalGross, 2, '.', '')],
|
||||
'Wystawiono paragon: ' . $result['receipt_number'],
|
||||
['receipt_number' => $result['receipt_number'], 'config_id' => $configId, 'total_gross' => $result['total_gross']],
|
||||
'system',
|
||||
$actorName
|
||||
);
|
||||
|
||||
// Chain automation: issuing receipt from one rule should trigger
|
||||
// rules listening on receipt.created. Uses generic chain context
|
||||
// with depth + rule deduplication to prevent loops.
|
||||
$this->emitEvent(
|
||||
'receipt.created',
|
||||
$orderId,
|
||||
@@ -432,10 +389,19 @@ final class AutomationService
|
||||
[
|
||||
'automation_source' => 'issue_receipt',
|
||||
'automation_rule' => $ruleName,
|
||||
'receipt_number' => $receiptNumber,
|
||||
'receipt_number' => $result['receipt_number'],
|
||||
'receipt_config_id' => $configId,
|
||||
]
|
||||
);
|
||||
} catch (ReceiptIssueException $e) {
|
||||
$this->orders->recordActivity(
|
||||
$orderId,
|
||||
'automation_receipt_failed',
|
||||
$actorName . ' - ' . $e->getMessage(),
|
||||
['receipt_config_id' => $configId],
|
||||
'system',
|
||||
$actorName
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
$this->orders->recordActivity(
|
||||
$orderId,
|
||||
@@ -593,175 +559,6 @@ final class AutomationService
|
||||
return $this->shipmentPackages->findLatestByOrderId($orderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $order
|
||||
* @param list<array<string, mixed>> $payments
|
||||
*/
|
||||
private function resolveIssueDate(string $mode, array $order, array $payments): string
|
||||
{
|
||||
if ($mode === 'order_date') {
|
||||
$orderedAt = trim((string) ($order['ordered_at'] ?? ''));
|
||||
if ($orderedAt !== '') {
|
||||
$timestamp = strtotime($orderedAt);
|
||||
if ($timestamp !== false) {
|
||||
return date('Y-m-d', $timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($mode === 'payment_date') {
|
||||
$firstPayment = $payments[0] ?? [];
|
||||
$paymentDate = trim((string) ($firstPayment['payment_date'] ?? ''));
|
||||
if ($paymentDate !== '') {
|
||||
$timestamp = strtotime($paymentDate);
|
||||
if ($timestamp !== false) {
|
||||
return date('Y-m-d', $timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return date('Y-m-d');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $receiptConfig
|
||||
* @param array<string, mixed> $order
|
||||
* @param list<array<string, mixed>> $payments
|
||||
*/
|
||||
private function resolveSaleDate(array $receiptConfig, array $order, array $payments, string $issueDate): string
|
||||
{
|
||||
$source = (string) ($receiptConfig['sale_date_source'] ?? 'issue_date');
|
||||
|
||||
if ($source === 'order_date') {
|
||||
$ordered = (string) ($order['ordered_at'] ?? '');
|
||||
if ($ordered !== '') {
|
||||
$ts = strtotime($ordered);
|
||||
return $ts !== false ? date('Y-m-d', $ts) : $issueDate;
|
||||
}
|
||||
}
|
||||
|
||||
if ($source === 'payment_date' && $payments !== []) {
|
||||
$lastPayment = $payments[0] ?? [];
|
||||
$payDate = (string) ($lastPayment['payment_date'] ?? '');
|
||||
if ($payDate !== '') {
|
||||
$ts = strtotime($payDate);
|
||||
return $ts !== false ? date('Y-m-d', $ts) : $issueDate;
|
||||
}
|
||||
}
|
||||
|
||||
return $issueDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $receiptConfig
|
||||
* @param array<string, mixed> $order
|
||||
*/
|
||||
private function resolveOrderReference(array $receiptConfig, array $order): ?string
|
||||
{
|
||||
$ref = (string) ($receiptConfig['order_reference'] ?? 'none');
|
||||
|
||||
if ($ref === 'orderpro') {
|
||||
return (string) ($order['internal_order_number'] ?? '');
|
||||
}
|
||||
|
||||
if ($ref === 'integration') {
|
||||
return (string) ($order['external_order_id'] ?? '');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildSellerSnapshot(): array
|
||||
{
|
||||
$seller = $this->companySettings->getSettings();
|
||||
|
||||
return [
|
||||
'company_name' => $seller['company_name'] ?? '',
|
||||
'tax_number' => $seller['tax_number'] ?? '',
|
||||
'street' => $seller['street'] ?? '',
|
||||
'city' => $seller['city'] ?? '',
|
||||
'postal_code' => $seller['postal_code'] ?? '',
|
||||
'phone' => $seller['phone'] ?? '',
|
||||
'email' => $seller['email'] ?? '',
|
||||
'bank_account' => $seller['bank_account'] ?? '',
|
||||
'bdo_number' => $seller['bdo_number'] ?? '',
|
||||
'regon' => $seller['regon'] ?? '',
|
||||
'court_register' => $seller['court_register'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $addresses
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function buildBuyerSnapshot(array $addresses): ?array
|
||||
{
|
||||
$buyerAddress = $this->resolveBuyerAddress($addresses);
|
||||
if ($buyerAddress === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $buyerAddress['name'] ?? '',
|
||||
'company_name' => $buyerAddress['company_name'] ?? '',
|
||||
'tax_number' => $buyerAddress['company_tax_number'] ?? '',
|
||||
'street' => trim((string) (($buyerAddress['street_name'] ?? '') . ' ' . ($buyerAddress['street_number'] ?? ''))),
|
||||
'city' => $buyerAddress['city'] ?? '',
|
||||
'postal_code' => $buyerAddress['zip_code'] ?? '',
|
||||
'phone' => $buyerAddress['phone'] ?? '',
|
||||
'email' => $buyerAddress['email'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $addresses
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function resolveBuyerAddress(array $addresses): ?array
|
||||
{
|
||||
$byType = [];
|
||||
foreach ($addresses as $address) {
|
||||
$type = (string) ($address['address_type'] ?? '');
|
||||
if ($type !== '' && !isset($byType[$type])) {
|
||||
$byType[$type] = $address;
|
||||
}
|
||||
}
|
||||
|
||||
return $byType['invoice'] ?? $byType['customer'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array<string, mixed>> $items
|
||||
* @return array{items:list<array<string,mixed>>,total_gross:float}
|
||||
*/
|
||||
private function buildItemsSnapshot(array $items): array
|
||||
{
|
||||
$itemsSnapshot = [];
|
||||
$totalGross = 0.0;
|
||||
foreach ($items as $item) {
|
||||
$qty = (float) ($item['quantity'] ?? 0);
|
||||
$price = $item['original_price_with_tax'] !== null ? (float) $item['original_price_with_tax'] : 0.0;
|
||||
$lineTotal = $qty * $price;
|
||||
$totalGross += $lineTotal;
|
||||
$itemsSnapshot[] = [
|
||||
'name' => $item['original_name'] ?? '',
|
||||
'quantity' => $qty,
|
||||
'price' => $price,
|
||||
'total' => $lineTotal,
|
||||
'sku' => $item['sku'] ?? '',
|
||||
'ean' => $item['ean'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $itemsSnapshot,
|
||||
'total_gross' => $totalGross,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
* @return array{chain_id:string,depth:int,executions:list<string>}
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Core\I18n\Translator;
|
||||
use App\Core\Support\Logger;
|
||||
use App\Core\View\Template;
|
||||
use App\Modules\Accounting\ReceiptRepository;
|
||||
use App\Modules\Accounting\ReceiptService;
|
||||
use App\Modules\Automation\AutomationRepository;
|
||||
use App\Modules\Automation\AutomationService;
|
||||
use App\Modules\Automation\AutomationExecutionLogRepository;
|
||||
@@ -207,15 +208,26 @@ final class CronHandlerFactory
|
||||
)
|
||||
);
|
||||
|
||||
$receiptRepository = new ReceiptRepository($this->db);
|
||||
$receiptConfigRepository = new ReceiptConfigRepository($this->db);
|
||||
|
||||
$receiptService = new ReceiptService(
|
||||
$receiptRepository,
|
||||
$receiptConfigRepository,
|
||||
$companySettingsRepository,
|
||||
$ordersRepository
|
||||
);
|
||||
|
||||
return new AutomationService(
|
||||
$automationRepository,
|
||||
$executionLogRepository,
|
||||
$emailService,
|
||||
$ordersRepository,
|
||||
$companySettingsRepository,
|
||||
new ReceiptRepository($this->db),
|
||||
new ReceiptConfigRepository($this->db),
|
||||
new ShipmentPackageRepository($this->db)
|
||||
$receiptRepository,
|
||||
$receiptConfigRepository,
|
||||
new ShipmentPackageRepository($this->db),
|
||||
$receiptService
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ use Throwable;
|
||||
|
||||
final class ShipmentTrackingHandler
|
||||
{
|
||||
private const ALLEGRO_EDGE_RATE_LIMIT_SECONDS = 60;
|
||||
|
||||
public function __construct(
|
||||
private readonly ShipmentTrackingRegistry $registry,
|
||||
private readonly ShipmentPackageRepository $repository,
|
||||
@@ -28,6 +30,8 @@ final class ShipmentTrackingHandler
|
||||
$updated = 0;
|
||||
$errors = 0;
|
||||
|
||||
$lastAllegroEdgeRequestTime = 0.0;
|
||||
|
||||
foreach ($packages as $package) {
|
||||
$provider = trim((string) ($package['provider'] ?? ''));
|
||||
$packageId = (int) ($package['id'] ?? 0);
|
||||
@@ -38,6 +42,20 @@ final class ShipmentTrackingHandler
|
||||
}
|
||||
|
||||
try {
|
||||
if ($provider === 'allegro_wza') {
|
||||
$carrierId = strtolower(trim((string) ($package['carrier_id'] ?? '')));
|
||||
$isInpost = str_contains($carrierId, 'inpost') || str_contains($carrierId, 'paczkomat');
|
||||
|
||||
if (!$isInpost) {
|
||||
$elapsed = microtime(true) - $lastAllegroEdgeRequestTime;
|
||||
if ($elapsed < self::ALLEGRO_EDGE_RATE_LIMIT_SECONDS) {
|
||||
$sleepTime = (int) ceil(self::ALLEGRO_EDGE_RATE_LIMIT_SECONDS - $elapsed);
|
||||
sleep($sleepTime);
|
||||
}
|
||||
$lastAllegroEdgeRequestTime = microtime(true);
|
||||
}
|
||||
}
|
||||
|
||||
$result = $service->getDeliveryStatus($package);
|
||||
if ($result !== null) {
|
||||
$previousStatus = trim((string) ($package['delivery_status'] ?? 'unknown'));
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Exceptions\AllegroApiException;
|
||||
use App\Core\Http\SslCertificateResolver;
|
||||
|
||||
final class AllegroApiClient
|
||||
{
|
||||
@@ -171,29 +172,6 @@ final class AllegroApiClient
|
||||
]);
|
||||
}
|
||||
|
||||
private function getCaBundlePath(): ?string
|
||||
{
|
||||
$envPath = (string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? '');
|
||||
if ($envPath !== '' && is_file($envPath)) {
|
||||
return $envPath;
|
||||
}
|
||||
$iniPath = (string) ini_get('curl.cainfo');
|
||||
if ($iniPath !== '' && is_file($iniPath)) {
|
||||
return $iniPath;
|
||||
}
|
||||
$candidates = [
|
||||
'C:/xampp/apache/bin/curl-ca-bundle.crt',
|
||||
'C:/xampp/php/extras/ssl/cacert.pem',
|
||||
'/etc/ssl/certs/ca-certificates.crt',
|
||||
];
|
||||
foreach ($candidates as $path) {
|
||||
if (is_file($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $opts
|
||||
* @return array<int, mixed>
|
||||
@@ -202,7 +180,7 @@ final class AllegroApiClient
|
||||
{
|
||||
$opts[CURLOPT_SSL_VERIFYPEER] = true;
|
||||
$opts[CURLOPT_SSL_VERIFYHOST] = 2;
|
||||
$caPath = $this->getCaBundlePath();
|
||||
$caPath = SslCertificateResolver::resolve();
|
||||
if ($caPath !== null) {
|
||||
$opts[CURLOPT_CAINFO] = $caPath;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use DateTimeImmutable;
|
||||
use App\Core\Constants\IntegrationSources;
|
||||
use App\Core\Constants\RedirectPaths;
|
||||
use App\Core\Exceptions\IntegrationConfigException;
|
||||
use App\Core\Http\RedirectPathResolver;
|
||||
use Throwable;
|
||||
|
||||
final class AllegroIntegrationController
|
||||
@@ -115,7 +116,7 @@ final class AllegroIntegrationController
|
||||
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$redirectTo = $this->resolveRedirectPath((string) $request->input('return_to', RedirectPaths::ALLEGRO_INTEGRATION));
|
||||
$redirectTo = RedirectPathResolver::resolve((string) $request->input('return_to', RedirectPaths::ALLEGRO_INTEGRATION), ['/settings/integrations'], RedirectPaths::ALLEGRO_INTEGRATION);
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect($redirectTo);
|
||||
@@ -383,19 +384,6 @@ final class AllegroIntegrationController
|
||||
return Response::redirect(RedirectPaths::ALLEGRO_INTEGRATION);
|
||||
}
|
||||
|
||||
private function resolveRedirectPath(string $candidate): string
|
||||
{
|
||||
$value = trim($candidate);
|
||||
if ($value === '') {
|
||||
return RedirectPaths::ALLEGRO_INTEGRATION;
|
||||
}
|
||||
if (!str_starts_with($value, '/settings/integrations')) {
|
||||
return RedirectPaths::ALLEGRO_INTEGRATION;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Exceptions\AllegroOAuthException;
|
||||
use App\Core\Http\SslCertificateResolver;
|
||||
|
||||
final class AllegroOAuthClient
|
||||
{
|
||||
@@ -127,29 +128,6 @@ final class AllegroOAuthClient
|
||||
return trim(strtolower($environment)) === 'production' ? 'production' : 'sandbox';
|
||||
}
|
||||
|
||||
private function getCaBundlePath(): ?string
|
||||
{
|
||||
$envPath = (string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? '');
|
||||
if ($envPath !== '' && is_file($envPath)) {
|
||||
return $envPath;
|
||||
}
|
||||
$iniPath = (string) ini_get('curl.cainfo');
|
||||
if ($iniPath !== '' && is_file($iniPath)) {
|
||||
return $iniPath;
|
||||
}
|
||||
$candidates = [
|
||||
'C:/xampp/apache/bin/curl-ca-bundle.crt',
|
||||
'C:/xampp/php/extras/ssl/cacert.pem',
|
||||
'/etc/ssl/certs/ca-certificates.crt',
|
||||
];
|
||||
foreach ($candidates as $path) {
|
||||
if (is_file($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $formData
|
||||
* @return array<string, mixed>
|
||||
@@ -179,7 +157,7 @@ final class AllegroOAuthClient
|
||||
],
|
||||
CURLOPT_POSTFIELDS => http_build_query($formData),
|
||||
];
|
||||
$caPath = $this->getCaBundlePath();
|
||||
$caPath = SslCertificateResolver::resolve();
|
||||
if ($caPath !== null) {
|
||||
$sslOpts[CURLOPT_CAINFO] = $caPath;
|
||||
}
|
||||
|
||||
@@ -4,34 +4,12 @@ declare(strict_types=1);
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Exceptions\ApaczkaApiException;
|
||||
use App\Core\Http\SslCertificateResolver;
|
||||
|
||||
final class ApaczkaApiClient
|
||||
{
|
||||
private const API_BASE_URL = 'https://www.apaczka.pl/api/v2';
|
||||
|
||||
private function getCaBundlePath(): ?string
|
||||
{
|
||||
$envPath = (string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? '');
|
||||
if ($envPath !== '' && is_file($envPath)) {
|
||||
return $envPath;
|
||||
}
|
||||
$iniPath = (string) ini_get('curl.cainfo');
|
||||
if ($iniPath !== '' && is_file($iniPath)) {
|
||||
return $iniPath;
|
||||
}
|
||||
$candidates = [
|
||||
'C:/xampp/apache/bin/curl-ca-bundle.crt',
|
||||
'C:/xampp/php/extras/ssl/cacert.pem',
|
||||
'/etc/ssl/certs/ca-certificates.crt',
|
||||
];
|
||||
foreach ($candidates as $path) {
|
||||
if (is_file($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
@@ -217,7 +195,7 @@ final class ApaczkaApiClient
|
||||
'User-Agent: orderPRO/1.0',
|
||||
],
|
||||
];
|
||||
$caPath = $this->getCaBundlePath();
|
||||
$caPath = SslCertificateResolver::resolve();
|
||||
if ($caPath !== null) {
|
||||
$sslOpts[CURLOPT_CAINFO] = $caPath;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Core\I18n\Translator;
|
||||
use App\Core\Security\Csrf;
|
||||
use App\Core\Support\Flash;
|
||||
use App\Core\View\Template;
|
||||
use App\Core\Http\RedirectPathResolver;
|
||||
use App\Modules\Auth\AuthService;
|
||||
use Throwable;
|
||||
|
||||
@@ -43,7 +44,7 @@ final class ApaczkaIntegrationController
|
||||
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$redirectTo = $this->resolveRedirectPath((string) $request->input('return_to', '/settings/integrations/apaczka'));
|
||||
$redirectTo = RedirectPathResolver::resolve((string) $request->input('return_to', '/settings/integrations/apaczka'), ['/settings/integrations'], '/settings/integrations/apaczka');
|
||||
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
@@ -74,7 +75,7 @@ final class ApaczkaIntegrationController
|
||||
|
||||
public function test(Request $request): Response
|
||||
{
|
||||
$redirectTo = $this->resolveRedirectPath((string) $request->input('return_to', '/settings/integrations/apaczka'));
|
||||
$redirectTo = RedirectPathResolver::resolve((string) $request->input('return_to', '/settings/integrations/apaczka'), ['/settings/integrations'], '/settings/integrations/apaczka');
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect($redirectTo);
|
||||
@@ -95,16 +96,4 @@ final class ApaczkaIntegrationController
|
||||
return Response::redirect($redirectTo);
|
||||
}
|
||||
|
||||
private function resolveRedirectPath(string $candidate): string
|
||||
{
|
||||
$value = trim($candidate);
|
||||
if ($value === '') {
|
||||
return '/settings/integrations/apaczka';
|
||||
}
|
||||
if (!str_starts_with($value, '/settings/integrations')) {
|
||||
return '/settings/integrations/apaczka';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Http\ToggleableRepositoryTrait;
|
||||
use PDO;
|
||||
|
||||
final class EmailMailboxRepository
|
||||
{
|
||||
use ToggleableRepositoryTrait;
|
||||
public function __construct(
|
||||
private readonly PDO $pdo,
|
||||
private readonly IntegrationSecretCipher $cipher
|
||||
@@ -151,9 +153,6 @@ final class EmailMailboxRepository
|
||||
|
||||
public function toggleStatus(int $id): void
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE email_mailboxes SET is_active = NOT is_active WHERE id = :id'
|
||||
);
|
||||
$statement->execute(['id' => $id]);
|
||||
$this->toggleActive('email_mailboxes', $id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Http\ToggleableRepositoryTrait;
|
||||
use PDO;
|
||||
|
||||
final class EmailTemplateRepository
|
||||
{
|
||||
use ToggleableRepositoryTrait;
|
||||
public function __construct(
|
||||
private readonly PDO $pdo
|
||||
) {
|
||||
@@ -114,10 +116,7 @@ final class EmailTemplateRepository
|
||||
|
||||
public function toggleStatus(int $id): void
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE email_templates SET is_active = NOT is_active WHERE id = :id'
|
||||
);
|
||||
$statement->execute(['id' => $id]);
|
||||
$this->toggleActive('email_templates', $id);
|
||||
}
|
||||
|
||||
public function duplicate(int $id): void
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Core\I18n\Translator;
|
||||
use App\Core\Security\Csrf;
|
||||
use App\Core\Support\Flash;
|
||||
use App\Core\View\Template;
|
||||
use App\Core\Http\RedirectPathResolver;
|
||||
use App\Modules\Auth\AuthService;
|
||||
use Throwable;
|
||||
|
||||
@@ -42,7 +43,7 @@ final class InpostIntegrationController
|
||||
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$redirectTo = $this->resolveRedirectPath((string) $request->input('return_to', '/settings/integrations/inpost'));
|
||||
$redirectTo = RedirectPathResolver::resolve((string) $request->input('return_to', '/settings/integrations/inpost'), ['/settings/integrations'], '/settings/integrations/inpost');
|
||||
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
@@ -77,16 +78,4 @@ final class InpostIntegrationController
|
||||
return Response::redirect($redirectTo);
|
||||
}
|
||||
|
||||
private function resolveRedirectPath(string $candidate): string
|
||||
{
|
||||
$value = trim($candidate);
|
||||
if ($value === '') {
|
||||
return '/settings/integrations/inpost';
|
||||
}
|
||||
if (!str_starts_with($value, '/settings/integrations')) {
|
||||
return '/settings/integrations/inpost';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Http\ToggleableRepositoryTrait;
|
||||
use PDO;
|
||||
use Throwable;
|
||||
|
||||
final class ReceiptConfigRepository
|
||||
{
|
||||
use ToggleableRepositoryTrait;
|
||||
public function __construct(
|
||||
private readonly PDO $pdo
|
||||
) {
|
||||
@@ -85,10 +87,7 @@ final class ReceiptConfigRepository
|
||||
|
||||
public function toggleStatus(int $id): void
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE receipt_configs SET is_active = NOT is_active WHERE id = :id'
|
||||
);
|
||||
$statement->execute(['id' => $id]);
|
||||
$this->toggleActive('receipt_configs', $id);
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
|
||||
@@ -3,31 +3,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Http\SslCertificateResolver;
|
||||
|
||||
final class ShopproApiClient
|
||||
{
|
||||
private function getCaBundlePath(): ?string
|
||||
{
|
||||
$envPath = (string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? '');
|
||||
if ($envPath !== '' && is_file($envPath)) {
|
||||
return $envPath;
|
||||
}
|
||||
$iniPath = (string) ini_get('curl.cainfo');
|
||||
if ($iniPath !== '' && is_file($iniPath)) {
|
||||
return $iniPath;
|
||||
}
|
||||
$candidates = [
|
||||
'C:/xampp/apache/bin/curl-ca-bundle.crt',
|
||||
'C:/xampp/php/extras/ssl/cacert.pem',
|
||||
'/etc/ssl/certs/ca-certificates.crt',
|
||||
];
|
||||
foreach ($candidates as $path) {
|
||||
if (is_file($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok:bool,http_code:int|null,message:string,items:array<int,array<string,mixed>>,total:int,page:int,per_page:int}
|
||||
*/
|
||||
@@ -337,7 +316,7 @@ final class ShopproApiClient
|
||||
|
||||
$opts[CURLOPT_HTTPHEADER] = $headers;
|
||||
|
||||
$caPath = $this->getCaBundlePath();
|
||||
$caPath = SslCertificateResolver::resolve();
|
||||
if ($caPath !== null) {
|
||||
$opts[CURLOPT_CAINFO] = $caPath;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Shipments;
|
||||
|
||||
use App\Core\Http\SslCertificateResolver;
|
||||
use App\Modules\Settings\InpostIntegrationRepository;
|
||||
use Throwable;
|
||||
|
||||
@@ -34,8 +35,7 @@ final class AllegroTrackingService implements ShipmentTrackingInterface
|
||||
return $this->fetchInpostStatus($trackingNumber);
|
||||
}
|
||||
|
||||
// Allegro Delivery (One Kurier), DHL, DPD via Allegro — brak publicznego API trackingu
|
||||
return null;
|
||||
return $this->fetchAllegroEdgeStatus($trackingNumber);
|
||||
}
|
||||
|
||||
private function fetchInpostStatus(string $trackingNumber): ?array
|
||||
@@ -75,6 +75,88 @@ final class AllegroTrackingService implements ShipmentTrackingInterface
|
||||
}
|
||||
}
|
||||
|
||||
private function fetchAllegroEdgeStatus(string $trackingNumber): ?array
|
||||
{
|
||||
try {
|
||||
$url = 'https://edge.allegro.pl/ad/tracking?packageNo=' . rawurlencode($trackingNumber);
|
||||
$response = $this->edgeApiRequest($url);
|
||||
|
||||
$statuses = $response['status'] ?? [];
|
||||
if (!is_array($statuses) || $statuses === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$latest = end($statuses);
|
||||
$description = trim((string) ($latest['description'] ?? ''));
|
||||
if ($description === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$slug = DeliveryStatus::slugifyAllegroDescription($description);
|
||||
|
||||
$normalized = DeliveryStatus::normalize('allegro_edge', $slug);
|
||||
|
||||
if ($normalized === DeliveryStatus::UNKNOWN) {
|
||||
$normalized = DeliveryStatus::guessStatusFromDescription($description);
|
||||
|
||||
error_log(sprintf(
|
||||
'[AllegroTracking] Niezmapowany status: "%s" (slug: %s, guessed: %s)',
|
||||
$description,
|
||||
$slug,
|
||||
$normalized
|
||||
));
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => $normalized,
|
||||
'status_raw' => $description,
|
||||
'description' => $description,
|
||||
];
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function edgeApiRequest(string $url): array
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
if ($ch === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$opts = [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Accept: application/vnd.allegro.internal.v1+json',
|
||||
'Content-Type: application/vnd.allegro.internal.v1+json',
|
||||
],
|
||||
];
|
||||
|
||||
$caPath = SslCertificateResolver::resolve();
|
||||
if ($caPath !== null) {
|
||||
$opts[CURLOPT_CAINFO] = $caPath;
|
||||
}
|
||||
|
||||
curl_setopt_array($ch, $opts);
|
||||
$body = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$ch = null;
|
||||
|
||||
if ($body === false || $httpCode < 200 || $httpCode >= 300) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$json = json_decode((string) $body, true);
|
||||
return is_array($json) ? $json : [];
|
||||
}
|
||||
|
||||
private function resolveInpostToken(): ?string
|
||||
{
|
||||
try {
|
||||
@@ -107,7 +189,7 @@ final class AllegroTrackingService implements ShipmentTrackingInterface
|
||||
],
|
||||
];
|
||||
|
||||
$caPath = $this->getCaBundlePath();
|
||||
$caPath = SslCertificateResolver::resolve();
|
||||
if ($caPath !== null) {
|
||||
$opts[CURLOPT_CAINFO] = $caPath;
|
||||
}
|
||||
@@ -125,20 +207,4 @@ final class AllegroTrackingService implements ShipmentTrackingInterface
|
||||
return is_array($json) ? $json : [];
|
||||
}
|
||||
|
||||
private function getCaBundlePath(): ?string
|
||||
{
|
||||
$candidates = [
|
||||
(string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? ''),
|
||||
(string) ini_get('curl.cainfo'),
|
||||
'C:/xampp/apache/bin/curl-ca-bundle.crt',
|
||||
'C:/xampp/php/extras/ssl/cacert.pem',
|
||||
'/etc/ssl/certs/ca-certificates.crt',
|
||||
];
|
||||
foreach ($candidates as $path) {
|
||||
if ($path !== '' && is_file($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +191,65 @@ final class DeliveryStatus
|
||||
'RETURNED' => 'Zwrócona do nadawcy',
|
||||
];
|
||||
|
||||
private const ALLEGRO_EDGE_MAP = [
|
||||
// Realne slugi z edge API (po slugify opisów)
|
||||
'przygotowana_przez_nadawce' => self::CREATED,
|
||||
'nadana' => self::CONFIRMED,
|
||||
'podjeta_z_maszyny_przez_kuriera' => self::IN_TRANSIT,
|
||||
'podjeta_z_punktu_przez_kuriera' => self::IN_TRANSIT,
|
||||
'podjeta_z_punktu' => self::IN_TRANSIT,
|
||||
'odebrana_przez_kuriera' => self::IN_TRANSIT,
|
||||
'przekazal_przesylke_do_magazynu' => self::IN_TRANSIT,
|
||||
'przekazana_do_magazynu' => self::IN_TRANSIT,
|
||||
'przesylka_wyjechala_w_droge_do_punktu_docelowego' => self::IN_TRANSIT,
|
||||
'w_sortowni' => self::IN_TRANSIT,
|
||||
'wyjechala_w_droge_do_punktu_docelowego' => self::IN_TRANSIT,
|
||||
'wyslana_z_sortowni' => self::IN_TRANSIT,
|
||||
'w_doreczeniu' => self::OUT_FOR_DELIVERY,
|
||||
'wydana_do_doreczenia' => self::OUT_FOR_DELIVERY,
|
||||
'dostarczana' => self::OUT_FOR_DELIVERY,
|
||||
'gotowa_do_odbioru' => self::READY_FOR_PICKUP,
|
||||
'oczekuje_na_odbior' => self::READY_FOR_PICKUP,
|
||||
'przesylka_oczekuje_na_odbior' => self::READY_FOR_PICKUP,
|
||||
'dostarczona' => self::DELIVERED,
|
||||
'doreczona' => self::DELIVERED,
|
||||
'odebrana' => self::DELIVERED,
|
||||
'zwrocona' => self::RETURNED,
|
||||
'zwrocona_do_nadawcy' => self::RETURNED,
|
||||
'anulowana' => self::CANCELLED,
|
||||
'odmowa_przyjecia' => self::PROBLEM,
|
||||
'uszkodzona' => self::PROBLEM,
|
||||
'zagubiona' => self::PROBLEM,
|
||||
];
|
||||
|
||||
private const ALLEGRO_EDGE_DESCRIPTIONS = [
|
||||
'przygotowana_przez_nadawce' => 'Przesyłka przygotowana przez nadawcę',
|
||||
'nadana' => 'Przesyłka nadana',
|
||||
'podjeta_z_maszyny_przez_kuriera' => 'Podjęta z maszyny przez kuriera',
|
||||
'podjeta_z_punktu_przez_kuriera' => 'Podjęta z punktu przez kuriera',
|
||||
'odebrana_przez_kuriera' => 'Odebrana przez kuriera',
|
||||
'przekazana_do_magazynu' => 'Przekazana do magazynu',
|
||||
'przesylka_wyjechala_w_droge_do_punktu_docelowego' => 'Wyjechała w drogę do punktu docelowego',
|
||||
'w_sortowni' => 'W sortowni',
|
||||
'wyjechala_w_droge_do_punktu_docelowego' => 'Wyjechała w drogę do punktu docelowego',
|
||||
'wyslana_z_sortowni' => 'Wysłana z sortowni',
|
||||
'w_doreczeniu' => 'W doręczeniu',
|
||||
'wydana_do_doreczenia' => 'Wydana do doręczenia',
|
||||
'dostarczana' => 'Dostarczana',
|
||||
'gotowa_do_odbioru' => 'Gotowa do odbioru',
|
||||
'oczekuje_na_odbior' => 'Oczekuje na odbiór',
|
||||
'przesylka_oczekuje_na_odbior' => 'Oczekuje na odbiór',
|
||||
'dostarczona' => 'Dostarczona',
|
||||
'doreczona' => 'Doręczona',
|
||||
'odebrana' => 'Odebrana',
|
||||
'zwrocona' => 'Zwrócona',
|
||||
'zwrocona_do_nadawcy' => 'Zwrócona do nadawcy',
|
||||
'anulowana' => 'Anulowana',
|
||||
'odmowa_przyjecia' => 'Odmowa przyjęcia',
|
||||
'uszkodzona' => 'Uszkodzona',
|
||||
'zagubiona' => 'Zagubiona',
|
||||
];
|
||||
|
||||
public const ALL_STATUSES = [
|
||||
self::UNKNOWN,
|
||||
self::CREATED,
|
||||
@@ -208,12 +267,14 @@ final class DeliveryStatus
|
||||
'inpost' => self::INPOST_MAP,
|
||||
'apaczka' => self::APACZKA_MAP,
|
||||
'allegro_wza' => self::ALLEGRO_MAP,
|
||||
'allegro_edge' => self::ALLEGRO_EDGE_MAP,
|
||||
];
|
||||
|
||||
private const PROVIDER_DESCRIPTIONS = [
|
||||
'inpost' => self::INPOST_DESCRIPTIONS,
|
||||
'apaczka' => self::APACZKA_DESCRIPTIONS,
|
||||
'allegro_wza' => self::ALLEGRO_DESCRIPTIONS,
|
||||
'allegro_edge' => self::ALLEGRO_EDGE_DESCRIPTIONS,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -267,6 +328,7 @@ final class DeliveryStatus
|
||||
'inpost' => self::INPOST_MAP,
|
||||
'apaczka' => self::APACZKA_MAP,
|
||||
'allegro_wza' => self::ALLEGRO_MAP,
|
||||
'allegro_edge' => self::ALLEGRO_EDGE_MAP,
|
||||
default => [],
|
||||
};
|
||||
|
||||
@@ -279,6 +341,7 @@ final class DeliveryStatus
|
||||
'inpost' => self::INPOST_DESCRIPTIONS,
|
||||
'apaczka' => self::APACZKA_DESCRIPTIONS,
|
||||
'allegro_wza' => self::ALLEGRO_DESCRIPTIONS,
|
||||
'allegro_edge' => self::ALLEGRO_EDGE_DESCRIPTIONS,
|
||||
default => [],
|
||||
};
|
||||
|
||||
@@ -295,6 +358,70 @@ final class DeliveryStatus
|
||||
return in_array($status, self::TERMINAL_STATUSES, true);
|
||||
}
|
||||
|
||||
public static function slugifyAllegroDescription(string $description): string
|
||||
{
|
||||
$text = trim($description);
|
||||
if ($text === '') {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// Usuń typowe prefiksy
|
||||
$text = preg_replace('/^Przesy[łl]ka zosta[łl]a\s+/ui', '', $text);
|
||||
$text = preg_replace('/^Kurier\s+/ui', '', $text);
|
||||
$text = preg_replace('/^Paczka zosta[łl]a\s+/ui', '', $text);
|
||||
|
||||
// Polskie znaki na ASCII
|
||||
$polish = ['ą','ć','ę','ł','ń','ó','ś','ź','ż','Ą','Ć','Ę','Ł','Ń','Ó','Ś','Ź','Ż'];
|
||||
$ascii = ['a','c','e','l','n','o','s','z','z','A','C','E','L','N','O','S','Z','Z'];
|
||||
$text = str_replace($polish, $ascii, $text);
|
||||
|
||||
// Lowercase, zamień nie-alfanumeryczne na podkreślenia
|
||||
$text = strtolower($text);
|
||||
$text = preg_replace('/[^a-z0-9]+/', '_', $text);
|
||||
$text = trim($text, '_');
|
||||
|
||||
return $text !== '' ? $text : 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyword-based fallback for unknown Allegro edge descriptions.
|
||||
* Used when slugified description is not in ALLEGRO_EDGE_MAP.
|
||||
*/
|
||||
public static function guessStatusFromDescription(string $description): string
|
||||
{
|
||||
$lower = mb_strtolower($description, 'UTF-8');
|
||||
|
||||
if (str_contains($lower, 'doręczon') || str_contains($lower, 'dostarczono') || str_contains($lower, 'odebrana przez odbiorc')) {
|
||||
return self::DELIVERED;
|
||||
}
|
||||
if (str_contains($lower, 'zwrócon') || str_contains($lower, 'zwrocona')) {
|
||||
return self::RETURNED;
|
||||
}
|
||||
if (str_contains($lower, 'anulowan')) {
|
||||
return self::CANCELLED;
|
||||
}
|
||||
if (str_contains($lower, 'doręczeni') || str_contains($lower, 'doreczenia') || str_contains($lower, 'wydana do')) {
|
||||
return self::OUT_FOR_DELIVERY;
|
||||
}
|
||||
if (str_contains($lower, 'sortowni') || str_contains($lower, 'magazyn') || str_contains($lower, 'w drodze') || str_contains($lower, 'tranzyt') || str_contains($lower, 'kurier') || str_contains($lower, 'podjęta') || str_contains($lower, 'podjeta') || str_contains($lower, 'wyjechał') || str_contains($lower, 'wyjechala')) {
|
||||
return self::IN_TRANSIT;
|
||||
}
|
||||
if (str_contains($lower, 'oczekuje na odb') || str_contains($lower, 'gotowa do odb') || (str_contains($lower, 'odbiór') && !str_contains($lower, 'w drodze'))) {
|
||||
return self::READY_FOR_PICKUP;
|
||||
}
|
||||
if (str_contains($lower, 'nadana') || str_contains($lower, 'nadano')) {
|
||||
return self::CONFIRMED;
|
||||
}
|
||||
if (str_contains($lower, 'przygotowan') || str_contains($lower, 'utworzon')) {
|
||||
return self::CREATED;
|
||||
}
|
||||
if (str_contains($lower, 'uszkodzon') || str_contains($lower, 'problem') || str_contains($lower, 'zagubion')) {
|
||||
return self::PROBLEM;
|
||||
}
|
||||
|
||||
return self::UNKNOWN;
|
||||
}
|
||||
|
||||
public static function trackingUrl(string $provider, string $trackingNumber, string $carrierId = ''): ?string
|
||||
{
|
||||
$number = trim($trackingNumber);
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace App\Modules\Shipments;
|
||||
|
||||
use App\Core\Exceptions\IntegrationConfigException;
|
||||
use App\Core\Http\SslCertificateResolver;
|
||||
use App\Core\Exceptions\ShipmentException;
|
||||
use App\Modules\Orders\OrdersRepository;
|
||||
use App\Modules\Settings\CompanySettingsRepository;
|
||||
@@ -328,7 +329,7 @@ final class InpostShipmentService implements ShipmentProviderInterface
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
];
|
||||
|
||||
$caPath = $this->getCaBundlePath();
|
||||
$caPath = SslCertificateResolver::resolve();
|
||||
if ($caPath !== null) {
|
||||
$opts[CURLOPT_CAINFO] = $caPath;
|
||||
}
|
||||
@@ -376,29 +377,6 @@ final class InpostShipmentService implements ShipmentProviderInterface
|
||||
return 'InPost API zwrocilo blad HTTP ' . $httpCode;
|
||||
}
|
||||
|
||||
private function getCaBundlePath(): ?string
|
||||
{
|
||||
$envPath = (string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? '');
|
||||
if ($envPath !== '' && is_file($envPath)) {
|
||||
return $envPath;
|
||||
}
|
||||
$iniPath = (string) ini_get('curl.cainfo');
|
||||
if ($iniPath !== '' && is_file($iniPath)) {
|
||||
return $iniPath;
|
||||
}
|
||||
$candidates = [
|
||||
'C:/xampp/apache/bin/curl-ca-bundle.crt',
|
||||
'C:/xampp/php/extras/ssl/cacert.pem',
|
||||
'/etc/ssl/certs/ca-certificates.crt',
|
||||
];
|
||||
foreach ($candidates as $path) {
|
||||
if (is_file($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $orderDetails
|
||||
* @param array<string, mixed> $formData
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Shipments;
|
||||
|
||||
use App\Core\Http\SslCertificateResolver;
|
||||
use App\Modules\Settings\InpostIntegrationRepository;
|
||||
use Throwable;
|
||||
|
||||
@@ -91,7 +92,7 @@ final class InpostTrackingService implements ShipmentTrackingInterface
|
||||
],
|
||||
];
|
||||
|
||||
$caPath = $this->getCaBundlePath();
|
||||
$caPath = SslCertificateResolver::resolve();
|
||||
if ($caPath !== null) {
|
||||
$opts[CURLOPT_CAINFO] = $caPath;
|
||||
}
|
||||
@@ -109,20 +110,4 @@ final class InpostTrackingService implements ShipmentTrackingInterface
|
||||
return is_array($json) ? $json : [];
|
||||
}
|
||||
|
||||
private function getCaBundlePath(): ?string
|
||||
{
|
||||
$candidates = [
|
||||
(string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? ''),
|
||||
(string) ini_get('curl.cainfo'),
|
||||
'C:/xampp/apache/bin/curl-ca-bundle.crt',
|
||||
'C:/xampp/php/extras/ssl/cacert.pem',
|
||||
'/etc/ssl/certs/ca-certificates.crt',
|
||||
];
|
||||
foreach ($candidates as $path) {
|
||||
if ($path !== '' && is_file($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user