=head1 ATTRIBUTES
-Most attributes for B::BP::Processor objects are defined by the module.
+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 transport
-=item debug - Debug level. This may be interpreted in various ways by
-the module.
+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 test_mode - Communicate with a test server instead of the production
-gateway. Not all processors support this.
-C<$processor->does('Business::BatchPayment::TestMode')> should tell whether
-it's supported.
+=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
=item output FILE
-If either of these is passed when constructing a Processor object, the
+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.
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';
-requires 'format_request', 'parse_response';
-
has 'transport' => (
is => 'rw',
does => 'Business::BatchPayment::Transport',
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
\%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 @items = @_;
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);
- map { $self->parse_response($_) } @responses;
+ 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;