feat: moneta 2.0, MONETA.Assistant payment module for PHPShop 6.7+
This commit is contained in:
@@ -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 === '');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 : '';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user