_<сумма>_UTC`: номер заказа PHPShop * (`phpshop_orders.uid`, например `4-36`), сумма формы в копейках и метка выдачи формы в UTC, * `4-36_1600000_20260930113005UTC`. */ final class TransactionId { /** * Номер заказа PHPShop. Штатно `<число>-<префикс>` (`PHPShopDone::setNum()`), * но хук `setNum` другого модуля может его переопределить, поэтому допускаются * латиница, цифры и дефис в пределах `phpshop_orders.uid varchar(64)` — без `_`. */ public const ORDER_UID = '[0-9A-Za-z](?:[0-9A-Za-z-]{0,62}[0-9A-Za-z])?'; public const SEPARATOR = '_'; /** Сумма в копейках без ведущих нулей; 16 цифр вмещают {@see Money::MAX}. */ private const AMOUNT = '0|[1-9][0-9]{0,15}'; /** * Пояс метки — всегда UTC, буквами. */ private const ZONE = 'UTC'; private const PATTERN = '/\A (?' . self::ORDER_UID . ') # номер заказа PHPShop ' . self::SEPARATOR . ' (?' . self::AMOUNT . ') # сумма формы в копейках ' . self::SEPARATOR . ' [0-9]{14} # метка времени YmdHis ' . self::ZONE . ' # пояс метки \z/x'; /** @var string */ private $orderUid; /** @var Money */ private $amount; private function __construct(string $orderUid, Money $amount) { $this->orderUid = $orderUid; $this->amount = $amount; } /** * @param Money $amount сумма формы (`MNT_AMOUNT`) * @param DateTimeImmutable $issuedAt момент выдачи формы, в любом поясе — метка пишется в UTC */ public static function build(string $orderUid, Money $amount, DateTimeImmutable $issuedAt): string { if (preg_match('/\A' . self::ORDER_UID . '\z/', $orderUid) !== 1) { throw new InvalidArgumentException('Номер заказа не подходит для MNT_TRANSACTION_ID.'); } return $orderUid . self::SEPARATOR . $amount->toMinorUnits() . self::SEPARATOR . $issuedAt->setTimezone(new DateTimeZone(self::ZONE))->format('YmdHis') . self::ZONE; } /** * Номер заказа и сумма формы из пришедшего `MNT_TRANSACTION_ID` или null, если строка не нашего формата. */ public static function parse(string $raw): ?self { if (preg_match(self::PATTERN, $raw, $matches) !== 1) { return null; } try { $amount = Money::fromMinorUnits((int) $matches['amount']); } catch (InvalidArgumentException $exception) { return null; } return new self($matches['uid'], $amount); } /** * Номер заказа из того, что ввёл администратор (фильтр журнала операций): сам номер (`33-57`) * или целиком `MNT_TRANSACTION_ID`, скопированный из кабинета MONETA.RU. Иначе — null. */ public static function orderUidFromInput(string $input): ?string { $input = trim($input); if (preg_match('/\A' . self::ORDER_UID . '\z/', $input) === 1) { return $input; } $transactionId = self::parse($input); return ($transactionId === null) ? null : $transactionId->getOrderUid(); } public function getOrderUid(): string { return $this->orderUid; } /** * Сумма платёжной формы, выпустившей этот номер. */ public function getAmount(): Money { return $this->amount; } }