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