summaryrefslogtreecommitdiff
path: root/BatchPayment/Processor.pm
blob: e02259ad0edaccb69d28632bcb81476cf99606c4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
=head1 NAME

Business::BatchPayment::Processor - Common interface for batch payment gateways

=head1 DESCRIPTION

Business::BatchPayment::Processor is a Moose role.  Modules implementing  
the protocol to talk to specific payment processing services should compose 
it.

There are two general schemes for interacting with a batch payment gateway:

=over 4

=item Request/Reply

In this mode, you (the merchant) assemble a batch of payment requests, 
including account numbers, and send it to the gateway.  At some point in
the future, the gateway sends back one or more reply batches indicating the
results of processing the payments.

When submitting a request batch, the merchant software marks each payment
request with a unique transaction ID (the "tid" field).  This should be 
stored somewhere.  When the reply batch is processed, each item will have 
a tid matching its request.

Note that some gateways will provide results only for approved payments,
or even only for declined payments.  It is then up to the merchant software
to follow a sensible policy for approving or declining payments whose 
ultimate status is unconfirmed.

=item One-Way

In this mode, the gateway transmits a batch file containing approved 
payments, without those payments being requested.  For example, most
commercial banks provide check lockbox services and periodically 
send the merchant a statement of payments received to the lockbox.  
This statement would be processed as a one-way batch.

=back

=head1 ATTRIBUTES

Most attributes for Processor objects are defined by the module.

=over 4

=item transport

See L<Business::BatchPayment::Transport>.  This must be set before calling
submit() or receive().  Some modules will set it themselves; others require a
transport to be supplied.  Check for the existence of a 'default_transport'
method.

=item debug

Debug level.  This may be interpreted in various ways by the module.

=item test_mode

Communicate with a test server instead of the production gateway.  Not all
processors support this.  Test for the L<Business::BatchPayment::TestMode>
role to determine if it's supported.

=item on_format_error

Callback to handle errors when formatting items.  Arguments are the Processor
object, the Item object, and the error thrown by C<format_item>.  The callback
can die to stop submitting the batch.

=item on_parse_error

Callback to handle errors when parsing items.  Arguments are the Processor
object, the Item object, and the error thrown by C<parse_item>.  The callback
can die to stop receiving the batch.

=back

=head1 OTHER PARAMETERS

=over 4

=item input FILE

=item output FILE

If either of these is passed when constructing a Processor object, the
transport will be replaced with a File transport with those parameters.
Specifying only 'input' will direct 'output' to /dev/null, and vice versa.

=back

=head1 METHODS

=over 4

=item submit BATCH

Send a batch of requests to the gateway.  BATCH must be a 
L<Business::BatchPayment::Batch>.

=item receive

Download/otherwise acquire the available confirmed transactions from the 
gateway, parse them, and return a list of L<Business::BatchPayment::Batch>
objects.  The items in these batches will have, at minimum, the 'approved' 
field and either the 'tid' or 'amount' field set.

=item format_request BATCH

Default method to serialize BATCH for submission.  Returns the formatted
text as a string.  By default, this calls C<format_header>, then 
C<format_item> on each Item in the batch, then C<format_trailer>, 
joining the output with no delimiters.  Override this if your processor
needs something different.

=item format_header BATCH

=item format_trailer BATCH

Optional methods to produce the header and trailer sections of the 
formatted batch.  By default these are empty strings.

=item format_item ITEM, BATCH

Required method (if using the default C<format_request>) to produce
the per-item part of the formatted batch.  By default this throws 
a fatal error.

=item parse_response DATA

Default method to deserialize a received batch.  Takes the string received
from the gateway, returns a L<Business::BatchPayment::Batch>.  By default,
calls C<parse_batch_id> on the entire batch, then splits DATA into lines 
and calls C<parse_item> on each line.

=item parse_batch_id DATA

Optional method to obtain the batch identifier from the received file.
By default this returns nothing.

=item parse_item LINE

Required method to parse a line from the received file.  Should 
return zero or more L<Business::BatchPayment::Item>s.

=cut

=back

=cut

package Business::BatchPayment::Processor;

use strict;
use Try::Tiny;
use Moose::Role;
with 'Business::BatchPayment::Debug';

has 'transport' => (
  is      => 'rw',
  does    => 'Business::BatchPayment::Transport',
  # possibly this part should be a separate role
  lazy    => 1,
  builder => 'default_transport',
);

sub default_transport {
  my $self = shift;
  die blessed($self). " requires a transport or input/output files\n";
}

has 'on_format_error' => (
  traits  => ['Code'],
  is      => 'rw',
  handles => { format_error => 'execute_method' },
  default => sub { \&default_on_error },
);

has 'on_parse_error' => (
  traits  => ['Code'],
  is      => 'rw',
  handles => { parse_error => 'execute_method' },
  default => sub { \&default_on_error },
);

sub default_on_error { #re-throw it
  my ($self, $item, $error) = @_;
  $DB::single = 1 if defined($DB::single);
  die $error;
};

# No error callbacks for other parts of this.  The per-item case 
# is special in that it might make sense to continue with the 
# other items.

around BUILDARGS => sub {
  my ($orig, $class, %args) = @_;
  %args = %{ $class->$orig(%args) }; #process as usual
  # then:
  if ( $args{input} or $args{output} ) {
    $args{transport} = Business::BatchPayment->create( 'Transport::File',
      input => $args{input},
      output => $args{output},
    );
  }
  \%args;
};

# override this if your processor produces one-way batches
sub incoming { 0 };

#top-level interface

sub submit {
  my $self = shift;
  my $batch = shift;
  my $request = $self->format_request($batch);
  warn $request if $self->debug >= 2;
  $self->transport->upload($request);
}
;
sub receive {
  my $self = shift;
  my @responses = $self->transport->download;
  warn join("\n\n", @responses) if $self->debug >= 2 and scalar(@responses);
  my @batches;
  foreach my $response (@responses) {
    push @batches, $self->parse_response($response);
  }
  @batches;
}

# next level down

sub format_request {
  my $self = shift;
  my $batch = shift;
  my $output = $self->format_header($batch);
  foreach my $item ($batch->elements) {
    try {
      $output .= $self->format_item($item, $batch);
    } catch {
      $self->format_error($item, $_);
    };
  }
  $output .= $self->format_trailer($batch);
  return $output;
}

sub parse_response {
  my $self = shift;
  my $input = shift;
  my $batch = Business::BatchPayment->create(Batch =>
    incoming => $self->incoming,
    batch_id => $self->parse_batch_id($input)
  );
  while ( $input =~ s/(.*)\n//m ) {
    my $row = $1;
    try { 
      $batch->push( $self->parse_item($row) );
    } catch {
      $self->parse_error($row, $_);
    };
  }
  $batch;
}

# nuts and bolts

sub format_header  { '' };
sub format_trailer { '' };
sub format_item { die "format_item unimplemented\n" }

sub parse_batch_id { '' };
sub parse_item { die "parse_item unimplemented\n" }

1;