use Modern::Perl;
use utf8;
use Mojo::Base -strict, -async_await, -signatures;
use Data::Dumper;
use experimental qw/switch/;
our $client;
our $abon_client;
our $config;
our $redis;
sub __
{
@_;
}
##############################################
our $commands = [
{command=>"help", name=>__("Помощь"), description=>__("Список доступных команд")},
{command=>"info", name=>__("Информация"), description=>__("Информация о пользователе")},
{command=>"balance", name=>__("Баланс"), description=>__("Проверка баланса"), main=>1, icon=>"\x{1F45B}"},
{command=>"service", name=>__("Сервисы"), description=>__("Подключенные сервисы")},
{command=>"credit", name=>__("Кредит"), description=>__("Установка кредита"), main=>1, icon=>"\x{1FAF0}"},
{command=>"card", name=>__("Карта пополнения"), description=>__("Оплата карточкой пополнения"), main=>1, icon=>"\x{1F4B3}"},
{command=>"payberry", name=>__("Payberry"), description=>__("Оплата через Payberry"), main=>1, icon=>"\x{1F17F}\x{FE0F}"},
{command=>"transfer", name=>__("Перевод"), description=>__("Перевод денег")},
{command=>"new_task", name=>__("Новая заявка"), description=>__("Новая заявка")},
{command=>"tasks", name=>__("Заявки"), description=>__("Открытые заявки"), main=>1, icon=>"\x{2692}\x{FE0F}"},
{command=>"support", name=>__("Техподдержка"), description=>__("Связь с техподдержкой")},
{command=>"logout", name=>__("Выход"), description=>__("Выход")},
];
#############################
our $lex_actions = {
xfer => {
name => __("перевод денег"),
canceled => __("отменён"),
},
card => {
name => __("пополнение карточкой"),
canceled => __("отмененo"),
},
};
our $lex_vars = {
xfer_to => __("номер личного счета"),
xfer_amount => __("сумму"),
card_code => __("код карточки"),
card_serial => __("номер карточки"),
};
##############################################
sub command_help($fsa, $info)
{
my @list = map { "/$_->{command} " . _($_->{description}) } @$commands;
reply($info, join("\n", @list));
}
async sub command_logout
{
my ($fsa, $info) = @_;
my $uid = $fsa->note("[uid]");
await $client->delete_p("client", "/client/$uid/telegram");
reply_with($info, {button_menu => 0}, _("Благодарим за использование нашего бота"));
$fsa->delete_note("[uid]");
$fsa->state("logged_out");
}
async sub command_balance
{
my ($fsa, $info) = @_;
my $uid = $fsa->note("[uid]");
my $money = await $abon_client->get_p($info, "client", "/client/$uid/money?human=1");
my $cur = $money->{human};
my @lines = (
sprintf("%s: %.2f $cur (%s %.2f $cur + %s %.2f $cur) ",
_("Ваш баланс"), $money->{balance}, ("депозит"), $money->{deposit}, _("кредит"), $money->{credit}),
);
push @lines, sprintf("%s: %s", _("Оплачено включительно до"), format_date($money->{last_day})) if $money->{last_day} ne "-";
push @lines, sprintf("%s: %d%%", _("Скидка"), $money->{reduction}) if $money->{reduction};
push @lines, sprintf("%s: %.2f $cur %s (%s)",
_("Последнее снятие"), $money->{last_withdrawal}->{sum}, format_time($money->{last_withdrawal}->{date}), $money->{last_withdrawal}->{comment})
if $money->{last_withdrawal}->{sum};
push @lines, sprintf("%s: %.2f $cur %s", _("Последний платеж"), $money->{last_payment}->{sum}, format_time($money->{last_payment}->{date}))
if $money->{last_payment}->{sum};
for (keys %{ $money->{accounts} })
{
push @lines, sprintf("%s: %.2f $cur", $_, $money->{accounts}->{$_});
}
reply($info, @lines);
}
async sub command_info
{
my ($fsa, $info) = @_;
my $uid = $fsa->note("[uid]");
my $client = await $abon_client->get_p($info, "client", "/client/$uid");
reply($info,
sprintf("%s: %d", _("Номер учетной записи"), $client->{uid}),
sprintf("%s: %s", _("Логин"), $client->{login}),
sprintf("%s: %s", _("ФИО"), $client->{fio}),
sprintf("%s: %s", _("Адрес"), $client->{address}),
sprintf("%s: %s", _("Телефон"), $client->{phone}),
);
}
async sub command_credit
{
my ($fsa, $info) = @_;
my $uid = $fsa->note("[uid]");
my $money = await $abon_client->get_p($info, "client", "/client/$uid/money?human=1");
if ($money->{credit} > 0)
{
return reply($info, sprintf("%s %.2f %s. %s", _("У вас уже установлен кредит"), $money->{credit}, $money->{human},
_("Продлевать кредит повторно до оплаты нельзя. При следующей оплате кредит будет погашен")));
}
reply_with($info, {
inline_menu => [[
{ text=>_("Я согласен с условиями"), callback_data=>"\x00/set-credit" },
]],
},
_("Вы можете самостоятельно установить кредит на два дня"),
_("Ограничения: только для физических лиц, продлевать кредит повторно до оплаты нельзя. При следующей оплате кредит будет погашен"),
);
}
async sub callback_set_credit
{
my ($fsa, $info) = @_;
my $uid = $fsa->note("[uid]");
my $res = await $abon_client->post_p($info, "client", "/client/$uid/credit", {human=>1});
return reply($info, _("Кредит не имеет смысла для бесплатных тарифных планов")) if $res->{credit} == 0;
await reply_with($info, {button_menu=>1},
sprintf("%s %.2f %s", _("Установлен кредит "), $res->{credit}, $res->{human}),
"",
);
command_balance($fsa, $info);
}
async sub command_service
{
my ($fsa, $info) = @_;
my $uid = $fsa->note("[uid]");
my $res = await $abon_client->get_p($info, "client", "/client/$uid/service?human=1&as-array=1");
my @list = map { sprintf("%s: %s (%s '%s')", $_->{name_ru}, format_wd($_->{tariff}, $_->{human}), _("тариф"), $_->{tariff}->{name_ru}) }
grep { !$_->{disabled} } @$res;
reply($info, @list);
};
######################################
async sub command_transfer
{
my ($fsa, $info) = @_;
reply($info, _("Введите номер личного счета пользователя, которому вы хотите перевести деньги со своего собственного счета"));
return needs_input($fsa, "xfer", "xfer_to");
}
async sub verify_xfer_to
{
my ($fsa, $target_uid, $info) = @_;
my $uid = $fsa->note("[uid]");
return _("Номер личного счета должен состоять из цифр") unless $target_uid =~ /^\d+$/;
return _("Нельзя перевести деньги себе самому") if $uid==$target_uid;
my $res = eval { await $abon_client->get_p($info, "client", "/client/$target_uid") };
if ($@)
{
die $@ unless $@->{code} == 404;
return _("Абонент") . " $target_uid " . _("не найден");
}
return _("Абонент") . " $target_uid " . _("отключен") if $res->{disabled};
$fsa->note("xfer_fio" => $res->{fio});
return undef;
}
async sub use_xfer_to
{
my ($fsa, $target_uid, $info) = @_;
my $uid = $fsa->note("[uid]");
my $nick = $fsa->note("xfer_fio");
my @fio = split(/\s+/, $nick);
if (@fio)
{
my $f = shift(@fio);
$nick = join(" ", (substr($f, 0, 1) . ".", @fio));
}
$fsa->note(xfer_nick => $nick);
reply(
$info, _("Получатель денег: ") . $nick,
_("Теперь введите сумму, которую хотите перевести"),
);
return needs_input($fsa, "xfer", "xfer_amount");
}
sub verify_xfer_amount
{
my ($fsa, $amount, $info) = @_;
my $uid = $fsa->note("[uid]");
$amount =~ s/,/./g;
$amount =~ s/[^\d\.]//g;
my $tmp = $amount;
my $count = $amount =~ tr/.//;
return _("Вы ввели неправильную сумму") if !$amount || $count>1;
}
async sub use_xfer_amount
{
my ($fsa, $amount, $info) = @_;
my $uid = $fsa->note("[uid]");
my $to_uid = $fsa->note("xfer_to");
my $nick = $fsa->note("xfer_nick");
reply_with($info, {
inline_menu => [[
{ text=>_("Подтвердите перевод"), callback_data=>"\x00/transfer" },
]]
},
sprintf("%.2f %s %s %d (%s)", $amount, $config->{currency}->{human}, _(" на счет абонента"), $to_uid, $nick)
);
return "command";
}
async sub callback_transfer
{
my ($fsa, $info) = @_;
my $uid = $fsa->note("[uid]");
my $to_uid = $fsa->note("xfer_to");
my $amount = $fsa->note("xfer_amount");
return unless $to_uid && $amount;
my $res = await $abon_client->post_p($info, "client", "/client/$uid/money/to/$to_uid", {
amount => $amount,
ip => "0.0.0.0",
via => "abonbot",
currency => $config->{currency}->{name},
});
reply_with($info, {button_menu=>1}, _("Деньги успешно переведены"));
command_balance($fsa, $info);
}
##########################################
async sub command_new_task
{
my ($fsa, $info) = @_;
my $uid = $fsa->note("[uid]");
my $res = await $abon_client->get_p($info, "task", "/task?created_by_client=$uid&list=new,work");
if ($res->{total})
{
return reply($info, _("Создание новой заявки невозможно, пока не будут решены уже открытые"));
}
reply($info, _("Изложите вашу проблему"));
$fsa->state("task_needs_descr");
};
async sub callback_task_post
{
my ($fsa, $info) = @_;
my $uid = $fsa->note("[uid]");
my $descr = $fsa->note("task_text");
return unless $descr;
my $params = {
description => $descr,
"for-client" => $uid,
client => $uid,
type => "client-issue",
list => "new",
task_attr => {
source => "telegram"
},
};
my $res = await $client->post_json_p("task", "/task", $params);
reply_with($info, {button_menu=>1}, _("Ваша заявка размещена"));
command_tasks($fsa, $info);
}
async sub callback_task_cancel
{
my ($fsa, $info) = @_;
$fsa->delete_note("task_text");
$fsa->delete_note("if_edited");
reply_with($info, {button_menu=>1}, _("Заявка отменена"));
}
async sub command_tasks
{
my ($fsa, $info) = @_;
my $uid = $fsa->note("[uid]");
my $res = await $abon_client->get_p($info, "task", "/task?created_by_client=$uid&list=new,work&sort=id&with_comments_for_client=1");
return reply($info, _("Открытых заявок нет")) unless $res->{total};
my @str = map {
_("Заявка") . " $_->{number}\n$_->{description}\n" . _("Комментариев") . " " . $_->{total_comments} . ", " . _(" не прочитано") . " " . $_->{unread_comments} . ""
} @{$res->{data}};
my $menu = [[
map {
{ text => _("Комментарии к заявке") . " " . $_->{number}, callback_data => "\x00/task $_->{entity}" }
} @{$res->{data}}
]];
if (@{$res->{data}})
{
reply_with($info, {inline_menu=>$menu}, @str);
}
else
{
reply($info, _("Комментариев пока нет"));
}
};
async sub callback_task
{
my ($fsa, $info, $task_id) = @_;
my $uid = $fsa->note("[uid]");
my $res = await $client->get_p("task", "/task/$task_id?with_comments_for_client=1");
my @str = map {
format_timestamp($_->{created}) . " " . "" . ($_->{created_by}->[0] eq "worker" ? _("Оператор") : _("Вы")) . ":\n"
. $_->{text}
} @{ $res->{comments} };
my $menu = [[
{text => _("Добавить комментарий"), callback_data => "\x00comment $task_id"},
]];
unless (@str)
{
$str[0] = "" . _("Комментариев пока нет") . "";
}
reply_with($info, {inline_menu=>$menu}, @str);
$client->post_p("task", "/task/$task_id/comment/read", {for_client=>$uid});
}
async sub callback_comment
{
my ($fsa, $info, $task_id) = @_;
$fsa->note(task_id => $task_id);
$fsa->state("task_needs_comment");
reply($info, _("Введите текст вашего ответа"));
}
##################################################
async sub command_payberry
{
my ($fsa, $info) = @_;
my $uid = $fsa->note("[uid]");
my $menu = [[
{text => _("Payberry"), url => $config->{pay}->{payberry_url} . "?acc=$uid"}
]];
reply_with($info, {inline_menu=>$menu}, _("Для оплаты перейдите по ссылке"));
}
#####################################
use constant FAIL_BLOCK => 10;
use constant FAIL_ASK_SERIAL => 3;
async sub command_card
{
my ($fsa, $info) = @_;
my $uid = $fsa->note("[uid]");
my $key = "card-guess-$uid";
my $failed = $redis->get($key) || 0;
if ($failed > FAIL_BLOCK)
{
return reply($info, _("Вы ввели неправильный код слишком много раз. Пополнение карточкой заблокировано на сутки"));
}
my $res = await $client->get_p("client", "/client/$uid");
if ($res->{disabled})
{
return reply($info, _("Пополнение счета недоступно отключенным пользователям"));
}
reply($info, _("Введите код карточки пополнения (16 цифр, можно разделять их знаком '-')"));
return needs_input($fsa, "card", "card_code");
}
sub verify_card_code($fsa, $code, $info)
{
$code =~ s/\D//g;
return _("Код карточки должен состоять из 16 цифр") unless $code =~ /^\d{16}$/;
return undef;
}
async sub use_card_code
{
my ($fsa, $code, $info) = @_;
my $uid = $fsa->note("[uid]");
my $key = "card-guess-$uid";
my $fails = $redis->get($key) || 0;
if ($fails > FAIL_ASK_SERIAL)
{
reply($info, _("Теперь введите номер карточки (7 цифр, можно разделять знаком'-')"));
return needs_input($fsa, "card", "card_serial");
}
else
{
pay_from_card($info, $uid, $code, "");
return "command";
}
}
sub verify_card_serial($fsa, $serial, $info)
{
$serial =~ s/\D//g;
return _("Номер карточки должен состоять из 7 цифр") unless $serial =~ /^\d{7}$/;
return undef;
}
async sub use_card_serial
{
my ($fsa, $serial, $info) = @_;
pay_from_card($info, $fsa->note("[uid]"), $fsa->note("card_code"), $serial);
}
async sub pay_from_card
{
my ($info, $uid, $code, $serial) = @_;
my $args = {
code => $code =~ s/\D//gr,
uid => $uid,
domain => $config->{domain},
source => "abonbot",
ip => "0.0.0.0",
};
$args->{serial} = $serial =~ s/\D//gr if $serial;
my $res = eval { await $client->post_p("card", "/redemption", $args) };
unless($@)
{
return reply_with($info, {button_menu=>1}, _("Ваш счёт пополнен на " . $res->{amount} . " " . $res->{currency_short_name_ru} . ". " . _("Оплачено включительно до ") . $res->{paid_until}));
};
given($@->{code})
{
when(400)
{
reply_with($info, {button_menu=>1}, _("Вы ввели неправильный номер или код карточки."), _("Пополнение прервано"));
my $key = "card-guess-$uid";
$redis->incr($key);
$redis->expire($key, 24 * 3600);
}
when(409)
{
reply_with($info, {button_menu=>1}, _("Карточка уже использована."), _("Пополнение прервано"));
}
when(406)
{
reply_with($info, {button_menu=>1}, _("Карточка не активирована. Обратитесь в службу поддержки."), _("Пополнение прервано"));
}
default
{
die $@;
}
}
};
#####################################################
sub command_support
{
my ($fsa, $info) = @_;
return reply($info, _("Телефоны техподдержки:"), @{$config->{support_phones}});
}
##################################################
sub needs_input($fsa, $action, $var)
{
$fsa->delete_note($var);
$fsa->note(input_var => $var);
$fsa->note(input_action => $action);
$fsa->state("needs_input");
return "needs_input";
}
##################################################
sub format_wd($rec, $cur)
{
return _("бесплатно") if $rec->{dayly} == 0 && $rec->{monthly} == 0;
my $m = sprintf("%.2f $cur %s", $rec->{monthly}, _("в месяц")) if $rec->{monthly} != 0;
my $d = sprintf("%.2f $cur %s", $rec->{dayly}, _("в день")) if $rec->{dayly} != 0;
return ("$m + $d") if $m && $d;
return $m if $m;
return $d if $d;
}
sub parse_error
{
my $e = shift;
return $e unless ref $e;
return "$e->{code} $e->{message} $e->{body}";
}
1;
# локализация