findOpenForItem($userId, $component, $paymentArea, $itemId); $created = false; if ($open !== null && $open->accountId === $accountId && $open->amount->equals($amount) && $open->snapshot->equals($snapshot) ) { return $open; } $created = true; return $this->create($accountId, $userId, $component, $paymentArea, $itemId, $amount, $currency, $snapshot); } public function create( int $accountId, int $userId, string $component, string $paymentArea, int $itemId, Money $amount, string $currency, OrderSnapshot $snapshot, ): Order { global $DB; if (strtoupper($currency) !== AssistantProtocol::CURRENCY) { throw new InvalidArgumentException(get_string('error:unsupportedcurrency', 'paygw_moneta')); } if (!$amount->isPositive()) { throw new InvalidArgumentException(get_string('error:invalidamount', 'paygw_moneta')); } $now = time(); $record = (object) [ 'paymentid' => null, 'accountid' => $accountId, 'userid' => $userId, 'component' => $component, 'paymentarea' => $paymentArea, 'itemid' => $itemId, 'merchantorderid' => \core\uuid::generate(), 'providertransactionid' => null, 'amount' => $amount->toDecimal(), 'currency' => AssistantProtocol::CURRENCY, 'status' => TransactionStatus::New->value, 'lasterror' => null, 'snapshot' => $snapshot->toJson(), 'timecreated' => $now, 'timemodified' => $now, 'timecompleted' => null, ]; $record->id = $DB->insert_record(self::TABLE, $record); return Order::fromRecord($record); } public function findByMerchantOrderId(string $merchantOrderId): ?Order { global $DB; $record = $DB->get_record(self::TABLE, ['merchantorderid' => $merchantOrderId]); return $record === false ? null : Order::fromRecord($record); } public function findById(int $id): ?Order { global $DB; $record = $DB->get_record(self::TABLE, ['id' => $id]); return $record === false ? null : Order::fromRecord($record); } public function findOpenForItem(int $userId, string $component, string $paymentArea, int $itemId): ?Order { global $DB; [$statusSql, $params] = $DB->get_in_or_equal( [TransactionStatus::New->value, TransactionStatus::Pending->value], SQL_PARAMS_NAMED, 'status', ); $records = $DB->get_records_select( self::TABLE, "userid = :userid AND component = :component AND paymentarea = :paymentarea AND itemid = :itemid AND status {$statusSql}", $params + [ 'userid' => $userId, 'component' => $component, 'paymentarea' => $paymentArea, 'itemid' => $itemId, ], 'timecreated DESC, id DESC', '*', 0, 1, ); $record = reset($records); return $record === false ? null : Order::fromRecord($record); } /** * Заказ покупателя по товару, у которого оплата прошла, а доставка сорвалась: * новую оплату начинать нельзя — повтор уведомления доставит этот. */ public function findFailedForItem(int $userId, string $component, string $paymentArea, int $itemId): ?Order { global $DB; $records = $DB->get_records( self::TABLE, [ 'userid' => $userId, 'component' => $component, 'paymentarea' => $paymentArea, 'itemid' => $itemId, 'status' => TransactionStatus::Failed->value, ], 'timecreated DESC, id DESC', '*', 0, 1, ); $record = reset($records); return $record === false ? null : Order::fromRecord($record); } /** * Сервис начал оплату: пришёл проверочный запрос. */ public function markPending(Order $order): Order { if ($order->status !== TransactionStatus::New) { return $order; } return $this->update($order, ['status' => TransactionStatus::Pending->value]); } /** * Оплата подтверждена и доставлена: фиксируем операцию сервиса и платёж ядра. */ public function markPaid(Order $order, string $providerTransactionId, int $paymentId): Order { if (!$order->status->isPayable()) { throw new InvalidArgumentException('Оплатить можно только неоплаченный и не отменённый заказ.'); } return $this->update($order, [ 'status' => TransactionStatus::Paid->value, 'providertransactionid' => $providerTransactionId, 'paymentid' => $paymentId, 'lasterror' => null, 'timecompleted' => time(), ]); } public function markCanceled(Order $order, RejectionReason $reason): Order { if (!$order->status->isOpen()) { throw new InvalidArgumentException('Отменить можно только открытый заказ.'); } return $this->update($order, ['status' => TransactionStatus::Canceled->value, 'lasterror' => $reason->value]); } /** * Зачисление сорвалось после подтверждённой оплаты — заказ требует внимания администратора. */ public function markFailed(Order $order, RejectionReason $reason): Order { if ($order->status === TransactionStatus::Paid) { throw new InvalidArgumentException('Оплаченный заказ нельзя пометить ошибочным.'); } return $this->update($order, ['status' => TransactionStatus::Failed->value, 'lasterror' => $reason->value]); } /** * Код причины последнего отказа без смены статуса (для отчёта администратора). */ public function noteError(Order $order, RejectionReason $reason): Order { return $this->update($order, ['lasterror' => $reason->value]); } /** * @param array $fields */ private function update(Order $order, array $fields): Order { global $DB; $record = (object) ($fields + ['id' => $order->id, 'timemodified' => time()]); $DB->update_record(self::TABLE, $record); $fresh = $DB->get_record(self::TABLE, ['id' => $order->id], '*', MUST_EXIST); return Order::fromRecord($fresh); } }