Revision 707 (by ahitrov, 2018/08/15 23:24:42) Transaction name by status

package payments::Provider::Sber;

use strict;
use warnings 'all';

use base 'payments::Provider::Base';
use Contenido::Globals;
use payments::Keeper;
use URI;
use URI::QueryParam;
use JSON::XS;
use Data::Dumper;

use constant (
	REG	=> 0, # Заказ зарегистрирован, но не оплачен;
	HOLD	=> 1, # Предавторизованная сумма захолдирована (для двухстадийных платежей);
	PAY	=> 2, # Проведена полная авторизация суммы заказа;
	CANCEL	=> 3, # Авторизация отменена;
	REFUND	=> 4, # По транзакции была проведена операция возврата;
	ACS	=> 5, # Инициирована авторизация через ACS банка-эмитента;
	REJECT	=> 6, # Авторизация отклонена.
);

our %STATUS = (
	0	=> { name => 'Init', message => 'Ожидание оплаты' },
	1	=> { name => 'Authorized', message => 'Предавторизованная сумма захолдирована', },
	2	=> { name => 'Charged', message => 'Успешная оплата', },
	3	=> { name => 'Rejected', message => 'Авторизация отменена', },
	4	=> { name => 'Refunded', message => 'По транзакции была проведена операция возврата', },
	5	=> { name => '3DS', message => 'Инициирована авторизация через ACS банка-эмитента', },
	6	=> { name => 'Error', message => 'Авторизация отклонена', },
);

sub new {
    my ($proto, %params) = @_;
    my $class = ref($proto) || $proto;
    my $self = {};
    my $prefix = $class =~ /\:\:(\w+)$/ ? lc($1) : undef;
    return      unless $prefix;

    $self->{payment_system} = $prefix;
    $self->{app_id} =		$state->{payments}{$prefix."_app_id"};
    $self->{secret} =		$state->{payments}{$prefix."_app_secret"};
    $self->{token} =		$state->{payments}{$prefix."_app_token"};
    $self->{session_timeout} =	exists $params{session_timeout} ? $params{session_timeout} : $state->{payments}->{$prefix."_session_timeout"};
    $self->{test_mode} =	exists $params{test_mode} ? $params{test_mode} : $state->{payments}->{$prefix."_test_mode"};
    $self->{return_url} =	$params{return_url} || $state->{payments}{$prefix."_return_url"};
    $self->{fail_url} =		$params{fail_url} || $state->{payments}{$prefix."_fail_url"};

    $self->{currency} =		$state->{payments}{$prefix."_currency_code"};
    $self->{payment_statuses} =	\%STATUS;

    my $host = 'https://'. ($self->{test_mode} ? '3dsec.sberbank.ru' : 'securepayments.sberbank.ru');
    $self->{api} = {
        init    => "$host/payment/rest/register.do",			# Регистрация заказа
        pay     => "$host/payment/rest/deposit.do",			# Запрос завершения оплаты заказа
        cancel  => "$host/payment/rest/reverse.do",			# Запрос отмены оплаты заказа
        refund  => "$host/payment/rest/refund.do",			# Запрос возврата средств оплаты заказа
        status  => "$host/payment/rest/getOrderStatusExtended.do",	# Получение статуса заказа
        is3ds   => "$host/payment/rest/verifyEnrollment.do",		# Запрос проверки вовлеченности карты в 3DS
    };
#    $self->{return_url} = '';

    $self->{result} = {};

    bless $self, $class;

    return $self;
}


############################################################################################################
# Одностадийные операции
############################################################################################################

=for rem INIT
# Регистрация заказа

