View | Details | Raw Unified | Return to bug 17705
Collapse All | Expand All

(-)a/Koha/PosTerminal/Client.pm (+101 lines)
Line 0 Link Here
1
package Koha::PosTerminal::Client;
2
3
# This file is part of Koha.
4
#
5
# Copyright 2014 BibLibre
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use IO::Socket::INET;
21
use IO::Socket::Timeout;
22
use Koha::PosTerminal::Message;
23
use Errno qw(ETIMEDOUT EWOULDBLOCK);
24
25
use constant {
26
    ERR_CONNECTION_FAILED => -1,
27
    ERR_NO_RESPONSE => -2
28
};
29
30
# auto-flush on socket
31
$| = 1;
32
33
sub new {
34
    my $class  = shift;
35
    my $self = {
36
        _ip => shift,
37
        _port => shift,
38
        _socket => 0,
39
    };
40
41
    bless $self, $class;
42
    return $self;
43
}
44
45
sub connect {
46
    my ( $self ) = @_;
47
48
    $self->{_socket} = new IO::Socket::INET (
49
        PeerHost => $self->{_ip},
50
        PeerPort => $self->{_port},
51
        Proto => 'tcp',
52
        Timeout => 5
53
    );
54
55
    if ($self->{_socket}) {
56
        IO::Socket::Timeout->enable_timeouts_on($self->{_socket});
57
        $self->{_socket}->read_timeout(60);
58
        $self->{_socket}->write_timeout(60);
59
    }
60
61
    return !!$self->{_socket};
62
}
63
64
sub disconnect {
65
    my ( $self ) = @_;
66
67
    $self->{_socket}->close();
68
}
69
70
sub send {
71
    my ( $self, $message ) = @_;
72
73
    # data to send to a server
74
    my $req = $message->getContent();
75
    my $size = $self->{_socket}->send($req);
76
}
77
78
sub receive {
79
    my ( $self ) = @_;
80
81
    my $socket = $self->{_socket};
82
83
#    my $response = <$socket>;
84
    my $response;
85
    $self->{_socket}->recv($response, 1024);
86
    if (!$response) {
87
        if (( 0+$! == ETIMEDOUT) || (0+$! == EWOULDBLOCK )) {
88
            return ERR_CONNECTION_FAILED;
89
        }
90
        else {
91
            return 0+$!; #ERR_NO_RESPONSE;
92
        }
93
    }
94
95
    my $msg = new Koha::PosTerminal::Message(Koha::PosTerminal::Message::DIR_RECEIVED);
96
    $msg->parse($response);
97
98
    return $msg;
99
}
100
101
1;
(-)a/Koha/PosTerminal/Message.pm (+232 lines)
Line 0 Link Here
1
package Koha::PosTerminal::Message;
2
3
# Copyright 2017 R-Bit Technology, s.r.o.
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use strict;
21
use warnings;
22
23
use Digest::CRC;
24
use Koha::PosTerminal::Message::Header;
25
use Koha::PosTerminal::Message::Field;
26
use Data::Dumper qw( Dumper );
27
28
use constant {
29
    STX => "\x02",
30
    ETX => "\x03",
31
    FS  => "\x1C",
32
    GS  => "\x1D",
33
    CR  => "\x0D",
34
    LF  => "\x0A"
35
};
36
37
use constant {
38
    F_PAID_AMOUNT       => "B",
39
    F_CURRENCY_CODE     => "I",
40
    F_TRANSACTION_TYPE  => "T",
41
    F_RESPONSE_CODE     => "R",
42
    F_CARD_NUMBER       => "P",
43
    F_CARD_PRODUCT      => "J",
44
    F_INVOICE_NUMBER    => "S",
45
    F_CODE_PAGE         => "f",
46
    F_RECEIPT           => "t",
47
    F_TRANSACTION_ID    => "n",
48
    F_APPLICATION_ID    => "a"
49
};
50
51
use constant {
52
    TTYPE_SALE                      => "00",
53
    TTYPE_PREAUTH                   => "01",
54
    TTYPE_PREAUTH_COMPLETION        => "02",
55
    TTYPE_REVERSAL                  => "10",
56
    TTYPE_REFUND                    => "04",
57
    TTYPE_ABORT                     => "12",
58
    TTYPE_POST_DATA_PRINTING        => "16",
59
    TTYPE_REPEAT_LAST_TRANSACTION   => "17"
60
};
61
62
use constant {
63
    DIR_SENT  => "SENT",
64
    DIR_RECEIVED => "RCVD"
65
};
66
67
sub new {
68
    my $class  = shift;
69
70
    my $self = {};
71
    $self->{_header} = Koha::PosTerminal::Message::Header->new();
72
    $self->{_fields} = ();
73
    $self->{_isValid} = 0;
74
    $self->{_direction} = shift;
75
76
    bless $self, $class;
77
    return $self;
78
}
79
80
sub getDirection {
81
    my( $self ) = @_;
82
    return $self->{_direction};
83
}
84
85
sub getHeader {
86
    my( $self ) = @_;
87
    return $self->{_header};
88
}
89
90
sub getContent {
91
    my( $self ) = @_;
92
93
    my $msg = $self->getHeader()->getContent();
94
    foreach my $field (@{$self->{_fields}}) {
95
        $msg .= FS.$field->name.$field->value;
96
    }
97
98
    return STX.$msg.ETX;
99
}
100
101
sub addField {
102
    my ( $self, $fieldName, $value ) = @_;
103
    my $field = Koha::PosTerminal::Message::Field->new({ name => $fieldName, value => $value });
104
    push(@{$self->{_fields}}, $field);
105
    $self->updateHeader();
106
}
107
108
sub getField {
109
    my ( $self, $fieldName ) = @_;
110
    foreach my $field (@{$self->{_fields}}) {
111
        if ( $field->name eq $fieldName ) {
112
            return $field;
113
        }
114
    }
115
    return 0;
116
}
117
118
sub fieldCount {
119
    my( $self ) = @_;
120
121
    return $self->{_fields} ? scalar @{$self->{_fields}} : 0;
122
}
123
124
sub updateHeader {
125
    my( $self ) = @_;
126
127
    my $dataPart = "";
128
    foreach my $field (@{$self->{_fields}}) {
129
        $dataPart .= FS.$field->name.$field->value;
130
    }
131
    $self->getHeader()->crc($self->getCrcHex($dataPart));
132
    $self->getHeader()->length(sprintf("%04X", length($dataPart)));
133
}
134
135
sub getCrcHex {
136
    my( $self, $data ) = @_;
137
138
    my $crc = Digest::CRC->new(width=>16, init => 0x0000, xorout => 0x0000,
139
                               refout => 0, poly => 0x11021, refin => 0, cont => 0);
140
    $crc->add($data);
141
    my $crcBin = $crc->digest;
142
    return sprintf("%04X",$crcBin);
143
}
144
145
sub isValid {
146
    my( $self ) = @_;
147
148
    return $self->{_isValid};
149
}
150
151
sub setValid {
152
    my ( $self, $valid ) = @_;
153
    $self->{_isValid} = $valid;
154
}
155
156
sub parse {
157
    my ( $self, $response ) = @_;
158
159
    my $hdr = $self->getHeader();
160
161
    my $first = substr $response, 0, 1;
162
    my $last = substr $response, -1;
163
164
    if (($first eq STX) && ($last eq ETX)) {
165
        $hdr->protocolType(substr $response, 1, 2);
166
        $hdr->protocolVersion(substr $response, 3, 2);
167
        $hdr->terminalID(substr $response, 5, 8);
168
        $hdr->dateTime(substr $response, 13, 12);
169
        $hdr->tags(substr $response, 25, 4);
170
        $hdr->length(substr $response, 29, 4);
171
        $hdr->crc(substr $response, 33, 4);
172
        $self->{_fields} = ();
173
        my $dataPart = substr $response, 37, -1;
174
        if ($hdr->crc eq $self->getCrcHex($dataPart)) {
175
            $self->parseFields($dataPart);
176
            $self->setValid(1);
177
        }
178
        else {
179
            $self->setValid(0);
180
        }
181
    }
182
#    print Dumper($self);
183
184
}
185
186
sub parseFields {
187
    my ( $self, $dataPart ) = @_;
188
    my $fs = FS;
189
    my @fields = split /$fs/, substr $dataPart, 1;
190
191
    foreach my $field (@fields) {
192
        my $fname = substr $field, 0, 1;
193
        my $fvalue = substr $field, 1;
194
        $self->addField($fname, $fvalue);
195
    }
196
    return 0;
197
}
198
199
sub decodeControlCharacters {
200
    my( $self, $msg ) = @_;
201
202
    $msg =~ s/[\x02]/\<STX\>/g;
203
    $msg =~ s/[\x03]/\<ETX\>/g;
204
    $msg =~ s/[\x1C]/\<FS\>/g;
205
    $msg =~ s/[\x1D]/\<GS\>/g;
206
    $msg =~ s/[\x0D]/\<CR\>/g;
207
    $msg =~ s/[\x0A]/\<LF\>/g;
208
    $msg =~ s/[\xFF]/\<0xFF\>/g;
209
    $msg =~ s/ /\<SPC\>/g;
210
211
    return $msg;
212
}
213
214
sub dumpString {
215
    my( $self ) = @_;
216
    return $self->decodeControlCharacters($self->getContent());
217
}
218
219
sub dumpObject {
220
    my( $self ) = @_;
221
    my $msg = $self->getHeader()->dumpObject();
222
223
    $msg .= "data:\n";
224
#    print Dumper($self);
225
#die();
226
    foreach my $field (@{$self->{_fields}}) {
227
        $msg .= "  ".$field->name.": '".$field->value."'\n";
228
    }
229
    return $msg;
230
}
231
232
1;
(-)a/Koha/PosTerminal/Message/Field.pm (+26 lines)
Line 0 Link Here
1
package Koha::PosTerminal::Message::Field;
2
3
# Copyright 2017 R-Bit Technology, s.r.o.
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use strict;
21
use warnings;
22
23
use base qw(Class::Accessor);
24
Koha::PosTerminal::Message::Field->mk_accessors(qw(name value));
25
26
1;
(-)a/Koha/PosTerminal/Message/Header.pm (+126 lines)
Line 0 Link Here
1
package Koha::PosTerminal::Message::Header;
2
3
# Copyright 2017 R-Bit Technology, s.r.o.
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use strict;
21
use warnings;
22
23
use DateTime qw();
24
25
use constant SPC => ' ';
26
use constant PROTOCOL_TYPE => "B1";
27
use constant PROTOCOL_VERSION => "01";
28
use constant TIMEZONE => "Europe/Prague";
29
use constant CRC_NO_DATA => "A5A5";
30
use constant NO_DATA_LENGTH => "0000";
31
use constant TAGS_EMPTY => "0000";
32
use constant TAGS_SIGNATURE_CHECK => 0x0001;
33
34
use base qw(Class::Accessor);
35
Koha::PosTerminal::Message::Header->mk_accessors(qw(protocolType protocolVersion terminalID dateTime tags length crc));
36
37
sub new {
38
   my $class = shift @_;
39
40
   my $self = $class->SUPER::new(@_);
41
   $self->protocolType(PROTOCOL_TYPE);
42
   $self->protocolVersion(PROTOCOL_VERSION);
43
   $self->terminalID(0);
44
   $self->dateTime(0);
45
   $self->tags(TAGS_EMPTY);
46
   $self->length(NO_DATA_LENGTH);
47
   $self->crc(CRC_NO_DATA);
48
49
   return $self;
50
}
51
52
sub terminalID {
53
        my($self) = shift;
54
55
        if( @_ ) {  # Setting
56
            my($terminalID) = @_;
57
58
            if (!$terminalID) {
59
                $terminalID = " " x 8;
60
            }
61
            return $self->set('terminalID', $terminalID);
62
        }
63
        else {
64
            return $self->get('terminalID');
65
        }
66
}
67
68
sub dateTime {
69
        my($self) = shift;
70
71
        if( @_ ) {  # Setting
72
            my($dateTime) = @_;
73
74
            if (!$dateTime) {
75
                my $dt = DateTime->now(time_zone => TIMEZONE);
76
                $dateTime = $dt->strftime('%y%m%d%H%M%S');
77
            }
78
            return $self->set('dateTime', $dateTime);
79
        }
80
        else {
81
            return $self->get('dateTime');
82
        }
83
}
84
85
sub isSignatureCheckRequired {
86
    my( $self ) = @_;
87
    return hex("0x" . $self->tags) & TAGS_SIGNATURE_CHECK;
88
}
89
90
sub getContent {
91
    my( $self ) = @_;
92
    my $content =
93
          $self->protocolType
94
        . $self->protocolVersion
95
        . $self->terminalID
96
        . $self->dateTime
97
        . $self->tags
98
        . $self->length
99
        . $self->crc;
100
    return $content;
101
}
102
103
sub dumpObject {
104
    my( $self ) = @_;
105
    my @dt = ( $self->dateTime =~ m/../g );
106
    my $obj =
107
          "protocol:\n"
108
        . "  type: '".$self->protocolType."'\n"
109
        . "  version: '".$self->protocolVersion."'\n"
110
        . "terminal ID: '".$self->terminalID."'\n"
111
        . "date:\n"
112
        . "  year: '".$dt[0]."'\n"
113
        . "  month: '".$dt[1]."'\n"
114
        . "  day: '".$dt[2]."'\n"
115
        . "time:\n"
116
        . "  hours: '".$dt[3]."'\n"
117
        . "  minutes: '".$dt[4]."'\n"
118
        . "  seconds: '".$dt[5]."'\n"
119
        . "tags: '".$self->tags."'\n"
120
        . "length: '".$self->length."'\n"
121
        . "crc: '".$self->crc."'\n";
122
123
    return $obj;
124
}
125
126
1;
(-)a/Koha/PosTerminal/Transaction.pm (+50 lines)
Line 0 Link Here
1
package Koha::PosTerminal::Transaction;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
6
# terms of the GNU General Public License as published by the Free Software
7
# Foundation; either version 3 of the License, or (at your option) any later
8
# version.
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License along
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18
use Modern::Perl;
19
20
use Carp;
21
22
use Koha::Database;
23
24
use base qw(Koha::Object);
25
26
=head1 NAME
27
28
Koha::PosTerminal::Transactions - Koha pos_terminal_transaction Object class
29
30
=head1 API
31
32
=head2 Class Methods
33
34
=cut
35
36
=head3 type
37
38
=cut
39
40
sub _type {
41
    return 'PosTerminalTransaction';
42
}
43
44
1;
45
46
=head1 AUTHOR
47
48
Radek Å iman <rbit@rbit.cz>
49
50
=cut
(-)a/Koha/PosTerminal/Transactions.pm (+56 lines)
Line 0 Link Here
1
package Koha::PosTerminal::Transactions;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
6
# terms of the GNU General Public License as published by the Free Software
7
# Foundation; either version 3 of the License, or (at your option) any later
8
# version.
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License along
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18
use Modern::Perl;
19
20
use Carp;
21
22
use Koha::Database;
23
24
use Koha::PosTerminal::Transaction;
25
26
use base qw(Koha::Objects);
27
28
=head1 NAME
29
30
Koha::PosTerminal::Transactions - Koha PosTerminal Transaction Object set class
31
32
=head1 API
33
34
=head2 Class Methods
35
36
=cut
37
38
=head3 type
39
40
=cut
41
42
sub _type {
43
    return 'PosTerminalTransaction';
44
}
45
46
sub object_class {
47
    return 'Koha::PosTerminal::Transaction';
48
}
49
50
1;
51
52
=head1 AUTHOR
53
54
Radek Å iman <rbit@rbit.cz>
55
56
=cut
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/members-pos-terminal-dialog.inc (+16 lines)
Line 0 Link Here
1
<!-- Modal -->
2
<div id="card_payment_dialog" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="cardPaymentModalLabel">
3
  <div class="modal-dialog" role="document">
