summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--BatchPayment/Batch.pm57
-rw-r--r--BatchPayment/Item.pm100
-rw-r--r--BatchPayment/Processor.pm8
-rw-r--r--BatchPayment/Transport/HTTPS.pm21
4 files changed, 174 insertions, 12 deletions
diff --git a/BatchPayment/Batch.pm b/BatchPayment/Batch.pm
index 0d6176b..b05eb80 100644
--- a/BatchPayment/Batch.pm
+++ b/BatchPayment/Batch.pm
@@ -21,20 +21,42 @@ but usually must be a positive integer, if it's used at all.
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',
@@ -59,4 +81,37 @@ has items => (
default => sub { [] },
);
+class_type 'DateTime';
+
+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;
diff --git a/BatchPayment/Item.pm b/BatchPayment/Item.pm
index f93f5b3..c719efc 100644
--- a/BatchPayment/Item.pm
+++ b/BatchPayment/Item.pm
@@ -106,6 +106,10 @@ Company name.
Billing address fields. Credit card processors may use these (especially
zip) for authentication.
+=item phone
+
+Customer phone number.
+
=cut
has [ qw(
@@ -119,6 +123,7 @@ has [ qw(
state
country
zip
+ phone
) ] => ( is => 'rw', isa => 'Str', default => '' );
=back
@@ -129,7 +134,9 @@ has [ qw(
=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
@@ -193,7 +200,54 @@ Credit card expiration, MMYY format.
=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
@@ -256,6 +310,28 @@ account again.
The message returned by the gateway. This may contain a value even
if the payment was successful (use C<approved> to determine that.)
+=item failure_status
+
+A normalized failure status, from the following list:
+
+=over 4
+
+=item expired
+
+=item nsf (non-sufficient funds / credit limit)
+
+=item stolen
+
+=item pickup
+
+=item blacklisted
+
+=item inactive
+
+=item decline (other card/transaction declines)
+
+=back
+
=back
=cut
@@ -271,8 +347,28 @@ has [qw(
assigned_token
)] => ( is => 'rw', isa => 'Str');
+enum FailureStatus => qw(
+ expired
+ nsf
+ stolen
+ pickup
+ blacklisted
+ inactive
+ decline
+);
+has failure_status => ( is => 'rw', isa => 'Maybe[FailureStatus]' );
+
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;
diff --git a/BatchPayment/Processor.pm b/BatchPayment/Processor.pm
index e02259a..a148c16 100644
--- a/BatchPayment/Processor.pm
+++ b/BatchPayment/Processor.pm
@@ -219,7 +219,7 @@ sub submit {
warn $request if $self->debug >= 2;
$self->transport->upload($request);
}
-;
+
sub receive {
my $self = shift;
my @responses = $self->transport->download;
@@ -237,9 +237,11 @@ 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, $_);
};
@@ -253,12 +255,14 @@ sub parse_response {
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, $_);
};
diff --git a/BatchPayment/Transport/HTTPS.pm b/BatchPayment/Transport/HTTPS.pm
index fdb2c35..0eb783b 100644
--- a/BatchPayment/Transport/HTTPS.pm
+++ b/BatchPayment/Transport/HTTPS.pm
@@ -14,29 +14,36 @@ use Net::HTTPS::Any 0.10;
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);
}