Processor modules may include C<batch_id> in a reply batch ONLY if
it is guaranteed to match the batch_id of a request batch AND all
the items in the reply batch come from that request batch. Otherwise,
-C<batch_id> must be null. It must always be null when using one-way
+C<batch_id> must be undef. It must always be undef when using one-way
(receive-only) workflow, since there are no request batches.
+=item process_date - The intended processing date for a request batch.
+If not set, it will default to the start of the next day; if that's
+not what you want, set it explicitly.
+
=item items - An arrayref of L<Business::BatchPayment::Item> objects
included in the batch.
+=item num
+
+If your processor uses C<format_header> and C<format_item>, this will
+be set to 0 by C<format_header> and incremented every time C<format_item>
+is called. Convenient for formats that require record numbers.
+
=back
+=head1 METHODS
+
+=over 4
+
+=item totals
+
+Returns a hash containing 'credit_count', 'credit_sum', 'payment_count',
+and 'payment_sum'. These are the number of credits, sum of credit amounts,
+number of payments, and sum of payment amounts.
+
=cut
package Business::BatchPayment::Batch;
use strict;
use Moose;
+use Moose::Util::TypeConstraints;
+use DateTime;
has incoming => (
is => 'rw',
default => sub { [] },
);
+class_type 'DateTime';
+coerce 'DateTime', from 'Int', via { DateTime->from_epoch($_) };
+
+has process_date => (
+ is => 'rw',
+ isa => 'DateTime',
+ coerce => 1,
+ default => sub {
+# warn "No batch process date set; assuming tomorrow.\n";
+ DateTime->today->add(days => 1);
+ },
+);
+
+has num => (
+ is => 'rw',
+ isa => 'Maybe[Int]',
+);
+
+sub totals {
+ my $self = shift;
+ my %totals = map {$_ => 0}
+ qw(credit_count credit_sum payment_count payment_sum);
+ foreach ($self->elements) {
+ if ($_->action eq 'credit') {
+ $totals{credit_count}++;
+ $totals{credit_sum} += $_->amount;
+ } elsif ( $_->action eq 'payment') {
+ $totals{payment_count}++;
+ $totals{payment_sum} += $_->amount;
+ }
+ }
+ %totals;
+}
+
1;
Billing address fields. Credit card processors may use these (especially
zip) for authentication.
+=item phone
+
+Customer phone number.
+
=cut
has [ qw(
state
country
zip
+ phone
) ] => ( is => 'rw', isa => 'Str', default => '' );
=back
=item process_date
-The date requested for processing.
+The date requested for processing. This is meaningful only if the
+processor allows different processing dates for items in the same
+batch.
=item invoice_number
=cut
has card_number => ( is => 'rw', isa => 'Str' );
-has expiration => ( is => 'rw', isa => 'Str' );
+has ['expiration_month', 'expiration_year'] => ( is => 'rw', isa => 'Int' );
+
+sub expiration {
+ # gets/sets expiration_month and _year in MMYY format
+ my $self = shift;
+ my $arg = shift;
+ if ( $arg ) {
+ # well, we said it's in MMYY format
+ my ($m, $y) = _parse_expiration($arg);
+ $self->expiration_month($m);
+ $self->expiration_year($y);
+ }
+ return sprintf('%02d/%02d',
+ $self->expiration_month,
+ $self->expiration_year % 2000);
+}
+
+sub _parse_expiration {
+ my $arg = shift;
+ if ( $arg =~ /^(\d\d)(\d\d)$/ ) {
+ return ($1, 2000 + $2);
+ } elsif ( $arg =~ /^(\d\d?)\W(\d\d)$/ ) {
+ return ($1, 2000 + $2);
+ } elsif ( $arg =~ /^(\d\d?)\W(\d\d\d\d)$/ ) {
+ return ($1, $2);
+ } elsif ( $arg =~ /^(\d\d?)\W\d\d?\W(\d\d\d\d)$/) {
+ return ($1, $3);
+ } else {
+ die "can't parse expiration date '$arg'";
+ }
+}
+
+sub payinfo {
+ # gets/sets either the card number, or the account number + routing code
+ # depending on the payment type
+ my $self = shift;
+ if ( $self->payment_type eq 'CC' ) {
+ $self->card_number(@_);
+ } elsif ( $self->payment_type eq 'ECHECK' ) {
+ my $arg = shift;
+ if ( $arg ) {
+ $arg =~ /^(\d+)@(\d+)$/ or die "Validation failed for payinfo";
+ $self->account_number($1);
+ $self->routing_code($2);
+ }
+ return ($self->account_number . '@' . $self->routing_code);
+ }
+}
=back
has check_number => ( is => 'rw', isa => 'Int' );
+around 'BUILDARGS' => sub {
+ my ($orig, $self, %args) = @_;
+ if ( $args{expiration} ) {
+ @args{'expiration_month', 'expiration_year'} =
+ _parse_expiration($args{expiration});
+ }
+ $self->$orig(%args);
+};
+
__PACKAGE__->meta->make_immutable;
1;
warn $request if $self->debug >= 2;
$self->transport->upload($request);
}
-;
+
sub receive {
my $self = shift;
my @responses = $self->transport->download;
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, $_);
};
my $input = shift;
my $batch = Business::BatchPayment->create(Batch =>
incoming => $self->incoming,
- batch_id => $self->parse_batch_id($input)
+ 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, $_);
};
with 'Business::BatchPayment::Transport';
has [ qw( host port get_path put_path ) ] => (
- is => 'ro',
+ is => 'rw',
isa => 'Str'
);
has 'content_type' => (
is => 'rw',
isa => 'Str',
- default => 'text/plain'
+ default => '', # application/x-www-form-urlencoded
);
sub https_post {
my $self = shift;
my $path = shift;
my $content = shift;
-
- warn "starting https_post...\n" if $self->debug;
- my ( $page, $response, %reply_headers ) = Net::HTTPS::Any::https_post(
+ my %post = (
host => $self->host,
port => $self->port,
path => $path,
- content => $content,
- debug => ($self->debug >= 3),
+ debug => ($self->debug > 3 ? 1 : 0),
+ 'Content-Type' => $self->content_type
);
+ if (ref $content and ref $content eq 'HASH') {
+ $post{'args'} = $content;
+ } else {
+ $post{'content'} = $content;
+ }
+
+ warn "starting https_post...\n" if $self->debug;
+ my ( $page, $response, %reply_headers ) = Net::HTTPS::Any::https_post(%post);
+
warn "PAGE:\n$page\n\nRESPONSE:\n$response\n\n" if $self->debug >= 2;
return ($page, $response, %reply_headers);
}