4
    <div class="modal-content">
5
      <div class="modal-header">
6
        <h4 class="modal-title" id="cardPaymentModalLabel">Card payment</h4>
7
      </div>
8
      <div class="modal-body">
9
        <p class="transaction-message"></p>
10
      </div>
11
      <div class="modal-footer">
12
        <button id="transaction-close" type="button" class="btn btn-default" data-dismiss="modal">Close</button>
13
      </div>
14
    </div><!-- /.modal-content -->
15
  </div><!-- /.modal-dialog -->
16
</div><!-- /.modal -->
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/members-pos-terminal-messages.inc (+21 lines)
Line 0 Link Here
1
<script type="text/javascript">
2
    //<![CDATA[
3
        var MSG_POS_IN_PROGRESS = _("Transaction is in progress...");
4
        var MSG_POS_SUCESS = _("Transaction successfully finished, thank you.");
5
        var MSG_POS_INIT = _("Initializing connection...");
6
        var MSG_POS_REQUEST_PAYMENT = _("Requesting payment transaction...");
7
        var MSG_POS_REQUEST_REFUND = _("Requesting refund transaction...");
8
        var MSG_POS_SENT_REQUEST = _("Request sent.");
9
        var MSG_POS_RECEIVED_MESSAGE = _("Request confirmed.");
