diff --git a/BorgunPay.pm b/BorgunPay.pm new file mode 100644 index 0000000..c593be8 --- /dev/null +++ b/BorgunPay.pm @@ -0,0 +1,431 @@ +package Koha::Plugin::Com::RBitTechnology::BorgunPay; + +use Modern::Perl; +use base qw(Koha::Plugins::Base); +use utf8; +use C4::Context; +use Koha::Account::Lines; +use LWP::UserAgent; +use HTTP::Headers; +use HTTP::Request::Common; +use JSON; +use HTML::Entities; +use Digest::SHA qw/hmac_sha256_hex hmac_sha256/; + +our $VERSION = "1.0.0"; + +our $metadata = { + name => 'Platební brána Borgun', + author => 'Radek Šiman', + description => 'Toto rozšíření poskytuje podporu online plateb s využitím brány Borgun.', + date_authored => '2018-02-25', + date_updated => '2018-02-25', + minimum_version => '16.05', + maximum_version => undef, + version => $VERSION +}; + +sub new { + my ( $class, $args ) = @_; + + ## We need to add our metadata here so our base class can access it + $args->{'metadata'} = $metadata; + $args->{'metadata'}->{'class'} = $class; + + ## Here, we call the 'new' method for our base class + ## This runs some additional magic and checking + ## and returns our actual $self + my $self = $class->SUPER::new($args); + $self->{'ua'} = LWP::UserAgent->new(); + + return $self; +} + +sub configure { + my ( $self, $args ) = @_; + my $cgi = $self->{'cgi'}; + + my $template = $self->get_template({ file => 'configure.tt' }); + my $phase = $cgi->param('phase'); + + my $table_clients = $self->get_qualified_table_name('clients'); + my $dbh = C4::Context->dbh; + + unless ( $phase ) { + my $query = "SELECT client_id, borrowernumber, firstname, surname, userid, secret FROM $table_clients INNER JOIN borrowers USING(borrowernumber) ORDER BY surname, firstname"; + my $sth = $dbh->prepare($query); + $sth->execute(); + + my @clients; + while ( my $row = $sth->fetchrow_hashref() ) { + push( @clients, $row ); + } + + print $cgi->header(-type => 'text/html', + -charset => 'utf-8'); + $template->param( + merchantid => $self->retrieve_data('merchantid'), + paymentgatewayid => $self->retrieve_data('paymentgatewayid'), + secretkey => $self->retrieve_data('secretkey'), + borgun_server => $self->retrieve_data('borgun_server'), + api_clients => \@clients + ); + print $template->output(); + return; + } + elsif ( $phase eq 'save_borgun' ) { + $self->store_data( + { + merchantid => scalar $cgi->param('merchantid'), + paymentgatewayid => scalar $cgi->param('paymentgatewayid'), + secretkey => scalar $cgi->param('secretkey'), + borgun_server => scalar $cgi->param('borgun_server'), + last_configured_by => C4::Context->userenv->{'number'}, + } + ); + } + elsif ( $phase eq 'save_clients' ) { + my $borrowernumber = $cgi->param('borrowernumber'); + my $secret = $cgi->param('secret'); + + if ( $borrowernumber && $secret ) { + my $query = "INSERT INTO $table_clients (secret, borrowernumber) VALUES (?, ?);"; + my $sth = $dbh->prepare($query); + $sth->execute($secret, $borrowernumber); + } + } + elsif ( $phase eq 'delete' ) { + my $client_id = $cgi->param('client_id'); + + if ( $client_id ) { + my $query = "DELETE FROM $table_clients WHERE client_id = ?;"; + my $sth = $dbh->prepare($query); + $sth->execute($client_id); + } + + my $staffClientUrl = C4::Context->preference('staffClientBaseURL'); + print $cgi->redirect(-uri => "$staffClientUrl/cgi-bin/koha/plugins/run.pl?class=Koha::Plugin::Com::RBitTechnology::BorgunPay&method=configure"); + } + + $self->go_home; +} + +sub check_params { + my ( $self, $args ) = @_; + my $cgi = $self->{'cgi'}; + + my $patron = $cgi->param('patron'); + my $return_url = $cgi->param('return_url'); + my $hmac_post = $cgi->param('hmac'); + + unless ($patron && $return_url && $hmac_post) { + $self->error({ errors => [ { message => 'Chybí jeden nebo více povinných parametrů.' } ], return_url => $return_url ? $return_url : 0 }); + return 0; + } + + my $userid = $cgi->param('userid'); + my $password = $cgi->param('password'); + my $borrowernumber = C4::Context->userenv->{'number'}; + + my $dbh = C4::Context->dbh; + my $table_clients = $self->get_qualified_table_name('clients'); + my $query = "SELECT secret FROM $table_clients WHERE borrowernumber = ?"; + my $sth = $dbh->prepare($query); + $sth->execute($borrowernumber); + unless ( $sth->rows ) { + $self->error({ errors => [ { message => "Pro přihlašovací jméno $userid neexistuje klientský záznam." } ], return_url => $return_url }); + return 0; + } + + my $row = $sth->fetchrow_hashref(); + my $hmac = hmac_sha256_hex("$userid|$password|$patron|$return_url", $row->{'secret'}); + + unless ( $hmac eq $hmac_post ) { + $self->error({ errors => [ { message => "Neoprávněný požadavek, nepodařilo se ověřit HMAC." } ], return_url => $return_url }); + return 0; + } + + unless ( $self->retrieve_data('secretkey') && $self->retrieve_data('merchantid') && $self->retrieve_data('paymentgatewayid') ) { + $self->error({ errors => [ { message => "Chybí nastavení parametrů platební brány SecretKey, MerchantID, PaymentGatewayID). Dokončete prosím konfiguraci platební brány." } ], return_url => $return_url }); + return 0; + } + + return 1; +} + +sub opac_online_payment { + my ( $self, $args ) = @_; + + return 1; +} + +sub opac_online_payment_begin { + my ( $self, $args ) = @_; + my $cgi = $self->{'cgi'}; + + return unless ( $self->check_params ); + my $return_url = $cgi->param('return_url'); + + my $staffClientUrl = "http://mbp.home.local:8081"; #C4::Context->preference('staffClientBaseURL'); + + my $table_trans = $self->get_qualified_table_name('transactions'); + my $table_items = $self->get_qualified_table_name('items'); + my $dbh = C4::Context->dbh; + + my $member = Koha::Patrons->find( { borrowernumber => scalar $cgi->param('patron') } ); + my @outstanding_fines; + @outstanding_fines = Koha::Account::Lines->search( + { + borrowernumber => scalar $cgi->param('patron'), + amountoutstanding => { '>' => 0 }, + } + ); + my $amount_to_pay = 0; + my @items; + foreach my $fine (@outstanding_fines) { + $amount_to_pay += int(100 * $fine->amountoutstanding) / 100; + push( @items, { name => $fine->description ? $fine->description : "Platba bez popisu", amount => int(100 * $fine->amountoutstanding) / 100 } ); + } + + unless ( scalar @items ) { + $self->error({ errors => [ { message => 'Nebyly nalezeny žádné položky k úhradě.' } ], return_url => $return_url }); + return; + } + + $dbh->do("START TRANSACTION"); + + my $query = "INSERT INTO $table_trans (paid, return_url) VALUES (NULL, ?)"; + my $sth = $dbh->prepare($query); + $sth->execute($return_url); + my $transaction_id = $dbh->last_insert_id(undef, undef, $table_trans, 'transaction_id'); + + my $returnUrlSuccess = "$staffClientUrl/cgi-bin/koha/svc/pay_api?phase=return&action=success"; + + my @hmac_fields = ( + $self->retrieve_data('merchantid'), + $returnUrlSuccess, + $returnUrlSuccess, + $transaction_id, + $amount_to_pay, + 'CZK' + ); + + my $params = { + 'merchantid' => $self->retrieve_data('merchantid'), + 'paymentgatewayid' => $self->retrieve_data('paymentgatewayid'), + 'checkhash' => hmac_sha256_hex(join('|', @hmac_fields), $self->retrieve_data('secretkey')), + 'orderid' => $transaction_id, + 'amount' => $amount_to_pay, + 'currency' => 'CZK', + 'language' => 'CZ', + 'buyername' => $member->firstname . ' ' . $member->surname, + 'buyeremail' => $member->email, + 'returnurlsuccess' => $returnUrlSuccess, + 'returnurlsuccessserver' => $returnUrlSuccess, + 'returnurlerror' => "$staffClientUrl/cgi-bin/koha/svc/pay_api?phase=return&action=error", + }; + + for my $i (0 .. $#items) { + my $fine = $items[$i]; + $params->{"Itemdescription_$i"} = $fine->{name}; + $params->{"Itemcount_$i"} = 1; + $params->{"Itemunitamount_$i"} = $fine->{amount}; + $params->{"Itemamount_$i"} = $params->{"Itemcount_$i"} * $params->{"Itemunitamount_$i"}; + } + + my $ua = LWP::UserAgent->new(); + my $request = POST $self->api, $params; + $request->header('Content-Type' => 'application/x-www-form-urlencoded'); + my $response = $ua->request($request); + + if (!$response->is_success) { + $self->error({ errors => [ { message => 'Nelze se připojit k platebnímu serveru.'} ], return_url => $return_url }); + return; + } + + my @values; + my @bindParams; + + $query = "INSERT INTO $table_items (accountlines_id, transaction_id) VALUES "; + foreach my $fine (@outstanding_fines) { + push( @values, "(?, ?)" ); + push( @bindParams, $fine->accountlines_id ); + push( @bindParams, $transaction_id ); + } + $query .= join(', ', @values); + $sth = $dbh->prepare($query); + + for my $i (0 .. $#bindParams) { + $sth->bind_param($i + 1, $bindParams[$i]); + } + + $sth->execute(); + + my %args = split /[&=]/, $response->content; + if ($args{ret} eq 'True') { + $query = "UPDATE $table_trans SET ticket=? WHERE transaction_id=?; "; + $sth = $dbh->prepare($query); + $sth->execute($args{ticket}, $transaction_id); + + $dbh->do("COMMIT"); + + print $cgi->redirect($self->api . "?ticket=" . $args{ticket}); + return; + } + + $dbh->do("COMMIT"); + + $self->error({ errors => [ { message => 'Nelze provést platbu (' . $args{message} . ').' } ], return_url => $return_url }); +} + +sub opac_online_payment_end { + my ( $self, $args ) = @_; + my $cgi = $self->{'cgi'}; + + my $dbh = C4::Context->dbh; + my $table_items = $self->get_qualified_table_name('items'); + my $table_trans = $self->get_qualified_table_name('transactions'); + + my $ticket = scalar $cgi->param('ticket'); + my $query = "SELECT transaction_id, return_url FROM $table_trans WHERE ticket = ?"; + my $sth = $dbh->prepare($query); + $sth->execute( $ticket ); + unless ( $sth->rows ) { + $self->error({ errors => [ { message => 'Platba s touto identifikací neexistuje nebo již byla uhrazena dříve.' } ] }); + return; + } + + my $row = $sth->fetchrow_hashref(); + my $return_url = $row->{'return_url'}; + my $transation_id = $row->{'transaction_id'}; + my $status = scalar $cgi->param('status'); + + my @hmac_fields = ( + $cgi->param('orderid'), + $cgi->param('amount'), + $cgi->param('currency') + ); + my $orderhash = hmac_sha256_hex(join('|', @hmac_fields), $self->retrieve_data('secretkey')); + + if ($orderhash ne $cgi->param('orderhash')) { + $self->error({ errors => [ { message => 'Byl použit neplatný validační kód. Platba zůstane neuhrazena.' } ], return_url => $return_url }); + return; + } + + if ( $status eq 'OK' ) { + $dbh->do("START TRANSACTION"); + + + $query = "SELECT accountlines_id, borrowernumber, amountoutstanding FROM $table_items " + ." INNER JOIN $table_trans USING(transaction_id)" + ." INNER JOIN accountlines USING(accountlines_id)" + ." WHERE transaction_id = ? AND paid IS NULL"; + $sth = $dbh->prepare($query); + $sth->execute( $transation_id ); + + my $note = "Borgun " . $ticket; + while ( my $row = $sth->fetchrow_hashref() ) { + my $account = Koha::Account->new( { patron_id => $row->{'borrowernumber'} } ); + $account->pay( + { + amount => $row->{'amountoutstanding'}, + lines => [ scalar Koha::Account::Lines->find($row->{'accountlines_id'}) ], + note => $note, + } + ); + } + + $query = "UPDATE $table_trans SET paid = NOW() WHERE transaction_id = ?"; + $sth = $dbh->prepare($query); + $sth->execute( $transation_id ); + + $dbh->do("COMMIT"); + + $self->message({ text => 'Platba byla přijata. Děkujeme za úhradu.', return_url => $return_url }); + } + else { + $self->error({ errors => [ { message => 'Platba nebyla uhrazena.' } ], return_url => $return_url }); + return + } + +} + +sub install { + my ( $self, $args ) = @_; + + my $table_items = $self->get_qualified_table_name('items'); + my $table_trans = $self->get_qualified_table_name('transactions'); + my $table_clients = $self->get_qualified_table_name('clients'); + + return C4::Context->dbh->do( " + CREATE TABLE `$table_trans` ( + `transaction_id` int NOT NULL AUTO_INCREMENT, + `ticket` varchar(20) DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `paid` timestamp NULL DEFAULT NULL, + `return_url` varchar(128) NOT NULL, + PRIMARY KEY (`transaction_id`) + ) ENGINE = INNODB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_czech_ci; + " ) && + C4::Context->dbh->do( " + CREATE TABLE `$table_items` ( + `accountlines_id` int NOT NULL, + `transaction_id` int NOT NULL, + PRIMARY KEY (`accountlines_id`, `transaction_id`), + CONSTRAINT `FK_borgunpay_accountlines` FOREIGN KEY (`accountlines_id`) REFERENCES `accountlines` (`accountlines_id`) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT `FK_borgunpay_transactions` FOREIGN KEY (`transaction_id`) REFERENCES `$table_trans` (`transaction_id`) ON UPDATE CASCADE ON DELETE CASCADE, + INDEX (`accountlines_id`), + INDEX (`transaction_id`) + ) ENGINE = INNODB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_czech_ci;" ) && + C4::Context->dbh->do( " + CREATE TABLE `$table_clients` ( + `client_id` int NOT NULL AUTO_INCREMENT, + `secret` varchar(64) NOT NULL, + `borrowernumber` int NOT NULL, + PRIMARY KEY (`client_id`), + INDEX (`borrowernumber`), + CONSTRAINT `FK_borgunpay_client_borrowers` FOREIGN KEY (`borrowernumber`) REFERENCES `borrowers` (`borrowernumber`) ON UPDATE CASCADE ON DELETE CASCADE + ) ENGINE = INNODB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_czech_ci; + "); +} + +sub uninstall { + my ( $self, $args ) = @_; + + my $table_items = $self->get_qualified_table_name('items'); + my $table_trans = $self->get_qualified_table_name('transactions'); + my $table_clients = $self->get_qualified_table_name('clients'); + + return C4::Context->dbh->do("DROP TABLE `$table_items`") && + C4::Context->dbh->do("DROP TABLE `$table_trans`") && + C4::Context->dbh->do("DROP TABLE `$table_clients`"); +} + +sub error { + my ( $self, $args ) = @_; + + my $template = $self->get_template({ file => 'dialog.tt' }); + $template->param( + error => 1, + report => $args->{'errors'}, + return_url => $args->{'return_url'} + ); + print $template->output(); +} + +sub message { + my ( $self, $args ) = @_; + + my $template = $self->get_template({ file => 'dialog.tt' }); + $template->param( + error => 0, + report => $args->{'text'}, + return_url => $args->{'return_url'} + ); + print $template->output(); +} + +sub api { + my ( $self, $args ) = @_; + return $self->retrieve_data('borgun_server') eq 'production' ? 'https://securepay.borgun.is/securepay/ticket.aspx': 'https://test.borgun.is/securepay/ticket.aspx'; +} \ No newline at end of file diff --git a/BorgunPay/configure.tt b/BorgunPay/configure.tt new file mode 100644 index 0000000..f6e4cdb --- /dev/null +++ b/BorgunPay/configure.tt @@ -0,0 +1,178 @@ +[% INCLUDE 'doc-head-open.inc' %] + Koha: Platební brána Borgun: Konfigurace +[% INCLUDE 'doc-head-close.inc' %] + + + + + +[% INCLUDE 'header.inc' %] + + +
+ + +

Platební brána Borgun: Konfigurace

+ + +
+ + + + +
+ Nastavení platební brány + +
    +
  1. + + +
  2. + +
  3. + + +
  4. + +
  5. + + +
  6. + +
  7. + + +
  8. +
+
+ +

+ + +

+
+ +
+ + + + +
+ Klientské přístupy k API Kohy +
    +
  1. + + + +
  2. + +
  3. + + +
  4. + +
  5. + [% IF (api_clients) %] + + + + + + + + + + + + [% FOREACH client IN api_clients %] + + + + + + + + [% END %] + +
    Číslo čtenářeJménoUživatelské jménoPřístupový klíč 
    [% client.borrowernumber %][% client.firstname %] [% client.surname %][% client.userid %][% client.secret %] + Odstranit +
    + [% ELSE %] +
    +

    Dosud neexistují přístupové účty

    +

    Pokud chcete umožnit online platby prostřednictvím API, vytvořte alespoň jeden účet.

    +
    + [% END %] +
  6. +
+
+ +

+ + +

+
+ +
+ + +[% INCLUDE 'intranet-bottom.inc' %] \ No newline at end of file diff --git a/BorgunPay/css/borgun.css b/BorgunPay/css/borgun.css new file mode 100644 index 0000000..9238c40 --- /dev/null +++ b/BorgunPay/css/borgun.css @@ -0,0 +1,12 @@ +.rbit-plugin .logo-links { margin-bottom: 2.5em; } +.rbit-plugin .logo-links a img { border: 1px solid transparent; } +.rbit-plugin .logo-links a:hover img { border: 1px solid #DEDEDE; } +.rbit-plugin fieldset { margin-bottom: 1em; } +.rbit-plugin fieldset.rows label, .rbit-plugin label { width: 10em; display: inline-block; text-align: right; margin-right: 1em; } +.rbit-plugin span.appended-label { margin: 0 1em; } +.rbit-plugin .form-field { margin: 5px 0; } +.rbit-plugin .form-field > * { vertical-align: middle; } +.rbit-plugin input.on-off { margin: 0 1em; } +.rbit-plugin input[type="number"] { width: 5em; } +.rbit-plugin .hint { font-style: italic; color: #555555; } +.rbit-plugin label.radio-label { width: auto; padding-left: 0.5em; } \ No newline at end of file diff --git a/BorgunPay/dialog.tt b/BorgunPay/dialog.tt new file mode 100644 index 0000000..aba9e69 --- /dev/null +++ b/BorgunPay/dialog.tt @@ -0,0 +1,47 @@ +[% USE Koha %] + +[% INCLUDE 'doc-head-open.inc' %] +[% INCLUDE 'doc-head-close.inc' %] + + + + +
+ + + +
+ +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/BorgunPay/koha_cz.png b/BorgunPay/koha_cz.png new file mode 100644 index 0000000..da7cf45 Binary files /dev/null and b/BorgunPay/koha_cz.png differ diff --git a/BorgunPay/logo.png b/BorgunPay/logo.png new file mode 100644 index 0000000..bbdeb65 Binary files /dev/null and b/BorgunPay/logo.png differ diff --git a/README.md b/README.md index 6ea3463..52f57f7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,40 @@ -# BorgunPay -Zásuvný modul pro ILS Koha, který implementuje platební bránu Borgun. Platba může být iniciována z OPACu nebo externíní aplikace (například VuFind). +![logo KohaCZ](https://github.com/open-source-knihovna/SmartWithdrawals/blob/master/SmartWithdrawals/koha_cz.png "Logo Česká komunita Koha") +![logo R-Bit Technology, s.r.o.](https://github.com/open-source-knihovna/SmartWithdrawals/blob/master/SmartWithdrawals/logo.png "Logo R-Bit Technology, s.r.o.") +![logo MK ČR](https://github.com/open-source-knihovna/SmartWithdrawals/blob/master/SmartWithdrawals/logo_mkcr.png "Logo MK ČR") + +Zásuvný modul vytvořila společnost R-Bit Technology, s. r. o. ve spolupráci s českou komunitou Koha, za finančního přispění Ministerstva kultury České republiky. + +# Úvod + +Zásuvný modul 'BorgunPay' implementuje možnost platit uživatelům knihovny poplatky on-line přes platební bránu Borgun. Platba může být iniciována jak z OPACu Kohy, tak i z téměř libovolného jiné systému (VuFind, Centrální portál knihoven atp.). Pokud je platba úspěšná, dojde k okamžité úhradě všech dlužných poplatků v systému. Součástí modulu je jednoduchá konfigurace, kde se vyplňují identifikační údaje pro bránu. + +# Instalace + +## Zprovoznění Zásuvných modulů + +Institut zásuvných modulů umožňuje rozšiřovat vlastnosti knihovního systému Koha dle specifických požadavků konkrétní knihovny. Zásuvný modul se instaluje prostřednictvím balíčku KPZ (Koha Plugin Zip), který obsahuje všechny potřebné soubory pro správné fungování modulu. + +Pro využití zásuvných modulů je nutné, aby správce systému tuto možnost povolil v nastavení. + +Nejprve je zapotřebí provést několik změn ve vaší instalaci Kohy: + +* V souboru koha-conf.xml změňte `0` na `1` +* Ověřte, že cesta k souborům ve složce `` existuje, je správná a že do této složky může webserver zapisovat +* Pokud je hodnota `` např. `/var/lib/koha/kohadev/plugins`, vložte následující kód do konfigurace webserveru: +``` +Alias /plugin/ "/var/lib/koha/kohadev/plugins/" + + Options +Indexes +FollowSymLinks + AllowOverride All + Require all granted + +``` +* Načtěte aktuální konfiguraci webserveru příkazem `sudo service apache2 reload` + +Jakmile je nastavení připraveno, budete potřebovat změnit systémovou konfigurační hodnotu UseKohaPlugins v administraci Kohy. Aktuální verzi modulu [stahujte v sekci Releases](https://github.com/open-source-knihovna/BorgunPay/releases). + +## Nastavení specifické pro modul + + + +Více informací, jak s nástrojem pracovat naleznete na [wiki](https://github.com/open-source-knihovna/BorgunPay/wiki)