$payment->init({
	# обязательные:
	uid		=> User ID
	orderNumber	=> ID заказа
	amount		=> Сумма платежа в копейках или в формате 0.00
	# необязательные:
	returnUrl	=> Адрес, на который требуется перенаправить пользователя в случае успешной оплаты.
				Если не прописан в config.mk, параметр ОБЯЗАТЕЛЬНЫЙ
	failUrl		=> Адрес, на который требуется перенаправить пользователя в случае неуспешной оплаты.
	description	=> Описание заказа в свободной форме. В процессинг банка для включения в финансовую 
				отчётность продавца передаются только первые 24 символа этого поля
	language	=> Язык в кодировке ISO 639-1
	pageView	=> DESKTOP || MOBILE (см. доку)
	jsonParams	=> { хеш дополнительныех параметров }
	sessionTimeoutSecs	=> Продолжительность жизни заказа в секундах.
	expirationDate		=> Дата и время окончания жизни заказа. Формат: yyyy-MM-ddTHH:mm:ss.
});
=cut
##########################################################
sub init {
    my $self = shift;
    my $opts = shift // {};

    unless ( %$opts && (exists $opts->{order} || exists $opts->{orderNumber} && exists $opts->{amount}) ) {
	$self->{result}{error} = 'Не указаны обязательные параметры: order, orderNumber или amount';
	return $self;
    }
    my $method = 'init';
    if ( !exists $opts->{returnUrl} ) {
	if ( $self->{return_url} ) {
		$opts->{returnUrl} = $self->{return_url};
	} else {
		$self->{result}{error} = 'Не указан параметр returnUrl и не заполнено значение по умолчанию в конфиге SBER_RETURN_URL';
		return $self;
	}
    }
    if ( !exists $opts->{failUrl} && $self->{fail_url} ) {
	$opts->{failUrl} = $self->{fail_url};
    }
    if ( !exists $opts->{sessionTimeoutSecs} && $self->{session_timeout} ) {
	$opts->{sessionTimeoutSecs} = $self->{session_timeout};
    }

    my $order;
    if ( exists $opts->{order} && ref $opts->{order} eq 'webshop::Order' ) {
	$order = delete $opts->{order};
	$opts->{orderNumber} = $order->id;
    }

    my $uid = delete $opts->{uid};
    unless ( $uid ) {
	if ( ref $order ) {
		$uid = $order->uid;
	} else {
		$self->{result}{error} = 'Не указан user id';
		return $self;
	}
    }

    ### Сумма должна быть в копейках. Если дробное (рубли.копейки) - преобразуем в копейки
    if ( ref $order ) {
	$opts->{amount} = int($order->sum_total * 100);
    } else {
	my $sum = $opts->{amount};
	if ( !$sum || $sum !~ /^[\d\,\.]+$/ ) {
		$self->{result}{error} = 'Не указана или неправильно указана сумма транзакции';
		return $self;
	}
	if ( $sum =~ /[,.]/ ) {
		$sum =~ s/\,/\./;
		$opts->{amount} = int($sum * 100);
	}
    }
    $opts->{jsonParams} = {}		unless exists $opts->{jsonParams};
    $opts->{jsonParams}{uid} = $uid;

    warn "Sberbank init args: ".Dumper($opts)		if $DEBUG;
    my $operation = $self->payment_operation_register({ 
		order_id	=> $opts->{orderNumber},
		name	=> 'create',
		uid	=> $uid,
		sum	=> $opts->{amount},
	});
    return $self	unless ref $operation;

    my $transaction = $self->get_transaction_by_order_id( $opts->{orderNumber} );
    if ( ref $transaction && $transaction->name ne 'Expired' ) {
	### Transaction already exists
	$self->{result}{success} = 1;
	$self->{result}{session_id} = $transaction->session_id;
	$self->{result}{transaction} = $transaction;
    } else {
	if ( ref $transaction && $transaction->name eq 'Expired' ) {
		$transaction->delete;
		$transaction = undef;
	}
	if ( !ref $transaction ) {
		my $req = $self->_createRequestGet( $method, $opts );
		my $ua = LWP::UserAgent->new;
		$ua->agent('Mozilla/5.0');
		my $result = $ua->get( $req );
		if ( $result->code == 200 ) {
			warn "Sberbank Init result: [".$result->decoded_content."]\n"	if $DEBUG;
			my $content = JSON::XS->new->decode( $result->decoded_content );
			warn Dumper $content						if $DEBUG;

			if ( ref $content && exists $content->{orderId} ) {
				my $now = Contenido::DateTime->new;
				$transaction = payments::Transaction->new( $keeper );
				$transaction->dtime( $now->ymd('-').' '.$now->hms );
				$transaction->provider( $self->{payment_system} );
				$transaction->session_id( $content->{orderId} );
				$transaction->status( $self->{test_mode} );
				$transaction->order_id( $opts->{orderNumber} );
				$transaction->operation_id( $operation->id );
				$transaction->currency_code( 'RUR' );
				$transaction->sum( $opts->{amount} );
				$transaction->form_url( $content->{formUrl} );
				$transaction->name( 'Init' );
				$transaction->success( 0 );
				$transaction->store;

				$self->{result}{success} = 1;
				$self->{result}{session_id} = $content->{orderId};
				$self->{result}{transaction} = $transaction;
			} elsif ( ref $content && exists $content->{errorCode} && $content->{errorCode} ) {
				$self->{result}{error} = Encode::encode('utf-8', $content->{errorMessage});
				warn "[".$result->decoded_content."]\n";
			} else {
				$self->{result}{error} = 'Sberbank Init failed';
				$self->{result}{responce} = $result->decoded_content;
				warn $self->{result}{error}."\n";
				warn "[".$result->decoded_content."]\n";
			}
		} else {
			$self->{result}{error} = 'Sberbank Init failed';
			$self->{result}{responce} = $result->status_line;
			warn $self->{result}{error}.": ".$result->status_line."\n";
			warn Dumper $result;
		}
	}
    }
    return $self;
}


