move cust_pay_batch::upload results subroutine to an FS::pay_batch method. upon...
[freeside.git] / FS / FS / cust_pay_batch.pm
1 package FS::cust_pay_batch;
2
3 use strict;
4 use vars qw( @ISA $DEBUG );
5 use FS::Record qw(dbh qsearch qsearchs);
6 use FS::payinfo_Mixin;
7 use FS::part_bill_event qw(due_events);
8 use Business::CreditCard 0.28;
9
10 @ISA = qw( FS::Record FS::payinfo_Mixin );
11
12 # 1 is mostly method/subroutine entry and options
13 # 2 traces progress of some operations
14 # 3 is even more information including possibly sensitive data
15 $DEBUG = 0;
16
17 =head1 NAME
18
19 FS::cust_pay_batch - Object methods for batch cards
20
21 =head1 SYNOPSIS
22
23   use FS::cust_pay_batch;
24
25   $record = new FS::cust_pay_batch \%hash;
26   $record = new FS::cust_pay_batch { 'column' => 'value' };
27
28   $error = $record->insert;
29
30   $error = $new_record->replace($old_record);
31
32   $error = $record->delete;
33
34   $error = $record->check;
35
36   $error = $record->retriable;
37
38 =head1 DESCRIPTION
39
40 An FS::cust_pay_batch object represents a credit card transaction ready to be
41 batched (sent to a processor).  FS::cust_pay_batch inherits from FS::Record.  
42 Typically called by the collect method of an FS::cust_main object.  The
43 following fields are currently supported:
44
45 =over 4
46
47 =item paybatchnum - primary key (automatically assigned)
48
49 =item batchnum - indentifies group in batch
50
51 =item payby - CARD/CHEK/LECB/BILL/COMP
52
53 =item payinfo
54
55 =item exp - card expiration 
56
57 =item amount 
58
59 =item invnum - invoice
60
61 =item custnum - customer 
62
63 =item payname - name on card 
64
65 =item first - name 
66
67 =item last - name 
68
69 =item address1 
70
71 =item address2 
72
73 =item city 
74
75 =item state 
76
77 =item zip 
78
79 =item country 
80
81 =item status
82
83 =back
84
85 =head1 METHODS
86
87 =over 4
88
89 =item new HASHREF
90
91 Creates a new record.  To add the record to the database, see L<"insert">.
92
93 Note that this stores the hash reference, not a distinct copy of the hash it
94 points to.  You can ask the object for a copy with the I<hash> method.
95
96 =cut
97
98 sub table { 'cust_pay_batch'; }
99
100 =item insert
101
102 Adds this record to the database.  If there is an error, returns the error,
103 otherwise returns false.
104
105 =item delete
106
107 Delete this record from the database.  If there is an error, returns the error,
108 otherwise returns false.
109
110 =item replace OLD_RECORD
111
112 Replaces the OLD_RECORD with this one in the database.  If there is an error,
113 returns the error, otherwise returns false.
114
115 =item check
116
117 Checks all fields to make sure this is a valid transaction.  If there is
118 an error, returns the error, otherwise returns false.  Called by the insert
119 and replace methods.
120
121 =cut
122
123 sub check {
124   my $self = shift;
125
126   my $error = 
127       $self->ut_numbern('paybatchnum')
128     || $self->ut_numbern('trancode') #deprecated
129     || $self->ut_money('amount')
130     || $self->ut_number('invnum')
131     || $self->ut_number('custnum')
132     || $self->ut_text('address1')
133     || $self->ut_textn('address2')
134     || $self->ut_text('city')
135     || $self->ut_textn('state')
136   ;
137
138   return $error if $error;
139
140   $self->getfield('last') =~ /^([\w \,\.\-\']+)$/ or return "Illegal last name";
141   $self->setfield('last',$1);
142
143   $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
144   $self->first($1);
145
146   $error = $self->payinfo_check();
147   return $error if $error;
148
149   if ( $self->exp eq '' ) {
150     return "Expiration date required"
151       unless $self->payby =~ /^(CHEK|DCHK|LECB|WEST)$/;
152     $self->exp('');
153   } else {
154     if ( $self->exp =~ /^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/ ) {
155       $self->exp("$1-$2-$3");
156     } elsif ( $self->exp =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
157       if ( length($2) == 4 ) {
158         $self->exp("$2-$1-01");
159       } elsif ( $2 > 98 ) { #should pry change to check for "this year"
160         $self->exp("19$2-$1-01");
161       } else {
162         $self->exp("20$2-$1-01");
163       }
164     } else {
165       return "Illegal expiration date";
166     }
167   }
168
169   if ( $self->payname eq '' ) {
170     $self->payname( $self->first. " ". $self->getfield('last') );
171   } else {
172     $self->payname =~ /^([\w \,\.\-\']+)$/
173       or return "Illegal billing name";
174     $self->payname($1);
175   }
176
177   #we have lots of old zips in there... don't hork up batch results cause of em
178   $self->zip =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/
179     or return "Illegal zip: ". $self->zip;
180   $self->zip($1);
181
182   $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
183   $self->country($1);
184
185   #$error = $self->ut_zip('zip', $self->country);
186   #return $error if $error;
187
188   #check invnum, custnum, ?
189
190   $self->SUPER::check;
191 }
192
193 =item cust_main
194
195 Returns the customer (see L<FS::cust_main>) for this batched credit card
196 payment.
197
198 =cut
199
200 sub cust_main {
201   my $self = shift;
202   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
203 }
204
205 =item retriable
206
207 Marks the corresponding event (see L<FS::cust_bill_event>) for this batched
208 credit card payment as retriable.  Useful if the corresponding financial
209 institution account was declined for temporary reasons and/or a manual 
210 retry is desired.
211
212 Implementation details: For the named customer's invoice, changes the
213 statustext of the 'done' (without statustext) event to 'retriable.'
214
215 =cut
216
217 sub retriable {
218   my $self = shift;
219
220   local $SIG{HUP} = 'IGNORE';        #Hmm
221   local $SIG{INT} = 'IGNORE';
222   local $SIG{QUIT} = 'IGNORE';
223   local $SIG{TERM} = 'IGNORE';
224   local $SIG{TSTP} = 'IGNORE';
225   local $SIG{PIPE} = 'IGNORE';
226
227   my $oldAutoCommit = $FS::UID::AutoCommit;
228   local $FS::UID::AutoCommit = 0;
229   my $dbh = dbh;
230
231   my $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
232     or return "event $self->eventnum references nonexistant invoice $self->invnum";
233
234   warn "cust_pay_batch->retriable working with self of " . $self->paybatchnum . " and invnum of " . $self->invnum;
235   my @cust_bill_event =
236     sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
237       grep {
238         $_->part_bill_event->eventcode =~ /\$cust_bill->batch_card/
239           && $_->status eq 'done'
240           && ! $_->statustext
241         }
242       $cust_bill->cust_bill_event;
243   # complain loudly if scalar(@cust_bill_event) > 1 ?
244   my $error = $cust_bill_event[0]->retriable;
245   if ($error ) {
246     # gah, even with transactions.
247     $dbh->commit if $oldAutoCommit; #well.
248     return "error marking invoice event retriable: $error";
249   }
250   '';
251 }
252
253 =back
254
255 =head1 BUGS
256
257 There should probably be a configuration file with a list of allowed credit
258 card types.
259
260 =head1 SEE ALSO
261
262 L<FS::cust_main>, L<FS::Record>
263
264 =cut
265
266 1;
267