Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fdc4cac593 | |||
| 8f67d9de0a |
@@ -162,7 +162,7 @@ $isCompactColumn = function(array $column): bool {
|
|||||||
|
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" class="btn btn-primary btn-sm">Szukaj</button>
|
<button type="submit" class="btn btn-primary btn-sm">Szukaj</button>
|
||||||
<a href="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="btn btn-default btn-sm">Wyczyść</a>
|
<a href="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="btn btn-default btn-sm js-table-filters-clear">Wyczyść</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -312,6 +312,40 @@ $isCompactColumn = function(array $column): bool {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Table state persistence — redirect ASAP to saved view
|
||||||
|
(function() {
|
||||||
|
var basePath = <?= json_encode($list->basePath); ?>;
|
||||||
|
var stateKey = 'tableListQuery_' + basePath;
|
||||||
|
var clearKey = 'tableListCleared_' + basePath;
|
||||||
|
|
||||||
|
var pathname = window.location.pathname.replace(/\/+$/, '/');
|
||||||
|
var bp = basePath.replace(/\/+$/, '/');
|
||||||
|
|
||||||
|
var queryPart = '';
|
||||||
|
if (pathname.length > bp.length && pathname.indexOf(bp) === 0) {
|
||||||
|
queryPart = pathname.substring(bp.length);
|
||||||
|
}
|
||||||
|
if (!queryPart && window.location.search) {
|
||||||
|
queryPart = window.location.search.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var justCleared = sessionStorage.getItem(clearKey) === '1';
|
||||||
|
sessionStorage.removeItem(clearKey);
|
||||||
|
|
||||||
|
if (queryPart) {
|
||||||
|
localStorage.setItem(stateKey, queryPart);
|
||||||
|
} else if (!justCleared) {
|
||||||
|
var saved = localStorage.getItem(stateKey);
|
||||||
|
if (saved) {
|
||||||
|
window.location.replace(basePath + saved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
(function($) {
|
(function($) {
|
||||||
if (!$) {
|
if (!$) {
|
||||||
@@ -550,5 +584,17 @@ $isCompactColumn = function(array $column): bool {
|
|||||||
$(document).on('change.tablePerPage', '.js-per-page-select', function() {
|
$(document).on('change.tablePerPage', '.js-per-page-select', function() {
|
||||||
$(this).closest('form').trigger('submit');
|
$(this).closest('form').trigger('submit');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Table state clear on "Wyczyść" ---
|
||||||
|
var stateStorageKey = 'tableListQuery_' + <?= json_encode($list->basePath); ?>;
|
||||||
|
var stateClearKey = 'tableListCleared_' + <?= json_encode($list->basePath); ?>;
|
||||||
|
|
||||||
|
$(document).off('click.tableClearState', '.js-table-filters-clear');
|
||||||
|
$(document).on('click.tableClearState', '.js-table-filters-clear', function() {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(stateStorageKey);
|
||||||
|
sessionStorage.setItem(stateClearKey, '1');
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
})(window.jQuery);
|
})(window.jQuery);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -184,13 +184,14 @@ $orderId = (int)($this -> order['id'] ?? 0);
|
|||||||
<?= $product[ 'message' ] != '' ? '<strong>Wiadomość:</strong> ' . $product['message'] : '';?>
|
<?= $product[ 'message' ] != '' ? '<strong>Wiadomość:</strong> ' . $product['message'] : '';?>
|
||||||
</div>
|
</div>
|
||||||
<div class="od-mobile-price-line">
|
<div class="od-mobile-price-line">
|
||||||
<?= (int)$product['quantity'];?> × <?= \Shared\Helpers\Helpers::decimal( $product['price_brutto_promo'] );?> = <?= \Shared\Helpers\Helpers::decimal( $product['price_brutto_promo'] * $product['quantity'] );?> zł
|
<? $effective = ((float)$product['price_brutto_promo'] > 0 && (float)$product['price_brutto_promo'] < (float)$product['price_brutto']) ? (float)$product['price_brutto_promo'] : (float)$product['price_brutto'];?>
|
||||||
|
<?= (int)$product['quantity'];?> × <?= \Shared\Helpers\Helpers::decimal( $effective );?> = <?= \Shared\Helpers\Helpers::decimal( $effective * $product['quantity'] );?> zł
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="tab-center"><?= $product[ 'quantity' ];?></td>
|
<td class="tab-center"><?= $product[ 'quantity' ];?></td>
|
||||||
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $product[ 'price_brutto' ] );?> zł</td>
|
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $product[ 'price_brutto' ] );?> zł</td>
|
||||||
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $product[ 'price_brutto_promo' ] );?> zł</td>
|
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $effective );?> zł</td>
|
||||||
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $product[ 'price_brutto_promo' ] * $product[ 'quantity' ] );?> zł</td>
|
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $effective * $product[ 'quantity' ] );?> zł</td>
|
||||||
</tr>
|
</tr>
|
||||||
<? endforeach; endif;?>
|
<? endforeach; endif;?>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -533,9 +533,26 @@ class OrderAdminService
|
|||||||
|
|
||||||
$error = '';
|
$error = '';
|
||||||
$sync_failed = false;
|
$sync_failed = false;
|
||||||
|
$max_attempts = 50; // ~8h przy cronie co 10 min
|
||||||
|
|
||||||
|
// Zamówienie jeszcze nie wysłane do Apilo — czekaj na crona
|
||||||
|
if (!(int)$order['apilo_order_id']) {
|
||||||
|
$attempts = (int)($task['attempts'] ?? 0) + 1;
|
||||||
|
if ($attempts >= $max_attempts) {
|
||||||
|
// Przekroczono limit prób — porzuć task
|
||||||
|
unset($queue[$key]);
|
||||||
|
} else {
|
||||||
|
$task['attempts'] = $attempts;
|
||||||
|
$task['last_error'] = 'awaiting_apilo_order';
|
||||||
|
$task['updated_at'] = date('Y-m-d H:i:s');
|
||||||
|
$queue[$key] = $task;
|
||||||
|
}
|
||||||
|
$processed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$payment_pending = !empty($task['payment']) && (int)$order['paid'] === 1;
|
$payment_pending = !empty($task['payment']) && (int)$order['paid'] === 1;
|
||||||
if ($payment_pending && (int)$order['apilo_order_id']) {
|
if ($payment_pending) {
|
||||||
if (!$this->syncApiloPayment($order)) {
|
if (!$this->syncApiloPayment($order)) {
|
||||||
$sync_failed = true;
|
$sync_failed = true;
|
||||||
$error = 'payment_sync_failed';
|
$error = 'payment_sync_failed';
|
||||||
@@ -543,7 +560,7 @@ class OrderAdminService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$status_pending = isset($task['status']) && $task['status'] !== null && $task['status'] !== '';
|
$status_pending = isset($task['status']) && $task['status'] !== null && $task['status'] !== '';
|
||||||
if (!$sync_failed && $status_pending && (int)$order['apilo_order_id']) {
|
if (!$sync_failed && $status_pending) {
|
||||||
if (!$this->syncApiloStatus($order, (int)$task['status'])) {
|
if (!$this->syncApiloStatus($order, (int)$task['status'])) {
|
||||||
$sync_failed = true;
|
$sync_failed = true;
|
||||||
$error = 'status_sync_failed';
|
$error = 'status_sync_failed';
|
||||||
@@ -631,7 +648,10 @@ class OrderAdminService
|
|||||||
self::appendApiloLog("SET AS PAID\n" . print_r($order, true));
|
self::appendApiloLog("SET AS PAID\n" . print_r($order, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($order['apilo_order_id'] && !$this->syncApiloPayment($order)) {
|
if (!$order['apilo_order_id']) {
|
||||||
|
// Zamówienie jeszcze nie wysłane do Apilo — kolejkuj sync płatności na później
|
||||||
|
self::queueApiloSync((int)$order['id'], true, null, 'awaiting_apilo_order');
|
||||||
|
} elseif (!$this->syncApiloPayment($order)) {
|
||||||
self::queueApiloSync((int)$order['id'], true, null, 'payment_sync_failed');
|
self::queueApiloSync((int)$order['id'], true, null, 'payment_sync_failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -652,7 +672,10 @@ class OrderAdminService
|
|||||||
self::appendApiloLog("UPDATE STATUS\n" . print_r($order, true));
|
self::appendApiloLog("UPDATE STATUS\n" . print_r($order, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($order['apilo_order_id'] && !$this->syncApiloStatus($order, $status)) {
|
if (!$order['apilo_order_id']) {
|
||||||
|
// Zamówienie jeszcze nie wysłane do Apilo — kolejkuj sync statusu na później
|
||||||
|
self::queueApiloSync((int)$order['id'], false, $status, 'awaiting_apilo_order');
|
||||||
|
} elseif (!$this->syncApiloStatus($order, $status)) {
|
||||||
self::queueApiloSync((int)$order['id'], false, $status, 'status_sync_failed');
|
self::queueApiloSync((int)$order['id'], false, $status, 'status_sync_failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
cron.php
3
cron.php
@@ -504,6 +504,9 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se
|
|||||||
echo '<p>Wysłałem zamówienie do apilo.com: ID: ' . $order['id'] . ' - ' . $response['id'] . '</p>';
|
echo '<p>Wysłałem zamówienie do apilo.com: ID: ' . $order['id'] . ' - ' . $response['id'] . '</p>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Po wysłaniu zamówień: przetwórz kolejkę sync (płatności/statusy oczekujące na apilo_order_id)
|
||||||
|
$orderAdminService->processApiloSyncQueue( 10 );
|
||||||
}
|
}
|
||||||
|
|
||||||
// sprawdzanie statusów zamówień w apilo.com jeżeli zamówienie nie jest zrealizowane, anulowane lub nieodebrane
|
// sprawdzanie statusów zamówień w apilo.com jeżeli zamówienie nie jest zrealizowane, anulowane lub nieodebrane
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ver. 0.311 (2026-02-23) - Fix race condition Apilo + persistence filtrów + poprawki cen
|
||||||
|
|
||||||
|
- **FIX**: Race condition — callback płatności przed wysłaniem zamówienia do Apilo nie synchronizował płatności (task trafiał w pustkę). Teraz `syncApiloPaymentIfNeeded` i `syncApiloStatusIfNeeded` kolejkują sync do retry gdy `apilo_order_id` jeszcze nie istnieje
|
||||||
|
- **FIX**: `processApiloSyncQueue` — zamówienia bez `apilo_order_id` były usuwane z kolejki bez synchronizacji. Teraz czekają (max 50 prób ~8h) aż cron wyśle zamówienie do Apilo
|
||||||
|
- **FIX**: Drugie wywołanie `processApiloSyncQueue` w cronie po wysyłce zamówień — sync płatności/statusów w tym samym cyklu
|
||||||
|
- **FIX**: Ceny w szczegółach zamówienia (admin + frontend) — gdy `price_brutto_promo` = 0 lub >= ceny regularnej, wyświetla cenę regularną zamiast 0 zł
|
||||||
|
- **NEW**: Persistence filtrów tabel w panelu admin — localStorage zapamiętuje ostatni widok (filtry, sortowanie, paginacja) i przywraca go przy powrocie do listy. Przycisk "Wyczyść" resetuje zapisany stan
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ver. 0.310 (2026-02-23) - Logi integracji w panelu admin
|
## ver. 0.310 (2026-02-23) - Logi integracji w panelu admin
|
||||||
|
|
||||||
- **NEW**: Zakładka "Logi" w sekcji Integracje — podgląd tabeli `pp_log` z paginacją, sortowaniem, filtrami (akcja, wiadomość, ID zamówienia) i rozwijalnym kontekstem JSON
|
- **NEW**: Zakładka "Logi" w sekcji Integracje — podgląd tabeli `pp_log` z paginacją, sortowaniem, filtrami (akcja, wiadomość, ID zamówienia) i rozwijalnym kontekstem JSON
|
||||||
|
|||||||
@@ -179,7 +179,7 @@
|
|||||||
'id': <?= (int)$product['product_id'];?>,
|
'id': <?= (int)$product['product_id'];?>,
|
||||||
'name': '<?= $product['name'];?>',
|
'name': '<?= $product['name'];?>',
|
||||||
'quantity': <?= $product['quantity'];?>,
|
'quantity': <?= $product['quantity'];?>,
|
||||||
'price': <?= $product['price_brutto_promo'];?>
|
'price': <?= ((float)$product['price_brutto_promo'] > 0 && (float)$product['price_brutto_promo'] < (float)$product['price_brutto']) ? (float)$product['price_brutto_promo'] : (float)$product['price_brutto'];?>
|
||||||
}<? if ( $product != end( $this -> order['products'] ) ) echo ',';?>
|
}<? if ( $product != end( $this -> order['products'] ) ) echo ',';?>
|
||||||
<? endforeach;?>
|
<? endforeach;?>
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -227,4 +227,110 @@ class OrderAdminServiceTest extends TestCase
|
|||||||
$service = $this->createService(null, null, $settingsRepo);
|
$service = $this->createService(null, null, $settingsRepo);
|
||||||
$this->assertSame(150.0, $service->getFreeDeliveryThreshold());
|
$this->assertSame(150.0, $service->getFreeDeliveryThreshold());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// processApiloSyncQueue — awaiting apilo_order_id
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
private function getQueuePath(): string
|
||||||
|
{
|
||||||
|
// Musi odpowiadać ścieżce w OrderAdminService::apiloSyncQueuePath()
|
||||||
|
// dirname(autoload/Domain/Order/, 2) = autoload/
|
||||||
|
return dirname(__DIR__, 4) . '/autoload/temp/apilo-sync-queue.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeQueue(array $queue): void
|
||||||
|
{
|
||||||
|
$path = $this->getQueuePath();
|
||||||
|
$dir = dirname($path);
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
mkdir($dir, 0777, true);
|
||||||
|
}
|
||||||
|
file_put_contents($path, json_encode($queue, JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readQueue(): array
|
||||||
|
{
|
||||||
|
$path = $this->getQueuePath();
|
||||||
|
if (!file_exists($path)) return [];
|
||||||
|
$content = file_get_contents($path);
|
||||||
|
return $content ? json_decode($content, true) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
$path = $this->getQueuePath();
|
||||||
|
if (file_exists($path)) {
|
||||||
|
unlink($path);
|
||||||
|
}
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProcessApiloSyncQueueKeepsTaskWhenApiloOrderIdIsNull(): void
|
||||||
|
{
|
||||||
|
// Zamówienie bez apilo_order_id — task powinien zostać w kolejce
|
||||||
|
$this->writeQueue([
|
||||||
|
'42' => [
|
||||||
|
'order_id' => 42,
|
||||||
|
'payment' => 1,
|
||||||
|
'status' => null,
|
||||||
|
'attempts' => 0,
|
||||||
|
'last_error' => 'awaiting_apilo_order',
|
||||||
|
'updated_at' => '2026-01-01 00:00:00',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$orderRepo = $this->createMock(OrderRepository::class);
|
||||||
|
$orderRepo->method('findRawById')
|
||||||
|
->with(42)
|
||||||
|
->willReturn([
|
||||||
|
'id' => 42,
|
||||||
|
'apilo_order_id' => null,
|
||||||
|
'paid' => 1,
|
||||||
|
'summary' => '100.00',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = new OrderAdminService($orderRepo);
|
||||||
|
$processed = $service->processApiloSyncQueue(10);
|
||||||
|
|
||||||
|
$this->assertSame(1, $processed);
|
||||||
|
|
||||||
|
$queue = $this->readQueue();
|
||||||
|
$this->assertArrayHasKey('42', $queue);
|
||||||
|
$this->assertSame('awaiting_apilo_order', $queue['42']['last_error']);
|
||||||
|
$this->assertSame(1, $queue['42']['attempts']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProcessApiloSyncQueueRemovesTaskAfterMaxAttempts(): void
|
||||||
|
{
|
||||||
|
// Task z 49 próbami — limit to 50, więc powinien zostać usunięty
|
||||||
|
$this->writeQueue([
|
||||||
|
'42' => [
|
||||||
|
'order_id' => 42,
|
||||||
|
'payment' => 1,
|
||||||
|
'status' => null,
|
||||||
|
'attempts' => 49,
|
||||||
|
'last_error' => 'awaiting_apilo_order',
|
||||||
|
'updated_at' => '2026-01-01 00:00:00',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$orderRepo = $this->createMock(OrderRepository::class);
|
||||||
|
$orderRepo->method('findRawById')
|
||||||
|
->with(42)
|
||||||
|
->willReturn([
|
||||||
|
'id' => 42,
|
||||||
|
'apilo_order_id' => null,
|
||||||
|
'paid' => 1,
|
||||||
|
'summary' => '100.00',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = new OrderAdminService($orderRepo);
|
||||||
|
$processed = $service->processApiloSyncQueue(10);
|
||||||
|
|
||||||
|
$this->assertSame(1, $processed);
|
||||||
|
|
||||||
|
$queue = $this->readQueue();
|
||||||
|
$this->assertArrayNotHasKey('42', $queue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
updates/0.30/ver_0.310.zip
Normal file
BIN
updates/0.30/ver_0.310.zip
Normal file
Binary file not shown.
25
updates/0.30/ver_0.310_manifest.json
Normal file
25
updates/0.30/ver_0.310_manifest.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"changelog": "NEW - Zakladka Logi w sekcji Integracje (podglad pp_log z paginacja, sortowaniem, filtrami)",
|
||||||
|
"version": "0.310",
|
||||||
|
"files": {
|
||||||
|
"added": [
|
||||||
|
"admin/templates/integrations/logs.php"
|
||||||
|
],
|
||||||
|
"deleted": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"modified": [
|
||||||
|
"admin/templates/site/main-layout.php",
|
||||||
|
"autoload/Domain/Integrations/IntegrationsRepository.php",
|
||||||
|
"autoload/admin/Controllers/IntegrationsController.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"checksum_zip": "sha256:e3b14e239230548aba203a83f01c91b00651e5114e92e162f6da7389c6a92975",
|
||||||
|
"sql": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"date": "2026-02-23",
|
||||||
|
"directories_deleted": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
<b>ver. 0.310 - 23.02.2026</b><br />
|
||||||
|
NEW - Zakladka Logi w sekcji Integracje (podglad pp_log z paginacja, sortowaniem, filtrami)
|
||||||
|
<hr>
|
||||||
<b>ver. 0.309 - 23.02.2026</b><br />
|
<b>ver. 0.309 - 23.02.2026</b><br />
|
||||||
NEW - ApiloLogger (logowanie operacji Apilo do pp_log), cache-busting CSS/JS w admin panelu, poprawki UI listy produktow, clipboard API
|
NEW - ApiloLogger (logowanie operacji Apilo do pp_log), cache-busting CSS/JS w admin panelu, poprawki UI listy produktow, clipboard API
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?
|
<?
|
||||||
$current_ver = 310;
|
$current_ver = 311;
|
||||||
|
|
||||||
for ($i = 1; $i <= $current_ver; $i++)
|
for ($i = 1; $i <= $current_ver; $i++)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user