feat: moneta 2.0, MONETA.Assistant payment module for PHPShop 6.7+
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\callback;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use MonetaPhpShop\ModuleInfo;
|
||||
use MonetaPhpShop\order\Order;
|
||||
use MonetaPhpShop\order\PaymentEligibility;
|
||||
use MonetaPhpShop\protocol\AssistantProtocol;
|
||||
use MonetaPhpShop\protocol\CallbackNotification;
|
||||
use MonetaPhpShop\protocol\CallbackResponse;
|
||||
use MonetaPhpShop\protocol\CheckResponse;
|
||||
use MonetaPhpShop\protocol\PayResponse;
|
||||
use MonetaPhpShop\protocol\ResultCode;
|
||||
use MonetaPhpShop\protocol\TransactionId;
|
||||
use MonetaPhpShop\Settings;
|
||||
use Throwable;
|
||||
|
||||
final class CallbackController
|
||||
{
|
||||
/** @var Settings */
|
||||
private $settings;
|
||||
|
||||
/** @var string */
|
||||
private $shopCurrency;
|
||||
|
||||
/** @var string */
|
||||
private $cms;
|
||||
|
||||
/** @var OrderGateway */
|
||||
private $orderGateway;
|
||||
|
||||
/** @var OperationJournal */
|
||||
private $operationJournal;
|
||||
|
||||
/** @var OrderLock */
|
||||
private $orderLock;
|
||||
|
||||
/** @var ReceiptSource */
|
||||
private $receiptSource;
|
||||
|
||||
/** @var Logger */
|
||||
private $logger;
|
||||
|
||||
/** @var Logger */
|
||||
private $rejectionLogger;
|
||||
|
||||
/**
|
||||
* @param string $shopCurrency ISO-êîä âàëþòû ìàãàçèíà (`getDefaultValutaIso(true)`)
|
||||
* @param string $cms ñòðîêà `MNT_CMS`
|
||||
* @param Logger|null $rejectionLogger îòêàçû äî ïðîâåðêè ïîäïèñè — êòî óãîäíî ìîæåò ñëàòü èõ
|
||||
* ïîòîêîì, ïîýòîìó æóðíàë äëÿ íèõ îáû÷íî ñ îãðàíè÷åíèåì ÷àñòîòû ({@see ThrottledLogger});
|
||||
* ïî óìîë÷àíèþ — `$logger`
|
||||
*/
|
||||
public function __construct(
|
||||
Settings $settings,
|
||||
string $shopCurrency,
|
||||
string $cms,
|
||||
OrderGateway $orderGateway,
|
||||
OperationJournal $operationJournal,
|
||||
OrderLock $orderLock,
|
||||
ReceiptSource $receiptSource,
|
||||
Logger $logger,
|
||||
?Logger $rejectionLogger = null
|
||||
) {
|
||||
$this->settings = $settings;
|
||||
$this->shopCurrency = $shopCurrency;
|
||||
$this->cms = $cms;
|
||||
$this->orderGateway = $orderGateway;
|
||||
$this->operationJournal = $operationJournal;
|
||||
$this->orderLock = $orderLock;
|
||||
$this->receiptSource = $receiptSource;
|
||||
$this->logger = $logger;
|
||||
$this->rejectionLogger = $rejectionLogger ?? $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ïàðàìåòðû óâåäîìëåíèÿ èç àäðåñà è òåëà âìåñòå, ïðè ñîâïàäåíèè èì¸í — èç òåëà.
|
||||
* Ìåòîä çàäà¸òñÿ â íàñòðîéêàõ ñ÷¸òà MONETA.RU, à ïðè POST MONETA.RU ïåðåíîñèò â òåëî è
|
||||
* ïàðàìåòðû èç àäðåñà Check URL.
|
||||
* Ëèøíåå èç àäðåñà íå îïàñíî: êàæäîå ïîëå ïîäïèñè âñ¸ ðàâíî ñâåðÿåòñÿ.
|
||||
*
|
||||
* @param array<mixed> $query `$_GET`
|
||||
* @param array<mixed> $body `$_POST`
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public static function requestFields(array $query, array $body): array
|
||||
{
|
||||
return array_replace($query, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $fields ïàðàìåòðû çàïðîñà ({@see requestFields()})
|
||||
*/
|
||||
public function handle(array $fields): CallbackResponse
|
||||
{
|
||||
if (!$this->settings->isConfigured()) {
|
||||
$this->rejectionLogger->error(
|
||||
'Óâåäîìëåíèå îòêëîíåíî: ìîäóëü íå íàñòðîåí (íîìåð ñ÷¸òà, êîä ïðîâåðêè öåëîñòíîñòè äàííûõ).',
|
||||
);
|
||||
|
||||
return CallbackResponse::fail();
|
||||
}
|
||||
|
||||
try {
|
||||
$notification = new CallbackNotification($fields);
|
||||
} catch (InvalidArgumentException $exception) {
|
||||
$this->rejectionLogger->error('Óâåäîìëåíèå îòêëîíåíî: ' . $exception->getMessage());
|
||||
|
||||
return CallbackResponse::fail();
|
||||
}
|
||||
|
||||
if (
|
||||
$notification->getAccountId() !== $this->settings->getAccountId()
|
||||
|| !$notification->isSignedBy($this->settings->signature())
|
||||
) {
|
||||
$this->rejectionLogger->error('Óâåäîìëåíèå îòêëîíåíî: íîìåð ñ÷¸òà èëè ïîäïèñü íå ñîâïàäàþò.');
|
||||
|
||||
return CallbackResponse::fail();
|
||||
}
|
||||
|
||||
$transactionId = TransactionId::parse($notification->getTransactionId());
|
||||
$order = ($transactionId === null) ? null : $this->orderGateway->findByUid($transactionId->getOrderUid());
|
||||
if ($transactionId === null || $order === null) {
|
||||
return $this->unknownOrder($notification);
|
||||
}
|
||||
|
||||
if (!$this->orderLock->acquire($order->getId())) {
|
||||
$this->operationJournal->note($notification, $order, CallbackEvent::LOCK_TIMEOUT, null);
|
||||
|
||||
return CallbackResponse::fail();
|
||||
}
|
||||
|
||||
try {
|
||||
// Âíóòðè áëîêèðîâêè — ñâåæèé ñíèìîê: ïàðàëëåëüíîå óâåäîìëåíèå ìîãëî åãî èçìåíèòü.
|
||||
$order = $this->orderGateway->findByUid($transactionId->getOrderUid()) ?? $order;
|
||||
|
||||
return $this->handleLocked($notification, $transactionId, $order);
|
||||
} finally {
|
||||
$this->orderLock->release($order->getId());
|
||||
}
|
||||
}
|
||||
|
||||
private function handleLocked(
|
||||
CallbackNotification $notification,
|
||||
TransactionId $transactionId,
|
||||
Order $order
|
||||
): CallbackResponse {
|
||||
$mismatch = $this->mismatch($notification, $transactionId, $order);
|
||||
if ($mismatch !== null) {
|
||||
$this->operationJournal->note($notification, $order, $mismatch, null);
|
||||
|
||||
return CallbackResponse::fail();
|
||||
}
|
||||
|
||||
return $notification->isCheck()
|
||||
? $this->check($notification, $order)
|
||||
: $this->pay($notification, $order);
|
||||
}
|
||||
|
||||
private function mismatch(CallbackNotification $notification, TransactionId $transactionId, Order $order): ?string
|
||||
{
|
||||
if ($notification->getCurrency() !== AssistantProtocol::CURRENCY_RUB) {
|
||||
return CallbackEvent::CURRENCY_MISMATCH;
|
||||
}
|
||||
|
||||
if ($notification->isTestMode() !== $this->settings->isTestMode()) {
|
||||
return CallbackEvent::TEST_MODE_MISMATCH;
|
||||
}
|
||||
|
||||
// Ñóììà ôîðìû çàøèòà â íîìåð è çàêðûòà ïîäïèñüþ ({@see TransactionId}): ñ íåé ñâåðÿåòñÿ
|
||||
// è ñóììà óâåäîìëåíèÿ, è çàêàç — èíà÷å ïåðâûå öèôðû ñóììû ôîðìû, ïåðåíåñ¸ííûå â
|
||||
// MNT_OPERATION_ID, ïðîâåëè áû çàêàç, ïîäåøåâåâøèé ïîñëå ïîêàçà ôîðìû.
|
||||
$amount = $notification->getAmount();
|
||||
if ($amount !== null && !$amount->equals($transactionId->getAmount())) {
|
||||
return CallbackEvent::AMOUNT_MISMATCH;
|
||||
}
|
||||
if (!$transactionId->getAmount()->equals($order->getAmount())) {
|
||||
return CallbackEvent::ORDER_AMOUNT_CHANGED;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function check(CallbackNotification $notification, Order $order): CallbackResponse
|
||||
{
|
||||
$event = CallbackEvent::CHECKED;
|
||||
if ($this->isPaid($order)) {
|
||||
$resultCode = ResultCode::paid();
|
||||
} else {
|
||||
$eligibility = PaymentEligibility::check($this->settings, $order, $this->shopCurrency);
|
||||
if ($eligibility === PaymentEligibility::PAYABLE) {
|
||||
$resultCode = ($notification->getAmount() === null)
|
||||
? ResultCode::withAmount()
|
||||
: ResultCode::awaitingPayment();
|
||||
} else {
|
||||
$resultCode = ResultCode::rejected();
|
||||
$event = $eligibility;
|
||||
}
|
||||
}
|
||||
|
||||
$response = new CheckResponse(
|
||||
$this->settings->getAccountId(),
|
||||
$notification->getTransactionId(),
|
||||
$order->getAmount(),
|
||||
$resultCode,
|
||||
$this->settings->signature(),
|
||||
$this->cms,
|
||||
);
|
||||
|
||||
if (!$this->settings->isFiscalization()) {
|
||||
$this->operationJournal->note($notification, $order, $event, $resultCode);
|
||||
|
||||
return $response->toXml($this->receiptSource->attributes($order));
|
||||
}
|
||||
|
||||
$receipt = $this->receiptSource->jsonReceipt($order);
|
||||
if ($receipt === null) {
|
||||
$this->operationJournal->note($notification, $order, CallbackEvent::RECEIPT_UNAVAILABLE, null);
|
||||
|
||||
return CallbackResponse::fail();
|
||||
}
|
||||
$this->operationJournal->note($notification, $order, $event, $resultCode);
|
||||
|
||||
return $response->toJson($receipt);
|
||||
}
|
||||
|
||||
private function pay(CallbackNotification $notification, Order $order): CallbackResponse
|
||||
{
|
||||
$paidOperation = $this->operationJournal->findPaidOperation($order->getId());
|
||||
if ($paidOperation !== null) {
|
||||
if ($paidOperation === $notification->getOperationId()) {
|
||||
return $this->payResponse($notification, $order, CallbackEvent::REPEAT, ResultCode::paid());
|
||||
}
|
||||
// Âòîðàÿ îïåðàöèÿ ïî óæå îïëà÷åííîìó çàêàçó: ïîêóïàòåëü çàïëàòèë äâàæäû, äåíüãè ñïèñàíû.
|
||||
// Îòâå÷àåì FAIL — ìàãàçèí íå ïîäòâåðæäàåò óâåäîìëåíèå: îïåðàöèÿ â MONETA.RU íå ïåðåõîäèò
|
||||
// â «Âûïîëíåíî», îñòà¸òñÿ «Óâåäîìëåíèå íå îòïðàâëåíî», Pay URL ïîâòîðÿåòñÿ äî ñóòîê
|
||||
// (MONETA.Assistant.ru.pdf) — òàê å¸ âèäíî â êàáèíåòå ñðåäè îáû÷íûõ îïëàò. Çà÷åñòü å¸ ìîäóëü
|
||||
// íå ìîæåò; ñòðîêà â æóðíàëå — ñèãíàë àäìèíèñòðàòîðó ñäåëàòü âîçâðàò â êàáèíåòå MONETA.RU.
|
||||
$this->operationJournal->note($notification, $order, CallbackEvent::DUPLICATE_OPERATION, null);
|
||||
|
||||
return CallbackResponse::fail();
|
||||
}
|
||||
|
||||
if ($order->getStatusId() === PaymentEligibility::STATUS_CANCELLED) {
|
||||
// Îòìåí¸ííûé çàêàç: ïîâòîðû áåññìûñëåííû, íî ñëåä â æóðíàëå îñòà¸òñÿ.
|
||||
return $this->payResponse($notification, $order, PaymentEligibility::CANCELLED, ResultCode::rejected());
|
||||
}
|
||||
if ($order->getPaymentMethodId() !== ModuleInfo::PAYMENT_METHOD_ID) {
|
||||
// Ïîêà ïîêóïàòåëü ïëàòèë, â çàêàçå ñìåíèëè ñïîñîá îïëàòû íà äðóãîé. Îòâå÷àåì FAIL:
|
||||
// MONETA.RU áóäåò ïîâòîðÿòü óâåäîìëåíèå, è îïëàòà çà÷ò¸òñÿ, êîãäà àäìèíèñòðàòîð
|
||||
// âåðí¸ò çàêàçó ñïîñîá îïëàòû «Ìîíåòà».
|
||||
$this->operationJournal->note($notification, $order, PaymentEligibility::OTHER_PAYMENT_METHOD, null);
|
||||
|
||||
return CallbackResponse::fail();
|
||||
}
|
||||
if (AssistantProtocol::normalizeCurrency($this->shopCurrency) === null) {
|
||||
// Âàëþòó âèòðèíû ñìåíèëè ñ ðóáëåé ïîñëå ïîêàçà ôîðìû: ñóììà çàêàçà óæå íå â ðóáëÿõ.
|
||||
// Òàê æå, êàê ñî ñïîñîáîì îïëàòû, — FAIL, ïîâòîðû è ñòðîêà â æóðíàëå äëÿ àäìèíèñòðàòîðà.
|
||||
$this->operationJournal->note($notification, $order, PaymentEligibility::UNSUPPORTED_CURRENCY, null);
|
||||
|
||||
return CallbackResponse::fail();
|
||||
}
|
||||
|
||||
$event = CallbackEvent::ALREADY_PAID_IN_SHOP;
|
||||
if (!$this->isPaid($order)) {
|
||||
try {
|
||||
$this->orderGateway->markPaid($order, $this->settings->getPaidStatus(), $order->getAmount());
|
||||
} catch (Throwable $exception) {
|
||||
$this->logger->error(
|
||||
sprintf(
|
||||
'Çàêàç %s íå ïåðåâåä¸í â îïëà÷åííûå: %s: %s',
|
||||
$order->getUid(),
|
||||
get_class($exception),
|
||||
$exception->getMessage(),
|
||||
),
|
||||
);
|
||||
$this->operationJournal->note($notification, $order, CallbackEvent::ORDER_UPDATE_FAILED, null);
|
||||
|
||||
return CallbackResponse::fail();
|
||||
}
|
||||
$event = CallbackEvent::PAID;
|
||||
}
|
||||
|
||||
// Îòìåòêó «îïëàòà çà÷òåíà» ïèøåì â æóðíàë òîëüêî ïîñëå òîãî, êàê çàêàç ñòàë îïëà÷åííûì.
|
||||
// Åñëè çàïèñü â æóðíàë íå óäàëàñü, îòâå÷àåì FAIL, è MONETA.RU ïîâòîðèò óâåäîìëåíèå.
|
||||
// Ïîâòîð óâèäèò, ÷òî çàêàç óæå îïëà÷åí, è òîëüêî äîïèøåò îòìåòêó
|
||||
// (ñîáûòèå ALREADY_PAID_IN_SHOP): ñòàòóñ íå ìåíÿåòñÿ, âòîðîãî ïèñüìà ïîêóïàòåëþ íåò.
|
||||
if (!$this->operationJournal->recordPaid($notification, $order, $event)) {
|
||||
return CallbackResponse::fail();
|
||||
}
|
||||
|
||||
return $this->response($notification, $order, ResultCode::paid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Îïëà÷åí ïî äàííûì ìàãàçèíà èëè ïî æóðíàëó ìîäóëÿ (ìàðêåð ìîã çàïèñàòüñÿ,
|
||||
* à ñòàòóñ ïîòîì ñìåíèë ìåíåäæåð).
|
||||
*/
|
||||
private function isPaid(Order $order): bool
|
||||
{
|
||||
return PaymentEligibility::isPaid($this->settings, $order)
|
||||
|| $this->operationJournal->findPaidOperation($order->getId()) !== null;
|
||||
}
|
||||
|
||||
private function payResponse(
|
||||
CallbackNotification $notification,
|
||||
Order $order,
|
||||
string $event,
|
||||
ResultCode $resultCode
|
||||
): CallbackResponse {
|
||||
$this->operationJournal->note($notification, $order, $event, $resultCode);
|
||||
|
||||
return $this->response($notification, $order, $resultCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Îòâåò íà Pay URL: `MNT_ATTRIBUTES` — òîëüêî â óñïåøíîì (`200`), íåóñïåøíûé — áåç íèõ.
|
||||
*/
|
||||
private function response(
|
||||
CallbackNotification $notification,
|
||||
Order $order,
|
||||
ResultCode $resultCode
|
||||
): CallbackResponse {
|
||||
$attributes = $resultCode->equals(ResultCode::paid()) ? $this->receiptSource->attributes($order) : [];
|
||||
|
||||
return (new PayResponse(
|
||||
$this->settings->getAccountId(),
|
||||
$notification->getTransactionId(),
|
||||
$order->getAmount(),
|
||||
$resultCode,
|
||||
$this->settings->signature(),
|
||||
$this->cms,
|
||||
))->toXml($attributes);
|
||||
}
|
||||
|
||||
private function unknownOrder(CallbackNotification $notification): CallbackResponse
|
||||
{
|
||||
if (!$notification->isCheck()) {
|
||||
$this->operationJournal->note($notification, null, CallbackEvent::UNKNOWN_ORDER, null);
|
||||
$this->logger->error(
|
||||
sprintf(
|
||||
'Îïëàòà ïî íåèçâåñòíîìó çàêàçó %s, îïåðàöèÿ %s.',
|
||||
$notification->getTransactionId(),
|
||||
$notification->getOperationId(),
|
||||
),
|
||||
);
|
||||
|
||||
return CallbackResponse::fail();
|
||||
}
|
||||
|
||||
$resultCode = ResultCode::rejected();
|
||||
$this->operationJournal->note($notification, null, CallbackEvent::UNKNOWN_ORDER, $resultCode);
|
||||
|
||||
// XML è ïðè âêëþ÷¸ííîé ôèñêàëèçàöèè: ÷åêà ó íåèçâåñòíîãî çàêàçà íåò, à JSON-ñõåìà
|
||||
// `cmsspecification.pdf` òðåáóåò õîòÿ áû îäíó ïîçèöèþ.
|
||||
return (new CheckResponse(
|
||||
$this->settings->getAccountId(),
|
||||
$notification->getTransactionId(),
|
||||
null,
|
||||
$resultCode,
|
||||
$this->settings->signature(),
|
||||
$this->cms,
|
||||
))->toXml();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\callback;
|
||||
|
||||
use MonetaPhpShop\order\PaymentEligibility;
|
||||
use MonetaPhpShop\protocol\ResultCode;
|
||||
|
||||
/**
|
||||
* ×òî ìîäóëü ñäåëàë ñ ïîäïèñàííûì óâåäîìëåíèåì — êîëîíêà `event` æóðíàëà îïåðàöèé.
|
||||
* Çíà÷åíèÿ ïåðñèñòÿòñÿ è ïîêàçûâàþòñÿ â æóðíàëå àäìèíêè — íå ïåðåèìåíîâûâàòü.
|
||||
* Ïðè÷èíû îòêàçà ïî çàêàçó áåðóòñÿ èç {@see \MonetaPhpShop\order\PaymentEligibility}.
|
||||
*/
|
||||
final class CallbackEvent
|
||||
{
|
||||
/** Ïðîâåðî÷íûé çàïðîñ, çàêàç ìîæíî îïëàòèòü (402 èëè 100). */
|
||||
public const CHECKED = 'checked';
|
||||
|
||||
/** Îïëàòà çà÷òåíà: çàêàç ïåðåâåä¸í â ñòàòóñ «ïîñëå îïëàòû». */
|
||||
public const PAID = 'paid';
|
||||
|
||||
/** Îïëàòà çà÷òåíà, íî çàêàç óæå áûë îïëà÷åí â ìàãàçèíå — ñòàòóñ íå òðîãàëè. */
|
||||
public const ALREADY_PAID_IN_SHOP = 'already_paid_in_shop';
|
||||
|
||||
/** Ïîâòîð óâåäîìëåíèÿ ïî óæå çà÷ò¸ííîé îïåðàöèè: îòâåò 200 áåç ïîáî÷íûõ ýôôåêòîâ. */
|
||||
public const REPEAT = 'repeat';
|
||||
|
||||
/** Âòîðàÿ îïåðàöèÿ MONETA.RU ïî îïëà÷åííîìó çàêàçó: äåíüãè ñïèñàíû äâàæäû, íóæåí ÷åëîâåê. */
|
||||
public const DUPLICATE_OPERATION = 'duplicate_operation';
|
||||
|
||||
/** Íîìåð çàêàçà èç `MNT_TRANSACTION_ID` íå íàéäåí. */
|
||||
public const UNKNOWN_ORDER = 'unknown_order';
|
||||
|
||||
/** Ñóììà çàêàçà èçìåíèëàñü ïîñëå ïîêàçà ôîðìû: îïëàòà ïî ñòàðîé ôîðìå íå ïðèíèìàåòñÿ. */
|
||||
public const ORDER_AMOUNT_CHANGED = 'order_amount_changed';
|
||||
|
||||
public const AMOUNT_MISMATCH = 'amount_mismatch';
|
||||
public const CURRENCY_MISMATCH = 'currency_mismatch';
|
||||
|
||||
/** `MNT_TEST_MODE` óâåäîìëåíèÿ íå ñîâïàë ñ òåñòîâûì ðåæèìîì ìîäóëÿ. */
|
||||
public const TEST_MODE_MISMATCH = 'test_mode_mismatch';
|
||||
|
||||
/** Çàêàç íå ïåðåâ¸ëñÿ â îïëà÷åííûå (îøèáêà ÿäðà PHPShop) — ïîâòîð óâåäîìëåíèÿ ïîïðîáóåò ñíîâà. */
|
||||
public const ORDER_UPDATE_FAILED = 'order_update_failed';
|
||||
|
||||
/** Ôèñêàëèçàöèÿ âêëþ÷åíà, à ÷åê äëÿ JSON-îòâåòà íå ñîáðàí. */
|
||||
public const RECEIPT_UNAVAILABLE = 'receipt_unavailable';
|
||||
|
||||
/** Íå äîæäàëèñü áëîêèðîâêè çàêàçà: ïàðàëëåëüíî îáðàáàòûâàåòñÿ äðóãîå óâåäîìëåíèå. */
|
||||
public const LOCK_TIMEOUT = 'lock_timeout';
|
||||
|
||||
/** Ïîäïèñè äëÿ æóðíàëà îïåðàöèé â àäìèíêå. */
|
||||
private const LABELS = [
|
||||
self::CHECKED => 'Ïðîâåðêà çàêàçà',
|
||||
self::PAID => 'Îïëàòà çà÷òåíà',
|
||||
self::ALREADY_PAID_IN_SHOP => 'Îïëàòà çà÷òåíà, çàêàç óæå áûë îïëà÷åí â ìàãàçèíå',
|
||||
self::REPEAT => 'Ïîâòîð óâåäîìëåíèÿ',
|
||||
self::DUPLICATE_OPERATION => 'Âòîðàÿ îïëàòà îïëà÷åííîãî çàêàçà: ñäåëàéòå âîçâðàò â ëè÷íîì êàáèíåòå MONETA.RU',
|
||||
self::UNKNOWN_ORDER => 'Çàêàç íå íàéäåí',
|
||||
self::ORDER_AMOUNT_CHANGED => 'Ñóììà çàêàçà èçìåíèëàñü ïîñëå ïîêàçà ôîðìû îïëàòû',
|
||||
self::AMOUNT_MISMATCH => 'Ñóììà íå ñîâïàäàåò ñ çàêàçîì',
|
||||
self::CURRENCY_MISMATCH => 'Âàëþòà íå ñîâïàäàåò',
|
||||
self::TEST_MODE_MISMATCH => 'Òåñòîâûé ðåæèì íå ñîâïàäàåò ñ íàñòðîéêîé ìîäóëÿ',
|
||||
self::ORDER_UPDATE_FAILED => 'Íå óäàëîñü ïåðåâåñòè çàêàç â îïëà÷åííûå',
|
||||
self::RECEIPT_UNAVAILABLE => '×åê íå ñîáðàí',
|
||||
self::LOCK_TIMEOUT => 'Çàêàç çàíÿò ïàðàëëåëüíûì óâåäîìëåíèåì',
|
||||
PaymentEligibility::NOT_CONFIGURED => 'Ìîäóëü íå íàñòðîåí',
|
||||
PaymentEligibility::OTHER_PAYMENT_METHOD => 'Ó çàêàçà äðóãîé ñïîñîá îïëàòû',
|
||||
PaymentEligibility::ALREADY_PAID => 'Çàêàç óæå îïëà÷åí',
|
||||
PaymentEligibility::CANCELLED => 'Çàêàç îòìåí¸í',
|
||||
PaymentEligibility::AWAITING_STATUS => 'Çàêàç åù¸ íå ïîäòâåðæä¸í ìåíåäæåðîì',
|
||||
PaymentEligibility::UNSUPPORTED_CURRENCY => 'Âàëþòà ìàãàçèíà íå ðóáëè',
|
||||
PaymentEligibility::ZERO_AMOUNT => 'Íóëåâàÿ ñóììà çàêàçà',
|
||||
];
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
/**
|
||||
* Çíà÷îê ñòðîêè æóðíàëà îïåðàöèé â àäìèíêå — glyphicon Bootstrap 3, êîòîðûé àäìèíêà PHPShop
|
||||
* ïîñòàâëÿåò ñî ñâîèìè òåìàìè (`admpanel/fonts/`, êëàññû `glyphicon-*` — â 6.7 â êàæäîé òåìå,
|
||||
* â 7.0 â `bootstrap.min.css`); öâåòà — êëàññû òåìû. Îïëàòà çà÷òåíà — çåë¸íàÿ ãàëî÷êà
|
||||
* â êðóæêå, îòêàç ìîäóëÿ (`FAIL` èëè êîä 500) — êðàñíûé êðåñòèê, ïðîâåðêà çàêàçà — ñåðûé
|
||||
* âîïðîñ; îñòàëüíîå (ïîâòîð óâåäîìëåíèÿ) — áåç çíà÷êà.
|
||||
*
|
||||
* @param int|null $resultCode `MNT_RESULT_CODE` îòâåòà ìîäóëÿ; null — îòâåò `FAIL`
|
||||
*/
|
||||
public static function iconHtml(string $event, ?int $resultCode): string
|
||||
{
|
||||
if ($event === self::PAID || $event === self::ALREADY_PAID_IN_SHOP) {
|
||||
return self::glyphicon('ok-sign', 'text-success');
|
||||
}
|
||||
if ($resultCode === null || $resultCode === ResultCode::REJECTED) {
|
||||
return self::glyphicon('remove-sign', 'text-danger');
|
||||
}
|
||||
if ($event === self::CHECKED) {
|
||||
return self::glyphicon('question-sign', 'text-muted');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private static function glyphicon(string $name, string $color): string
|
||||
{
|
||||
return '<span class="glyphicon glyphicon-' . $name . ' ' . $color . '" aria-hidden="true"></span>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Ïîäïèñü ñîáûòèÿ; íåèçâåñòíûé êîä (èç áóäóùåé âåðñèè) ïîêàçûâàåòñÿ êàê åñòü.
|
||||
*/
|
||||
public static function label(string $event): string
|
||||
{
|
||||
return self::LABELS[$event] ?? $event;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\callback;
|
||||
|
||||
/**
|
||||
* Общий на все запросы счётчик для {@see ThrottledLogger}.
|
||||
*/
|
||||
interface LogThrottle
|
||||
{
|
||||
/**
|
||||
* Можно ли записать строку сейчас.
|
||||
*
|
||||
* @param int $now unix-время
|
||||
* @param int $interval не чаще одной строки за столько секунд
|
||||
*
|
||||
* @return int|null null — строку пропустить (она учтена в счётчике); число — сколько строк
|
||||
* пропущено с прошлой записи
|
||||
*/
|
||||
public function pass(int $now, int $interval): ?int;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\callback;
|
||||
|
||||
/**
|
||||
* Технический журнал колбэка (не журнал операций): то, что нельзя писать в БД —
|
||||
* неподписанные запросы, сбои. Без секретов и без тела запроса.
|
||||
*/
|
||||
interface Logger
|
||||
{
|
||||
public function error(string $message): void;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\callback;
|
||||
|
||||
use MonetaPhpShop\order\Order;
|
||||
use MonetaPhpShop\protocol\CallbackNotification;
|
||||
use MonetaPhpShop\protocol\ResultCode;
|
||||
|
||||
/**
|
||||
* Æóðíàë îïåðàöèé ìîäóëÿ: êàæäîå ïîäïèñàííîå óâåäîìëåíèå è ìàðêåð çà÷ò¸ííîé îïëàòû.
|
||||
* Ìàðêåð — óíèêàëüíûé ïî çàêàçó: ýòî è åñòü èäåìïîòåíòíîñòü Pay URL íà MyISAM áåç òðàíçàêöèé.
|
||||
*/
|
||||
interface OperationJournal
|
||||
{
|
||||
/**
|
||||
* `MNT_OPERATION_ID` îïëàòû, óæå çà÷ò¸ííîé ïî çàêàçó, èëè null.
|
||||
*/
|
||||
public function findPaidOperation(int $orderId): ?string;
|
||||
|
||||
/**
|
||||
* Çàïèñàòü çà÷ò¸ííóþ îïëàòó. false — ïî çàêàçó ìàðêåð óæå åñòü (ñðàáîòàë UNIQUE).
|
||||
*/
|
||||
public function recordPaid(CallbackNotification $notification, Order $order, string $event): bool;
|
||||
|
||||
/**
|
||||
* Çàïèñàòü óâåäîìëåíèå áåç çà÷¸òà îïëàòû.
|
||||
*
|
||||
* @param ResultCode|null $resultCode null — îòâåòèëè òåêñòîì `FAIL`
|
||||
*/
|
||||
public function note(
|
||||
CallbackNotification $notification,
|
||||
?Order $order,
|
||||
string $event,
|
||||
?ResultCode $resultCode
|
||||
): void;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\callback;
|
||||
|
||||
use MonetaPhpShop\Money;
|
||||
use MonetaPhpShop\order\Order;
|
||||
|
||||
/**
|
||||
* Çàêàçû ìàãàçèíà ñî ñòîðîíû êîëáýêà.
|
||||
*/
|
||||
interface OrderGateway
|
||||
{
|
||||
/**
|
||||
* Êàæäûé âûçîâ ÷èòàåò ÁÄ çàíîâî: âíóòðè áëîêèðîâêè íóæåí ñâåæèé ñíèìîê.
|
||||
*
|
||||
* @phpstan-impure
|
||||
*/
|
||||
public function findByUid(string $uid): ?Order;
|
||||
|
||||
/**
|
||||
* Îòìåòèòü îïëàòó ñðåäñòâàìè ÿäðà: çàïèñü â æóðíàëå îïëàò PHPShop, ñìåíà ñòàòóñà
|
||||
* (ñ ïèñüìîì ïîêóïàòåëþ è ñïèñàíèåì ñî ñêëàäà, êàê íàñòðîåíî ó ñòàòóñà), ôëàã `paid`.
|
||||
*/
|
||||
public function markPaid(Order $order, int $paidStatus, Money $amount): void;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\callback;
|
||||
|
||||
/**
|
||||
* Áëîêèðîâêà çàêàçà íà âðåìÿ îáðàáîòêè óâåäîìëåíèÿ: ïîâòîð Pay URL, ïðèøåäøèé
|
||||
* ïàðàëëåëüíî ñ ïåðâûì, æä¸ò åãî è âèäèò óæå çàïèñàííûé ìàðêåð îïëàòû.
|
||||
*/
|
||||
interface OrderLock
|
||||
{
|
||||
public function acquire(int $orderId): bool;
|
||||
|
||||
public function release(int $orderId): void;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\callback;
|
||||
|
||||
use MonetaPhpShop\order\Order;
|
||||
|
||||
interface ReceiptSource
|
||||
{
|
||||
/**
|
||||
* `MNT_ATTRIBUTES` XML-îòâåòà: `INVENTORY`, `CLIENT`, `CUSTOMER`, `PHONE`, ñòðîêè â UTF-8.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function attributes(Order $order): array;
|
||||
|
||||
/**
|
||||
* Îáúåêò `receipt` JSON-îòâåòà Check URL èëè null, åñëè ÷åê íå ñîáðàòü.
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function jsonReceipt(Order $order): ?array;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\callback;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Æóðíàë îòêàçîâ äî ïðîâåðêè ïîäïèñè: àäðåñ êîëáýêà ïóáëè÷íûé, è çàïèñü íà êàæäûé ìóñîðíûé
|
||||
* çàïðîñ ðàçäóâàëà áû æóðíàë îøèáîê PHP. Ïèøåò íå ÷àùå ñòðîêè çà èíòåðâàë è â íåé ñîîáùàåò,
|
||||
* ñêîëüêî îòêàçîâ ïðîïóùåíî. Åñëè ñ÷¸ò÷èê íåäîñòóïåí (òàáëèöû íåò — ìîäóëü âûêëþ÷åí), ïèøåò âñ¸.
|
||||
*/
|
||||
final class ThrottledLogger implements Logger
|
||||
{
|
||||
public const INTERVAL = 60;
|
||||
|
||||
/** @var Logger */
|
||||
private $logger;
|
||||
|
||||
/** @var LogThrottle */
|
||||
private $throttle;
|
||||
|
||||
/** @var callable(): int */
|
||||
private $clock;
|
||||
|
||||
/**
|
||||
* @param callable(): int|null $clock unix-âðåìÿ; ïî óìîë÷àíèþ `time()`
|
||||
*/
|
||||
public function __construct(Logger $logger, LogThrottle $throttle, ?callable $clock = null)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
$this->throttle = $throttle;
|
||||
$this->clock = $clock ?? 'time';
|
||||
}
|
||||
|
||||
public function error(string $message): void
|
||||
{
|
||||
try {
|
||||
$skipped = $this->throttle->pass(($this->clock)(), self::INTERVAL);
|
||||
} catch (Throwable $exception) {
|
||||
$this->logger->error($message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($skipped === null) {
|
||||
return;
|
||||
}
|
||||
if ($skipped > 0) {
|
||||
$message .= sprintf(
|
||||
' Ïåðåä ýòèì íå çàïèñàíî îòêàçîâ: %d (íå ÷àùå ñòðîêè â %d ñ).',
|
||||
$skipped,
|
||||
self::INTERVAL,
|
||||
);
|
||||
}
|
||||
$this->logger->error($message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user