package Business::BatchPayment::Item; use strict; use Moose; use Moose::Util::TypeConstraints; use MooseX::UndefTolerant; use DateTime; =head1 NAME Business::BatchPayment::Item =head1 DESCRIPTION A Business::BatchPayment::Item represents a single payment request or reply (approval or rejection). When submitting a batch, the merchant system constructs B::BP::Item objects for each attempted payment in the batch. Results downloaded from the gateway are returned as a list of Items with the 'approved' field set to a true or false value. =head1 REQUIRED ATTRIBUTES =over 4 =item action "payment" or "credit". Most processors support only "payment". "payment" is defined as "money transfer FROM the account identified in the Item TO the account identified by the Processor object's login settings." "credit" is the other direction. =cut enum 'Action' => [qw(payment credit)]; coerce 'Action', from 'Str', via { lc $_ }; has action => ( is => 'rw', isa => 'Action', default => 'payment', required => 1, coerce => 1, ); =item payment_type "CC" or "ECHECK". Most processors will only support one or the other, and if set on the Processor object, this is not required. =cut # are we okay with these names? enum 'PaymentType' => [qw( CC ECHECK )]; has payment_type => ( is => 'rw', isa => 'PaymentType' ); =item amount the amount, as a decimal number. Required only in request items. =cut # perhaps we should apply roles that distinguish request and reply items? # they have different required fields. has amount => ( is => 'rw', isa => 'Num', ); =item tid transaction identifier. Requests must provide this. It's a token of some kind to be passed to the gateway and used to identify the reply. For now it's required to be an integer. An invoice number would be a good choice. =cut has tid => ( is => 'rw', isa => 'Int' ); =back =head1 OPTIONAL ATTRIBUTES =head2 Customer Information =over 4 =item customer_id A customer number or other identifier, for the merchant's use. =item first_name First name. =item last_name Last name. =item company Company name. =item address, address2, city, state, country, zip Billing address fields. Credit card processors may use these (especially zip) for authentication. =item phone Customer phone number. =cut has [ qw( customer_id first_name last_name company address address2 city state country zip phone ) ] => ( is => 'rw', isa => 'Str', default => '' ); =back =head2 Transaction Information =over 4 =item process_date 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 An invoice number, for your use. =item recurring_billing A flag indicating whether this is a "recurring" transaction. Different processors interpret this differently, but the interface defines three kinds of payments: N: non-recurring; the customer is not expected to make another payment using the same card or bank account. F: first use; the customer has never used this card/account before but is expected to use it again. S: subsequent; the customer has used the card/account in the past. =cut class_type 'DateTime'; coerce 'DateTime', from 'Int', via { DateTime->from_epoch($_) }; has process_date => ( is => 'rw', isa => 'DateTime', coerce => 1 ); has invoice_number => ( is => 'rw', isa => 'Str' ); enum 'Recurring_Flag' => [ 'N', 'F', 'S' ]; has recurring_billing => ( is => 'rw', isa => 'Recurring_Flag' ); =back =head2 Bank Transfer / ACH / EFT =over 4 =item account_number Bank account number. =item routing_code Bank's routing code. =item account_type Can be 'personal checking', 'personal savings', 'business checking', or 'business savings'. =cut enum 'Account_Type' => [ 'personal checking', 'personal savings', 'business checking', 'business savings', ]; coerce 'Account_Type', from 'Str', via { lc $_ }; has account_number => ( is => 'rw', isa => 'Str' ); has routing_code => ( is => 'rw', isa => 'Str' ); has account_type => ( is => 'rw', isa => 'Account_Type', coerce => 1 ); =back =head2 Credit Card =over 4 =item card_number Credit card number. =item expiration Credit card expiration, MMYY format. =cut has card_number => ( 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 =head2 Tokenized Payment =over 4 =item pay_by_token If your gateway supports it, this may be provided instead of card_number/account_number. See also C below. =cut has pay_by_token => ( is => 'rw', isa => 'Str' ); =back =head1 REPLY ATTRIBUTES =over 4 =item approved Boolean field for whether the item was approved. This will always be set on replies. =item payment_date The date the payment was processed, as a DateTime object. =item order_number The transaction identifier returned by the gateway (not to be confused with 'tid', which is a transaction identifier assigned by the merchant system). This is usually the identifier for performing other operations on the transaction, like voiding or refunding it. =item authorization The authorization code, probably only meaningful for credit cards. Should be undef (or not present) if the transaction wasn't approved. =item check_number The check number, probably only meaningful if this transaction was processed from a paper check. =item assigned_token In tokenized systems which store the customer's account number or credit card for future transactions, this is the token assigned to identify that account. Pass it as 'pay_by_token' to use that payment account again. =item error_message The message returned by the gateway. This may contain a value even if the payment was successful (use C 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 has approved => ( is => 'rw', isa => 'Maybe[Bool]' ); has payment_date => ( is => 'rw', isa => 'DateTime' ); has [qw( authorization error_message order_number 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;