feat: paygw_moneta 1.0.0, MONETA.Assistant payment gateway for Moodle
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace paygw_moneta\local\order;
|
||||
|
||||
use core_payment\helper;
|
||||
use moodle_exception;
|
||||
use paygw_moneta\local\GatewayConfig;
|
||||
use paygw_moneta\local\Money;
|
||||
use paygw_moneta\local\protocol\AssistantProtocol;
|
||||
use paygw_moneta\local\receipt\Client;
|
||||
use paygw_moneta\local\receipt\Receipt;
|
||||
use paygw_moneta\local\receipt\ReceiptText;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Создание заказа для оплачиваемого объекта Moodle: сумма от ядра, снимок
|
||||
* настроек шлюза, чек с одной позицией, покупатель из профиля.
|
||||
*
|
||||
* @package paygw_moneta
|
||||
* @copyright 2026 Moneta Labs {@link https://moneta.ru/}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class OrderFactory
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TransactionRepository $repository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param stdClass $user покупатель (`$USER`): id, email, phone1, phone2, ФИО
|
||||
* @param GatewayConfig $config настройки шлюза для этого объекта (`GatewayConfigRepository::forPayable`)
|
||||
* @param bool|null $created true — заказ создан сейчас, false — переиспользован открытый
|
||||
*/
|
||||
public function start(
|
||||
string $component,
|
||||
string $paymentArea,
|
||||
int $itemId,
|
||||
stdClass $user,
|
||||
GatewayConfig $config,
|
||||
?bool &$created = null,
|
||||
): Order {
|
||||
if (!in_array(
|
||||
GatewayConfig::GATEWAY,
|
||||
helper::get_available_gateways($component, $paymentArea, $itemId),
|
||||
true,
|
||||
)) {
|
||||
throw new moodle_exception('gatewaynotfound', 'payment');
|
||||
}
|
||||
|
||||
if (!$config->acceptsNewOrders()) {
|
||||
throw new moodle_exception('gatewaynotfound', 'payment');
|
||||
}
|
||||
|
||||
// Оплата у сервиса прошла, доставка сорвалась: новый заказ создавать нельзя,
|
||||
// иначе покупатель заплатит дважды, а повтор уведомления по старому доставит его.
|
||||
if ($this->repository->findFailedForItem((int) $user->id, $component, $paymentArea, $itemId) !== null) {
|
||||
throw new moodle_exception('error:orderinprogress', 'paygw_moneta');
|
||||
}
|
||||
|
||||
$payable = helper::get_payable($component, $paymentArea, $itemId);
|
||||
$currency = $payable->get_currency();
|
||||
if ($currency !== AssistantProtocol::CURRENCY) {
|
||||
throw new moodle_exception('error:unsupportedcurrency', 'paygw_moneta');
|
||||
}
|
||||
$surcharge = helper::get_gateway_surcharge(GatewayConfig::GATEWAY);
|
||||
$amount = Money::fromFloat(helper::get_rounded_cost($payable->get_amount(), $currency, $surcharge));
|
||||
if (!$amount->isPositive()) {
|
||||
throw new moodle_exception('error:invalidamount', 'paygw_moneta');
|
||||
}
|
||||
|
||||
$itemName = self::itemName($component, $paymentArea, $itemId);
|
||||
|
||||
$client = Client::fromContacts(
|
||||
email: (string) ($user->email ?? ''),
|
||||
phone: self::phone($user),
|
||||
name: self::fullName($user),
|
||||
fallbackEmail: $config->receiptEmail,
|
||||
) ?? throw new moodle_exception('gatewaynotfound', 'payment');
|
||||
|
||||
$snapshot = new OrderSnapshot(
|
||||
accountNumber: $config->accountNumber,
|
||||
testMode: $config->testMode,
|
||||
fiscalization: $config->fiscalization,
|
||||
vat: $config->vat,
|
||||
receipt: Receipt::forCourse($itemName, $amount, $client, $config->vat),
|
||||
description: $itemName,
|
||||
subscriberId: self::subscriberId($user),
|
||||
);
|
||||
|
||||
return $this->repository->obtain(
|
||||
accountId: $payable->get_account_id(),
|
||||
userId: (int) $user->id,
|
||||
component: $component,
|
||||
paymentArea: $paymentArea,
|
||||
itemId: $itemId,
|
||||
amount: $amount,
|
||||
currency: $currency,
|
||||
snapshot: $snapshot,
|
||||
created: $created,
|
||||
);
|
||||
}
|
||||
|
||||
private static function itemName(string $component, string $paymentArea, int $itemId): string
|
||||
{
|
||||
global $DB;
|
||||
|
||||
if ($component === 'enrol_fee' && $paymentArea === 'fee') {
|
||||
$courseId = $DB->get_field('enrol', 'courseid', ['id' => $itemId, 'enrol' => 'fee']);
|
||||
$name = $courseId ? $DB->get_field('course', 'fullname', ['id' => $courseId]) : false;
|
||||
if (is_string($name) && ReceiptText::sanitize($name) !== '') {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
|
||||
return get_string('genericitem', 'paygw_moneta');
|
||||
}
|
||||
|
||||
public static function subscriberId(stdClass $user): string
|
||||
{
|
||||
$email = trim((string) ($user->email ?? ''));
|
||||
if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL) !== false) {
|
||||
return $email;
|
||||
}
|
||||
|
||||
return Client::normalizePhone(self::phone($user)) ?? (string) $user->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Первый заполненный телефон профиля: рабочий, затем мобильный.
|
||||
*/
|
||||
private static function phone(stdClass $user): string
|
||||
{
|
||||
$phone = trim((string) ($user->phone1 ?? ''));
|
||||
|
||||
return $phone !== '' ? $phone : trim((string) ($user->phone2 ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* ФИО для чека в порядке «Фамилия Имя Отчество» — как `name` объекта
|
||||
* `client` в `kassaspecification.pdf`, независимо от настройки отображения имён.
|
||||
*/
|
||||
private static function fullName(stdClass $user): string
|
||||
{
|
||||
$parts = array_filter(
|
||||
array_map(
|
||||
static fn(string $field): string => trim((string) ($user->$field ?? '')),
|
||||
['lastname', 'firstname', 'middlename'],
|
||||
),
|
||||
static fn(string $part): bool => $part !== '',
|
||||
);
|
||||
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user