fix 'Can't call method "setup" on an undefined value' error when using into rates...
[freeside.git] / FS / FS / cust_pay_pending.pm
1 package FS::cust_pay_pending;
2
3 use strict;
4 use vars qw( @ISA  @encrypted_fields );
5 use FS::Record qw( qsearch qsearchs dbh ); #dbh for _upgrade_data
6 use FS::payinfo_transaction_Mixin;
7 use FS::cust_main_Mixin;
8 use FS::cust_main;
9 use FS::cust_pkg;
10 use FS::cust_pay;
11
12 @ISA = qw( FS::payinfo_transaction_Mixin FS::cust_main_Mixin FS::Record );
13
14 @encrypted_fields = ('payinfo');
15
16 =head1 NAME
17
18 FS::cust_pay_pending - Object methods for cust_pay_pending records
19
20 =head1 SYNOPSIS
21
22   use FS::cust_pay_pending;
23
24   $record = new FS::cust_pay_pending \%hash;
25   $record = new FS::cust_pay_pending { 'column' => 'value' };
26
27   $error = $record->insert;
28
29   $error = $new_record->replace($old_record);
30
31   $error = $record->delete;
32
33   $error = $record->check;
34
35 =head1 DESCRIPTION
36
37 An FS::cust_pay_pending object represents an pending payment.  It reflects 
38 local state through the multiple stages of processing a real-time transaction
39 with an external gateway.  FS::cust_pay_pending inherits from FS::Record.  The
40 following fields are currently supported:
41
42 =over 4
43
44 =item paypendingnum
45
46 Primary key
47
48 =item custnum
49
50 Customer (see L<FS::cust_main>)
51
52 =item paid
53
54 Amount of this payment
55
56 =item _date
57
58 Specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
59 L<Time::Local> and L<Date::Parse> for conversion functions.
60
61 =item payby
62
63 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
64
65 =item payinfo
66
67 Payment Information (See L<FS::payinfo_Mixin> for data format)
68
69 =item paymask
70
71 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
72
73 =item paydate
74
75 Expiration date
76
77 =item payunique
78
79 Unique identifer to prevent duplicate transactions.
80
81 =item pkgnum
82
83 Desired pkgnum when using experimental package balances.
84
85 =item status
86
87 Pending transaction status, one of the following:
88
89 =over 4
90
91 =item new
92
93 Aquires basic lock on payunique
94
95 =item pending
96
97 Transaction is pending with the gateway
98
99 =item thirdparty
100
101 Customer has been sent to an off-site payment gateway to complete processing
102
103 =item authorized
104
105 Only used for two-stage transactions that require a separate capture step
106
107 =item captured
108
109 Transaction completed with payment gateway (sucessfully), not yet recorded in
110 the database
111
112 =item declined
113
114 Transaction completed with payment gateway (declined), not yet recorded in
115 the database
116
117 =item done
118
119 Transaction recorded in database
120
121 =back
122
123 =item statustext
124
125 Additional status information.
126
127 =item gatewaynum
128
129 L<FS::payment_gateway> id.
130
131 =item paynum - 
132
133
134 =back
135
136 =head1 METHODS
137
138 =over 4
139
140 =item new HASHREF
141
142 Creates a new pending payment.  To add the pending payment to the database, see L<"insert">.
143
144 Note that this stores the hash reference, not a distinct copy of the hash it
145 points to.  You can ask the object for a copy with the I<hash> method.
146
147 =cut
148
149 # the new method can be inherited from FS::Record, if a table method is defined
150
151 sub table { 'cust_pay_pending'; }
152
153 =item insert
154
155 Adds this record to the database.  If there is an error, returns the error,
156 otherwise returns false.
157
158 =cut
159
160 # the insert method can be inherited from FS::Record
161
162 =item delete
163
164 Delete this record from the database.
165
166 =cut
167
168 # the delete method can be inherited from FS::Record
169
170 =item replace OLD_RECORD
171
172 Replaces the OLD_RECORD with this one in the database.  If there is an error,
173 returns the error, otherwise returns false.
174
175 =cut
176
177 # the replace method can be inherited from FS::Record
178
179 =item check
180
181 Checks all fields to make sure this is a valid pending payment.  If there is
182 an error, returns the error, otherwise returns false.  Called by the insert
183 and replace methods.
184
185 =cut
186
187 # the check method should currently be supplied - FS::Record contains some
188 # data checking routines
189
190 sub check {
191   my $self = shift;
192
193   my $error = 
194     $self->ut_numbern('paypendingnum')
195     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
196     || $self->ut_money('paid')
197     || $self->ut_numbern('_date')
198     || $self->ut_textn('payunique')
199     || $self->ut_text('status')
200     #|| $self->ut_textn('statustext')
201     || $self->ut_anything('statustext')
202     #|| $self->ut_money('cust_balance')
203     || $self->ut_hexn('session_id')
204     || $self->ut_foreign_keyn('paynum', 'cust_pay', 'paynum' )
205     || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
206     || $self->payinfo_check() #payby/payinfo/paymask/paydate
207   ;
208   return $error if $error;
209
210   $self->_date(time) unless $self->_date;
211
212   # UNIQUE index should catch this too, without race conditions, but this
213   # should give a better error message the other 99.9% of the time...
214   if ( length($self->payunique) ) {
215     my $cust_pay_pending = qsearchs('cust_pay_pending', {
216       'payunique'     => $self->payunique,
217       'paypendingnum' => { op=>'!=', value=>$self->paypendingnum },
218     });
219     if ( $cust_pay_pending ) {
220       #well, it *could* be a better error message
221       return "duplicate transaction - a payment with unique identifer ".
222              $self->payunique. " already exists";
223     }
224   }
225
226   $self->SUPER::check;
227 }
228
229 =item cust_main
230
231 Returns the associated L<FS::cust_main> record if any.  Otherwise returns false.
232
233 =cut
234
235 sub cust_main {
236   my $self = shift;
237   qsearchs('cust_main', { custnum => $self->custnum } );
238 }
239
240
241 #these two are kind-of false laziness w/cust_main::realtime_bop
242 #(currently only used when resolving pending payments manually)
243
244 =item insert_cust_pay
245
246 Sets the status of this pending pament to "done" (with statustext
247 "captured (manual)"), and inserts a payment record (see L<FS::cust_pay>).
248
249 Currently only used when resolving pending payments manually.
250
251 =cut
252
253 sub insert_cust_pay {
254   my $self = shift;
255
256   my $cust_pay = new FS::cust_pay ( {
257      'custnum'  => $self->custnum,
258      'paid'     => $self->paid,
259      '_date'    => $self->_date, #better than passing '' for now
260      'payby'    => $self->payby,
261      'payinfo'  => $self->payinfo,
262      'paybatch' => $self->paybatch,
263      'paydate'  => $self->paydate,
264   } );
265
266   my $oldAutoCommit = $FS::UID::AutoCommit;
267   local $FS::UID::AutoCommit = 0;
268   my $dbh = dbh;
269
270   #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
271
272   my $error = $cust_pay->insert;#($options{'manual'} ? ( 'manual' => 1 ) : () );
273
274   if ( $error ) {
275     # gah.
276     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
277     return $error;
278   }
279
280   $self->status('done');
281   $self->statustext('captured (manual)');
282   $self->paynum($cust_pay->paynum);
283   my $cpp_done_err = $self->replace;
284
285   if ( $cpp_done_err ) {
286
287     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
288     return $cpp_done_err;
289
290   } else {
291
292     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
293     return ''; #no error
294
295   }
296
297 }
298
299 =item decline [ STATUSTEXT ]
300
301 Sets the status of this pending payment to "done" (with statustext
302 "declined (manual)" unless otherwise specified).
303
304 Currently only used when resolving pending payments manually.
305
306 =cut
307
308 sub decline {
309   my $self = shift;
310   my $statustext = shift || "declined (manual)";
311
312   #could send decline email too?  doesn't seem useful in manual resolution
313
314   $self->status('done');
315   $self->statustext($statustext);
316   $self->replace;
317 }
318
319 # _upgrade_data
320 #
321 # Used by FS::Upgrade to migrate to a new database.
322
323 sub _upgrade_data {  #class method
324   my ($class, %opts) = @_;
325
326   my $sql =
327     "DELETE FROM cust_pay_pending WHERE status = 'new' AND _date < ".(time-600);
328
329   my $sth = dbh->prepare($sql) or die dbh->errstr;
330   $sth->execute or die $sth->errstr;
331
332 }
333
334 =back
335
336 =head1 BUGS
337
338 =head1 SEE ALSO
339
340 L<FS::Record>, schema.html from the base documentation.
341
342 =cut
343
344 1;
345