10
        var MSG_POS_RECEIVED_RESPONSE = _("Response received.");
11
        var MSG_POS_SENT_CONFIRMATION = _("Transaction confirmed.");
12
        var MSG_POS_DISCONNECTED = _("Device disconnected.");
13
        var MSG_POS_ERR_TRANSACTION_REJECTED = _("Transaction rejected.");
14
        var MSG_POS_ERR_REQUEST_REJECTED = _("Connection request rejected.");
15
        var MSG_POS_ERR_CONNECTION_FAILED = _("Connection interrupted.");
16
        var MSG_POS_ERR = _("Error:");
17
        var MSG_POS_ERR_CONNECTION_ABORTED = _("Connection aborted.");
18
        var MSG_POS_ERR_EXPIRED = _("Session timed out. Please log in again.");
19
        var MSG_POS_ACTIVITY_MESSAGE = _("Activity message received.");
20
    //]]>
21
</script>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref (+12 lines)
Lines 157-162 Circulation: Link Here
157
                  yes: Allow
157
                  yes: Allow
158
                  no: "Don't allow"
158
                  no: "Don't allow"
159
            - patrons to submit notes about checked out items.
159
            - patrons to submit notes about checked out items.
160
        -
161
            - Payment terminal communicates at IP address
162
            - pref: PosTerminalIP
