From 85101b988140c1d0114d3176115aab0864011b16 Mon Sep 17 00:00:00 2001 From: Nikita Kiselev Date: Fri, 1 Aug 2025 09:49:54 +0300 Subject: [PATCH] feat(order): order process enchancements --- .../controller/extension/tgshop/handle.php | 7 + .../upload/oc_telegram_shop/.env | 1 + .../upload/oc_telegram_shop/.env.example | 1 + .../upload/oc_telegram_shop/composer.json | 3 +- .../upload/oc_telegram_shop/composer.lock | 463 +++++++++++++++++- .../framework/Application.php | 4 + .../framework/ErrorHandler.php | 20 +- .../framework/Http/Request.php | 5 + .../framework/Support/helpers.php | 41 +- .../OrderValidationFailedException.php | 28 ++ .../src/Handlers/OrderHandler.php | 152 +----- .../src/Services/CartService.php | 9 +- .../src/Services/OrderCreateService.php | 177 +++++++ spa/src/stores/CheckoutStore.js | 20 +- spa/src/utils/ftch.js | 1 + spa/src/views/Checkout.vue | 15 +- 16 files changed, 781 insertions(+), 166 deletions(-) create mode 100644 module/oc_telegram_shop/upload/oc_telegram_shop/.env create mode 100644 module/oc_telegram_shop/upload/oc_telegram_shop/.env.example create mode 100644 module/oc_telegram_shop/upload/oc_telegram_shop/src/Exceptions/OrderValidationFailedException.php create mode 100644 module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php diff --git a/module/oc_telegram_shop/upload/catalog/controller/extension/tgshop/handle.php b/module/oc_telegram_shop/upload/catalog/controller/extension/tgshop/handle.php index c078bde..a0f2674 100755 --- a/module/oc_telegram_shop/upload/catalog/controller/extension/tgshop/handle.php +++ b/module/oc_telegram_shop/upload/catalog/controller/extension/tgshop/handle.php @@ -26,7 +26,14 @@ class Controllerextensiontgshophandle extends Controller $app = ApplicationFactory::create([ 'oc_config_tax' => $this->config->get('config_tax'), 'oc_default_currency' => $this->config->get('config_currency'), + // ID группы покупателей, которая будет использоаваться в заказах через Телеграм. 'oc_customer_group_id' => $this->config->get('config_customer_group_id'), + // ID магазина, для которого будут создаваться заказы из Телеграм + 'oc_store_id' => 0, + // Название магазина, для которого будут создаваться заказы из Телеграм + 'oc_store_name' => $this->config->get('config_name'), + // ID статуса, с которым будут создаваться заказы через Телеграм по умолчанию. + 'oc_order_status_id' => 1, 'timezone' => $this->config->get('config_timezone', 'UTC'), 'language_id' => (int) $this->config->get('config_language_id'), 'shop_base_url' => HTTPS_SERVER, diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/.env b/module/oc_telegram_shop/upload/oc_telegram_shop/.env new file mode 100644 index 0000000..ea0466f --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/.env @@ -0,0 +1 @@ +APP_DEBUG=true diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/.env.example b/module/oc_telegram_shop/upload/oc_telegram_shop/.env.example new file mode 100644 index 0000000..97c57ae --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/.env.example @@ -0,0 +1 @@ +APP_DEBUG=false diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/composer.json b/module/oc_telegram_shop/upload/oc_telegram_shop/composer.json index 705da14..c664a69 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/composer.json +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/composer.json @@ -21,7 +21,8 @@ "psr/container": "^2.0", "ext-json": "*", "intervention/image": "^2.7", - "rakit/validation": "^1.4" + "rakit/validation": "^1.4", + "vlucas/phpdotenv": "^5.6" }, "require-dev": { "roave/security-advisories": "dev-latest" diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/composer.lock b/module/oc_telegram_shop/upload/oc_telegram_shop/composer.lock index ea8ba70..c2b22ea 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/composer.lock +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/composer.lock @@ -4,8 +4,70 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0862025c427a0e17dfde4fb54820c5c0", + "content-hash": "39c53e4dd9f5fa4822c5c5c87b882768", "packages": [ + { + "name": "graham-campbell/result-type", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:45:45+00:00" + }, { "name": "guzzlehttp/psr7", "version": "2.7.1", @@ -206,6 +268,81 @@ ], "time": "2022-05-21T17:30:32+00:00" }, + { + "name": "phpoption/phpoption", + "version": "1.9.3", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:41:07+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -456,6 +593,330 @@ "source": "https://github.com/ralouphie/getallheaders/tree/develop" }, "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.3", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-04-30T23:37:27+00:00" } ], "packages-dev": [ diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Application.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Application.php index 1266c65..ea8a3a8 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Application.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Application.php @@ -2,6 +2,7 @@ namespace Openguru\OpenCartFramework; +use Dotenv\Dotenv; use InvalidArgumentException; use Openguru\OpenCartFramework\Config\Settings; use Openguru\OpenCartFramework\Container\Container; @@ -49,6 +50,9 @@ class Application extends Container return new Settings($container->getConfigValue()); }); + $dotenv = Dotenv::createImmutable(__DIR__ . '/../'); + $dotenv->load(); + $errorHandler = new ErrorHandler( $this->get(Logger::class) ); diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ErrorHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ErrorHandler.php index e13a728..625ab8c 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ErrorHandler.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ErrorHandler.php @@ -49,15 +49,20 @@ class ErrorHandler if (PHP_SAPI === 'cli') { echo $exception->getMessage() . PHP_EOL; } else { - (new JsonResponse([ + $debug = env('APP_DEBUG'); + + $data = $debug ? [ 'exception' => get_class($exception), 'message' => $exception->getMessage(), 'file' => $exception->getFile(), 'line' => $exception->getLine(), 'trace' => $exception->getTrace(), - ], Response::HTTP_INTERNAL_SERVER_ERROR))->send(); - } + ] : [ + 'message' => 'Server Error.', + ]; + (new JsonResponse($data, Response::HTTP_INTERNAL_SERVER_ERROR))->send(); + } exit(1); } @@ -80,11 +85,16 @@ class ErrorHandler if (PHP_SAPI === 'cli') { echo $message . PHP_EOL; } else { - (new JsonResponse([ + $debug = getenv('APP_DEBUG'); + $data = $debug ? [ 'message' => $message, 'file' => $error['file'], 'line' => $error['line'], - ], Response::HTTP_INTERNAL_SERVER_ERROR))->send(); + ] : [ + 'message' => 'Server Error.', + ]; + + (new JsonResponse($data, Response::HTTP_INTERNAL_SERVER_ERROR))->send(); } exit(1); diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Http/Request.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Http/Request.php index e1aa6a7..c8bc945 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Http/Request.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Http/Request.php @@ -75,4 +75,9 @@ class Request { return $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? null; } + + public function getUserAgent(): ?string + { + return $_SERVER['HTTP_USER_AGENT'] ?? null; + } } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Support/helpers.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Support/helpers.php index 180f2ae..b7b74ea 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Support/helpers.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Support/helpers.php @@ -4,7 +4,7 @@ use Openguru\OpenCartFramework\Application; use Openguru\OpenCartFramework\Config\Settings; use Openguru\OpenCartFramework\Support\Utils; -if (!function_exists('table')) { +if (! function_exists('table')) { function db_table(string $name): string { $prefix = Application::getInstance()->getConfigValue('db.prefix'); @@ -13,7 +13,7 @@ if (!function_exists('table')) { } } -if (!function_exists('column')) { +if (! function_exists('column')) { function db_column($column): string { if (strpos($column, '.') !== false) { @@ -30,30 +30,59 @@ if (!function_exists('column')) { } } -if (!function_exists('resources_path')) { +if (! function_exists('resources_path')) { function resources_path(string $path = ''): string { return BP_BASE_PATH . '/resources/' . $path; } } -if (!function_exists('base_path')) { +if (! function_exists('base_path')) { function base_path(string $path = ''): string { return BP_BASE_PATH . '/' . $path; } } -if (!function_exists('config')) { +if (! function_exists('config')) { function config(string $key, $default = null) { return Application::getInstance()->get(Settings::class)->get($key, $default); } } -if (!function_exists('dd')) { +if (! function_exists('dd')) { function dd(): void { Utils::dd(func_get_args()); } } + +if (! function_exists('env')) { + function env(string $key, $default = null) + { + $value = getenv($key) !== false + ? getenv($key) + : ($_ENV[$key] ?? $default); + + $lower = strtolower($value); + + if ($lower === 'true' || $lower === '(true)') { + return true; + } + + if ($lower === 'false' || $lower === '(false)') { + return false; + } + + if ($lower === 'null' || $lower === '(null)') { + return null; + } + + if ($lower === 'empty' || $lower === '(empty)') { + return ''; + } + + return $value; + } +} diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Exceptions/OrderValidationFailedException.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Exceptions/OrderValidationFailedException.php new file mode 100644 index 0000000..1180cd0 --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Exceptions/OrderValidationFailedException.php @@ -0,0 +1,28 @@ +errorBag = $errorBag; + + parent::__construct($message, $code, $previous); + } + + public function getErrorBag(): ErrorBag + { + return $this->errorBag; + } +} diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/OrderHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/OrderHandler.php index 2833c09..979586a 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/OrderHandler.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/OrderHandler.php @@ -2,158 +2,34 @@ namespace App\Handlers; -use App\Decorators\OcRegistryDecorator; -use App\Services\CartService; -use Openguru\OpenCartFramework\Config\Settings; +use App\Exceptions\OrderValidationFailedException; +use App\Services\OrderCreateService; use Openguru\OpenCartFramework\Http\JsonResponse; use Openguru\OpenCartFramework\Http\Request; use Openguru\OpenCartFramework\Http\Response; -use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface; -use Rakit\Validation\Validator; -use RuntimeException; class OrderHandler { - private ConnectionInterface $database; - private CartService $cartService; - private OcRegistryDecorator $oc; - private Settings $settings; + private OrderCreateService $orderCreateService; - public function __construct( - ConnectionInterface $database, - CartService $cartService, - OcRegistryDecorator $registry, - Settings $settings - ) { - $this->database = $database; - $this->cartService = $cartService; - $this->oc = $registry; - $this->settings = $settings; + public function __construct(OrderCreateService $orderCreateService) + { + $this->orderCreateService = $orderCreateService; } public function store(Request $request): JsonResponse { - $validator = new Validator(); + try { + $this->orderCreateService->create($request->json(), [ + 'ip' => $request->getClientIp(), + 'user_agent' => $request->getUserAgent(), + ]); - $validation = $validator->make($request->json(), [ - 'firstName' => 'required', - 'lastName' => 'required', - 'email' => 'required|email', - 'phone' => 'required', - 'address' => 'required', - 'comment' => 'required', - ]); - - $validation->validate(); - - if ($validation->fails()) { - $errors = $validation->errors(); + return new JsonResponse([], Response::HTTP_CREATED); + } catch (OrderValidationFailedException $exception) { return new JsonResponse([ - 'data' => $errors->firstOfAll(), + 'data' => $exception->getErrorBag()->firstOfAll(), ], Response::HTTP_UNPROCESSABLE_ENTITY); } - - $now = date('Y-m-d H:i:s'); - $storeId = 0; - $storeName = 'Ваш магазин'; - - $cart = $this->cartService->getCart(); - $total = $cart['total'] ?? 0; - $orderStatusId = 1; - $products = $cart['products'] ?? []; - $totals = $cart['totals'] ?? []; - $customerGroupId = $this->settings->get('oc_customer_group_id'); - - $languageId = $this->oc->config->get('config_language_id'); - $currencyId = $this->oc->currency->getId($this->oc->session->data['currency']); - $currencyCode = $this->oc->session->data['currency']; - $currencyValue = $this->oc->currency->getValue($this->oc->session->data['currency']); - - $orderData = [ - 'store_id' => $storeId, - 'store_name' => $storeName, - 'firstname' => $request->json('firstName'), - 'lastname' => $request->json('lastName'), - 'email' => $request->json('email'), - 'telephone' => $request->json('phone'), - 'comment' => $request->json('comment'), - 'shipping_address_1' => $request->json('address'), - 'total' => $total, - 'order_status_id' => $orderStatusId, - 'ip' => $request->getClientIp(), - 'date_added' => $now, - 'date_modified' => $now, - 'language_id' => $languageId, - 'currency_id' => $currencyId, - 'currency_code' => $currencyCode, - 'currency_value' => $currencyValue, - 'customer_group_id' => $customerGroupId, - ]; - - $this->database->transaction( - function () use ($orderData, $products, $totals, $orderStatusId, $now) { - $success = $this->database->insert(db_table('order'), $orderData); - - if (! $success) { - [, $error] = $this->database->getLastError(); - throw new RuntimeException("Failed to insert row into order. Error: $error"); - } - - $orderId = $this->database->lastInsertId(); - - // Insert products - foreach ($products as $product) { - $success = $this->database->insert(db_table('order_product'), [ - 'order_id' => $orderId, - 'product_id' => $product['product_id'], - 'name' => $product['name'], - 'model' => $product['model'], - 'quantity' => $product['quantity'], - 'price' => $product['price_numeric'], - 'total' => $product['total_numeric'], - 'reward' => $product['reward_numeric'], - ]); - - if (! $success) { - [, $error] = $this->database->getLastError(); - throw new RuntimeException("Failed to insert row into order_product. Error: $error"); - } - } - - // Insert totals - foreach ($totals as $total) { - $success = $this->database->insert(db_table('order_total'), [ - 'order_id' => $orderId, - 'code' => $total['code'], - 'title' => $total['title'], - 'value' => $total['value'], - 'sort_order' => $total['sort_order'], - ]); - - if (! $success) { - [, $error] = $this->database->getLastError(); - throw new RuntimeException("Failed to insert row into order_total. Error: $error"); - } - } - - // Insert history - $success = $this->database->insert(db_table('order_history'), [ - 'order_id' => $orderId, - 'order_status_id' => $orderStatusId, - 'notify' => 0, - 'comment' => 'Заказ оформлен через Telegram Mini App', - 'date_added' => $now, - ]); - - if (! $success) { - [, $error] = $this->database->getLastError(); - throw new RuntimeException("Failed to insert row into order_history. Error: $error"); - } - } - ); - - $this->cartService->flush(); - - return new JsonResponse([]); } } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/CartService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/CartService.php index 8563c7f..5debc2a 100644 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/CartService.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/CartService.php @@ -111,10 +111,13 @@ class CartService } } - $option_data[] = array( + $option_data[] = [ + 'product_option_id' => (int) $option['product_option_id'], + 'product_option_value_id' => (int) $option['product_option_value_id'], 'name' => $option['name'], - 'value' => (utf8_strlen($value) > 20 ? utf8_substr($value, 0, 20) . '..' : $value) - ); + 'value' => (utf8_strlen($value) > 20 ? utf8_substr($value, 0, 20) . '..' : $value), + 'type' => $option['type'], + ]; } $priceNumeric = 0; diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php new file mode 100644 index 0000000..650b8d8 --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/OrderCreateService.php @@ -0,0 +1,177 @@ +database = $database; + $this->cartService = $cartService; + $this->oc = $registry; + $this->settings = $settings; + } + + public function create(array $data, array $meta = []) + { + $this->validate($data); + + $now = date('Y-m-d H:i:s'); + $storeId = $this->settings->get('oc_store_id'); + $storeName = $this->settings->get('oc_store_name'); + $orderStatusId = $this->settings->get('oc_order_status_id'); + $customerGroupId = $this->settings->get('oc_customer_group_id'); + $languageId = $this->oc->config->get('config_language_id'); + $currencyId = $this->oc->currency->getId($this->oc->session->data['currency']); + $currencyCode = $this->oc->session->data['currency']; + $currencyValue = $this->oc->currency->getValue($this->oc->session->data['currency']); + + $cart = $this->cartService->getCart(); + $total = $cart['total'] ?? 0; + $products = $cart['products'] ?? []; + $totals = $cart['totals'] ?? []; + + $orderData = [ + 'store_id' => $storeId, + 'store_name' => $storeName, + 'firstname' => $data['firstName'], + 'lastname' => $data['lastName'], + 'email' => $data['email'], + 'telephone' => $data['phone'], + 'comment' => $data['comment'], + 'shipping_address_1' => $data['address'], + 'total' => $total, + 'order_status_id' => $orderStatusId, + 'ip' => $meta['ip'] ?? '', + 'forwarded_ip' => $meta['ip'] ?? '', + 'user_agent' => $meta['user_agent'] ?? '', + 'date_added' => $now, + 'date_modified' => $now, + 'language_id' => $languageId, + 'currency_id' => $currencyId, + 'currency_code' => $currencyCode, + 'currency_value' => $currencyValue, + 'customer_group_id' => $customerGroupId, + ]; + + $this->database->transaction( + function () use ($orderData, $products, $totals, $orderStatusId, $now) { + $success = $this->database->insert(db_table('order'), $orderData); + + if (! $success) { + [, $error] = $this->database->getLastError(); + throw new RuntimeException("Failed to insert row into order. Error: $error"); + } + + $orderId = $this->database->lastInsertId(); + + // Insert products + foreach ($products as $product) { + $success = $this->database->insert(db_table('order_product'), [ + 'order_id' => $orderId, + 'product_id' => $product['product_id'], + 'name' => $product['name'], + 'model' => $product['model'], + 'quantity' => $product['quantity'], + 'price' => $product['price_numeric'], + 'total' => $product['total_numeric'], + 'reward' => $product['reward_numeric'], + ]); + + if (! $success) { + [, $error] = $this->database->getLastError(); + throw new RuntimeException("Failed to insert row into order_product. Error: $error"); + } + + $orderProductId = $this->database->lastInsertId(); + foreach ($product['option'] as $option) { + $success = $this->database->insert(db_table('order_option'), [ + 'order_id' => $orderId, + 'order_product_id' => $orderProductId, + 'product_option_id' => $option['product_option_id'], + 'product_option_value_id' => $option['product_option_value_id'], + 'name' => $option['name'], + 'value' => $option['value'], + 'type' => $option['type'], + ]); + + if (! $success) { + [, $error] = $this->database->getLastError(); + throw new RuntimeException("Failed to insert row into order_option. Error: $error"); + } + } + } + + // Insert totals + foreach ($totals as $total) { + $success = $this->database->insert(db_table('order_total'), [ + 'order_id' => $orderId, + 'code' => $total['code'], + 'title' => $total['title'], + 'value' => $total['value'], + 'sort_order' => $total['sort_order'], + ]); + + if (! $success) { + [, $error] = $this->database->getLastError(); + throw new RuntimeException("Failed to insert row into order_total. Error: $error"); + } + } + + // Insert history + $success = $this->database->insert(db_table('order_history'), [ + 'order_id' => $orderId, + 'order_status_id' => $orderStatusId, + 'notify' => 0, + 'comment' => 'Заказ оформлен через Telegram Mini App', + 'date_added' => $now, + ]); + + if (! $success) { + [, $error] = $this->database->getLastError(); + throw new RuntimeException("Failed to insert row into order_history. Error: $error"); + } + } + ); + + $this->cartService->flush(); + } + + private function validate(array $data): void + { + $validator = new Validator(); + + $validation = $validator->make($data, [ + 'firstName' => 'required', + 'lastName' => 'required', + 'email' => 'required|email', + 'phone' => 'required', + 'address' => 'required', + 'comment' => 'required', + ]); + + $validation->validate(); + + if ($validation->fails()) { + throw new OrderValidationFailedException($validation->errors()); + } + } +} \ No newline at end of file diff --git a/spa/src/stores/CheckoutStore.js b/spa/src/stores/CheckoutStore.js index ddbb9e2..7a062cf 100644 --- a/spa/src/stores/CheckoutStore.js +++ b/spa/src/stores/CheckoutStore.js @@ -25,16 +25,18 @@ export const useCheckoutStore = defineStore('checkout', { actions: { async makeOrder() { - await storeOrder(this.customer) - .catch(error => { - if (error.response?.status === 422) { - this.validationErrors = error.response._data.data; - } else { - console.error('Unexpected error', error); - } - }); + try { + await storeOrder(this.customer); + await useCartStore().getProducts(); + } catch (error) { + if (error.response?.status === 422) { + this.validationErrors = error.response._data.data; + } else { + console.error('Server error', error); + } - await useCartStore().getProducts(); + throw error; + } }, clearError(field) { diff --git a/spa/src/utils/ftch.js b/spa/src/utils/ftch.js index 2596f77..41e0a90 100644 --- a/spa/src/utils/ftch.js +++ b/spa/src/utils/ftch.js @@ -3,6 +3,7 @@ import {ofetch} from "ofetch"; const BASE_URL = '/'; export const apiFetch = ofetch.create({ + throwHttpErrors: true, onRequest({request, options}) { const initData = window.Telegram?.WebApp?.initData diff --git a/spa/src/views/Checkout.vue b/spa/src/views/Checkout.vue index e5f8d54..bf35fda 100644 --- a/spa/src/views/Checkout.vue +++ b/spa/src/views/Checkout.vue @@ -53,7 +53,8 @@
+ class="fixed px-4 pb-10 pt-4 bottom-0 left-0 w-full bg-base-200 z-50 flex flex-col justify-between items-center gap-2 border-t-1 border-t-base-300"> +
{{ error }}
@@ -64,12 +65,20 @@ import {useCheckoutStore} from "@/stores/CheckoutStore.js"; import TgInput from "@/components/Form/TgInput.vue"; import TgTextarea from "@/components/Form/TgTextarea.vue"; import {useRouter} from "vue-router"; +import {ref} from "vue"; const checkout = useCheckoutStore(); const router = useRouter(); +const error = ref(null); + async function onCreateBtnClick() { - await checkout.makeOrder(); - router.push({name: 'order_created'}); + try { + error.value = null; + await checkout.makeOrder(); + router.push({name: 'order_created'}); + } catch { + error.value = 'Невозможно создать заказ.'; + } }