=for rem STATUS
# Расширенный запрос состояния заказа

$payment->status({
	# обязательные:
	orderNumber	=> ID заказа в магазине. Если в объекте присутствует транзакция, будет браться из транзакции
	# необязательные:
	language	=> Язык в кодировке ISO 639-1
});

Результат:

orderStatus:

По значению этого параметра определяется состояние заказа в платёжной системе. Список возможных значений приведён в списке
ниже. Отсутствует, если заказ не был найден.
0 - Заказ зарегистрирован, но не оплачен;
1 - Предавторизованная сумма захолдирована (для двухстадийных платежей);
2 - Проведена полная авторизация суммы заказа;
3 - Авторизация отменена;
4 - По транзакции была проведена операция возврата;
5 - Инициирована авторизация через ACS банка-эмитента;
6 - Авторизация отклонена.

errorCode:

Код ошибки. Возможны следующие варианты.
0 - Обработка запроса прошла без системных ошибок;
1 - Ожидается [orderId] или [orderNumber];
5 - Доступ запрещён;
5 - Пользователь должен сменить свой пароль;
6 - Заказ не найден;
7 - Системная ошибка.

=cut
##########################################################
sub status {
    my $self = shift;
    my $opts = shift // {};

    unless ( exists $opts->{orderNumber}
		|| exists $self->{result} && exists $self->{result}{transaction} && ref $self->{result}{transaction}
		|| exists $opts->{transaction} && ref $opts->{transaction} ) {
	$self->{result}{error} = 'Не указан обязательный параметр orderNumber или не получена транзакция';
	return $self;
    }
    my $method = 'status';
    my $transaction;
    if ( exists $opts->{transaction} ) {
	$transaction = delete $opts->{transaction};
    } elsif ( exists $self->{result}{transaction} ) {
	$transaction = $self->{result}{transaction};
    } else {
	$transaction = $self->get_transaction_by_order_id( $opts->{orderNumber} );
    }
    unless ( ref $transaction ) {
	$self->{result}{error} = "Не найдена транзакция для order_id=".$opts->{orderNumber};
	return $self;
    }
    $opts->{orderNumber} = $transaction->order_id;
    $opts->{orderId} = $transaction->session_id;
    warn "Sberbank status opts: ".Dumper($opts)					if $DEBUG;

    my $req = $self->_createRequestGet( $method, $opts );
    my $ua = LWP::UserAgent->new;
    $ua->agent('Mozilla/5.0');
    my $result = $ua->get( $req );
    my $return_data = {};
    if ( $result->code == 200 ) {
	warn "Sberbank Status: [".$result->decoded_content."]\n"		if $DEBUG;
	my $content = JSON::XS->new->decode( $result->decoded_content );
	warn Dumper $content						if $DEBUG;
  
	if ( ref $content && exists $content->{orderStatus} && exists $content->{orderNumber} ) {
		$self->{result} = {
			success	=> 1,
			status	=> $content->{orderStatus},
			action	=> $content->{actionCode},
			action_description	=> Encode::encode('utf-8', $content->{actionCodeDescription}),
			amount	=> $content->{amount},
			time_ms	=> $content->{date},
			ip	=> $content->{ip},
			transaction     => $transaction,
		};
	} elsif ( ref $content && exists $content->{errorCode} && $content->{errorCode} ) {
		$self->{result}{error} = $content->{errorMessage};
		warn "[".$result->decoded_content."]\n";
	} else {
		$self->{result}{error} = 'Sberbank Status failed';
		$self->{result}{responce} = $result->decoded_content;
		warn $self->{result}{error}."\n";
		warn "[".$result->decoded_content."]\n";
	}
    } else {
	$self->{result}{error} = 'Sberbank Status failed';
	$self->{result}{responce} = $result->status_line;
	warn $self->{result}{error}.": ".$result->status_line."\n";
	warn Dumper $result;
    }

    return $self;
}


=for rem REFUND
# Возврат средств