163
            - and port
164
            - pref: PosTerminalPort
165
              class: integer
166
            - . Leave the IP address blank to disable this payment option.
167
        -
168
            - Payment terminal uses
169
            - pref: PosTerminalCurrencyCode
170
              class: integer
171
            - as currency code number. Please see <a href="http://www.iso.org/iso/home/standards/currency_codes.htm" target="_blank">ISO 4217</a> for a full list of assigned numbers.
160
172
161
    Checkout Policy:
173
    Checkout Policy:
162
        -
174
        -
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/members/boraccount.tt (+6 lines)
Lines 75-80 Link Here
75
          [% IF ( account.payment ) %]
75
          [% IF ( account.payment ) %]
76
            <a href="boraccount.pl?action=reverse&amp;accountlines_id=[% account.accountlines_id %]&amp;borrowernumber=[% account.borrowernumber %]" class="btn btn-default btn-xs"><i class="fa fa-undo"></i> Reverse</a>
76
            <a href="boraccount.pl?action=reverse&amp;accountlines_id=[% account.accountlines_id %]&amp;borrowernumber=[% account.borrowernumber %]" class="btn btn-default btn-xs"><i class="fa fa-undo"></i> Reverse</a>
77
            <a href="boraccount.pl?action=void&amp;accountlines_id=[% account.accountlines_id %]&amp;borrowernumber=[% account.borrowernumber %]" class="btn btn-default btn-xs"><i class="fa fa-ban"></i> Void</a>
77
            <a href="boraccount.pl?action=void&amp;accountlines_id=[% account.accountlines_id %]&amp;borrowernumber=[% account.borrowernumber %]" class="btn btn-default btn-xs"><i class="fa fa-ban"></i> Void</a>
78
            [% IF (account.amountoutstanding+0 + account.amount+0 != 0 ) %]
79
            <a href="boraccount.pl?action=reverse&amp;accountlines_id=[% account.accountlines_id %]&amp;borrowernumber=[% account.borrowernumber %]" class="btn btn-default btn-xs" onclick="refundPayment(this.href, [% account.accountlines_id %], [% -1*account.amount %]);return false;"><i class="fa fa-undo"></i> Refund to card</a>
80
            [% END %]
78
          [% ELSE %][% SET footerjs = 1 %]
81
          [% ELSE %][% SET footerjs = 1 %]
79
            &nbsp;
82
            &nbsp;
80
          [% END %]
83
          [% END %]
Lines 108-114 Link Here
108
[% MACRO jsinclude BLOCK %]
111
[% MACRO jsinclude BLOCK %]
109
    [% INCLUDE 'datatables.inc' %]
