=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. 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 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. 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. 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. No defined return value, but processors may optionally set the 'processor_id' field on the input batch, which should then be stored and passed to receive. =item receive Download/otherwise acquire the available confirmed transactions from the gateway, parse them, and return a list of L objects. The items in these batches will have, at minimum, the 'approved' field and either the 'tid' or 'amount' field set. Accepts an optional list of processor_id strings, if required by your processor. =item format_request BATCH Default method to serialize BATCH for submission. Returns the formatted text as a string. By default, this calls C, then C on each Item in the batch, then C, 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) 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. By default, calls C on the entire batch, then splits DATA into lines and calls C 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 Ls. =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); $batch->num(0); foreach my $item ($batch->elements) { try { $output .= $self->format_item($item, $batch); $batch->num( $batch->num + 1 ); } 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), num => 0, ); while ( $input =~ s/(.*)\n//m ) { my $row = $1; try { $batch->push( $self->parse_item($row) ); $batch->num( $batch->num + 1 ); } 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;