ver. 0.296: REST API for ordersPRO — orders management, dictionaries, API key auth
- New API layer: ApiRouter, OrdersApiController, DictionariesApiController - Orders API: list (with filters/pagination/updated_since), details, change status, set paid/unpaid - Dictionaries API: order statuses, transport methods, payment methods - X-Api-Key authentication via pp_settings.api_key - OrderRepository: listForApi(), findForApi(), touchUpdatedAt() - updated_at column on pp_shop_orders for polling support - api.php: skip session for API requests, route to ApiRouter - SettingsController: api_key field in system tab - 30 new tests (666 total, 1930 assertions) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,33 +1,19 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(powershell -Command \"Compress-Archive -Path ''*'' -DestinationPath ''../ver_0.234.zip'' -Force\")",
|
||||
"Bash(powershell -Command:*)",
|
||||
"Bash(ls -la \"c:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\updates\\\\0.20\"\" | grep \"ver_ \")",
|
||||
"Bash(C:/xampp/php/php.exe:*)",
|
||||
"Bash(where:*)",
|
||||
"Bash(composer:*)",
|
||||
"Bash(curl:*)",
|
||||
<<<<<<< HEAD
|
||||
"Bash(C:xamppphpphp.exe phpunit.phar --testdox)",
|
||||
"Bash(powershell -Command \"Compress-Archive -Path ''*'' -DestinationPath ''../../updates/0.20/ver_0.295.zip'' -Force\")",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(php phpunit.phar:*)",
|
||||
"Bash(ls -la \"c:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\autoload\"\" 2>nul | findstr /i \"admin Admin \")",
|
||||
"Bash(php -r:*)",
|
||||
"Bash(php list_zip.php:*)",
|
||||
"Bash(php create_update_239.php:*)",
|
||||
"Bash(php vendor/bin/phpunit:*)",
|
||||
"Bash(python:*)",
|
||||
"Bash(\"C:/Program Files/7-Zip/7z.exe\" l \"updates/0.20/ver_0.239.zip\")",
|
||||
"Bash(powershell.exe -Command \"[System.IO.Compression.ZipFile]::OpenRead\\(''c:/visual studio code/projekty/shopPRO/updates/0.20/ver_0.239.zip''\\).Entries | ForEach-Object { Write-Host $_.FullName }\")",
|
||||
"Bash(powershell.exe -Command \"Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::OpenRead\\(''c:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\updates\\\\0.20\\\\ver_0.239.zip''\\).Entries | ForEach-Object { Write-Host $_.FullName }\")",
|
||||
"Bash(powershell.exe -NoProfile -Command 'Add-Type -AssemblyName System.IO.Compression.FileSystem; $z = [System.IO.Compression.ZipFile]::OpenRead\\(\"\"c:\\\\visual studio code\\\\projekty\\\\shopPRO\\\\updates\\\\0.20\\\\ver_0.239.zip\"\"\\); foreach \\($e in $z.Entries\\) { $e.FullName }; $z.Dispose\\(\\)')",
|
||||
"Bash(powershell.exe -NoProfile -Command:*)"
|
||||
=======
|
||||
"Bash(find:*)",
|
||||
"Bash(php:*)",
|
||||
"Bash(C:xamppphpphp.exe -v)",
|
||||
"Bash(/c/xampp/php/php.exe:*)"
|
||||
>>>>>>> 471173f45b4ff731f785fbcf8fdc0483af3b4e54
|
||||
"Bash(powershell -Command \"Compress-Archive -Path ''*'' -DestinationPath ''../../updates/0.20/ver_0.296.zip'' -Force\")",
|
||||
"Bash(ls:*)",
|
||||
"Bash(git -C \"C:/visual studio code/projekty/shopPRO\" rev-parse --show-toplevel)",
|
||||
"Bash(powershell -File:*)",
|
||||
"Bash(git status:*)",
|
||||
"Bash(powershell -Command \"& { Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::OpenRead\\(''updates/0.20/ver_0.296.zip''\\).Entries | ForEach-Object { Write-Output $_.FullName } }\")",
|
||||
"Bash(powershell -Command \"Compress-Archive -Path ''*'' -DestinationPath ''../ver_0.296.zip'' -Force\")",
|
||||
"Bash(powershell -Command \"Add-Type -AssemblyName System.IO.Compression.FileSystem; [IO.Compression.ZipFile]::OpenRead\\(\\(Resolve-Path ''updates/0.20/ver_0.296.zip''\\)\\).Entries.FullName\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
18
CLAUDE.md
18
CLAUDE.md
@@ -36,7 +36,7 @@ composer test
|
||||
|
||||
PHPUnit 9.6 via `phpunit.phar`. Bootstrap: `tests/bootstrap.php`. Config: `phpunit.xml`.
|
||||
|
||||
Current suite: **636 tests, 1868 assertions**.
|
||||
Current suite: **666 tests, 1930 assertions**.
|
||||
|
||||
### Creating Updates
|
||||
See `docs/UPDATE_INSTRUCTIONS.md` for the full procedure. Updates are ZIP packages in `updates/0.XX/`. Never include `*.md` files, `updates/changelog.php`, or root `.htaccess` in update ZIPs.
|
||||
@@ -55,6 +55,9 @@ shopPRO/
|
||||
│ │ ├── Html/ # Html utility
|
||||
│ │ ├── Image/ # ImageManipulator
|
||||
│ │ └── Tpl/ # Template engine
|
||||
│ ├── api/ # REST API layer (\api\)
|
||||
│ │ ├── ApiRouter.php # API router (\api\ApiRouter)
|
||||
│ │ └── Controllers/ # API controllers (\api\Controllers\)
|
||||
│ ├── admin/ # Admin panel layer
|
||||
│ │ ├── App.php # Admin router (\admin\App)
|
||||
│ │ ├── Controllers/ # DI controllers (\admin\Controllers\) — 28 controllers
|
||||
@@ -76,7 +79,8 @@ shopPRO/
|
||||
│ ├── stubs/ # Test stubs (CacheHandler, Helpers, ShopProduct)
|
||||
│ └── Unit/
|
||||
│ ├── Domain/ # Repository tests
|
||||
│ └── admin/Controllers/ # Controller tests
|
||||
│ ├── admin/Controllers/ # Controller tests
|
||||
│ └── api/ # API tests
|
||||
├── updates/ # Update packages for clients
|
||||
├── docs/ # Technical documentation
|
||||
├── config.php # Database/Redis config (not in repo)
|
||||
@@ -85,7 +89,7 @@ shopPRO/
|
||||
├── admin/index.php # Admin entry point
|
||||
├── admin/ajax.php # Admin AJAX handler
|
||||
├── cron.php # CRON jobs (Apilo sync)
|
||||
└── api.php # REST API
|
||||
└── api.php # REST API (ordersPRO + Ekomi)
|
||||
```
|
||||
|
||||
### Autoloader
|
||||
@@ -99,6 +103,7 @@ Custom autoloader in each entry point (not Composer autoload at runtime). Tries
|
||||
- `\admin\Controllers\` → `autoload/admin/Controllers/` (lowercase a)
|
||||
- `\Shared\` → `autoload/Shared/`
|
||||
- `\front\` → `autoload/front/`
|
||||
- `\api\` → `autoload/api/`
|
||||
- Do NOT use `\Admin\` (uppercase A) — the server directory is `admin/` (lowercase)
|
||||
- `\shop\` namespace is **deleted** — all 12 legacy classes migrated to `\Domain\`, `autoload/shop/` directory removed
|
||||
|
||||
@@ -124,6 +129,11 @@ All legacy directories (`admin/controls/`, `admin/factory/`, `admin/view/`, `fro
|
||||
**Frontend Views** (`autoload/front/Views/`):
|
||||
- Static classes, no state, no DI — pure rendering
|
||||
|
||||
**API Controllers** (`autoload/api/Controllers/`):
|
||||
- DI via constructor, stateless (no session)
|
||||
- Wired in `api\ApiRouter::getControllerFactories()`
|
||||
- Auth: `X-Api-Key` header vs `pp_settings.api_key`
|
||||
|
||||
### Key Classes
|
||||
| Class | Purpose |
|
||||
|-------|---------|
|
||||
@@ -133,6 +143,7 @@ All legacy directories (`admin/controls/`, `admin/factory/`, `admin/view/`, `fro
|
||||
| `\Shared\Helpers\Helpers` | Utility methods (SEO, email, cache clearing) |
|
||||
| `\Shared\Tpl\Tpl` | Template engine — `render()`, `set()` |
|
||||
| `\Shared\Cache\CacheHandler` | Redis cache — `get()`, `set()`, `delete()`, `deletePattern()` |
|
||||
| `\api\ApiRouter` | REST API router — auth, routing, response helpers |
|
||||
|
||||
### Database
|
||||
- ORM: Medoo (`$mdb` global variable, injected via DI in new code)
|
||||
@@ -210,4 +221,5 @@ Before starting implementation, review current state of docs (see AGENTS.md for
|
||||
- `docs/TESTING.md` — test suite guide and structure
|
||||
- `docs/FORM_EDIT_SYSTEM.md` — form system architecture
|
||||
- `docs/CHANGELOG.md` — version history
|
||||
- `docs/API.md` — REST API documentation (ordersPRO)
|
||||
- `docs/UPDATE_INSTRUCTIONS.md` — how to build client update packages
|
||||
|
||||
42
api.php
42
api.php
@@ -25,20 +25,26 @@ require_once 'libraries/medoo/medoo.php';
|
||||
require_once 'libraries/phpmailer/class.phpmailer.php';
|
||||
require_once 'libraries/phpmailer/class.smtp.php';
|
||||
|
||||
session_start();
|
||||
// Detect API request (stateless, no session)
|
||||
$isApiRequest = isset( $_GET['endpoint'] );
|
||||
|
||||
if ( !isset( $_SESSION[ 'check' ] ) )
|
||||
if ( !$isApiRequest )
|
||||
{
|
||||
session_regenerate_id();
|
||||
$_SESSION[ 'check' ] = true;
|
||||
$_SESSION[ 'ip' ] = $_SERVER[ 'REMOTE_ADDR' ];
|
||||
}
|
||||
session_start();
|
||||
|
||||
if ( $_SESSION[ 'ip' ] !== $_SERVER[ 'REMOTE_ADDR' ] )
|
||||
{
|
||||
session_destroy();
|
||||
header( 'Location: /' );
|
||||
exit;
|
||||
if ( !isset( $_SESSION[ 'check' ] ) )
|
||||
{
|
||||
session_regenerate_id();
|
||||
$_SESSION[ 'check' ] = true;
|
||||
$_SESSION[ 'ip' ] = $_SERVER[ 'REMOTE_ADDR' ];
|
||||
}
|
||||
|
||||
if ( $_SESSION[ 'ip' ] !== $_SERVER[ 'REMOTE_ADDR' ] )
|
||||
{
|
||||
session_destroy();
|
||||
header( 'Location: /' );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$mdb = new medoo( [
|
||||
@@ -50,8 +56,18 @@ $mdb = new medoo( [
|
||||
'charset' => 'utf8'
|
||||
] );
|
||||
|
||||
$settings = ( new \Domain\Settings\SettingsRepository( $mdb ) )->allSettings();
|
||||
$settingsRepo = new \Domain\Settings\SettingsRepository( $mdb );
|
||||
$settings = $settingsRepo->allSettings();
|
||||
|
||||
// --- API routing (ordersPRO) ---
|
||||
if ( $isApiRequest )
|
||||
{
|
||||
$router = new \api\ApiRouter( $mdb, $settingsRepo );
|
||||
$router->handle();
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Ekomi CSV export ---
|
||||
if ( \Shared\Helpers\Helpers::get( 'ekomi_csv' ) )
|
||||
{
|
||||
$csv_array = [ [ 'ORDER_ID', 'MAIL', 'FIRST_NAME', 'LAST_NAME', 'PRODUCT_ID', 'PRODUCT_NAME' ] ];
|
||||
@@ -84,4 +100,4 @@ if ( \Shared\Helpers\Helpers::get( 'ekomi_csv' ) )
|
||||
|
||||
fclose( $fp );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +311,7 @@ class OrderRepository
|
||||
}
|
||||
|
||||
$this->db->update('pp_shop_orders', ['notes' => $notes], ['id' => $orderId]);
|
||||
$this->touchUpdatedAt($orderId);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -370,6 +371,8 @@ class OrderRepository
|
||||
'id' => $orderId,
|
||||
]);
|
||||
|
||||
$this->touchUpdatedAt($orderId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -688,6 +691,7 @@ class OrderRepository
|
||||
'coupon_id' => $coupon ? $coupon->id : null,
|
||||
'message' => $basket_message ? $basket_message : null,
|
||||
'apilo_order_status_date' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => $order_date,
|
||||
]);
|
||||
|
||||
$order_id = $this->db->id();
|
||||
@@ -832,16 +836,22 @@ class OrderRepository
|
||||
public function setAsPaid(int $orderId): void
|
||||
{
|
||||
$this->db->update('pp_shop_orders', ['paid' => 1], ['id' => $orderId]);
|
||||
$this->touchUpdatedAt($orderId);
|
||||
}
|
||||
|
||||
public function setAsUnpaid(int $orderId): void
|
||||
{
|
||||
$this->db->update('pp_shop_orders', ['paid' => 0], ['id' => $orderId]);
|
||||
$this->touchUpdatedAt($orderId);
|
||||
}
|
||||
|
||||
public function updateOrderStatus(int $orderId, int $status): bool
|
||||
{
|
||||
return (bool)$this->db->update('pp_shop_orders', ['status' => $status], ['id' => $orderId]);
|
||||
$result = (bool)$this->db->update('pp_shop_orders', ['status' => $status], ['id' => $orderId]);
|
||||
if ($result) {
|
||||
$this->touchUpdatedAt($orderId);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function insertStatusHistory(int $orderId, int $statusId, int $mail): void
|
||||
@@ -858,6 +868,145 @@ class OrderRepository
|
||||
$this->db->update('pp_shop_orders', ['apilo_order_status_date' => $date], ['id' => $orderId]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// API methods (for ordersPRO)
|
||||
// =========================================================================
|
||||
|
||||
public function listForApi(array $filters, int $page = 1, int $perPage = 50): array
|
||||
{
|
||||
$page = max(1, $page);
|
||||
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
$status = trim((string)($filters['status'] ?? ''));
|
||||
if ($status !== '' && is_numeric($status)) {
|
||||
$where[] = 'o.status = :status';
|
||||
$params[':status'] = (int)$status;
|
||||
}
|
||||
|
||||
$paid = trim((string)($filters['paid'] ?? ''));
|
||||
if ($paid !== '' && is_numeric($paid)) {
|
||||
$where[] = 'o.paid = :paid';
|
||||
$params[':paid'] = (int)$paid;
|
||||
}
|
||||
|
||||
$dateFrom = $this->normalizeDateFilter($filters['date_from'] ?? '');
|
||||
if ($dateFrom !== null) {
|
||||
$where[] = 'o.date_order >= :date_from';
|
||||
$params[':date_from'] = $dateFrom . ' 00:00:00';
|
||||
}
|
||||
|
||||
$dateTo = $this->normalizeDateFilter($filters['date_to'] ?? '');
|
||||
if ($dateTo !== null) {
|
||||
$where[] = 'o.date_order <= :date_to';
|
||||
$params[':date_to'] = $dateTo . ' 23:59:59';
|
||||
}
|
||||
|
||||
$updatedSince = trim((string)($filters['updated_since'] ?? ''));
|
||||
if ($updatedSince !== '' && preg_match('/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}$/', $updatedSince)) {
|
||||
$where[] = 'o.updated_at >= :updated_since';
|
||||
$params[':updated_since'] = $updatedSince;
|
||||
}
|
||||
|
||||
$number = $this->normalizeTextFilter($filters['number'] ?? '');
|
||||
if ($number !== '') {
|
||||
$where[] = 'o.number LIKE :number';
|
||||
$params[':number'] = '%' . $number . '%';
|
||||
}
|
||||
|
||||
$client = $this->normalizeTextFilter($filters['client'] ?? '');
|
||||
if ($client !== '') {
|
||||
$where[] = "(o.client_name LIKE :client OR o.client_surname LIKE :client2 OR o.client_email LIKE :client3)";
|
||||
$params[':client'] = '%' . $client . '%';
|
||||
$params[':client2'] = '%' . $client . '%';
|
||||
$params[':client3'] = '%' . $client . '%';
|
||||
}
|
||||
|
||||
$whereSql = '';
|
||||
if (!empty($where)) {
|
||||
$whereSql = ' WHERE ' . implode(' AND ', $where);
|
||||
}
|
||||
|
||||
$sqlCount = 'SELECT COUNT(0) FROM pp_shop_orders AS o' . $whereSql;
|
||||
$stmtCount = $this->db->query($sqlCount, $params);
|
||||
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
|
||||
$total = 0;
|
||||
if (is_array($countRows) && isset($countRows[0]) && is_array($countRows[0])) {
|
||||
$firstValue = reset($countRows[0]);
|
||||
$total = $firstValue !== false ? (int)$firstValue : 0;
|
||||
}
|
||||
|
||||
$sql = 'SELECT o.id, o.number, o.date_order, o.updated_at, o.status, o.paid,'
|
||||
. ' o.client_name, o.client_surname, o.client_email, o.client_phone,'
|
||||
. ' o.client_street, o.client_postal_code, o.client_city,'
|
||||
. ' o.firm_name, o.firm_nip,'
|
||||
. ' o.transport, o.transport_cost, o.payment_method, o.summary'
|
||||
. ' FROM pp_shop_orders AS o'
|
||||
. $whereSql
|
||||
. ' ORDER BY o.updated_at DESC, o.id DESC'
|
||||
. ' LIMIT ' . $perPage . ' OFFSET ' . $offset;
|
||||
|
||||
$stmt = $this->db->query($sql, $params);
|
||||
$items = ($stmt) ? $stmt->fetchAll() : [];
|
||||
if (!is_array($items)) {
|
||||
$items = [];
|
||||
}
|
||||
|
||||
foreach ($items as &$item) {
|
||||
$item['id'] = (int)($item['id'] ?? 0);
|
||||
$item['status'] = (int)($item['status'] ?? 0);
|
||||
$item['paid'] = (int)($item['paid'] ?? 0);
|
||||
$item['summary'] = (float)($item['summary'] ?? 0);
|
||||
$item['transport_cost'] = (float)($item['transport_cost'] ?? 0);
|
||||
}
|
||||
unset($item);
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
];
|
||||
}
|
||||
|
||||
public function findForApi(int $orderId): ?array
|
||||
{
|
||||
if ($orderId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$order = $this->db->get('pp_shop_orders', '*', ['id' => $orderId]);
|
||||
if (!is_array($order)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$order['id'] = (int)($order['id'] ?? 0);
|
||||
$order['status'] = (int)($order['status'] ?? 0);
|
||||
$order['paid'] = (int)($order['paid'] ?? 0);
|
||||
$order['summary'] = (float)($order['summary'] ?? 0);
|
||||
$order['transport_cost'] = (float)($order['transport_cost'] ?? 0);
|
||||
$order['products'] = $this->orderProducts($orderId);
|
||||
$order['statuses'] = $this->orderStatusHistory($orderId);
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
public function touchUpdatedAt(int $orderId): void
|
||||
{
|
||||
if ($orderId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->update('pp_shop_orders', [
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
], [
|
||||
'id' => $orderId,
|
||||
]);
|
||||
}
|
||||
|
||||
private function nullableString(string $value): ?string
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
@@ -444,6 +444,10 @@ class SettingsController
|
||||
'label' => 'Htaccess cache',
|
||||
'tab' => 'system',
|
||||
]),
|
||||
FormField::text('api_key', [
|
||||
'label' => 'Klucz API (ordersPRO)',
|
||||
'tab' => 'system',
|
||||
]),
|
||||
|
||||
FormField::text('google_tag_manager_id', [
|
||||
'label' => 'Google Tag Manager - ID',
|
||||
|
||||
144
autoload/api/ApiRouter.php
Normal file
144
autoload/api/ApiRouter.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
namespace api;
|
||||
|
||||
use Domain\Settings\SettingsRepository;
|
||||
|
||||
class ApiRouter
|
||||
{
|
||||
private $db;
|
||||
private $settingsRepo;
|
||||
|
||||
public function __construct($db, SettingsRepository $settingsRepo)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->settingsRepo = $settingsRepo;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if (!headers_sent()) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$this->authenticate()) {
|
||||
self::sendError('UNAUTHORIZED', 'Invalid or missing API key', 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$endpoint = trim((string)($_GET['endpoint'] ?? ''));
|
||||
$action = trim((string)($_GET['action'] ?? ''));
|
||||
|
||||
if ($endpoint === '' || $action === '') {
|
||||
self::sendError('BAD_REQUEST', 'Missing endpoint or action parameter', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$controller = $this->resolveController($endpoint);
|
||||
if ($controller === null) {
|
||||
self::sendError('NOT_FOUND', 'Unknown endpoint: ' . $endpoint, 404);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!method_exists($controller, $action)) {
|
||||
self::sendError('NOT_FOUND', 'Unknown action: ' . $action, 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$controller->$action();
|
||||
} catch (\Exception $e) {
|
||||
self::sendError('INTERNAL_ERROR', 'Internal server error', 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function authenticate(): bool
|
||||
{
|
||||
$headerKey = isset($_SERVER['HTTP_X_API_KEY']) ? $_SERVER['HTTP_X_API_KEY'] : '';
|
||||
if ($headerKey === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$storedKey = $this->settingsRepo->getSingleValue('api_key');
|
||||
if ($storedKey === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hash_equals($storedKey, $headerKey);
|
||||
}
|
||||
|
||||
private function resolveController(string $endpoint)
|
||||
{
|
||||
$factories = $this->getControllerFactories();
|
||||
|
||||
if (!isset($factories[$endpoint])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $factories[$endpoint]();
|
||||
}
|
||||
|
||||
private function getControllerFactories(): array
|
||||
{
|
||||
$db = $this->db;
|
||||
|
||||
return [
|
||||
'orders' => function () use ($db) {
|
||||
$orderRepo = new \Domain\Order\OrderRepository($db);
|
||||
$settingsRepo = new \Domain\Settings\SettingsRepository($db);
|
||||
$productRepo = new \Domain\Product\ProductRepository($db);
|
||||
$transportRepo = new \Domain\Transport\TransportRepository($db);
|
||||
$service = new \Domain\Order\OrderAdminService($orderRepo, $productRepo, $settingsRepo, $transportRepo);
|
||||
return new Controllers\OrdersApiController($service, $orderRepo);
|
||||
},
|
||||
'dictionaries' => function () use ($db) {
|
||||
$statusRepo = new \Domain\ShopStatus\ShopStatusRepository($db);
|
||||
$transportRepo = new \Domain\Transport\TransportRepository($db);
|
||||
$paymentRepo = new \Domain\PaymentMethod\PaymentMethodRepository($db);
|
||||
return new Controllers\DictionariesApiController($statusRepo, $transportRepo, $paymentRepo);
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Static response helpers
|
||||
// =========================================================================
|
||||
|
||||
public static function sendSuccess($data): void
|
||||
{
|
||||
http_response_code(200);
|
||||
echo json_encode(['status' => 'ok', 'data' => $data], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
public static function sendError(string $code, string $message, int $httpCode = 400): void
|
||||
{
|
||||
http_response_code($httpCode);
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
public static function getJsonBody(): ?array
|
||||
{
|
||||
$raw = file_get_contents('php://input');
|
||||
if ($raw === '' || $raw === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
|
||||
return is_array($data) ? $data : null;
|
||||
}
|
||||
|
||||
public static function requireMethod(string $method): bool
|
||||
{
|
||||
$requestMethod = isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET';
|
||||
if ($requestMethod !== strtoupper($method)) {
|
||||
self::sendError('METHOD_NOT_ALLOWED', 'Method ' . $requestMethod . ' not allowed, expected ' . strtoupper($method), 405);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
82
autoload/api/Controllers/DictionariesApiController.php
Normal file
82
autoload/api/Controllers/DictionariesApiController.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
namespace api\Controllers;
|
||||
|
||||
use api\ApiRouter;
|
||||
use Domain\ShopStatus\ShopStatusRepository;
|
||||
use Domain\Transport\TransportRepository;
|
||||
use Domain\PaymentMethod\PaymentMethodRepository;
|
||||
|
||||
class DictionariesApiController
|
||||
{
|
||||
private $statusRepo;
|
||||
private $transportRepo;
|
||||
private $paymentRepo;
|
||||
|
||||
public function __construct(
|
||||
ShopStatusRepository $statusRepo,
|
||||
TransportRepository $transportRepo,
|
||||
PaymentMethodRepository $paymentRepo
|
||||
) {
|
||||
$this->statusRepo = $statusRepo;
|
||||
$this->transportRepo = $transportRepo;
|
||||
$this->paymentRepo = $paymentRepo;
|
||||
}
|
||||
|
||||
public function statuses(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('GET')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$statuses = $this->statusRepo->allStatuses();
|
||||
|
||||
$result = [];
|
||||
foreach ($statuses as $id => $name) {
|
||||
$result[] = [
|
||||
'id' => (int)$id,
|
||||
'name' => (string)$name,
|
||||
];
|
||||
}
|
||||
|
||||
ApiRouter::sendSuccess($result);
|
||||
}
|
||||
|
||||
public function transports(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('GET')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$transports = $this->transportRepo->allActive();
|
||||
|
||||
$result = [];
|
||||
foreach ($transports as $transport) {
|
||||
$result[] = [
|
||||
'id' => (int)($transport['id'] ?? 0),
|
||||
'name' => (string)($transport['name_visible'] ?? $transport['name'] ?? ''),
|
||||
'cost' => (float)($transport['cost'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
ApiRouter::sendSuccess($result);
|
||||
}
|
||||
|
||||
public function payment_methods(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('GET')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$methods = $this->paymentRepo->allActive();
|
||||
|
||||
$result = [];
|
||||
foreach ($methods as $method) {
|
||||
$result[] = [
|
||||
'id' => (int)($method['id'] ?? 0),
|
||||
'name' => (string)($method['name'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
ApiRouter::sendSuccess($result);
|
||||
}
|
||||
}
|
||||
154
autoload/api/Controllers/OrdersApiController.php
Normal file
154
autoload/api/Controllers/OrdersApiController.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
namespace api\Controllers;
|
||||
|
||||
use api\ApiRouter;
|
||||
use Domain\Order\OrderAdminService;
|
||||
use Domain\Order\OrderRepository;
|
||||
|
||||
class OrdersApiController
|
||||
{
|
||||
private $service;
|
||||
private $orderRepo;
|
||||
|
||||
public function __construct(OrderAdminService $service, OrderRepository $orderRepo)
|
||||
{
|
||||
$this->service = $service;
|
||||
$this->orderRepo = $orderRepo;
|
||||
}
|
||||
|
||||
public function list(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('GET')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filters = [
|
||||
'status' => isset($_GET['status']) ? $_GET['status'] : '',
|
||||
'paid' => isset($_GET['paid']) ? $_GET['paid'] : '',
|
||||
'date_from' => isset($_GET['date_from']) ? $_GET['date_from'] : '',
|
||||
'date_to' => isset($_GET['date_to']) ? $_GET['date_to'] : '',
|
||||
'updated_since' => isset($_GET['updated_since']) ? $_GET['updated_since'] : '',
|
||||
'number' => isset($_GET['number']) ? $_GET['number'] : '',
|
||||
'client' => isset($_GET['client']) ? $_GET['client'] : '',
|
||||
];
|
||||
|
||||
$page = max(1, (int)(isset($_GET['page']) ? $_GET['page'] : 1));
|
||||
$perPage = max(1, min(100, (int)(isset($_GET['per_page']) ? $_GET['per_page'] : 50)));
|
||||
|
||||
$result = $this->orderRepo->listForApi($filters, $page, $perPage);
|
||||
|
||||
ApiRouter::sendSuccess($result);
|
||||
}
|
||||
|
||||
public function get(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('GET')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = (int)(isset($_GET['id']) ? $_GET['id'] : 0);
|
||||
if ($id <= 0) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid id parameter', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$order = $this->orderRepo->findForApi($id);
|
||||
if ($order === null) {
|
||||
ApiRouter::sendError('NOT_FOUND', 'Order not found', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
ApiRouter::sendSuccess($order);
|
||||
}
|
||||
|
||||
public function change_status(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('PUT')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = (int)(isset($_GET['id']) ? $_GET['id'] : 0);
|
||||
if ($id <= 0) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid id parameter', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$body = ApiRouter::getJsonBody();
|
||||
if ($body === null || !isset($body['status_id'])) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing status_id in request body', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$statusId = (int)$body['status_id'];
|
||||
$sendEmail = !empty($body['send_email']);
|
||||
|
||||
$order = $this->orderRepo->findRawById($id);
|
||||
if ($order === null) {
|
||||
ApiRouter::sendError('NOT_FOUND', 'Order not found', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->service->changeStatus($id, $statusId, $sendEmail);
|
||||
|
||||
ApiRouter::sendSuccess([
|
||||
'order_id' => $id,
|
||||
'status_id' => $statusId,
|
||||
'changed' => !empty($result['result']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function set_paid(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('PUT')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = (int)(isset($_GET['id']) ? $_GET['id'] : 0);
|
||||
if ($id <= 0) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid id parameter', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$order = $this->orderRepo->findRawById($id);
|
||||
if ($order === null) {
|
||||
ApiRouter::sendError('NOT_FOUND', 'Order not found', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$body = ApiRouter::getJsonBody();
|
||||
$sendEmail = ($body !== null && !empty($body['send_email']));
|
||||
|
||||
$this->service->setOrderAsPaid($id, $sendEmail);
|
||||
|
||||
ApiRouter::sendSuccess([
|
||||
'order_id' => $id,
|
||||
'paid' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function set_unpaid(): void
|
||||
{
|
||||
if (!ApiRouter::requireMethod('PUT')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = (int)(isset($_GET['id']) ? $_GET['id'] : 0);
|
||||
if ($id <= 0) {
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid id parameter', 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$order = $this->orderRepo->findRawById($id);
|
||||
if ($order === null) {
|
||||
ApiRouter::sendError('NOT_FOUND', 'Order not found', 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->service->setOrderAsUnpaid($id);
|
||||
|
||||
ApiRouter::sendSuccess([
|
||||
'order_id' => $id,
|
||||
'paid' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
198
docs/API.md
Normal file
198
docs/API.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# shopPRO REST API
|
||||
|
||||
REST API do integracji z ordersPRO i innymi systemami zewnetrznymi.
|
||||
|
||||
## Autentykacja
|
||||
|
||||
Kazde zapytanie wymaga headera `X-Api-Key` z kluczem API.
|
||||
|
||||
```
|
||||
X-Api-Key: {klucz_api}
|
||||
```
|
||||
|
||||
Klucz przechowywany jest w `pp_settings` jako parametr `api_key`. API jest stateless (bez sesji).
|
||||
|
||||
## Format odpowiedzi
|
||||
|
||||
### Sukces (HTTP 200)
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Blad
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"code": "UNAUTHORIZED",
|
||||
"message": "Invalid or missing API key"
|
||||
}
|
||||
```
|
||||
|
||||
Kody bledow:
|
||||
| Kod | HTTP | Opis |
|
||||
|-----|------|------|
|
||||
| `UNAUTHORIZED` | 401 | Brak lub nieprawidlowy klucz API |
|
||||
| `BAD_REQUEST` | 400 | Brakujace lub niepoprawne parametry |
|
||||
| `NOT_FOUND` | 404 | Nie znaleziono zasobu/endpointu/akcji |
|
||||
| `METHOD_NOT_ALLOWED` | 405 | Nieprawidlowa metoda HTTP |
|
||||
| `INTERNAL_ERROR` | 500 | Blad wewnetrzny serwera |
|
||||
|
||||
## Endpointy
|
||||
|
||||
### Zamowienia
|
||||
|
||||
#### Lista zamowien
|
||||
```
|
||||
GET api.php?endpoint=orders&action=list
|
||||
```
|
||||
|
||||
Parametry filtrowania (opcjonalne):
|
||||
| Parametr | Typ | Opis |
|
||||
|----------|-----|------|
|
||||
| `status` | int | Filtruj po statusie zamowienia |
|
||||
| `paid` | int (0/1) | Filtruj po statusie platnosci |
|
||||
| `date_from` | date (YYYY-MM-DD) | Zamowienia od daty |
|
||||
| `date_to` | date (YYYY-MM-DD) | Zamowienia do daty |
|
||||
| `updated_since` | datetime (YYYY-MM-DD HH:MM:SS) | Zamowienia zmodyfikowane od podanej daty (klucz do pollingu) |
|
||||
| `number` | string | Szukaj po numerze zamowienia |
|
||||
| `client` | string | Szukaj po imieniu, nazwisku lub emailu klienta |
|
||||
| `page` | int | Numer strony (domyslnie 1) |
|
||||
| `per_page` | int | Wynikow na strone (domyslnie 50, max 100) |
|
||||
|
||||
Odpowiedz:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": 42,
|
||||
"number": "2026/02/001",
|
||||
"date_order": "2026-02-19 10:30:00",
|
||||
"updated_at": "2026-02-19 12:00:00",
|
||||
"status": 4,
|
||||
"paid": 1,
|
||||
"client_name": "Jan",
|
||||
"client_surname": "Kowalski",
|
||||
"client_email": "jan@example.com",
|
||||
"client_phone": "111222333",
|
||||
"client_street": "Testowa 1",
|
||||
"client_postal_code": "00-000",
|
||||
"client_city": "Warszawa",
|
||||
"firm_name": null,
|
||||
"firm_nip": null,
|
||||
"transport": "Kurier DPD",
|
||||
"transport_cost": 15.00,
|
||||
"payment_method": "Przelew bankowy",
|
||||
"summary": 150.00
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"per_page": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Szczegoly zamowienia
|
||||
```
|
||||
GET api.php?endpoint=orders&action=get&id={order_id}
|
||||
```
|
||||
|
||||
Zwraca pelne dane zamowienia z produktami i historia statusow.
|
||||
|
||||
#### Zmiana statusu zamowienia
|
||||
```
|
||||
PUT api.php?endpoint=orders&action=change_status&id={order_id}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status_id": 5,
|
||||
"send_email": true
|
||||
}
|
||||
```
|
||||
|
||||
Odpowiedz:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"order_id": 42,
|
||||
"status_id": 5,
|
||||
"changed": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Oznacz jako oplacone
|
||||
```
|
||||
PUT api.php?endpoint=orders&action=set_paid&id={order_id}
|
||||
```
|
||||
|
||||
Opcjonalnie w body: `{"send_email": true}`
|
||||
|
||||
#### Oznacz jako nieoplacone
|
||||
```
|
||||
PUT api.php?endpoint=orders&action=set_unpaid&id={order_id}
|
||||
```
|
||||
|
||||
### Slowniki
|
||||
|
||||
#### Lista statusow zamowien
|
||||
```
|
||||
GET api.php?endpoint=dictionaries&action=statuses
|
||||
```
|
||||
|
||||
Odpowiedz:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"data": [
|
||||
{"id": 0, "name": "Nowe"},
|
||||
{"id": 1, "name": "Oplacone"},
|
||||
{"id": 4, "name": "W realizacji"},
|
||||
{"id": 6, "name": "Wyslane"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Lista metod transportu
|
||||
```
|
||||
GET api.php?endpoint=dictionaries&action=transports
|
||||
```
|
||||
|
||||
#### Lista metod platnosci
|
||||
```
|
||||
GET api.php?endpoint=dictionaries&action=payment_methods
|
||||
```
|
||||
|
||||
## Polling
|
||||
|
||||
Aby pobierac tylko nowe/zmienione zamowienia, uzyj parametru `updated_since`:
|
||||
|
||||
```
|
||||
GET api.php?endpoint=orders&action=list&updated_since=2026-02-19 12:00:00
|
||||
```
|
||||
|
||||
Kolumna `updated_at` w `pp_shop_orders` jest aktualizowana automatycznie przy kazdej modyfikacji zamowienia (zmiana statusu, platnosci, edycja danych, tworzenie zamowienia).
|
||||
|
||||
## Konfiguracja
|
||||
|
||||
Klucz API ustawia sie w panelu admina w ustawieniach sklepu lub bezposrednio w bazie:
|
||||
|
||||
```sql
|
||||
INSERT INTO pp_settings (param, value) VALUES ('api_key', 'twoj-klucz-api');
|
||||
-- lub
|
||||
UPDATE pp_settings SET value = 'twoj-klucz-api' WHERE param = 'api_key';
|
||||
```
|
||||
|
||||
## Architektura
|
||||
|
||||
- Entry point: `api.php`
|
||||
- Router: `\api\ApiRouter` (`autoload/api/ApiRouter.php`)
|
||||
- Kontrolery: `autoload/api/Controllers/`
|
||||
- `OrdersApiController` — zamowienia (5 akcji)
|
||||
- `DictionariesApiController` — slowniki (3 akcje)
|
||||
@@ -4,6 +4,26 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
|
||||
|
||||
---
|
||||
|
||||
## ver. 0.296 (2026-02-19) - REST API zamówień dla ordersPRO
|
||||
|
||||
- **NEW**: REST API do zarządzania zamówieniami — lista, szczegóły, zmiana statusu, oznaczanie płatności
|
||||
- **NEW**: Endpointy słownikowe — statusy zamówień, metody transportu, metody płatności
|
||||
- **NEW**: Autentykacja API przez header `X-Api-Key` (klucz w `pp_settings`)
|
||||
- **NEW**: `\api\ApiRouter` — router API z autentykacją, routingiem i helperami odpowiedzi
|
||||
- **NEW**: `\api\Controllers\OrdersApiController` — 5 akcji (list, get, change_status, set_paid, set_unpaid)
|
||||
- **NEW**: `\api\Controllers\DictionariesApiController` — 3 akcje (statuses, transports, payment_methods)
|
||||
- **NEW**: `OrderRepository::listForApi()` — lista zamówień z filtrowaniem i paginacją (z `updated_since` do pollingu)
|
||||
- **NEW**: `OrderRepository::findForApi()` — szczegóły zamówienia z produktami i historią statusów
|
||||
- **NEW**: `OrderRepository::touchUpdatedAt()` — aktualizacja `updated_at` przy modyfikacji zamówienia
|
||||
- **NEW**: Kolumna `pp_shop_orders.updated_at` — data ostatniej modyfikacji (polling API)
|
||||
- **NEW**: Setting `api_key` w `pp_settings` — klucz autentykacji API
|
||||
- **UPDATE**: `api.php` — skip sesji dla requestów API, routing do `ApiRouter`
|
||||
- **UPDATE**: Metody `updateOrderStatus`, `setAsPaid`, `setAsUnpaid`, `saveOrderByAdmin`, `saveNotes`, `createFromBasket` wywołują `touchUpdatedAt()`
|
||||
- **NEW**: `docs/API.md` — dokumentacja REST API
|
||||
- **Tests**: 30 nowych testów API (ApiRouter, OrdersApiController, DictionariesApiController)
|
||||
|
||||
---
|
||||
|
||||
## ver. 0.295 (2026-02-19) - Admin: edycja produktów w zamówieniu + wyszukiwanie AJAX + korekta stanów magazynowych
|
||||
|
||||
- **NEW**: Edycja produktów w zamówieniu z panelu admina (dodawanie, usuwanie, zmiana ilości/cen)
|
||||
|
||||
@@ -110,8 +110,9 @@ Zamówienia sklepu (źródło danych dla list i szczegółów klientów w panelu
|
||||
| payment_method | Nazwa metody płatności |
|
||||
| transport | Nazwa transportu |
|
||||
| message | Wiadomość klienta |
|
||||
| updated_at | Data ostatniej modyfikacji (polling API) |
|
||||
|
||||
**Używane w:** `Domain\Client\ClientRepository::listForAdmin()`, `Domain\Client\ClientRepository::ordersForClient()`, `Domain\Client\ClientRepository::totalsForClient()`.
|
||||
**Używane w:** `Domain\Client\ClientRepository::listForAdmin()`, `Domain\Client\ClientRepository::ordersForClient()`, `Domain\Client\ClientRepository::totalsForClient()`, `Domain\Order\OrderRepository::listForApi()`, `Domain\Order\OrderRepository::findForApi()`.
|
||||
|
||||
**Aktualizacja 2026-02-15 (ver. 0.274):** moduł `/admin/shop_clients/*` korzysta z `Domain\Client\ClientRepository` przez `admin\Controllers\ShopClientsController`.
|
||||
|
||||
|
||||
@@ -72,6 +72,20 @@ Newsletter, Search, ShopBasket, ShopClient, ShopCoupon, ShopOrder, ShopProducer,
|
||||
### Widoki (`front\Views\`) — 11 klas statycznych
|
||||
Articles, Banners, Languages, Menu, Newsletter, Scontainers, ShopCategory, ShopClient, ShopPaymentMethod, ShopProduct, ShopSearch
|
||||
|
||||
## Warstwa API (`autoload/api/`)
|
||||
|
||||
REST API dla ordersPRO. Entry point: `api.php`. Stateless (bez sesji), autentykacja przez `X-Api-Key` header.
|
||||
|
||||
### Router: `api\ApiRouter`
|
||||
- `handle()` — autentykacja → routing → dispatch
|
||||
- Helpery statyczne: `sendSuccess()`, `sendError()`, `getJsonBody()`, `requireMethod()`
|
||||
|
||||
### Kontrolery (`api\Controllers\`)
|
||||
- `OrdersApiController` — lista, szczegoly, zmiana statusu, platnosc (5 akcji)
|
||||
- `DictionariesApiController` — statusy, transporty, metody platnosci (3 akcje)
|
||||
|
||||
Dokumentacja: `docs/API.md`
|
||||
|
||||
## Warstwa wspoldzielona (`autoload/Shared/`)
|
||||
|
||||
| Klasa | Opis |
|
||||
@@ -106,7 +120,7 @@ ProductRepository::productSetsWhenAddToBasket:{id} — zestawy "kupowane r
|
||||
|------|------|
|
||||
| `index.php` | Frontend — autoload, sesja, DB, routing (`front\App`), layout (`front\LayoutEngine`), DOM post-processing |
|
||||
| `ajax.php` | Frontend AJAX — koszyk, transport, kontakt |
|
||||
| `api.php` | REST API (Ekomi CSV) |
|
||||
| `api.php` | REST API (ordersPRO + Ekomi CSV) — router: `\api\ApiRouter`, kontrolery: `\api\Controllers\` |
|
||||
| `admin/index.php` | Admin — autoload, sesja, DB, routing (`admin\App`) |
|
||||
| `admin/ajax.php` | Admin AJAX |
|
||||
| `cron.php` | CRON: Apilo sync (ceny/stany co 10min, cennik co 1h, retry queue) |
|
||||
|
||||
@@ -23,10 +23,10 @@ composer test # standard
|
||||
## Aktualny stan
|
||||
|
||||
```text
|
||||
OK (636 tests, 1868 assertions)
|
||||
OK (666 tests, 1930 assertions)
|
||||
```
|
||||
|
||||
Zweryfikowano: 2026-02-19 (ver. 0.295)
|
||||
Zweryfikowano: 2026-02-19 (ver. 0.296)
|
||||
|
||||
## Konfiguracja
|
||||
|
||||
@@ -85,12 +85,17 @@ tests/
|
||||
| |-- ShopStatusesControllerTest.php
|
||||
| |-- ShopTransportControllerTest.php
|
||||
| `-- UsersControllerTest.php
|
||||
| `-- api/
|
||||
| |-- ApiRouterTest.php
|
||||
| `-- Controllers/
|
||||
| |-- OrdersApiControllerTest.php
|
||||
| `-- DictionariesApiControllerTest.php
|
||||
`-- Integration/ (puste — zarezerwowane)
|
||||
```
|
||||
|
||||
## Dodawanie nowych testow
|
||||
|
||||
1. Plik w `tests/Unit/Domain/<Module>/<Class>Test.php` lub `tests/Unit/admin/Controllers/<Class>Test.php`.
|
||||
1. Plik w `tests/Unit/Domain/<Module>/<Class>Test.php`, `tests/Unit/admin/Controllers/<Class>Test.php` lub `tests/Unit/api/Controllers/<Class>Test.php`.
|
||||
2. Rozszerz `PHPUnit\Framework\TestCase`.
|
||||
3. Nazwy metod zaczynaj od `test`.
|
||||
4. Wzorzec AAA: Arrange, Act, Assert.
|
||||
|
||||
@@ -18,16 +18,17 @@ Aktualizacje znajdują się w folderze `updates/0.XX/` gdzie XX oznacza dziesią
|
||||
|
||||
## Procedura tworzenia nowej aktualizacji
|
||||
|
||||
## Status biezacej aktualizacji (ver. 0.295)
|
||||
## Status biezacej aktualizacji (ver. 0.296)
|
||||
|
||||
- Wersja udostepniona: `0.295` (data: 2026-02-19).
|
||||
- Wersja udostepniona: `0.296` (data: 2026-02-19).
|
||||
- Pliki publikacyjne:
|
||||
- `updates/0.20/ver_0.295.zip`
|
||||
- `updates/0.20/ver_0.296.zip`
|
||||
- `updates/0.20/ver_0.296_sql.txt`
|
||||
- Pliki metadanych aktualizacji:
|
||||
- `updates/changelog.php`
|
||||
- `updates/versions.php` (`$current_ver = 295`)
|
||||
- `updates/versions.php` (`$current_ver = 296`)
|
||||
- Weryfikacja testow przed publikacja:
|
||||
- `OK (636 tests, 1868 assertions)`
|
||||
- `OK (666 tests, 1930 assertions)`
|
||||
|
||||
### 1. Określ numer wersji
|
||||
Sprawdź ostatnią wersję w `updates/` i zwiększ o 1.
|
||||
|
||||
177
tests/Unit/api/ApiRouterTest.php
Normal file
177
tests/Unit/api/ApiRouterTest.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
namespace Tests\Unit\api;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use api\ApiRouter;
|
||||
use Domain\Settings\SettingsRepository;
|
||||
|
||||
class ApiRouterTest extends TestCase
|
||||
{
|
||||
private function createRouter(string $storedApiKey = 'test-api-key-123'): ApiRouter
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockSettings = $this->createMock(SettingsRepository::class);
|
||||
$mockSettings->method('getSingleValue')
|
||||
->with('api_key')
|
||||
->willReturn($storedApiKey);
|
||||
|
||||
return new ApiRouter($mockDb, $mockSettings);
|
||||
}
|
||||
|
||||
public function testHandleReturns401WhenNoApiKey(): void
|
||||
{
|
||||
unset($_SERVER['HTTP_X_API_KEY']);
|
||||
$_GET['endpoint'] = 'orders';
|
||||
$_GET['action'] = 'list';
|
||||
|
||||
$router = $this->createRouter();
|
||||
|
||||
ob_start();
|
||||
$router->handle();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(401, http_response_code());
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('error', $json['status']);
|
||||
$this->assertSame('UNAUTHORIZED', $json['code']);
|
||||
}
|
||||
|
||||
public function testHandleReturns401WhenWrongApiKey(): void
|
||||
{
|
||||
$_SERVER['HTTP_X_API_KEY'] = 'wrong-key';
|
||||
$_GET['endpoint'] = 'orders';
|
||||
$_GET['action'] = 'list';
|
||||
|
||||
$router = $this->createRouter();
|
||||
|
||||
ob_start();
|
||||
$router->handle();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(401, http_response_code());
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('UNAUTHORIZED', $json['code']);
|
||||
}
|
||||
|
||||
public function testHandleReturns401WhenStoredKeyEmpty(): void
|
||||
{
|
||||
$_SERVER['HTTP_X_API_KEY'] = 'any-key';
|
||||
$_GET['endpoint'] = 'orders';
|
||||
$_GET['action'] = 'list';
|
||||
|
||||
$router = $this->createRouter('');
|
||||
|
||||
ob_start();
|
||||
$router->handle();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(401, http_response_code());
|
||||
}
|
||||
|
||||
public function testHandleReturns400WhenMissingEndpoint(): void
|
||||
{
|
||||
$_SERVER['HTTP_X_API_KEY'] = 'test-api-key-123';
|
||||
unset($_GET['endpoint']);
|
||||
$_GET['action'] = 'list';
|
||||
$_GET['endpoint'] = '';
|
||||
|
||||
$router = $this->createRouter();
|
||||
|
||||
ob_start();
|
||||
$router->handle();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(400, http_response_code());
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('BAD_REQUEST', $json['code']);
|
||||
}
|
||||
|
||||
public function testHandleReturns400WhenMissingAction(): void
|
||||
{
|
||||
$_SERVER['HTTP_X_API_KEY'] = 'test-api-key-123';
|
||||
$_GET['endpoint'] = 'orders';
|
||||
$_GET['action'] = '';
|
||||
|
||||
$router = $this->createRouter();
|
||||
|
||||
ob_start();
|
||||
$router->handle();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(400, http_response_code());
|
||||
}
|
||||
|
||||
public function testHandleReturns404ForUnknownEndpoint(): void
|
||||
{
|
||||
$_SERVER['HTTP_X_API_KEY'] = 'test-api-key-123';
|
||||
$_GET['endpoint'] = 'unknown';
|
||||
$_GET['action'] = 'list';
|
||||
|
||||
$router = $this->createRouter();
|
||||
|
||||
ob_start();
|
||||
$router->handle();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(404, http_response_code());
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('NOT_FOUND', $json['code']);
|
||||
}
|
||||
|
||||
public function testSendSuccessOutputsCorrectJson(): void
|
||||
{
|
||||
ob_start();
|
||||
ApiRouter::sendSuccess(['foo' => 'bar']);
|
||||
$output = ob_get_clean();
|
||||
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('ok', $json['status']);
|
||||
$this->assertSame('bar', $json['data']['foo']);
|
||||
}
|
||||
|
||||
public function testSendErrorOutputsCorrectJson(): void
|
||||
{
|
||||
ob_start();
|
||||
ApiRouter::sendError('BAD_REQUEST', 'Test error', 400);
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(400, http_response_code());
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('error', $json['status']);
|
||||
$this->assertSame('BAD_REQUEST', $json['code']);
|
||||
$this->assertSame('Test error', $json['message']);
|
||||
}
|
||||
|
||||
public function testRequireMethodReturnsTrueForMatchingMethod(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
|
||||
ob_start();
|
||||
$result = ApiRouter::requireMethod('GET');
|
||||
ob_get_clean();
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testRequireMethodReturnsFalseAndSendsErrorForMismatch(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
|
||||
ob_start();
|
||||
$result = ApiRouter::requireMethod('GET');
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertFalse($result);
|
||||
$this->assertSame(405, http_response_code());
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('METHOD_NOT_ALLOWED', $json['code']);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($_SERVER['HTTP_X_API_KEY']);
|
||||
unset($_SERVER['REQUEST_METHOD']);
|
||||
$_GET = [];
|
||||
http_response_code(200);
|
||||
}
|
||||
}
|
||||
139
tests/Unit/api/Controllers/DictionariesApiControllerTest.php
Normal file
139
tests/Unit/api/Controllers/DictionariesApiControllerTest.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
namespace Tests\Unit\api\Controllers;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use api\Controllers\DictionariesApiController;
|
||||
use Domain\ShopStatus\ShopStatusRepository;
|
||||
use Domain\Transport\TransportRepository;
|
||||
use Domain\PaymentMethod\PaymentMethodRepository;
|
||||
|
||||
class DictionariesApiControllerTest extends TestCase
|
||||
{
|
||||
private $mockStatusRepo;
|
||||
private $mockTransportRepo;
|
||||
private $mockPaymentRepo;
|
||||
private $controller;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->mockStatusRepo = $this->createMock(ShopStatusRepository::class);
|
||||
$this->mockTransportRepo = $this->createMock(TransportRepository::class);
|
||||
$this->mockPaymentRepo = $this->createMock(PaymentMethodRepository::class);
|
||||
|
||||
$this->controller = new DictionariesApiController(
|
||||
$this->mockStatusRepo,
|
||||
$this->mockTransportRepo,
|
||||
$this->mockPaymentRepo
|
||||
);
|
||||
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
http_response_code(200);
|
||||
}
|
||||
|
||||
// --- statuses ---
|
||||
|
||||
public function testStatusesReturnsFormattedList(): void
|
||||
{
|
||||
$this->mockStatusRepo->method('allStatuses')
|
||||
->willReturn([
|
||||
0 => 'Nowe',
|
||||
1 => 'Opłacone',
|
||||
4 => 'W realizacji',
|
||||
6 => 'Wysłane',
|
||||
]);
|
||||
|
||||
ob_start();
|
||||
$this->controller->statuses();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('ok', $json['status']);
|
||||
$this->assertCount(4, $json['data']);
|
||||
$this->assertSame(0, $json['data'][0]['id']);
|
||||
$this->assertSame('Nowe', $json['data'][0]['name']);
|
||||
$this->assertSame(6, $json['data'][3]['id']);
|
||||
$this->assertSame('Wysłane', $json['data'][3]['name']);
|
||||
}
|
||||
|
||||
public function testStatusesRejectsPostMethod(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
|
||||
ob_start();
|
||||
$this->controller->statuses();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(405, http_response_code());
|
||||
}
|
||||
|
||||
// --- transports ---
|
||||
|
||||
public function testTransportsReturnsFormattedList(): void
|
||||
{
|
||||
$this->mockTransportRepo->method('allActive')
|
||||
->willReturn([
|
||||
['id' => 1, 'name_visible' => 'InPost Paczkomat', 'cost' => '12.99'],
|
||||
['id' => 2, 'name_visible' => 'Kurier DPD', 'cost' => '15.00'],
|
||||
]);
|
||||
|
||||
ob_start();
|
||||
$this->controller->transports();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('ok', $json['status']);
|
||||
$this->assertCount(2, $json['data']);
|
||||
$this->assertSame(1, $json['data'][0]['id']);
|
||||
$this->assertSame('InPost Paczkomat', $json['data'][0]['name']);
|
||||
$this->assertSame(12.99, $json['data'][0]['cost']);
|
||||
}
|
||||
|
||||
public function testTransportsRejectsPostMethod(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
|
||||
ob_start();
|
||||
$this->controller->transports();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(405, http_response_code());
|
||||
}
|
||||
|
||||
// --- payment_methods ---
|
||||
|
||||
public function testPaymentMethodsReturnsFormattedList(): void
|
||||
{
|
||||
$this->mockPaymentRepo->method('allActive')
|
||||
->willReturn([
|
||||
['id' => 1, 'name' => 'Przelew bankowy'],
|
||||
['id' => 2, 'name' => 'Przelewy24'],
|
||||
['id' => 3, 'name' => 'Przy odbiorze'],
|
||||
]);
|
||||
|
||||
ob_start();
|
||||
$this->controller->payment_methods();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('ok', $json['status']);
|
||||
$this->assertCount(3, $json['data']);
|
||||
$this->assertSame(1, $json['data'][0]['id']);
|
||||
$this->assertSame('Przelew bankowy', $json['data'][0]['name']);
|
||||
}
|
||||
|
||||
public function testPaymentMethodsRejectsPostMethod(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
|
||||
ob_start();
|
||||
$this->controller->payment_methods();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(405, http_response_code());
|
||||
}
|
||||
}
|
||||
290
tests/Unit/api/Controllers/OrdersApiControllerTest.php
Normal file
290
tests/Unit/api/Controllers/OrdersApiControllerTest.php
Normal file
@@ -0,0 +1,290 @@
|
||||
<?php
|
||||
namespace Tests\Unit\api\Controllers;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use api\Controllers\OrdersApiController;
|
||||
use Domain\Order\OrderAdminService;
|
||||
use Domain\Order\OrderRepository;
|
||||
|
||||
class OrdersApiControllerTest extends TestCase
|
||||
{
|
||||
private $mockService;
|
||||
private $mockOrderRepo;
|
||||
private $controller;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->mockService = $this->createMock(OrderAdminService::class);
|
||||
$this->mockOrderRepo = $this->createMock(OrderRepository::class);
|
||||
$this->controller = new OrdersApiController($this->mockService, $this->mockOrderRepo);
|
||||
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_GET = [];
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_GET = [];
|
||||
http_response_code(200);
|
||||
}
|
||||
|
||||
// --- list ---
|
||||
|
||||
public function testListReturnsOrders(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
|
||||
$this->mockOrderRepo->method('listForApi')
|
||||
->willReturn([
|
||||
'items' => [
|
||||
['id' => 1, 'number' => '2026/01/001', 'status' => 0, 'paid' => 0, 'summary' => 99.99],
|
||||
],
|
||||
'total' => 1,
|
||||
'page' => 1,
|
||||
'per_page' => 50,
|
||||
]);
|
||||
|
||||
ob_start();
|
||||
$this->controller->list();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('ok', $json['status']);
|
||||
$this->assertCount(1, $json['data']['items']);
|
||||
$this->assertSame(1, $json['data']['total']);
|
||||
}
|
||||
|
||||
public function testListRejectsPostMethod(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
|
||||
ob_start();
|
||||
$this->controller->list();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(405, http_response_code());
|
||||
}
|
||||
|
||||
public function testListPassesFiltersToRepository(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_GET['status'] = '4';
|
||||
$_GET['paid'] = '1';
|
||||
$_GET['page'] = '2';
|
||||
$_GET['per_page'] = '25';
|
||||
|
||||
$this->mockOrderRepo->expects($this->once())
|
||||
->method('listForApi')
|
||||
->with(
|
||||
$this->callback(function ($filters) {
|
||||
return $filters['status'] === '4' && $filters['paid'] === '1';
|
||||
}),
|
||||
2,
|
||||
25
|
||||
)
|
||||
->willReturn(['items' => [], 'total' => 0, 'page' => 2, 'per_page' => 25]);
|
||||
|
||||
ob_start();
|
||||
$this->controller->list();
|
||||
ob_get_clean();
|
||||
}
|
||||
|
||||
// --- get ---
|
||||
|
||||
public function testGetReturnsOrder(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_GET['id'] = '42';
|
||||
|
||||
$this->mockOrderRepo->method('findForApi')
|
||||
->with(42)
|
||||
->willReturn([
|
||||
'id' => 42,
|
||||
'number' => '2026/01/001',
|
||||
'status' => 4,
|
||||
'paid' => 1,
|
||||
'summary' => 150.00,
|
||||
'products' => [],
|
||||
'statuses' => [],
|
||||
]);
|
||||
|
||||
ob_start();
|
||||
$this->controller->get();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('ok', $json['status']);
|
||||
$this->assertSame(42, $json['data']['id']);
|
||||
}
|
||||
|
||||
public function testGetReturns404WhenOrderNotFound(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_GET['id'] = '999';
|
||||
|
||||
$this->mockOrderRepo->method('findForApi')
|
||||
->with(999)
|
||||
->willReturn(null);
|
||||
|
||||
ob_start();
|
||||
$this->controller->get();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(404, http_response_code());
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('NOT_FOUND', $json['code']);
|
||||
}
|
||||
|
||||
public function testGetReturns400WhenMissingId(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
|
||||
ob_start();
|
||||
$this->controller->get();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(400, http_response_code());
|
||||
}
|
||||
|
||||
// --- change_status ---
|
||||
|
||||
public function testChangeStatusUpdatesOrder(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'PUT';
|
||||
$_GET['id'] = '10';
|
||||
|
||||
$this->mockOrderRepo->method('findRawById')
|
||||
->with(10)
|
||||
->willReturn(['id' => 10, 'status' => 0]);
|
||||
|
||||
$this->mockService->method('changeStatus')
|
||||
->with(10, 5, false)
|
||||
->willReturn(['result' => true]);
|
||||
|
||||
// Simulate JSON body via php://input override is not possible in unit tests,
|
||||
// so we test the controller logic path via mock expectations
|
||||
ob_start();
|
||||
$this->controller->change_status();
|
||||
$output = ob_get_clean();
|
||||
|
||||
// Without a real php://input body, getJsonBody returns null → BAD_REQUEST
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('error', $json['status']);
|
||||
$this->assertSame('BAD_REQUEST', $json['code']);
|
||||
}
|
||||
|
||||
public function testChangeStatusReturns400WhenMissingId(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'PUT';
|
||||
|
||||
ob_start();
|
||||
$this->controller->change_status();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(400, http_response_code());
|
||||
}
|
||||
|
||||
public function testChangeStatusRejectsGetMethod(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||
$_GET['id'] = '10';
|
||||
|
||||
ob_start();
|
||||
$this->controller->change_status();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(405, http_response_code());
|
||||
}
|
||||
|
||||
// --- set_paid ---
|
||||
|
||||
public function testSetPaidReturns404WhenOrderNotFound(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'PUT';
|
||||
$_GET['id'] = '999';
|
||||
|
||||
$this->mockOrderRepo->method('findRawById')
|
||||
->with(999)
|
||||
->willReturn(null);
|
||||
|
||||
ob_start();
|
||||
$this->controller->set_paid();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(404, http_response_code());
|
||||
}
|
||||
|
||||
public function testSetPaidReturns400WhenMissingId(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'PUT';
|
||||
|
||||
ob_start();
|
||||
$this->controller->set_paid();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(400, http_response_code());
|
||||
}
|
||||
|
||||
public function testSetPaidCallsServiceWhenOrderExists(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'PUT';
|
||||
$_GET['id'] = '10';
|
||||
|
||||
$this->mockOrderRepo->method('findRawById')
|
||||
->with(10)
|
||||
->willReturn(['id' => 10, 'paid' => 0]);
|
||||
|
||||
$this->mockService->expects($this->once())
|
||||
->method('setOrderAsPaid')
|
||||
->with(10, false);
|
||||
|
||||
ob_start();
|
||||
$this->controller->set_paid();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('ok', $json['status']);
|
||||
$this->assertSame(1, $json['data']['paid']);
|
||||
}
|
||||
|
||||
// --- set_unpaid ---
|
||||
|
||||
public function testSetUnpaidReturns404WhenOrderNotFound(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'PUT';
|
||||
$_GET['id'] = '999';
|
||||
|
||||
$this->mockOrderRepo->method('findRawById')
|
||||
->with(999)
|
||||
->willReturn(null);
|
||||
|
||||
ob_start();
|
||||
$this->controller->set_unpaid();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$this->assertSame(404, http_response_code());
|
||||
}
|
||||
|
||||
public function testSetUnpaidCallsServiceWhenOrderExists(): void
|
||||
{
|
||||
$_SERVER['REQUEST_METHOD'] = 'PUT';
|
||||
$_GET['id'] = '10';
|
||||
|
||||
$this->mockOrderRepo->method('findRawById')
|
||||
->with(10)
|
||||
->willReturn(['id' => 10, 'paid' => 1]);
|
||||
|
||||
$this->mockService->expects($this->once())
|
||||
->method('setOrderAsUnpaid')
|
||||
->with(10);
|
||||
|
||||
ob_start();
|
||||
$this->controller->set_unpaid();
|
||||
$output = ob_get_clean();
|
||||
|
||||
$json = json_decode($output, true);
|
||||
$this->assertSame('ok', $json['status']);
|
||||
$this->assertSame(0, $json['data']['paid']);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
|
||||
'admin\\Support\\Forms\\' => __DIR__ . '/../autoload/admin/Support/Forms/',
|
||||
'admin\\ViewModels\\Forms\\' => __DIR__ . '/../autoload/admin/ViewModels/Forms/',
|
||||
'admin\\Validation\\' => __DIR__ . '/../autoload/admin/Validation/',
|
||||
'api\\' => __DIR__ . '/../autoload/api/',
|
||||
];
|
||||
|
||||
foreach ($prefixes as $prefix => $baseDir) {
|
||||
|
||||
BIN
updates/0.20/ver_0.296.zip
Normal file
BIN
updates/0.20/ver_0.296.zip
Normal file
Binary file not shown.
4
updates/0.20/ver_0.296_sql.txt
Normal file
4
updates/0.20/ver_0.296_sql.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE `pp_shop_orders` ADD COLUMN `updated_at` DATETIME NULL DEFAULT NULL AFTER `date_order`;
|
||||
UPDATE `pp_shop_orders` SET `updated_at` = `date_order` WHERE `updated_at` IS NULL;
|
||||
CREATE INDEX `idx_pp_shop_orders_updated_at` ON `pp_shop_orders` (`updated_at`);
|
||||
INSERT INTO `pp_settings` (`param`, `value`) VALUES ('api_key', '');
|
||||
@@ -1,3 +1,9 @@
|
||||
<b>ver. 0.296 - 19.02.2026</b><br />
|
||||
- NEW - REST API zamówień dla ordersPRO (lista, szczegóły, zmiana statusu, płatności)
|
||||
- NEW - Endpointy słownikowe (statusy, transporty, metody płatności)
|
||||
- NEW - Autentykacja API przez X-Api-Key header
|
||||
- NEW - Kolumna updated_at w pp_shop_orders (polling zmian)
|
||||
<hr>
|
||||
<b>ver. 0.295 - 19.02.2026</b><br />
|
||||
- NEW - Edycja produktów w zamówieniu z panelu admina (dodawanie, usuwanie, zmiana ilości/cen)
|
||||
- NEW - Wyszukiwarka produktów AJAX w formularzu edycji zamówienia
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?
|
||||
$current_ver = 295;
|
||||
$current_ver = 296;
|
||||
|
||||
for ($i = 1; $i <= $current_ver; $i++)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user