failure status
[Business-BatchPayment.git] / BatchPayment / Item.pm
1 package Business::BatchPayment::Item;
2
3 use strict;
4 use Moose;
5 use Moose::Util::TypeConstraints;
6 use MooseX::UndefTolerant;
7 use DateTime;
8
9 =head1 NAME
10
11 Business::BatchPayment::Item
12
13 =head1 DESCRIPTION
14
15 A Business::BatchPayment::Item represents a single payment request or 
16 reply (approval or rejection).  When submitting a batch, the merchant 
17 system constructs B::BP::Item objects for each attempted payment in 
18 the batch.  Results downloaded from the gateway are returned as a 
19 list of Items with the 'approved' field set to a true or false value. 
20
21 =head1 REQUIRED ATTRIBUTES
22
23 =over 4
24
25 =item action
26
27 "payment" or "credit".  Most processors support only "payment".
28 "payment" is defined as "money transfer FROM the account identified in the 
29 Item TO the account identified by the Processor object's login settings."
30 "credit" is the other direction.
31
32 =cut
33
34 enum 'Action' => qw(payment credit);
35 coerce 'Action', from 'Str', via { lc $_ };
36 has action => (
37   is  => 'rw',
38   isa => 'Action',
39   default => 'payment',
40   required => 1,
41   coerce => 1,
42 );
43
44 =item payment_type
45
46 "CC" or "ECHECK".  Most processors will only support one or the other, 
47 and if set on the Processor object, this is not required.
48
49 =cut
50
51 # are we okay with these names?
52 enum 'PaymentType' => qw( CC ECHECK );
53 has payment_type => ( is  => 'rw', isa => 'PaymentType' );
54
55 =item amount
56
57 the amount, as a decimal number.  Required only in request
58 items.
59
60 =cut
61
62 # perhaps we should apply roles that distinguish request and reply items?
63 # they have different required fields.
64 has amount => (
65   is  => 'rw',
66   isa => 'Num',
67 );
68
69 =item tid
70
71 transaction identifier.  Requests must provide this.  It's a token of 
72 some kind to be passed to the gateway and used to identify the reply.  
73 For now it's required to be an integer.  An invoice number would be 
74 a good choice.
75
76 =cut
77
78 has tid => ( is  => 'rw', isa => 'Int' );
79
80 =back
81
82 =head1 OPTIONAL ATTRIBUTES
83
84 =head2 Customer Information
85
86 =over 4
87
88 =item customer_id
89
90 A customer number or other identifier, for the merchant's use.
91
92 =item first_name
93
94 First name.
95
96 =item last_name
97
98 Last name.
99
100 =item company
101
102 Company name.
103
104 =item address, address2, city, state, country, zip
105
106 Billing address fields.  Credit card processors may use these (especially
107 zip) for authentication.
108
109 =item phone
110
111 Customer phone number.
112
113 =cut
114
115 has [ qw(
116   customer_id
117   first_name
118   last_name
119   company
120   address
121   address2
122   city
123   state
124   country
125   zip
126   phone
127 ) ] => ( is => 'rw', isa => 'Str', default => '' );
128
129 =back
130
131 =head2 Transaction Information
132
133 =over 4
134
135 =item process_date
136
137 The date requested for processing.  This is meaningful only if the 
138 processor allows different processing dates for items in the same 
139 batch.
140
141 =item invoice_number
142
143 An invoice number, for your use.
144
145 =cut
146
147 class_type 'DateTime';
148 coerce 'DateTime', from 'Int', via { DateTime->from_epoch($_) };
149 has process_date    => ( is => 'rw', isa => 'DateTime', coerce => 1 );
150
151 has invoice_number  => ( is => 'rw', isa => 'Str' );
152
153 =back
154
155 =head2 Bank Transfer / ACH / EFT
156
157 =over 4
158
159 =item account_number
160
161 Bank account number.
162
163 =item routing_code
164
165 Bank's routing code.
166
167 =item account_type
168
169 Can be 'personal checking', 'personal savings', 'business checking', 
170 or 'business savings'.
171
172 =cut
173
174 enum 'Account_Type' => [
175   'personal checking',
176   'personal savings',
177   'business checking',
178   'business savings',
179 ];
180 coerce 'Account_Type', from 'Str', via { lc $_ };
181
182 has account_number  => ( is => 'rw', isa => 'Str' );
183 has routing_code    => ( is => 'rw', isa => 'Str' );
184 has account_type    => ( is => 'rw', isa => 'Account_Type', coerce => 1 );
185
186 =back
187
188 =head2 Credit Card
189
190 =over 4
191
192 =item card_number
193
194 Credit card number.
195
196 =item expiration
197
198 Credit card expiration, MMYY format.
199
200 =cut
201
202 has card_number     => ( is => 'rw', isa => 'Str' );
203 has ['expiration_month', 'expiration_year'] => ( is => 'rw', isa => 'Int' );
204
205 sub expiration {
206   # gets/sets expiration_month and _year in MMYY format
207   my $self = shift;
208   my $arg = shift;
209   if ( $arg ) {
210     # well, we said it's in MMYY format
211     my ($m, $y) = _parse_expiration($arg);
212     $self->expiration_month($m);
213     $self->expiration_year($y);
214   }
215   return sprintf('%02d/%02d',
216     $self->expiration_month,
217     $self->expiration_year % 2000);
218 }
219
220 sub _parse_expiration {
221   my $arg = shift;
222   if ( $arg =~ /^(\d\d)(\d\d)$/ ) {
223     return ($1, 2000 + $2);
224   } elsif ( $arg =~ /^(\d\d?)\W(\d\d)$/ ) {
225     return ($1, 2000 + $2);
226   } elsif ( $arg =~ /^(\d\d?)\W(\d\d\d\d)$/ ) {
227     return ($1, $2);
228   } elsif ( $arg =~ /^(\d\d?)\W\d\d?\W(\d\d\d\d)$/) {
229     return ($1, $3);
230   } else {
231     die "can't parse expiration date '$arg'";
232   }
233 }
234
235 sub payinfo {
236   # gets/sets either the card number, or the account number + routing code
237   # depending on the payment type
238   my $self = shift;
239   if ( $self->payment_type eq 'CC' ) {
240     $self->card_number(@_);
241   } elsif ( $self->payment_type eq 'ECHECK' ) {
242     my $arg = shift;
243     if ( $arg ) {
244       $arg =~ /^(\d+)@(\d+)$/ or die "Validation failed for payinfo";
245       $self->account_number($1);
246       $self->routing_code($2);
247     }
248     return ($self->account_number . '@' . $self->routing_code);
249   }
250 }
251
252 =back
253
254 =head2 Tokenized Payment
255
256 =over 4
257
258 =item pay_by_token
259
260 If your gateway supports it, this may be 
261 provided instead of card_number/account_number.  See also 
262 C<assigned_token> below.
263
264 =cut
265
266 has pay_by_token    => ( is => 'rw', isa => 'Str' );
267
268 =back
269
270 =head1 REPLY ATTRIBUTES
271
272 =over 4
273
274 =item approved 
275
276 Boolean field for whether the item was approved.  This 
277 will always be set on replies.
278
279 =item payment_date 
280
281 The date the payment was processed, as a DateTime
282 object.
283
284 =item order_number 
285
286 The transaction identifier returned by the gateway
287 (not to be confused with 'tid', which is a transaction identifier assigned
288 by the merchant system).  This is usually the identifier for performing 
289 other operations on the transaction, like voiding or refunding it.
290
291 =item authorization
292
293 The authorization code, probably only meaningful for credit cards.  
294 Should be undef (or not present) if the transaction wasn't approved.
295
296 =item check_number
297
298 The check number, probably only meaningful if this transaction was
299 processed from a paper check.
300
301 =item assigned_token
302
303 In tokenized systems which store the customer's account number or 
304 credit card for future transactions, this is the token assigned to 
305 identify that account.  Pass it as 'pay_by_token' to use that payment 
306 account again.
307
308 =item error_message
309
310 The message returned by the gateway.  This may contain a value even 
311 if the payment was successful (use C<approved> to determine that.)
312
313 =item failure_status
314
315 A normalized failure status, from the following list:
316
317 =over 4
318
319 =item expired
320
321 =item nsf (non-sufficient funds / credit limit)
322
323 =item stolen
324
325 =item pickup
326
327 =item blacklisted
328
329 =item inactive
330
331 =item decline (other card/transaction declines)
332
333 =back
334
335 =back
336
337 =cut
338
339 has approved        => ( is => 'rw', isa => 'Maybe[Bool]' );
340
341 has payment_date    => ( is => 'rw', isa => 'DateTime' );
342
343 has [qw( 
344   authorization
345   error_message
346   order_number
347   assigned_token
348 )] => ( is => 'rw', isa => 'Str');
349
350 enum FailureStatus => qw(
351   expired
352   nsf
353   stolen
354   pickup
355   blacklisted
356   inactive
357   decline
358 );
359 has failure_status  => ( is => 'rw', isa => 'Maybe[FailureStatus]' );
360
361 has check_number => ( is => 'rw', isa => 'Int' );
362
363 around 'BUILDARGS' => sub {
364   my ($orig, $self, %args) = @_;
365   if ( $args{expiration} ) {
366     @args{'expiration_month', 'expiration_year'} =
367       _parse_expiration($args{expiration}); 
368   }
369   $self->$orig(%args);
370 };
371
372 __PACKAGE__->meta->make_immutable;
373
374 1;