$payment->refund({
	# обязательные:
	uid		=> User ID
	orderNumber	=> ID заказа
#	orderId		=> Номер заказа в платежной системе. Уникален в пределах системы (session_id).
#			   Если в объекте присутствует транзакция, будет браться из транзакции
	amount		=> Сумма платежа в копейках или в формате 0.00
});
=cut
##########################################################
sub refund {
    my $self = shift;
    my $opts = shift // {};

    unless ( %$opts && exists $opts->{orderNumber} && exists $opts->{amount} ) {
	$self->{result}{error} = 'Не указаны обязательные параметры: orderNumber или amount';
	return $self;
    }
    my $method = 'refund';

    my $uid = delete $opts->{uid};
    unless ( $uid ) {
	$self->{result}{error} = 'Не указан user id';
	return $self;
    }

    ### Сумма должна быть в копейках. Если дробное (рубли.копейки) - преобразуем в копейки
    my $sum = $opts->{amount};
    if ( !$sum || $sum !~ /^[\d\,\.]+$/ ) {
	$self->{result}{error} = 'Не указана или неправильно указана сумма транзакции';
	return $self;
    }
    if ( $sum =~ /[,.]/ ) {
	$sum =~ s/\,/\./;
	$opts->{amount} = int($sum * 100);
    }

    warn "Sberbank refund args: ".Dumper($opts)		if $DEBUG;
    my $operation = $self->payment_operation_register({
		order_id	=> $opts->{orderNumber},
		name	=> 'refund',
		uid	=> $uid,
		sum	=>  $opts->{amount},
	});
    return $self	unless ref $operation;

    my $transaction = $self->get_transaction_by_order_id( $opts->{orderNumber} );
    if ( ref $transaction && $transaction->name eq 'Charged' ) {
	$opts->{orderId} = $transaction->session_id;
	my $order_id = delete $opts->{orderNumber};
	my $req = $self->_createRequestGet( $method, $opts );
	my $ua = LWP::UserAgent->new;
	$ua->agent('Mozilla/5.0');
	my $result = $ua->get( $req );
	if ( $result->code == 200 ) {
		warn "Sberbank Refund result: [".$result->decoded_content."]\n"	if $DEBUG;
		my $content = JSON::XS->new->decode( $result->decoded_content );
		warn Dumper $content						if $DEBUG;

		if ( ref $content && exists $content->{orderId} ) {
			my $now = Contenido::DateTime->new;
			my $transaction = payments::Transaction->new( $keeper );
			$transaction->dtime( $now->ymd('-').' '.$now->hms );
			$transaction->provider( $self->{payment_system} );
			$transaction->session_id( $opts->{orderId} );
			$transaction->status( $self->{test_mode} );
			$transaction->order_id( $order_id );
			$transaction->operation_id( $operation->id );
			$transaction->currency_code( 'RUR' );
			$transaction->sum( $opts->{amount} );
			$transaction->name( 'Refunded' );
			$transaction->success( 0 );
			$transaction->store;

			$self->{result}{success} = 1;
			$self->{result}{session_id} = $content->{orderId};
			$self->{result}{transaction} = $transaction;
		} elsif ( ref $content && exists $content->{errorCode} && $content->{errorCode} ) {
			$self->{result}{error} = Encode::encode('utf-8', $content->{errorMessage});
			warn "[".$result->decoded_content."]\n";
		} else {
			$self->{result}{error} = 'Sberbank Refund failed';
			$self->{result}{responce} = $result->decoded_content;
			warn $self->{result}{error}."\n";
			warn "[".$result->decoded_content."]\n";
		}
        } else {
		$self->{result}{error} = 'Sberbank Refund failed';
		$self->{result}{responce} = $result->status_line;
		warn $self->{result}{error}.": ".$result->status_line."\n";
		warn Dumper $result;
	}
    }
    return $self;
}


sub GetNameByResultStatus {
    my ($self, $status) = @_;
    if ( exists $STATUS{$status} ) {
	return $STATUS{$status}{name};
    } else {
	return 'Error';
    }
}

sub _createRequestGet {
    my ($self, $method, $opts) = @_;
    return	unless $method && exists $self->{api}{$method};
    $opts //= {};

    my $req = URI->new( $self->{api}{$method} );
    if ( $self->{token} ) {
	$req->query_param( token => $self->{token} );
    } else {
	$req->query_param( userName => $self->{app_id} );
	$req->query_param( password => $self->{secret} );
    }
    foreach my $key ( keys %$opts ) {
	if ( $key eq 'jsonParams' && ref $opts->{$key} ) {
		$opts->{$key} = encode_json $opts->{$key};
	}
	$req->query_param( $key => $opts->{$key} );
    }
    return $req;
}

1;