112
    [% INCLUDE 'datatables.inc' %]
110
    [% INCLUDE 'columns_settings.inc' %]
113
    [% INCLUDE 'columns_settings.inc' %]
114
    [% INCLUDE 'members-pos-terminal-messages.inc' %]
111
    [% Asset.js("js/members-menu.js") %]
115
    [% Asset.js("js/members-menu.js") %]
116
    [% Asset.js("js/payments.js") %]
112
    <script type="text/javascript">
117
    <script type="text/javascript">
113
        var dateformat = "[% Koha.Preference('dateformat') %]";
118
        var dateformat = "[% Koha.Preference('dateformat') %]";
114
        $(document).ready(function() {
119
        $(document).ready(function() {
Lines 141-144 Link Here
141
    </script>
146
    </script>
142
[% END %]
147
[% END %]
143
148
149
[% INCLUDE 'members-pos-terminal-dialog.inc' %]
144
[% INCLUDE 'intranet-bottom.inc' %]
150
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/members/paycollect.tt (-1 / +12 lines)
Lines 46-52 Link Here
46
[% END %]
46
[% END %]
47
47
48
[% IF ( pay_individual ) %]
48
[% IF ( pay_individual ) %]
49
    <form name="payindivfine" id="payindivfine" method="post" action="/cgi-bin/koha/members/paycollect.pl">
49
    <form name="payindivfine" id="payindivfine" method="post" onsubmit="return makePayment(this);" action="/cgi-bin/koha/members/paycollect.pl">
50
    <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
50
    <input type="hidden" name="csrf_token" value="[% csrf_token %]" />
51
    <input type="hidden" name="borrowernumber" id="borrowernumber" value="[% patron.borrowernumber %]" />
51
    <input type="hidden" name="borrowernumber" id="borrowernumber" value="[% patron.borrowernumber %]" />
52
    <input type="hidden" name="pay_individual" id="pay_individual" value="[% pay_individual %]" />
52
    <input type="hidden" name="pay_individual" id="pay_individual" value="[% pay_individual %]" />
Lines 100-105 Link Here
100
            </select>
100
            </select>
101
        </li>
101
        </li>
102
    [% END %]
102
    [% END %]
103
[%# FIXME - add this to payment type select box %]
104
[% IF Koha.Preference('PosTerminalIP') %]
105
    <li>
106
        <label for="bycard">Pay by card: </label>
107
        <input type="checkbox" name="bycard" id="bycard" value="1"/>
108
    </li>
109
[% END %]
110
103
</ol>
111
</ol>
104
</fieldset>
112
</fieldset>
105
113
Lines 203-209 Link Here
203
</div>
211
</div>
204
212
205
[% MACRO jsinclude BLOCK %]
213
[% MACRO jsinclude BLOCK %]
214
    [% INCLUDE 'members-pos-terminal-messages.inc' %]
206
    [% Asset.js("js/members-menu.js") %]
215
    [% Asset.js("js/members-menu.js") %]
216
    [% Asset.js("js/payments.js") %]
207
    <script type= "text/javascript">
217
    <script type= "text/javascript">
208
        $(document).ready(function() {
218
        $(document).ready(function() {
209
            $('#payindivfine, #payfine').preventDoubleFormSubmit();
219
            $('#payindivfine, #payfine').preventDoubleFormSubmit();
Lines 281-284 Link Here
281
    </script>
291
    </script>
282
[% END %]
292
[% END %]
283
293
294
[% INCLUDE 'members-pos-terminal-dialog.inc' %]
284
[% INCLUDE 'intranet-bottom.inc' %]
295
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/js/payments.js (+205 lines)
Line 0 Link Here
1
var posTransactionTimer = 0;
2
var formPayment = 0;
3
var posTransactionSucceeded = 0;
4
5
function callSvcApi(data, callbacks) {
6
    $.post('/cgi-bin/koha/svc/pos_terminal', data, function( response ) {
7
        if (callbacks.success) {
8
            callbacks.success(response);
9
        }
10
    })
11
        .fail(function(response) {
12
            if (callbacks.fail) {
13
                callbacks.fail(response);
14
            }
15
        })
16
        .always(function(response) {
17
            if (callbacks.always) {
18
                callbacks.always(response);
19
            }
20
        });
21
}
22
23
function showMessage(status) {
24
    var msg = "";
25
26
    if (status == "new") {
27
        msg = MSG_POS_IN_PROGRESS;
28
    }
29
    else if (status == "success") {
30
        msg = MSG_POS_SUCESS;
31
    }
32
    else if (status == "init") {
33
        msg = MSG_POS_INIT;
34
    }
35
    else if (status == "request-payment") {
36
        msg = MSG_POS_REQUEST_PAYMENT;
37
    }
38
    else if (status == "request-refund") {
39
        msg = MSG_POS_REQUEST_REFUND;
40
    }
41
    else if (status == "sent-request") {
42
        msg = MSG_POS_SENT_REQUEST;
43
    }
44
    else if (status == "received-message") {
45
        msg = MSG_POS_RECEIVED_MESSAGE;
46
    }
47
    else if (status == "received-response") {
48
        msg = MSG_POS_RECEIVED_RESPONSE;
49
    }
50
    else if (status == "sent-confirmation") {
51
        msg = MSG_POS_SENT_CONFIRMATION;
52
    }
53
    else if (status == "activity-message") {
54
        msg = MSG_POS_ACTIVITY_MESSAGE;
55
    }
56
    else if (status == "disconnected") {
57
        msg = MSG_POS_DISCONNECTED;
58
    }
59
    else if (status == "connection-error") {
60
        msg = "Connection error.";
61
    }
62
    else if (status == "ERR_TRANSACTION_REJECTED") {
63
        msg = MSG_POS_ERR_TRANSACTION_REJECTED;
64
    }
65
    else if (status == "ERR_REQUEST_REJECTED") {
66
        msg = MSG_POS_ERR_REQUEST_REJECTED;
67
    }
68
    else if (status == "ERR_CONNECTION_FAILED") {
69
        msg = MSG_POS_ERR_CONNECTION_FAILED;
70
    }
71
    else if (status.lastIndexOf("ERR_") === 0) {
72
        msg = MSG_POS_ERR + " " + status + ". " + MSG_POS_ERR_CONNECTION_ABORTED;
73
    }
74
    else if (status == "expired") {
75
        msg = MSG_POS_ERR_EXPIRED
76
    }
77
    else {
78
        msg = status;
79
    }
80
81
    $("#card_payment_dialog .transaction-message").text(msg);
82
}
83
84
function checkStatus(transaction_id) {
85
    callSvcApi(
86
        {transaction_id: transaction_id, action: "status"},
87
        {
88
            success: function(xml) {
89
                var status = $(xml).find('status').first().text();
90
                showMessage(status);
91
                if ((status.lastIndexOf("ERR_") === 0) || (status == "success") || (status == "expired")) {
92
                    formPayment = (status == "success") ? formPayment : 0;
93
                    posTransactionSucceeded = (status == "success");
94
                    $("#card_payment_dialog button").prop("disabled", false);
95
                }
96
                else {
97
                    posTransactionTimer = setTimeout(function() { checkStatus(transaction_id); }, 5000);
98
                }
99
            }
100
        }
101
    );
102
}
103
104
// ---------------- payments
105
function requestPayment(transaction_id) {
106
    showMessage("request-payment");
107
    callSvcApi(
108
        {
109
            transaction_id: transaction_id,
110
            action: "request-payment",
111
            paid: parseInt($('#paid').val())
112
        },
113
        { }
114
    );
115
}
116
117
function startPaymentTransaction(accountlines_id) {
118
    showMessage('init');
119
    $("#card_payment_dialog button").click(closePaymentTransaction);
120
    $("#card_payment_dialog button").prop("disabled", true);
121
    $("#card_payment_dialog").modal({
122
        backdrop: 'static',
123
        keyboard: false
124
    });
125
    callSvcApi(
126
        {accountlines_id: accountlines_id},
127
        {
128
            success: function(xml) {
129
                var transaction_id = $(xml).find('transaction_id').first().text();
130
                posTransactionTimer = setTimeout(function() { checkStatus(transaction_id); }, 5000);
131
                requestPayment(transaction_id);
132
            },
133
            fail: function(xml) { alert(MSG_POS_ERR + " " + $(xml).find('status').first().text()); },
134
        }
135
    );
136
}
137
138
function closePaymentTransaction() {
139
    clearTimeout(posTransactionTimer);
140
    if (formPayment && posTransactionSucceeded) {
141
        formPayment.submit();
142
    }
143
    else {
144
        $("body, form input[type='submit'], form button[type='submit'], form a").removeClass('waiting');
145
    }
146
}
147
148
function makePayment(form) {
149
    if ($("#bycard").is(':checked')) {
150
        formPayment = form;
151
        startPaymentTransaction($("#accountlines_id").val())
152
    }
153
    else {
154
        formPayment = 0;
155
        form.submit();
156
    }
157
158
    return false;   // always return false not to submit the form automatically
159
}
160
161
// ---------------- refund
162
function refundPayment(href, accountlines_id, amount) {
163
    showMessage('init');
164
    $("#card_payment_dialog button").click((function() { closeRefundTransaction(href); }));
165
    $("#card_payment_dialog button").prop("disabled", true);
166
    $("#card_payment_dialog").modal({
167
        backdrop: 'static',
168
        keyboard: false
169
    });
170
    callSvcApi(
171
        {accountlines_id: accountlines_id},
172
        {
173
            success: function(xml) {
174
                var transaction_id = $(xml).find('transaction_id').first().text();
175
                $("#card_payment_dialog button").click((function() { closeRefundTransaction(href, transaction_id); }));
176
                posTransactionTimer = setTimeout(function() { checkStatus(transaction_id); }, 5000);
177
                requestRefund(transaction_id, amount);
178
            },
179
            fail: function(xml) { alert(MSG_POS_ERR + " " + $(xml).find('status').first().text()); },
180
        }
181
    );
182
}
183
184
function requestRefund(transaction_id, amount) {
185
    showMessage("request-refund");
186
    callSvcApi(
187
        {
188
            transaction_id: transaction_id,
189
            action: "request-refund",
190
            paid: amount
191
        },
192
        { }
193
    );
194
}
195
196
function closeRefundTransaction(href, transaction_id) {
197
    clearTimeout(posTransactionTimer);
198
    if (posTransactionSucceeded) {
199
        window.location.href = href;
200
    }
201
    else {
202
        $("body, form input[type='submit'], form button[type='submit'], form a").removeClass('waiting');
203
    }
204
    posTransactionSucceeded = 0;
205
}
(-)a/members/paycollect.pl (+1 lines)
Lines 62-67 my $branch = C4::Context->userenv->{'branch'}; Link Here
62
62
63
my $total_due = $patron->account->balance;
63
my $total_due = $patron->account->balance;
64
my $total_paid = $input->param('paid');
64
my $total_paid = $input->param('paid');
65
my $by_card    = $input->param('bycard');
65
66
66
my $individual   = $input->param('pay_individual');
67
my $individual   = $input->param('pay_individual');
67
my $writeoff     = $input->param('writeoff_individual');
68
my $writeoff     = $input->param('writeoff_individual');
(-)a/svc/pos_terminal (-1 / +250 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# Copyright 2017 R-Bit technology, s.r.o.
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
#
20
21
use Modern::Perl;
22
23
use CGI qw ( -utf8 );
24
use C4::Auth qw( check_api_auth );
25
use C4::Context;
26
use JSON qw( to_json );
27
use IO::Socket::INET;
28
use Koha::PosTerminal::Message;
29
use Koha::PosTerminal::Client;
30
use Koha::PosTerminal::Transaction;
31
use Koha::PosTerminal::Transactions;
32
use XML::Simple;
33
use Scalar::Util qw( looks_like_number );
34
use DateTime;
35
use Data::Dumper;
36
37
my $query = new CGI;
38
my ($status, $cookie, $sessionID) = check_api_auth($query, { updatecharges => 1 } );
39
my $transactionId = $query->param('transaction_id');
40
my $accountlinesId = $query->param('accountlines_id');
41
my $action = $query->param('action') || q{};
42
my $result = {
43
    status => $status,
44
    transaction_id => defined($transactionId) ? $transactionId : -1
45
};
46
47
binmode STDOUT, ":encoding(UTF-8)";
48
print $query->header(
49
    -type => 'text/xml',
50
    -charset => 'UTF-8'
51
);
52
53
if ($status eq 'ok') {
54
55
    if (!$action) {
56
        start_transaction($result, $accountlinesId);
57
    }
58
59
    elsif ($action eq 'status') {
60
        get_transaction_status($result, $transactionId);
61
    }
62
63
    elsif (($action eq 'request-payment') || ($action eq 'request-refund') || ($action eq 'abort')) {
64
        my $transaction = Koha::PosTerminal::Transactions->find($transactionId);
65
        my $client = new Koha::PosTerminal::Client(
66
            C4::Context->preference('PosTerminalIP'),
67
            C4::Context->preference('PosTerminalPort'),
68
        );
69
70
        if ($client->connect()) {
71
            $transaction->set({status => 'connected'})->store();
72
73
            if ($action eq 'abort') {
74
                my $abort = abort_transaction($client, $transaction);
75
                my $field = $abort->getField($abort->F_RESPONSE_CODE);
76
                $result->{response_code} = $field ? $field->value : 0;
77
                $result->{status} = "abort";
78
                $transaction->set({status => $result->{status}, response_code => $result->{response_code}})->store();
79
                print XMLout($result, NoAttr => 1, RootName => 'response', XMLDecl => 1);
80
                exit 0;
81
            }
82
83
            my $transactionRequest  = send_transaction_request($client, $transaction, $action eq 'request-payment' ? Koha::PosTerminal::Message->TTYPE_SALE : Koha::PosTerminal::Message->TTYPE_REFUND, scalar $query->param('paid'));
84
85
            my $transactionMessage  = receive_transaction_message($client, $transaction);
86
87
            my $field = $transactionMessage->getField($transactionMessage->F_RESPONSE_CODE);
88
            $result->{response_code} = $field ? $field->value : 0;
89
            if ($result->{response_code} <= 10) {
90
91
                my $transactionResponse = receive_transaction_response($client, $transaction);
92
                if (!looks_like_number($transactionResponse)) {
93
                    send_confirmation_message($client, $transaction, $transactionResponse);
94
95
                    $client->disconnect();
96
                    $transaction->set({status => 'disconnected'})->store();
97
98
                    $field = $transactionResponse->getField($transactionResponse->F_RESPONSE_CODE);
99
                    $result->{response_code} = $field ? $field->value : 0;
100
101
                    if ($result->{response_code} <= 10) {
102
                        if ( $field = $transactionResponse->getField($transactionResponse->F_CARD_NUMBER) ) {
103
                            $result->{cardnumber} = $field->value;
104
                            $result->{status} = "success";
105
                        }
106
                        else {
107
                            $result->{status} = "ERR_NO_CARD_NUMBER";
108
                        }
109
                    }
110
                    else {
111
                        $result->{status} = "ERR_TRANSACTION_REJECTED";
112
                    }
113
                }
114
                else {
115
                    $result->{status} = "ERR_CONNECTION_FAILED";
116
                }
117
            }
118
            else {
119
                $result->{status} = "ERR_REQUEST_REJECTED";
120
            }
121
        }
122
        else {
123
            $result->{status} = "ERR_NO_CONNECTION";
124
        }
125
        $transaction->set({status => $result->{status}, response_code => $result->{response_code}})->store();
126
    }
127
    elsif ($action eq 'request-refund') {
128
    }
129
}
130
131
print XMLout($result, NoAttr => 1, RootName => 'response', XMLDecl => 1);
132
133
exit 0;
134
135
sub log_communication {
136
    my ( $terminalMsg, $transactionId ) = @_;
137
138
    my $transaction = Koha::PosTerminal::Transactions->find($transactionId);
139
    my $now = DateTime->now();
140
    my $message = "[" . $now->ymd . " " . $now->hms ."] data: ";
141
    if (looks_like_number($terminalMsg)) {
142
        $message .= "error " . $terminalMsg . "\n";
143
    }
144
    else {
145
        $message .= $terminalMsg->fieldCount() . ", " . $terminalMsg->getDirection() . ": " . $terminalMsg->decodeControlCharacters($terminalMsg->getContent()) . "\n";
146
    }
147
    $transaction->set({message_log => (defined $transaction->message_log ? $transaction->message_log : "") . $message})->store();
148
}
149
150
sub start_transaction {
151
    my ( $result, $accountlinesId ) = @_;
152
153
    my $transaction = Koha::PosTerminal::Transaction->new({accountlines_id => $accountlinesId, status => 'new' })->store();
154
    $result->{status} = $transaction->status;
155
    $result->{transaction_id} = $transaction->id;
156
}
157
158
sub abort_transaction {
159
    my ( $client, $transaction ) = @_;
160
161
    # send abort message
162
    my $abort = new Koha::PosTerminal::Message(Koha::PosTerminal::Message::DIR_SENT);
163
    my $hdrAbort = $abort->getHeader();
164
die(Dumper($transaction ? $transaction->getHeader() : "BUBU"));
165
    my $hdrTransaction = $transaction->getHeader();
166
167
    $hdrAbort->dateTime($hdrTransaction->dateTime());
168
    $hdrAbort->terminalID($hdrTransaction->terminalID());
169
    $hdrAbort->protocolType($hdrTransaction->protocolType());
170
    $hdrAbort->protocolVersion($hdrTransaction->protocolVersion());
171
    $abort->addField($abort->F_TRANSACTION_TYPE, $abort->TTYPE_ABORT);
172
    $client->send($abort);
173
174
    $abort->set({status => 'abort'})->store();
175
    log_communication($abort, $transaction->id);
176
177
    return $abort;
178
}
179
180
sub get_transaction_status {
181
    my ( $result, $transactionId ) = @_;
182
183
    my $transaction = Koha::PosTerminal::Transactions->find($transactionId);
184
    $result->{status} = $transaction->status;
185
}
186
187
sub send_transaction_request {
188
    my ( $client, $transaction, $type, $paid ) = @_;
189
190
    # send transaction request
191
    my $transactionRequest = new Koha::PosTerminal::Message(Koha::PosTerminal::Message::DIR_SENT);
192
    $transactionRequest->getHeader()->dateTime(0);
193
    $transactionRequest->addField($transactionRequest->F_TRANSACTION_TYPE, $type);
194
    $transactionRequest->addField($transactionRequest->F_PAID_AMOUNT, $paid * 100);
195
    $transactionRequest->addField($transactionRequest->F_INVOICE_NUMBER, $transaction->accountlines_id);
196
    $transactionRequest->addField($transactionRequest->F_CURRENCY_CODE, C4::Context->preference('PosTerminalCurrencyCode'));
197
    $client->send($transactionRequest);
198
    $transaction->set({status => 'sent-request'})->store();
199
    log_communication($transactionRequest, $transaction->id);
200
201
    return $transactionRequest;
202
}
203
204
sub receive_transaction_message {
205
    my ( $client, $transaction ) = @_;
206
207
    # receive transaction message
208
    my $transactionMessage = $client->receive();
209
210
    $transaction->set({status => 'received-message'})->store();
211
    log_communication($transactionMessage, $transaction->id);
212
213
    return $transactionMessage;
214
}
215
216
sub receive_transaction_response {
217
    my ( $client, $transaction ) = @_;
218
    my $transactionResponse;
219
    my $status;
220
221
    # receive transaction response
222
    for (;;) {
223
        $transactionResponse = $client->receive();
224
        $status = looks_like_number($transactionResponse) ? 'ERR_CONNECTION_FAILED'
225
                                                          : ($transactionResponse->fieldCount() ? 'received-response' : 'activity-message');
226
        $transaction->set({status => $status})->store();
227
        log_communication($transactionResponse, $transaction->id);
228
229
        last if (looks_like_number($transactionResponse) || $transactionResponse->fieldCount());
230
    }
231
232
    return $transactionResponse;
233
}
234
235
sub send_confirmation_message {
236
    my ( $client, $transaction, $transactionResponse ) = @_;
237
238
    # send confirmation message
239
    my $confirmation = new Koha::PosTerminal::Message(Koha::PosTerminal::Message::DIR_SENT);
240
    my $hdrConfirm = $confirmation->getHeader();
241
    my $hdrResponse = $transactionResponse->getHeader();
242
    $hdrConfirm->dateTime($hdrResponse->dateTime());
243
    $hdrConfirm->terminalID($hdrResponse->terminalID());
244
    $hdrConfirm->protocolType($hdrResponse->protocolType());
245
    $hdrConfirm->protocolVersion($hdrResponse->protocolVersion());
246
    $client->send($confirmation);
247
248
    $transaction->set({status => 'sent-confirmation'})->store();
249
    log_communication($confirmation, $transaction->id);
250
}

Return to bug 17705