feat(order): order process enchancements

This commit is contained in:
Nikita Kiselev
2025-08-01 09:49:54 +03:00
parent c057f4be76
commit 85101b9881
16 changed files with 781 additions and 166 deletions

View File

@@ -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,

View File

@@ -0,0 +1 @@
APP_DEBUG=true

View File

@@ -0,0 +1 @@
APP_DEBUG=false

View File

@@ -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"

View File

@@ -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": [

View File

@@ -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)
);

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Exceptions;
use Rakit\Validation\ErrorBag;
use RuntimeException;
use Throwable;
class OrderValidationFailedException extends RuntimeException
{
private ErrorBag $errorBag;
public function __construct(
ErrorBag $errorBag,
string $message = 'Validation failed',
int $code = 422,
Throwable $previous = null
) {
$this->errorBag = $errorBag;
parent::__construct($message, $code, $previous);
}
public function getErrorBag(): ErrorBag
{
return $this->errorBag;
}
}

View File

@@ -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();
$validation = $validator->make($request->json(), [
'firstName' => 'required',
'lastName' => 'required',
'email' => 'required|email',
'phone' => 'required',
'address' => 'required',
'comment' => 'required',
try {
$this->orderCreateService->create($request->json(), [
'ip' => $request->getClientIp(),
'user_agent' => $request->getUserAgent(),
]);
$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([]);
}
}

View File

@@ -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;

View File

@@ -0,0 +1,177 @@
<?php
namespace App\Services;
use App\Decorators\OcRegistryDecorator;
use App\Exceptions\OrderValidationFailedException;
use Openguru\OpenCartFramework\Config\Settings;
use Openguru\OpenCartFramework\Http\JsonResponse;
use Openguru\OpenCartFramework\Http\Response;
use Openguru\OpenCartFramework\QueryBuilder\Connections\ConnectionInterface;
use Rakit\Validation\Validator;
use RuntimeException;
class OrderCreateService
{
private ConnectionInterface $database;
private CartService $cartService;
private OcRegistryDecorator $oc;
private Settings $settings;
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 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());
}
}
}

View File

@@ -25,16 +25,18 @@ export const useCheckoutStore = defineStore('checkout', {
actions: {
async makeOrder() {
await storeOrder(this.customer)
.catch(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('Unexpected error', error);
console.error('Server error', error);
}
});
await useCartStore().getProducts();
throw error;
}
},
clearError(field) {

View File

@@ -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

View File

@@ -53,7 +53,8 @@
</div>
<div
class="fixed px-4 pb-10 pt-4 bottom-0 left-0 w-full bg-base-200 z-50 flex justify-between items-center gap-2 border-t-1 border-t-base-300">
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">
<div v-if="error" class="text-error text-sm">{{ error }}</div>
<button class="btn btn-primary w-full" @click="onCreateBtnClick">Создать заказ</button>
</div>
</div>
@@ -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() {
try {
error.value = null;
await checkout.makeOrder();
router.push({name: 'order_created'});
} catch {
error.value = 'Невозможно создать заказ.';
}
}
</script>