error callbacks, more structure for parse/format methods
[Business-BatchPayment.git] / BatchPayment / Processor.pm
index 26f5ba0..2093132 100644 (file)
@@ -41,22 +41,38 @@ This statement would be processed as a one-way batch.
 
 =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
 
@@ -68,7 +84,7 @@ it's supported.
 
 =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.
 
@@ -90,17 +106,57 @@ 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';
 
-requires 'format_request', 'parse_response';
-
 has 'transport' => (
   is      => 'rw',
   does    => 'Business::BatchPayment::Transport',
@@ -109,6 +165,11 @@ has 'transport' => (
   builder => 'default_transport',
 );
 
+sub default_transport {
+  my $self = shift;
+  die blessed($self). " requires a transport or input/output files\n";
+}
+
 around BUILDARGS => sub {
   my ($orig, $class, %args) = @_;
   %args = %{ $class->$orig(%args) }; #process as usual
@@ -122,6 +183,11 @@ around BUILDARGS => sub {
   \%args;
 };
 
+# override this if your processor produces one-way batches
+sub incoming { 0 };
+
+#top-level interface
+
 sub submit {
   my $self = shift;
   my $batch = shift;
@@ -138,4 +204,71 @@ sub receive {
   map { $self->parse_response($_) } @responses;
 }
 
+# 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($self, $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" }
+
+sub default_on_error { #re-throw it
+  my ($self, $item, $error) = @_;
+  die $error;
+};
+
+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 },
+);
+
+# 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.
+
 1;