feat: moneta 2.0, MONETA.Assistant payment module for PHPShop 6.7+

This commit is contained in:
2026-10-27 11:45:22 +03:00
parent 78b7405637
commit f8c12ce44a
82 changed files with 7387 additions and 58 deletions
+55
View File
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace MonetaPhpShop\host;
/**
* Ñîäåðæèìîå êàðòî÷êè îïëàòû äëÿ øàáëîíà `templates/payment_forma.tpl`: îïèñàíèå îïëàòû
* (`@payment_info@`), ñóììà ê îïëàòå (`@moneta_total@`) è ôîðìà ñ êíîïêîé èëè òåêñò î ñîñòîÿíèè
* çàêàçà (`@payment_forma@`).
* Âñ¸ — â cp1251, áåç ïåðåêîäèðîâêè: áëîê öåëèêîì ïåðåêîäèðóåò õóê.
*/
final class CheckoutBlock
{
/** @var string */
private $form;
/** @var string */
private $total;
/** @var string */
private $info;
/**
* @param string $total ñóììà ê îïëàòå, HTML ñî çíàêîì âàëþòû âèòðèíû; ïóñòàÿ — çàêàç ñåé÷àñ
* íå îïëàòèòü, ñòðîêè ñóììû íåò
* @param string $info «Îïèñàíèå îïëàòû», ãîòîâûé HTML; òîëüêî âìåñòå ñ ôîðìîé îïëàòû
*/
public function __construct(string $form, string $total = '', string $info = '')
{
$this->form = $form;
$this->total = $total;
$this->info = $info;
}
public function getInfo(): string
{
return $this->info;
}
public function getForm(): string
{
return $this->form;
}
public function getTotal(): string
{
return $this->total;
}
public function isEmpty(): bool
{
return ($this->form === '');
}
}
+184
View File
@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace MonetaPhpShop\host;
use MonetaPhpShop\form\AmountText;
use MonetaPhpShop\form\PaymentForm;
use MonetaPhpShop\form\ReturnAccess;
use MonetaPhpShop\form\ReturnUrls;
use MonetaPhpShop\order\Order;
use MonetaPhpShop\order\PaymentEligibility;
use MonetaPhpShop\protocol\AssistantProtocol;
use MonetaPhpShop\Settings;
/**
* Êíîïêà îïëàòû äëÿ õóêîâ âèòðèíû: ïîñëå îôîðìëåíèÿ çàêàçà (`send_to_order`) è â ëè÷íîì
* êàáèíåòå (`userorderpaymentlink`). Îøèáêà ìîäóëÿ íå äîëæíà ðîíÿòü ñòðàíèöó ìàãàçèíà:
* ïîêóïàòåëü âèäèò íåéòðàëüíûé òåêñò, ïîäðîáíîñòè — â æóðíàëå PHP.
*/
final class CheckoutView
{
private const MESSAGES = [
PaymentEligibility::NOT_CONFIGURED => 'Îïëàòà ÷åðåç Ìîíåòó âðåìåííî íåäîñòóïíà. Ñâÿæèòåñü ñ ìàãàçèíîì.',
PaymentEligibility::ALREADY_PAID => 'Çàêàç îïëà÷åí.',
PaymentEligibility::CANCELLED => 'Çàêàç îòìåí¸í.',
PaymentEligibility::AWAITING_STATUS => 'Çàêàç îáðàáàòûâàåòñÿ ìåíåäæåðîì.',
PaymentEligibility::UNSUPPORTED_CURRENCY => 'Îïëàòà ÷åðåç Ìîíåòó âîçìîæíà òîëüêî â ðóáëÿõ.',
PaymentEligibility::ZERO_AMOUNT => 'Îïëàòà íå òðåáóåòñÿ.',
];
/** Òåêñò êíîïêè îïëàòû; íà êàðòî÷êå ïîñëå îôîðìëåíèÿ ê íåìó äîáàâëÿåòñÿ ñóììà. */
private const BUTTON_TITLE = 'Îïëàòèòü';
private const UNAVAILABLE = 'Îïëàòà âðåìåííî íåäîñòóïíà. Ïîïðîáóéòå ïîçæå èëè ñâÿæèòåñü ñ ìàãàçèíîì.';
/** @var SettingsRepository */
private $settingsRepository;
/** @var OrderRepository */
private $orderRepository;
/** @var ShopContext */
private $shopContext;
public function __construct(
SettingsRepository $settingsRepository,
OrderRepository $orderRepository,
ShopContext $shopContext
) {
$this->settingsRepository = $settingsRepository;
$this->orderRepository = $orderRepository;
$this->shopContext = $shopContext;
}
public static function create(): self
{
return new self(new SettingsRepository(), new OrderRepository(), new ShopContext());
}
/**
* Áëîê íà ñòðàíèöå «Çàêàç îôîðìëåí»: ñóììà è êíîïêà, åñëè çàêàç óæå ìîæíî îïëàòèòü, èíà÷å
* òåêñò î ñîñòîÿíèè çàêàçà. Ðàçìåòêó ñîáèðàåò øàáëîí `payment_forma.tpl` â õóêå; ÷àñòè — â cp1251.
*/
public function afterCheckout(int $orderId): CheckoutBlock
{
return $this->block($orderId, true);
}
/**
* ß÷åéêà «Ñïîñîá îïëàòû» â ëè÷íîì êàáèíåòå: îïèñàíèå îïëàòû è êíîïêà èëè ñîñòîÿíèå çàêàçà, áåç ñóììû.
*/
public function inAccount(int $orderId): string
{
$block = $this->block($orderId, false);
if ($block->isEmpty()) {
return '';
}
$info = ($block->getInfo() === '') ? '' : '<p class="moneta-payment-info">' . $block->getInfo() . '</p>';
return HostCharset::toPage('<div class="moneta-payment">' . $info . $block->getForm() . '</div>');
}
/**
* ×òî ïîêàçàòü ïî ñîñòîÿíèþ çàêàçà: ôîðìó îïëàòû ñ ñóììîé, åñëè çàêàç ìîæíî îïëàòèòü,
* èíà÷å — òåêñò î ñîñòîÿíèè.
*
* @param string $formHtml ôîðìà îïëàòû; íóæíà òîëüêî äëÿ {@see PaymentEligibility::PAYABLE}
* @param string $total ñóììà ê îïëàòå òåêñòîì; ïîêàçûâàåòñÿ òîëüêî âìåñòå ñ ôîðìîé
* @param string $description «Îïèñàíèå îïëàòû» îáû÷íûì òåêñòîì; ïîêàçûâàåòñÿ òîëüêî âìåñòå ñ ôîðìîé:
* «Îïëàòèòå çàêàç» áåç êíîïêè (çàêàç æä¸ò ìåíåäæåðà) ñáèâàë áû ñ òîëêó
*/
public static function compose(
string $eligibility,
string $formHtml,
string $total = '',
string $description = ''
): CheckoutBlock {
if ($eligibility === PaymentEligibility::PAYABLE) {
return new CheckoutBlock($formHtml, $total, self::descriptionHtml($description));
}
return new CheckoutBlock('<p>' . (self::MESSAGES[$eligibility] ?? self::UNAVAILABLE) . '</p>');
}
private function block(int $orderId, bool $afterCheckout): CheckoutBlock
{
try {
$settings = $this->settingsRepository->load();
$order = $this->orderRepository->findById($orderId);
if ($order === null) {
return new CheckoutBlock('');
}
// Ëè÷íûé êàáèíåò îòêðûâàåò çàêàç ïî îäíîìó uid, íå ñâåðÿÿ âëàäåëüöà
// (`users.core/action_order_info.php` ÿäðà): ôîðìó è çàïèñü â ñåññèþ — òîëüêî âëàäåëüöó.
if (!$afterCheckout && !ReturnAccess::isOwner($order, $this->shopContext->sessionUserId())) {
return new CheckoutBlock('');
}
$currency = $this->shopContext->orderCurrency();
$eligibility = PaymentEligibility::check($settings, $order, $currency);
if ($eligibility !== PaymentEligibility::PAYABLE) {
return self::compose($eligibility, '');
}
$this->shopContext->rememberPayableOrder($order->getId());
// Íà êàðòî÷êå ïîñëå îôîðìëåíèÿ — ñóììà îòäåëüíîé ñòðîêîé è â êíîïêå,
// ñî çíàêîì âàëþòû âèòðèíû; â ÿ÷åéêå ëè÷íîãî êàáèíåòà — êîðîòêàÿ êíîïêà.
$amount = '';
$currencyIcon = '';
if ($afterCheckout) {
$amount = AmountText::format($order->getAmount(), $this->shopContext->priceDecimals());
$currencyIcon = $this->shopContext->currencyIcon();
}
$suffix = ($currencyIcon === '') ? '' : ' ' . $currencyIcon;
$formHtml = $this->form(
$settings,
$order,
(string) AssistantProtocol::normalizeCurrency($currency),
($amount === '') ? self::BUTTON_TITLE : self::BUTTON_TITLE . ' ' . $amount,
($amount === '') ? '' : $suffix,
);
$total = ($amount === '') ? '' : $amount . $suffix;
$description = HostCharset::fromDatabase($settings->getPaymentDescription());
return self::compose($eligibility, $formHtml, $total, $description);
} catch (\Throwable $exception) {
(new ErrorLogLogger())->error(
'Êíîïêà îïëàòû çàêàçà ' . $orderId . ' íå ïîñòðîåíà: ' . $exception->getMessage(),
);
return new CheckoutBlock('<p>' . self::UNAVAILABLE . '</p>');
}
}
/**
* Òåêñò ìåð÷àíòà èç íàñòðîåê — â HTML: ýêðàíèðîâàíèå, ïåðåíîñû ñòðîê — `<br>`.
*/
private static function descriptionHtml(string $text): string
{
$text = trim($text);
return ($text === '') ? '' : nl2br(htmlspecialchars($text, ENT_QUOTES, 'Windows-1251'), false);
}
private function form(
Settings $settings,
Order $order,
string $currency,
string $buttonTitle,
string $buttonSuffixHtml
): string {
return PaymentForm::create(
$settings,
$order,
$currency,
$this->shopContext->now(),
new ReturnUrls($this->shopContext->returnPageUrl()),
'Îïëàòà çàêàçà ¹' . $order->getUid(),
$this->shopContext->cms(),
AssistantProtocol::normalizeLocale($this->shopContext->shopLocale()),
)->toHtml($buttonTitle, $buttonSuffixHtml);
}
}
+131
View File
@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace MonetaPhpShop\host;
use mysqli;
use mysqli_sql_exception;
use mysqli_stmt;
use RuntimeException;
/**
* Ïîäãîòîâëåííûå çàïðîñû ïîâåðõ ñîåäèíåíèÿ ÿäðà PHPShop (`$PHPShopBase->link_db`).
*
* `PHPShopOrm` ñîáèðàåò SQL êîíêàòåíàöèåé — äëÿ çàïèñåé êîëáýêà, ãäå â çàïðîñ ïîïàäàþò
* äàííûå èçâíå, ìîäóëü õîäèò â ÁÄ ñàì. Îøèáêà çàïðîñà — {@see RuntimeException} ñ êîäîì
* îøèáêè MySQL (1062 — äóáëèêàò êëþ÷à) ïðè ëþáîì `mysqli_report()`: â PHP 7.4 ïî óìîë÷àíèþ
* mysqli ìîë÷èò, ñ 8.1 áðîñàåò èñêëþ÷åíèÿ.
*/
final class Database
{
/** Äóáëèêàò óíèêàëüíîãî êëþ÷à. */
public const ER_DUP_ENTRY = 1062;
/** @var mysqli */
private $link;
public function __construct(mysqli $link)
{
$this->link = $link;
}
public static function fromHost(): self
{
// Êîëáýê è âèòðèíà äåðæàò ñîåäèíåíèå â $PHPShopBase, ÿäðî äóáëèðóåò åãî â ãëîáàëüíûé $link_db.
$base = $GLOBALS['PHPShopBase'] ?? null;
$link = is_object($base) && isset($base->link_db) ? $base->link_db : ($GLOBALS['link_db'] ?? null);
if (!$link instanceof mysqli) {
throw new RuntimeException('Íåò ñîåäèíåíèÿ ÿäðà PHPShop ñ ÁÄ.');
}
return new self($link);
}
/**
* @param array<int, int|string|null> $params
*
* @return array<int, array<string, mixed>>
*/
public function fetchAll(string $sql, array $params = []): array
{
$statement = $this->run($sql, $params);
try {
$result = $statement->get_result();
if ($result === false) {
throw new RuntimeException('Çàïðîñ íå âåðíóë ñòðîê: ' . $statement->error, $statement->errno);
}
$rows = [];
while (is_array($row = $result->fetch_assoc())) {
$rows[] = $row;
}
return $rows;
} finally {
$statement->close();
}
}
/**
* @param array<int, int|string|null> $params
*
* @return mixed ïåðâàÿ êîëîíêà ïåðâîé ñòðîêè èëè null
*/
public function fetchValue(string $sql, array $params = [])
{
$rows = $this->fetchAll($sql, $params);
if ($rows === []) {
return null;
}
return reset($rows[0]);
}
/**
* @param array<int, int|string|null> $params
*
* @return int ÷èñëî çàòðîíóòûõ ñòðîê
*/
public function execute(string $sql, array $params = []): int
{
$statement = $this->run($sql, $params);
$affected = $statement->affected_rows;
$statement->close();
return (int) $affected;
}
/**
* @param array<int, int|string|null> $params
*/
private function run(string $sql, array $params): mysqli_stmt
{
try {
$statement = $this->link->prepare($sql);
if ($statement === false) {
throw new RuntimeException('Çàïðîñ íå ïîäãîòîâëåí: ' . $this->link->error, $this->link->errno);
}
if ($params !== []) {
$types = '';
foreach ($params as $param) {
$types .= is_int($param) ? 'i' : 's';
}
$statement->bind_param($types, ...$params);
}
if (!$statement->execute()) {
$error = new RuntimeException('Çàïðîñ íå âûïîëíåí: ' . $statement->error, $statement->errno);
$statement->close();
throw $error;
}
return $statement;
} catch (mysqli_sql_exception $exception) {
throw new RuntimeException(
'Îøèáêà MySQL: ' . $exception->getMessage(),
(int) $exception->getCode(),
$exception,
);
}
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace MonetaPhpShop\host;
use MonetaPhpShop\callback\Logger;
/**
* {@see Logger} â æóðíàë îøèáîê PHP (`error_log`), êàê ó øòàòíûõ ìîäóëåé PHPShop.
* Ñòðîêè ìîäóëÿ ïîñëå ñáîðêè — cp1251, æóðíàë ÷èòàþò â UTF-8: ïåðåêîäèðóåì.
*/
final class ErrorLogLogger implements Logger
{
public function error(string $message): void
{
error_log('moneta: ' . HostCharset::toUtf8($message));
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace MonetaPhpShop\host;
/**
* Êîäèðîâêè íà ãðàíèöå ñ PHPShop.
*
* Ìîäóëü âíóòðè ðàáîòàåò â cp1251 — â íåé åãî èñõîäíèêè ïîñëå ñáîðêè è øòàòíàÿ áàçà PHPShop.
* Ìàãàçèí ñ áàçîé `utf-8` (`$PHPShopBase->codBase`) îòäà¸ò ñòðîêè èç ÁÄ â UTF-8 è âûâîäèò
* ñòðàíèöû â UTF-8 — íà ýòîé ãðàíèöå ñòðîêè ïåðåêîäèðóþòñÿ, êàê â øòàòíûõ ìîäóëÿõ ÿäðà.
* Âñ¸, ÷òî óõîäèò â Ìîíåòó (JSON, XML), — UTF-8.
*/
final class HostCharset
{
private function __construct() {}
public static function isUtf8Shop(): bool
{
$base = $GLOBALS['PHPShopBase'] ?? null;
return is_object($base) && isset($base->codBase) && ($base->codBase === 'utf-8');
}
/**
* Ñòðîêà èç ÁÄ ìàãàçèíà -> cp1251.
*/
public static function fromDatabase(string $value): string
{
if (!self::isUtf8Shop()) {
return $value;
}
$converted = iconv('UTF-8', 'Windows-1251//IGNORE', $value);
return ($converted === false) ? '' : $converted;
}
/**
* Ñòðîêà ìîäóëÿ (cp1251) -> êîäèðîâêà ÁÄ ìàãàçèíà.
*/
public static function toDatabase(string $value): string
{
return self::isUtf8Shop() ? self::toUtf8($value) : $value;
}
/**
* HTML ìîäóëÿ (cp1251) -> êîäèðîâêà ñòðàíèöû ìàãàçèíà.
*/
public static function toPage(string $html): string
{
return self::isUtf8Shop() ? self::toUtf8($html) : $html;
}
/**
* cp1251 -> UTF-8 äëÿ Ìîíåòû. Áàéò 0x98 â cp1251 íå îïðåäåë¸í — íà í¸ì iconv âåðíóë áû false
* íà âñþ ñòðîêó (íàçâàíèå ïîçèöèè â ÷åêå ñòàëî áû ïóñòûì), ïîýòîìó îí óäàëÿåòñÿ çàðàíåå.
* Íå //IGNORE: åãî ïîääåðæêà çàâèñèò îò ðåàëèçàöèè iconv (glibc — äà, musl — íåò).
*/
public static function toUtf8(string $value): string
{
$converted = iconv('Windows-1251', 'UTF-8', str_replace("\x98", '', $value));
return ($converted === false) ? '' : $converted;
}
}
+158
View File
@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace MonetaPhpShop\host;
use Closure;
use MonetaPhpShop\callback\Logger;
use MonetaPhpShop\callback\ReceiptSource;
use MonetaPhpShop\order\Order;
use MonetaPhpShop\receipt\Receipt;
use MonetaPhpShop\receipt\ReceiptBuilder;
use MonetaPhpShop\receipt\Vat;
use MonetaPhpShop\receipt\VatRateWarnings;
use MonetaPhpShop\Settings;
/**
* ×åê çàêàçà äëÿ îòâåòîâ Check/Pay URL. Ñòàâêè ÍÄÑ — ôèêñèðîâàííûå èç íàñòðîåê ìîäóëÿ
* («ÍÄÑ íà òîâàðû», «ÍÄÑ íà äîñòàâêó»), à ïî óìîë÷àíèþ èç PHPShop: âûêëþ÷åí ÍÄÑ
* â íàñòðîéêàõ ìàãàçèíà — «áåç ÍÄÑ» íà âñåõ ñòðîêàõ; âêëþ÷¸í — ñèñòåìíàÿ ñòàâêà ó òîâàðîâ
* è ñòàâêà ñïîñîáà äîñòàâêè (`phpshop_delivery.ofd_nds`, ïóñòî — ñèñòåìíàÿ) ó äîñòàâêè.
*
* Ñòàâêó íå èç ïåðå÷íÿ êàññû (â PHPShop ýòî ñâîáîäíîå ÷èñëî, íàïðèìåð îïå÷àòêà `23`) ìîäóëü íå óãàäûâàåò:
* ïîçèöèÿ óõîäèò «áåç ÍÄÑ», à â æóðíàë PHP ïèøåòñÿ ïðåäóïðåæäåíèå — òî æå ïîêàçûâàþò íàñòðîéêè ìîäóëÿ
* ({@see VatRateWarnings}).
*
* Áåç ôèñêàëèçàöèè àòðèáóòû óðåçàíû, à «Email äëÿ ÷åêîâ» íå ïîäñòàâëÿåòñÿ: ÷åê íå ïðîáèâàåòñÿ,
* MONETA.RU òîëüêî ñîõðàíÿåò àòðèáóòû â îïåðàöèè.
*
* Ñáîé ñáîðêè ÷åêà íå äîëæåí ðîíÿòü ïîäòâåðæäåíèå îïëàòû: àòðèáóòû òîãäà ïóñòûå, JSON — null.
*/
final class HostReceiptSource implements ReceiptSource
{
/** @var Settings */
private $settings;
/** @var HostVatRates */
private $vatRates;
/** @var Logger */
private $logger;
/** @var array<int, Receipt> */
private $receipts = [];
public function __construct(Settings $settings, Database $database, Logger $logger)
{
$this->settings = $settings;
$this->vatRates = new HostVatRates($database);
$this->logger = $logger;
}
public function attributes(Order $order): array
{
$receipt = $this->receipt($order);
return ($receipt !== null)
? $receipt->toAttributes($this->settings->isFiscalization(), $this->settings->getTaxSystem())
: [];
}
public function jsonReceipt(Order $order): ?array
{
$receipt = $this->receipt($order);
return ($receipt !== null) ? $receipt->toJsonReceipt() : null;
}
private function receipt(Order $order): ?Receipt
{
if (isset($this->receipts[$order->getId()])) {
return $this->receipts[$order->getId()];
}
try {
$builder = new ReceiptBuilder(
Closure::fromCallable([HostCharset::class, 'toUtf8']),
$this->settings->getPaymentMethod(),
$this->settings->getPaymentObject(),
$this->settings->getGoodsVat()->getFixed(),
);
$receiptEmail = $this->settings->isFiscalization() ? $this->settings->getReceiptEmail() : '';
$receipt = $builder->build($order, $this->goodsVat($order), $this->deliveryVat($order), $receiptEmail);
} catch (\Throwable $exception) {
$this->logger->error(
sprintf(
'×åê çàêàçà %s íå ñîáðàí: %s: %s',
$order->getUid(),
get_class($exception),
$exception->getMessage(),
),
);
return null;
}
return $this->receipts[$order->getId()] = $receipt;
}
private function goodsVat(Order $order): Vat
{
$fixed = $this->settings->getGoodsVat()->getFixed();
if ($fixed !== null) {
return $fixed;
}
if (!$this->vatRates->isEnabled()) {
return Vat::none();
}
return $this->platformVat($order, $this->vatRates->systemPercent(), 'ñèñòåìíàÿ ñòàâêà PHPShop (òîâàðû)');
}
private function deliveryVat(Order $order): Vat
{
$fixed = $this->settings->getDeliveryVat()->getFixed();
if ($fixed !== null) {
return $fixed;
}
if (!$this->vatRates->isEnabled()) {
return Vat::none();
}
$percent = $this->vatRates->deliveryPercent($order->getDeliveryMethodId());
if ($percent !== '') {
return $this->platformVat($order, $percent, 'ñòàâêà ñïîñîáà äîñòàâêè');
}
return $this->platformVat($order, $this->vatRates->systemPercent(), 'ñèñòåìíàÿ ñòàâêà PHPShop (äîñòàâêà)');
}
/**
* Ñòàâêà èç PHPShop; íå èç ïåðå÷íÿ êàññû — «áåç ÍÄÑ» è ïðåäóïðåæäåíèå, åñëè ÷åê ïðîáèâàåòñÿ.
*/
private function platformVat(Order $order, string $percent, string $source): Vat
{
$vat = Vat::tryFromPercent($percent);
if ($vat !== null) {
return $vat;
}
if ($this->settings->isFiscalization()) {
$this->logger->error(
sprintf(
'Çàêàç %s: %s %s íå èç ïåðå÷íÿ êàññû — ïîçèöèÿ óõîäèò â ÷åê «áåç ÍÄÑ».'
. ' Èñïðàâüòå ñòàâêó â PHPShop èëè çàäàéòå ñòàâêó â íàñòðîéêàõ ìîäóëÿ.',
$order->getUid(),
$source,
//  æóðíàë — òîëüêî òî, ÷òî ïîõîæå íà ÷èñëî: çíà÷åíèå ââîäèò àäìèíèñòðàòîð.
VatRateWarnings::shown((string) preg_replace('/[^0-9.,%+\- ]/', '?', substr($percent, 0, 16))),
),
);
}
return Vat::none();
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace MonetaPhpShop\host;
/**
* Ñòàâêè ÍÄÑ èç íàñòðîåê PHPShop: âêëþ÷¸í ëè ÍÄÑ ìàãàçèíà (`nds_enabled`), ñèñòåìíàÿ ñòàâêà
* (`nds`) è «Çíà÷åíèå ÍÄÑ» ñïîñîáîâ äîñòàâêè (`phpshop_delivery.ofd_nds`). Ñòàâêà ó òîâàðà
* â ÿäðå PHPShop íå õðàíèòñÿ.
*/
final class HostVatRates
{
/** @var Database */
private $database;
public function __construct(Database $database)
{
$this->database = $database;
}
public function isEnabled(): bool
{
return !empty(self::systemParam('nds_enabled'));
}
public function systemPercent(): string
{
return self::systemParam('nds');
}
/**
* «Çíà÷åíèå ÍÄÑ» ñïîñîáà äîñòàâêè êàê ââ¸ë ìåð÷àíò; ïóñòî — ó äîñòàâêè ñâîåé ñòàâêè íåò.
*/
public function deliveryPercent(int $deliveryId): string
{
$table = self::deliveryTable();
if ($deliveryId <= 0 || $table === null) {
return '';
}
return trim((string) $this->database->fetchValue(
'SELECT ofd_nds FROM `' . $table . '` WHERE id = ? LIMIT 1',
[$deliveryId],
));
}
/**
* Âêëþ÷¸ííûå ñïîñîáû äîñòàâêè (áåç ïàïîê): [íàçâàíèå â cp1251, «Çíà÷åíèå ÍÄÑ»].
*
* @return array<int, array{0: string, 1: string}>
*/
public function enabledDeliveries(): array
{
$table = self::deliveryTable();
if ($table === null) {
return [];
}
$deliveries = [];
$rows = $this->database->fetchAll(
'SELECT city, ofd_nds FROM `' . $table . "` WHERE enabled = '1' AND (is_folder IS NULL OR is_folder <> '1')"
. ' ORDER BY num, id',
);
foreach ($rows as $row) {
$deliveries[] = [
HostCharset::fromDatabase((string) ($row['city'] ?? '')),
trim((string) ($row['ofd_nds'] ?? '')),
];
}
return $deliveries;
}
private static function deliveryTable(): ?string
{
$table = $GLOBALS['SysValue']['base']['delivery'] ?? null;
// Èìÿ òàáëèöû èä¸ò â SQL: ÿâíûé êëàññ, à íå \w — ñì. OperationRepository::table().
return (is_string($table) && preg_match('/\A[0-9A-Za-z_]+\z/', $table) === 1) ? $table : null;
}
private static function systemParam(string $name): string
{
$system = $GLOBALS['PHPShopSystem'] ?? null;
if (!is_object($system) || !method_exists($system, 'getParam')) {
return '';
}
$value = $system->getParam($name);
return is_scalar($value) ? (string) $value : '';
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace MonetaPhpShop\host;
use MonetaPhpShop\callback\LogThrottle;
use MonetaPhpShop\ModuleInfo;
use RuntimeException;
/**
* {@see LogThrottle} â îòäåëüíîé òàáëèöå `[base] moneta_log_throttle` (îäíà ñòðîêà): `logged_at` —
* âðåìÿ ïîñëåäíåé çàïèñàííîé ñòðîêè, `skipped` — ñêîëüêî ïðîïóùåíî ïîñëå íå¸. Íå â òàáëèöå
* íàñòðîåê: çàïèñü â MyISAM áëîêèðóåò òàáëèöó öåëèêîì, à íàñòðîéêè ÷èòàåò êàæäûé êîëáýê è êàæäàÿ
* ñòðàíèöà ñ ôîðìîé îïëàòû — ïîòîê ìóñîðíûõ çàïðîñîâ çàäåðæèâàë áû íàñòîÿùèå óâåäîìëåíèÿ.
*
* Çàõâàò ìåòêè — îäèí àòîìàðíûé çàïðîñ, ïîýòîìó èç îäíîâðåìåííûõ çàïðîñîâ ñòðîêó ïèøåò îäèí;
* ñ÷¸ò÷èê ïðîïóñêîâ ïðè ãîíêå ìîæåò ñáèòüñÿ íà åäèíèöó. Ñòðîêè íåò
* (óäàëèëè ðóêàìè, ìèãðàöèÿ íå âñòàâèëà) — òîò æå çàïðîñ å¸ ñîçäà¸ò: èíà÷å îáà UPDATE
* çàòðàãèâàëè áû 0 ñòðîê è îòêàçû ìîë÷à ïðîïàäàëè áû èç æóðíàëà.
*/
final class MysqlLogThrottle implements LogThrottle
{
/** Åäèíñòâåííàÿ ñòðîêà ñ÷¸ò÷èêà (`install/module.sql`). */
private const ROW_ID = 1;
/** @var Database */
private $database;
public function __construct(Database $database)
{
$this->database = $database;
}
public function pass(int $now, int $interval): ?int
{
$table = self::table();
// Çàòðîíóòûå ñòðîêè: 1 — ñòðîêè íå áûëî è îíà âñòàâëåíà, 2 — ìåòêà îáíîâëåíà,
// 0 — îêíî åù¸ îòêðûòî è ñòðîêà íå èçìåíèëàñü. Âðåìÿ â áóäóùåì (÷àñû ñåðâåðà ïåðåâåëè
// íàçàä) — íå æäàòü, ïîêà äîãîíÿò.
$claimed = $this->database->execute(
'INSERT INTO `' . $table . '` (`id`, `logged_at`, `skipped`) VALUES (' . self::ROW_ID . ', ?, 0)'
. ' ON DUPLICATE KEY UPDATE `logged_at` = IF(`logged_at` <= ? OR `logged_at` > ?, ?, `logged_at`)',
[$now, $now - $interval, $now, $now],
);
if ($claimed === 0) {
$this->database->execute('UPDATE `' . $table . '` SET `skipped` = `skipped` + 1');
return null;
}
$skipped = (int) $this->database->fetchValue('SELECT `skipped` FROM `' . $table . '` LIMIT 1');
if ($skipped > 0) {
$this->database->execute(
'UPDATE `' . $table . '` SET `skipped` = GREATEST(`skipped` - ?, 0)',
[$skipped],
);
}
return $skipped;
}
public static function table(): string
{
$table = $GLOBALS['SysValue']['base'][ModuleInfo::PATH]['moneta_log_throttle'] ?? null;
// ßâíûé êëàññ, à íå \w: â cp1251-ëîêàëè PHPShop \w ïðîïóñòèë áû êèðèëëèöó (êàê â OperationRepository).
if (!is_string($table) || preg_match('/\A[0-9A-Za-z_]+\z/', $table) !== 1) {
throw new RuntimeException('Íåò òàáëèöû ñ÷¸ò÷èêà æóðíàëà â êîíôèãóðàöèè ìîäóëÿ.');
}
return $table;
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace MonetaPhpShop\host;
use MonetaPhpShop\callback\OrderLock;
/**
* Áëîêèðîâêà çàêàçà ÷åðåç `GET_LOCK` MySQL: òàáëèöû PHPShop — MyISAM, òðàíçàêöèé íåò.
* Èìÿ áëîêèðîâêè îáùåå äëÿ ñåðâåðà MySQL, ïîýòîìó â íåãî âõîäèò èìÿ áàçû: äâà ìàãàçèíà
* íà îäíîì ñåðâåðå äðóã äðóãà íå æäóò. Áëîêèðîâêà ñíèìàåòñÿ è ïðè îáðûâå ñîåäèíåíèÿ.
*/
final class MysqlOrderLock implements OrderLock
{
/** Ñêîëüêî æäàòü ïàðàëëåëüíîå óâåäîìëåíèå ïî òîìó æå çàêàçó, ñåêóíä. */
private const TIMEOUT = 10;
private const NAME = "CONCAT('moneta_', MD5(CONCAT(DATABASE(), ':', ?)))";
/** @var Database */
private $database;
public function __construct(Database $database)
{
$this->database = $database;
}
public function acquire(int $orderId): bool
{
return (int) $this->database->fetchValue('SELECT GET_LOCK(' . self::NAME . ', ?)', [$orderId, self::TIMEOUT]) === 1;
}
public function release(int $orderId): void
{
$this->database->fetchValue('SELECT RELEASE_LOCK(' . self::NAME . ')', [$orderId]);
}
}
+133
View File
@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace MonetaPhpShop\host;
use MonetaPhpShop\callback\OperationJournal;
use MonetaPhpShop\ModuleInfo;
use MonetaPhpShop\order\Order;
use MonetaPhpShop\protocol\CallbackNotification;
use MonetaPhpShop\protocol\ResultCode;
use MonetaPhpShop\protocol\TransactionId;
use RuntimeException;
/**
* Æóðíàë îïåðàöèé — òàáëèöà `[base] moneta_operations` èç `inc/config.ini` ìîäóëÿ.
*
* Ñòðîêà íà êàæäîå ïîäïèñàííîå óâåäîìëåíèå; ó ñòðîêè çà÷ò¸ííîé îïëàòû çàïîëíåí
* `paid_order_id` ñ UNIQUE-êëþ÷îì — âòîðîé çà÷¸ò òîãî æå çàêàçà íåâîçìîæåí äàæå áåç áëîêèðîâêè.
* Ïîäïèñü è êîä ïðîâåðêè öåëîñòíîñòè íå õðàíÿòñÿ.
*/
final class OperationRepository implements OperationJournal
{
private const COMMAND_PAY = 'PAY';
/** @var Database */
private $database;
public function __construct(Database $database)
{
$this->database = $database;
}
public function findPaidOperation(int $orderId): ?string
{
$value = $this->database->fetchValue(
'SELECT operation_id FROM `' . self::table() . '` WHERE paid_order_id = ? LIMIT 1',
[$orderId],
);
return $value === null ? null : (string) $value;
}
public function recordPaid(CallbackNotification $notification, Order $order, string $event): bool
{
try {
$this->insert($notification, $order, $event, ResultCode::paid(), $order->getId());
} catch (RuntimeException $exception) {
if ($exception->getCode() === Database::ER_DUP_ENTRY) {
return false;
}
throw $exception;
}
return true;
}
public function note(
CallbackNotification $notification,
?Order $order,
string $event,
?ResultCode $resultCode
): void {
$this->insert($notification, $order, $event, $resultCode, null);
}
/**
* Ïîñëåäíèå çàïèñè æóðíàëà äëÿ àäìèíêè, íîâûå ñâåðõó.
*
* @param string|null $orderUid òîëüêî çàïèñè ýòîãî çàêàçà ({@see TransactionId::orderUidFromInput()}):
* ñ ýòèì `order_uid` èëè — åñëè çàêàç íå ðàñïîçíàí — ñ `MNT_TRANSACTION_ID` âèäà `<íîìåð>_…`
*
* @return array<int, array<string, mixed>>
*/
public function latest(int $limit, ?string $orderUid = null): array
{
$where = '';
$params = [];
if ($orderUid !== null) {
// Â íîìåðå çàêàçà íåò `_` è `%` (TransactionId::ORDER_UID) — ýêðàíèðîâàòü â LIKE
// íóæíî òîëüêî ñàì ðàçäåëèòåëü `_`.
$where = ' WHERE order_uid = ? OR transaction_id LIKE ?';
$params = [$orderUid, $orderUid . '\\' . TransactionId::SEPARATOR . '%'];
}
$params[] = $limit;
return $this->database->fetchAll(
'SELECT id, created_at, command, transaction_id, operation_id, order_id, order_uid, amount,'
. ' test_mode, result_code, event FROM `' . self::table() . '`' . $where . ' ORDER BY id DESC LIMIT ?',
$params,
);
}
public static function table(): string
{
$table = $GLOBALS['SysValue']['base'][ModuleInfo::PATH]['moneta_operations'] ?? null;
// ßâíûé êëàññ, à íå \w: áåç /u \w ñëåäóåò ëîêàëè, à PHPShop ñòàâèò ñâîþ (setlocale â
// class/base.class.php) — â cp1251 \w ïðîïóñòèë áû êèðèëëèöó â èìÿ òàáëèöû, êîòîðîå èä¸ò â SQL.
if (!is_string($table) || preg_match('/\A[0-9A-Za-z_]+\z/', $table) !== 1) {
throw new RuntimeException('Ìîäóëü íå óñòàíîâëåí: íåò òàáëèöû îïåðàöèé â êîíôèãóðàöèè.');
}
return $table;
}
private function insert(
CallbackNotification $notification,
?Order $order,
string $event,
?ResultCode $resultCode,
?int $paidOrderId
): void {
$amount = $notification->getAmount();
$this->database->execute(
'INSERT INTO `' . self::table() . '` (created_at, command, transaction_id, operation_id, order_id,'
. ' order_uid, amount, test_mode, result_code, event, paid_order_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[
time(),
$notification->isCheck() ? $notification->getCommand() : self::COMMAND_PAY,
$notification->getTransactionId(),
$notification->getOperationId(),
($order !== null) ? $order->getId() : 0,
($order !== null) ? $order->getUid() : '',
($amount !== null) ? $amount->toDecimal() : '',
$notification->getRawTestMode(),
($resultCode !== null) ? $resultCode->getValue() : null,
$event,
$paidOrderId,
],
);
}
}
+136
View File
@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace MonetaPhpShop\host;
use MonetaPhpShop\callback\OrderGateway;
use MonetaPhpShop\Money;
use MonetaPhpShop\order\Order;
use MonetaPhpShop\protocol\TransactionId;
use MonetaPhpShop\Settings;
use PHPShopOrderFunction;
use PHPShopOrm;
use RuntimeException;
/**
* Çàêàçû PHPShop (`phpshop_orders`).
*
* `PHPShopOrm` ñîáèðàåò SQL êîíêàòåíàöèåé, ïîýòîìó â óñëîâèÿ ïîïàäàåò òîëüêî òî, ÷òî óæå
* ïðîâåðåíî: öåëîå ÷èñëî èëè íîìåð çàêàçà ïî øàáëîíó {@see TransactionId::ORDER_UID}
* (ëàòèíèöà, öèôðû, äåôèñ — áåç êàâû÷åê).
*/
final class OrderRepository implements OrderGateway
{
/** Èìÿ ñïîñîáà îïëàòû â æóðíàëå îïëàò ÿäðà (`phpshop_payment.name`). */
private const PAYMENT_LOG_NAME = 'Moneta';
/** Íàçâàíèå ñòàòóñà 101, åñëè åãî åù¸ íåò, — êàê ó ÿäðà (`PHPShopPaymentResult::set_order_status_101()`). */
private const STATUS_PAID_BY_GATEWAY_NAME = 'Îïëà÷åíî ïëàòåæíûìè ñèñòåìàìè';
public function findById(int $id): ?Order
{
if ($id <= 0) {
return null;
}
return $this->findOne(['id' => '=' . $id]);
}
public function findByUid(string $uid): ?Order
{
if (preg_match('/\A' . TransactionId::ORDER_UID . '\z/', $uid) !== 1) {
return null;
}
return $this->findOne(['uid' => "='" . $uid . "'"]);
}
/**
* Êàê øòàòíûå ìîäóëè ÿäðà: ñòðîêà â æóðíàëå îïëàò, `changeStatus()` (ïèñüìî ïîêóïàòåëþ,
* ñêëàä, ìåññåíäæåðû — ïî íàñòðîéêàì ñòàòóñà) è ôëàã `paid`. `ofd()` ÿäðà íå âûçûâàåòñÿ:
* ÷åê ïðîáèâàåò êàññà MONETA.
*/
public function markPaid(Order $order, int $paidStatus, Money $amount): void
{
$database = Database::fromHost();
if ($paidStatus === Settings::STATUS_PAID_BY_GATEWAY) {
$this->ensurePaidByGatewayStatus($database);
}
$this->logPayment($database, $order, $amount);
$orderFunction = new PHPShopOrderFunction($order->getId());
$orderFunction->changeStatus($paidStatus, $order->getStatusId());
$orderFunction->changePaymentStatus(1);
}
/**
* `phpshop_payment`: êëþ÷ — íîìåð çàêàçà áåç äåôèñà ÷èñëîì (`4-36` -> 436), ïî íåìó ÿäðî
* ïîêàçûâàåò äàòó îïëàòû (`PHPShopOrderFunction::checkPay()`). Íîìåð íå øòàòíîãî âèäà
* (ïåðåîïðåäåë¸í õóêîì `setNum`) â ýòó òàáëèöó íå çàïèñûâàåòñÿ — çàïèñü ïðîïóñêàåòñÿ.
*/
private function logPayment(Database $database, Order $order, Money $amount): void
{
if (preg_match('/\A([0-9]+)-([0-9]+)\z/', $order->getUid(), $matches) !== 1) {
return;
}
$database->execute(
'INSERT IGNORE INTO `' . self::table('payment') . '` (uid, name, sum, datas) VALUES (?, ?, ?, ?)',
[(int) ($matches[1] . $matches[2]), self::PAYMENT_LOG_NAME, $amount->toDecimal(), time()],
);
}
private function ensurePaidByGatewayStatus(Database $database): void
{
$database->execute(
'INSERT IGNORE INTO `' . self::table('order_status') . "` (id, name, color, external_code, bot_message)"
. " VALUES (?, ?, '#ccff00', '', '')",
[Settings::STATUS_PAID_BY_GATEWAY, HostCharset::toDatabase(self::STATUS_PAID_BY_GATEWAY_NAME)],
);
}
private static function table(string $key): string
{
$table = $GLOBALS['SysValue']['base'][$key] ?? null;
// ßâíûé êëàññ, à íå \w: áåç /u \w ñëåäóåò ëîêàëè, à PHPShop ñòàâèò ñâîþ (setlocale â
// class/base.class.php) — â cp1251 \w ïðîïóñòèë áû êèðèëëèöó â èìÿ òàáëèöû, êîòîðîå èä¸ò â SQL.
if (!is_string($table) || preg_match('/\A[0-9A-Za-z_]+\z/', $table) !== 1) {
throw new RuntimeException("Íåò òàáëèöû ÿäðà {$key} â êîíôèãóðàöèè.");
}
return $table;
}
/**
* @param array<string, string> $where
*/
private function findOne(array $where): ?Order
{
$orm = new PHPShopOrm($GLOBALS['SysValue']['base']['orders']);
$orm->debug = false;
$row = $orm->select(['*'], $where, false, ['limit' => 1]);
if (!is_array($row) || empty($row['id'])) {
return null;
}
$details = @unserialize((string) ($row['orders'] ?? ''), ['allowed_classes' => false]);
if (!is_array($details)) {
$details = [];
}
foreach (['fio', 'tel'] as $field) {
if (isset($row[$field]) && is_string($row[$field])) {
$row[$field] = HostCharset::fromDatabase($row[$field]);
}
}
// Íàçâàíèÿ òîâàðîâ â êîðçèíå — â êîäèðîâêå ÁÄ, êàê è îñòàëüíûå ñòðîêè çàêàçà.
if (isset($details['Cart']['cart']) && is_array($details['Cart']['cart'])) {
foreach ($details['Cart']['cart'] as $key => $product) {
if (is_array($product) && isset($product['name']) && is_string($product['name'])) {
$details['Cart']['cart'][$key]['name'] = HostCharset::fromDatabase($product['name']);
}
}
}
return Order::fromRow($row, $details);
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace MonetaPhpShop\host;
use MonetaPhpShop\ModuleInfo;
use MonetaPhpShop\Settings;
use PHPShopOrm;
use RuntimeException;
/**
* Íàñòðîéêè ìîäóëÿ èç òàáëèöû `[base] moneta_system` åãî `inc/config.ini`.
*/
final class SettingsRepository
{
public function load(): Settings
{
$orm = new PHPShopOrm(self::table());
$orm->debug = false;
$row = $orm->select(['*'], false, false, ['limit' => 1]);
if (!is_array($row)) {
throw new RuntimeException('Íåò ñòðîêè íàñòðîåê ìîäóëÿ.');
}
return Settings::fromRow($row);
}
/**
* Ñòðîêà íàñòðîåê åù¸ îò âñòðîåííîãî ìîäóëÿ PHPShop: ôàéëû çàìåíåíû, à «Îáíîâèòü ìîäóëü»
* (ìèãðàöèÿ `updates/1.2/`) íå íàæàòà — íîâûõ êîëîíîê â òàáëèöå íåò.
*
* @param array<string, mixed> $row ñòðîêà òàáëèöû íàñòðîåê
*/
public static function needsUpgrade(array $row): bool
{
return !array_key_exists('account_id', $row);
}
public static function table(): string
{
$table = $GLOBALS['SysValue']['base'][ModuleInfo::PATH]['moneta_system'] ?? null;
if (!is_string($table) || ($table === '')) {
throw new RuntimeException('Ìîäóëü íå óñòàíîâëåí: íåò òàáëèöû íàñòðîåê â êîíôèãóðàöèè.');
}
return $table;
}
}
+171
View File
@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace MonetaPhpShop\host;
use DateTimeImmutable;
use MonetaPhpShop\CmsInfo;
use MonetaPhpShop\form\ReturnAccess;
use MonetaPhpShop\ModuleInfo;
/**
* Îêðóæåíèå ìàãàçèíà: àäðåñà, âàëþòà, ÿçûê, âåðñèè. API äëÿ ýòîãî ó PHPShop íåò — äàííûå ëåæàò
* â ãëîáàëüíûõ ïåðåìåííûõ, êîòîðûå ÿäðî çàïîëíÿåò ïðè çàïóñêå, è øòàòíûå ìîäóëè ÷èòàþò èõ òàê æå:
* - `$GLOBALS['SysValue']` — ðàçîáðàííûé `phpshop/inc/config.ini` (êàòàëîã óñòàíîâêè, âåðñèÿ ÿäðà);
* - `$GLOBALS['PHPShopSystem']` — íàñòðîéêè âèòðèíû (âàëþòà, çíàêè ïîñëå òî÷êè, çíàê âàëþòû);
* - `$_SERVER` — õîñò è HTTPS òåêóùåãî çàïðîñà, `$_SESSION` — ÿçûê âèòðèíû è çàêàçû ïîêóïàòåëÿ.
* Âñå îáðàùåíèÿ ê íèì ñîáðàíû çäåñü, ÷òîáû îñòàëüíûå êëàññû ìîäóëÿ ïîëó÷àëè çíà÷åíèÿ
* ïàðàìåòðàìè è ïðîâåðÿëèñü òåñòàìè áåç ÿäðà. Åñëè îáúåêòà ÿäðà íåò, ìåòîäû âîçâðàùàþò
* ïóñòîå çíà÷åíèå, à íå ïàäàþò.
*/
final class ShopContext
{
/** Ïóòü ñòðàíèöû âîçâðàòà ïîêóïàòåëÿ (`[core] moneta` â `inc/config.ini`). */
private const RETURN_PAGE = '/moneta/';
/** Åäèíàÿ òî÷êà Check URL è Pay URL. */
private const CALLBACK_PATH = '/phpshop/modules/' . ModuleInfo::PATH . '/payment/callback.php';
/**
* Àäðåñ ìàãàçèíà: ñõåìà + õîñò (ÿäðî ñòðîèò ññûëêè èç `SERVER_NAME`) + êàòàëîã óñòàíîâêè.
*/
public function baseUrl(): string
{
$host = (string) ($_SERVER['SERVER_NAME'] ?? '');
return ($this->isHttps() ? 'https://' : 'http://') . $host . $this->basePath();
}
public function returnPageUrl(): string
{
return $this->baseUrl() . self::RETURN_PAGE;
}
/**
* Ïóòü ñòðàíèöû âîçâðàòà îò êîðíÿ ñàéòà, áåç ñõåìû è õîñòà — äëÿ `Location` õóêîâ `/success`
* è `/fail`. `SERVER_NAME` íà Apache ïî óìîë÷àíèþ áåð¸òñÿ èç çàãîëîâêà `Host` çàïðîñà, è
* êýøèðóþùèé ïðîêñè ïåðåä ìàãàçèíîì ìîã áû ðàçäàòü ðåäèðåêò íà ÷óæîé õîñò.
*/
public function returnPagePath(): string
{
return $this->basePath() . self::RETURN_PAGE;
}
public function callbackUrl(): string
{
return $this->baseUrl() . self::CALLBACK_PATH;
}
/**
* ISO-êîä âàëþòû çàêàçà ñ ó÷¸òîì âèòðèíû (ìóëüòèñàéò).
*/
public function orderCurrency(): string
{
$system = $GLOBALS['PHPShopSystem'] ?? null;
if (!is_object($system) || !method_exists($system, 'getDefaultValutaIso')) {
return '';
}
return (string) $system->getDefaultValutaIso(true);
}
/**
* ßçûê âèòðèíû òåêóùåãî ïîêóïàòåëÿ: ÿäðî êëàä¸ò åãî â ñåññèþ (`PHPShopElements`).
*/
public function shopLocale(): string
{
$locale = $_SESSION['lang'] ?? '';
return is_string($locale) ? $locale : '';
}
/**
* Çàïîìíèòü â ñåññèè ïîêóïàòåëÿ çàêàç, ôîðìó îïëàòû êîòîðîãî åìó ïîêàçàëè: ïî íåìó ñòðàíèöà
* âîçâðàòà ïîêàæåò ñòàòóñ è ãîñòþ, îôîðìèâøåìó çàêàç áåç âõîäà ({@see ReturnAccess}).
*/
public function rememberPayableOrder(int $orderId): void
{
if (session_status() !== PHP_SESSION_ACTIVE) {
return;
}
$_SESSION[ReturnAccess::SESSION_KEY] = ReturnAccess::remember(
$_SESSION[ReturnAccess::SESSION_KEY] ?? null,
$orderId,
);
}
/**
* ID âîøåäøåãî ïîêóïàòåëÿ (`$_SESSION['UsersId']`); 0 — íå âîø¸ë.
*/
public function sessionUserId(): int
{
return (int) ($_SESSION['UsersId'] ?? 0);
}
/**
* Çíàêîâ ïîñëå òî÷êè â öåíàõ âèòðèíû (`admoption.price_znak`).
*/
public function priceDecimals(): int
{
$system = $GLOBALS['PHPShopSystem'] ?? null;
if (!is_object($system) || !method_exists($system, 'getSerilizeParam')) {
return 0;
}
return (int) $system->getSerilizeParam('admoption.price_znak');
}
/**
* Çíàê âàëþòû çàêàçà òàê, êàê åãî âûâîäèò âèòðèíà: äëÿ ðóáëÿ ÿäðî îòäà¸ò
* `<span class=rubznak>p</span>` — øðèôò òåìû ðèñóåò «p» çíàêîì ðóáëÿ; äëÿ äðóãîé âàëþòû —
* îáîçíà÷åíèå èç ñïðàâî÷íèêà. HTML ÿäðà, â cp1251.
*/
public function currencyIcon(): string
{
$system = $GLOBALS['PHPShopSystem'] ?? null;
if (!is_object($system) || !method_exists($system, 'getValutaIcon')) {
return '';
}
return HostCharset::fromDatabase(trim((string) $system->getValutaIcon(true)));
}
/**
* `MNT_CMS`: âåðñèÿ PHPShop — èç `[upload] version` êîíôèãà ÿäðà (`670`).
*/
public function cms(): string
{
$uploadVersion = (string) ($GLOBALS['SysValue']['upload']['version'] ?? '');
return CmsInfo::format(CmsInfo::hostVersion($uploadVersion), PHP_VERSION, ModuleInfo::VERSION);
}
/**
* Òåêóùåå âðåìÿ. Ïîÿñ âûñòàâëÿåò ÿäðî èç íàñòðîåê ìàãàçèíà (`PHPShopSystem::setTimeZone()`),
* à åñëè íàñòðîéêà ïóñòà èëè íåâåðíà — äåéñòâóåò ïîÿñ PHP ñåðâåðà; ìåòêå
* `MNT_TRANSACTION_ID` ýòî íåâàæíî, îíà ïèøåòñÿ â UTC.
*/
public function now(): DateTimeImmutable
{
return new DateTimeImmutable('now');
}
/**
* Êàòàëîã óñòàíîâêè PHPShop áåç ñëýøà íà êîíöå (`[dir] dir` â `inc/config.ini`), ïóñòî — â êîðíå ñàéòà.
*/
private function basePath(): string
{
return rtrim((string) ($GLOBALS['SysValue']['dir']['dir'] ?? ''), '/');
}
private function isHttps(): bool
{
$https = strtolower((string) ($_SERVER['HTTPS'] ?? ''));
if (($https !== '') && ($https !== 'off')) {
return true;
}
return strtolower((string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https';
}
}