1 package FS::cust_pay_pending;
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;
11 @ISA = qw( FS::payinfo_transaction_Mixin FS::cust_main_Mixin FS::Record );
13 @encrypted_fields = ('payinfo');
17 FS::cust_pay_pending - Object methods for cust_pay_pending records
21 use FS::cust_pay_pending;
23 $record = new FS::cust_pay_pending \%hash;
24 $record = new FS::cust_pay_pending { 'column' => 'value' };
26 $error = $record->insert;
28 $error = $new_record->replace($old_record);
30 $error = $record->delete;
32 $error = $record->check;
36 An FS::cust_pay_pending object represents an pending payment. It reflects
37 local state through the multiple stages of processing a real-time transaction
38 with an external gateway. FS::cust_pay_pending inherits from FS::Record. The
39 following fields are currently supported:
49 Customer (see L<FS::cust_main>)
53 Amount of this payment
57 Specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
58 L<Time::Local> and L<Date::Parse> for conversion functions.
62 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
66 Payment Information (See L<FS::payinfo_Mixin> for data format)
70 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
78 Unique identifer to prevent duplicate transactions.
82 Pending transaction status, one of the following:
88 Aquires basic lock on payunique
92 Transaction is pending with the gateway
96 Only used for two-stage transactions that require a separate capture step
100 Transaction completed with payment gateway (sucessfully), not yet recorded in
105 Transaction completed with payment gateway (declined), not yet recorded in
110 Transaction recorded in database
116 Additional status information.
120 #=item cust_balance -
133 Creates a new pending payment. To add the pending payment to the database, see L<"insert">.
135 Note that this stores the hash reference, not a distinct copy of the hash it
136 points to. You can ask the object for a copy with the I<hash> method.
140 # the new method can be inherited from FS::Record, if a table method is defined
142 sub table { 'cust_pay_pending'; }
146 Adds this record to the database. If there is an error, returns the error,
147 otherwise returns false.
151 # the insert method can be inherited from FS::Record
155 Delete this record from the database.
159 # the delete method can be inherited from FS::Record
161 =item replace OLD_RECORD
163 Replaces the OLD_RECORD with this one in the database. If there is an error,
164 returns the error, otherwise returns false.
168 # the replace method can be inherited from FS::Record
172 Checks all fields to make sure this is a valid pending payment. If there is
173 an error, returns the error, otherwise returns false. Called by the insert
178 # the check method should currently be supplied - FS::Record contains some
179 # data checking routines
185 $self->ut_numbern('paypendingnum')
186 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
187 || $self->ut_money('paid')
188 || $self->ut_numbern('_date')
189 || $self->ut_textn('payunique')
190 || $self->ut_text('status')
191 #|| $self->ut_textn('statustext')
192 || $self->ut_anything('statustext')
193 #|| $self->ut_money('cust_balance')
194 || $self->ut_foreign_keyn('paynum', 'cust_pay', 'paynum' )
195 || $self->payinfo_check() #payby/payinfo/paymask/paydate
197 return $error if $error;
199 $self->_date(time) unless $self->_date;
201 # UNIQUE index should catch this too, without race conditions, but this
202 # should give a better error message the other 99.9% of the time...
203 if ( length($self->payunique) ) {
204 my $cust_pay_pending = qsearchs('cust_pay_pending', {
205 'payunique' => $self->payunique,
206 'paypendingnum' => { op=>'!=', value=>$self->paypendingnum },
208 if ( $cust_pay_pending ) {
209 #well, it *could* be a better error message
210 return "duplicate transaction - a payment with unique identifer ".
211 $self->payunique. " already exists";
218 #these two are kind-of false laziness w/cust_main::realtime_bop
219 #(currently only used when resolving pending payments manually)
221 =item insert_cust_pay
223 Sets the status of this pending pament to "done" (with statustext
224 "captured (manual)"), and inserts a payment record (see L<FS::cust_pay>).
226 Currently only used when resolving pending payments manually.
230 sub insert_cust_pay {
233 my $cust_pay = new FS::cust_pay ( {
234 'custnum' => $self->custnum,
235 'paid' => $self->paid,
236 '_date' => $self->_date, #better than passing '' for now
237 'payby' => $self->payby,
238 'payinfo' => $self->payinfo,
239 'paybatch' => $self->paybatch,
240 'paydate' => $self->paydate,
243 my $oldAutoCommit = $FS::UID::AutoCommit;
244 local $FS::UID::AutoCommit = 0;
247 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
249 my $error = $cust_pay->insert;#($options{'manual'} ? ( 'manual' => 1 ) : () );
253 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
257 $self->status('done');
258 $self->statustext('captured (manual)');
259 $self->paynum($cust_pay->paynum);
260 my $cpp_done_err = $self->replace;
262 if ( $cpp_done_err ) {
264 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
265 return $cpp_done_err;
269 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
278 Sets the status of this pending pament to "done" (with statustext
279 "declined (manual)").
281 Currently only used when resolving pending payments manually.
288 #could send decline email too? doesn't seem useful in manual resolution
290 $self->status('done');
291 $self->statustext("declined (manual)");
297 # Used by FS::Upgrade to migrate to a new database.
299 sub _upgrade_data { #class method
300 my ($class, %opts) = @_;
303 "DELETE FROM cust_pay_pending WHERE status = 'new' AND _date < ".(time-600);
305 my $sth = dbh->prepare($sql) or die dbh->errstr;
306 $sth->execute or die $sth->errstr;
316 L<FS::Record>, schema.html from the base documentation.