X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcust_refund.pm;h=097ed2df332af7bec72bf17efa0ead6b95dd50ed;hp=3af8305a254f4a48807e612e3a6a4914fd876ac4;hb=79981f908752492b00cb4bdce3dceb0636fb81dd;hpb=aa69f07119fbed68a84f889fc4bea93790a9a628 diff --git a/FS/FS/cust_refund.pm b/FS/FS/cust_refund.pm index 3af8305a2..097ed2df3 100644 --- a/FS/FS/cust_refund.pm +++ b/FS/FS/cust_refund.pm @@ -1,15 +1,26 @@ package FS::cust_refund; use strict; -use vars qw( @ISA ); +use base qw( FS::otaker_Mixin FS::payinfo_transaction_Mixin FS::cust_main_Mixin + FS::Record ); +use vars qw( @encrypted_fields $me $DEBUG $ignore_empty_reasonnum ); use Business::CreditCard; -use FS::Record qw( qsearchs dbh ); -use FS::UID qw(getotaker); +use FS::Record qw( qsearch qsearchs dbh dbdef ); +use FS::CurrentUser; use FS::cust_credit; use FS::cust_credit_refund; +use FS::cust_pay_refund; use FS::cust_main; +use FS::reason_type; +use FS::reason; -@ISA = qw( FS::Record ); +$me = '[ FS::cust_refund ]'; +$DEBUG = 0; + +$ignore_empty_reasonnum = 0; + +@encrypted_fields = ('payinfo'); +sub nohistory_fields { ('payinfo'); } =head1 NAME @@ -38,24 +49,59 @@ inherits from FS::Record. The following fields are currently supported: =over 4 -=item refundnum - primary key (assigned automatically for new refunds) +=item refundnum + +primary key (assigned automatically for new refunds) + +=item custnum + +customer (see L) + +=item refund + +Amount of the refund + +=item reason -=item custnum - customer (see L) +Text stating the reason for the refund ( deprecated ) -=item refund - Amount of the refund +=item reasonnum -=item _date - specified as a UNIX timestamp; see L. Also see +Reason (see L) + +=item _date + +specified as a UNIX timestamp; see L. Also see L and L for conversion functions. -=item payby - `CARD' (credit cards), `BILL' (billing), or `COMP' (free) +=item payby + +Payment Type (See L for valid payby values) + +=item payinfo + +Payment Information (See L for data format) + +=item paymask + +Masked payinfo (See L for how this works) + +=item paybatch -=item payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username) +text field for tracking card processing -=item paybatch - text field for tracking card processing +=item usernum -=item otaker - order taker (assigned automatically, see L) +order taker (see L -=item closed - books closed flag, empty or `Y' +=item closed + +books closed flag, empty or `Y' + +=item gatewaynum, processor, auth, order_number + +Same as for L, but specifically the result of realtime +authorization of the refund. =back @@ -77,12 +123,14 @@ Adds this refund to the database. For backwards-compatibility and convenience, if the additional field crednum is defined, an FS::cust_credit_refund record for the full amount of the refund -will be created. In this case, custnum is optional. +will be created. Or (this time for convenience and consistancy), if the +additional field paynum is defined, an FS::cust_pay_refund record for the full +amount of the refund will be created. In both cases, custnum is optional. =cut sub insert { - my $self = shift; + my ($self, %options) = @_; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -95,13 +143,34 @@ sub insert { local $FS::UID::AutoCommit = 0; my $dbh = dbh; + unless ($self->reasonnum) { + my $result = $self->reason( $self->getfield('reason'), + exists($options{ 'reason_type' }) + ? ('reason_type' => $options{ 'reason_type' }) + : (), + ); + unless($result) { + $dbh->rollback if $oldAutoCommit; + return "failed to set reason for $me"; #: ". $dbh->errstr; + } + } + + $self->setfield('reason', ''); + if ( $self->crednum ) { - my $cust_credit = qsearchs('cust_bill', { 'invnum' => $self->invnum } ) + my $cust_credit = qsearchs('cust_credit', { 'crednum' => $self->crednum } ) or do { $dbh->rollback if $oldAutoCommit; return "Unknown cust_credit.crednum: ". $self->crednum; }; $self->custnum($cust_credit->custnum); + } elsif ( $self->paynum ) { + my $cust_pay = qsearchs('cust_pay', { 'paynum' => $self->paynum } ) + or do { + $dbh->rollback if $oldAutoCommit; + return "Unknown cust_pay.paynum: ". $self->paynum; + }; + $self->custnum($cust_pay->custnum); } my $error = $self->check; @@ -126,6 +195,18 @@ sub insert { return $error; } #$self->custnum($cust_credit_refund->cust_credit->custnum); + } elsif ( $self->paynum ) { + my $cust_pay_refund = new FS::cust_pay_refund { + 'paynum' => $self->paynum, + 'refundnum' => $self->refundnum, + 'amount' => $self->refund, + '_date' => $self->_date, + }; + $error = $cust_pay_refund->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } } @@ -135,8 +216,16 @@ sub insert { } -sub upgrade_replace { #1.3.x->1.4.x +=item delete + +Unless the closed flag is set, deletes this refund and all associated +applications (see L and L). + +=cut + +sub delete { my $self = shift; + return "Can't delete closed refund" if $self->closed =~ /^Y/i; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -149,30 +238,23 @@ sub upgrade_replace { #1.3.x->1.4.x local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $error = $self->check; - return $error if $error; - - my %new = $self->hash; - my $new = FS::cust_refund->new(\%new); + foreach my $cust_credit_refund ( $self->cust_credit_refund ) { + my $error = $cust_credit_refund->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } - if ( $self->crednum ) { - my $cust_credit_refund = new FS::cust_credit_refund { - 'crednum' => $self->crednum, - 'refundnum' => $self->refundnum, - 'amount' => $self->refund, - '_date' => $self->_date, - }; - $error = $cust_credit_refund->insert; + foreach my $cust_pay_refund ( $self->cust_pay_refund ) { + my $error = $cust_pay_refund->delete; if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; } - $new->custnum($cust_credit_refund->cust_credit->custnum); - } else { - die; } - $error = $new->SUPER::replace($self); + my $error = $self->SUPER::delete(@_); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -184,26 +266,20 @@ sub upgrade_replace { #1.3.x->1.4.x } -=item delete - -Currently unimplemented (accounting reasons). - -=cut - -sub delete { - my $self = shift; - return "Can't delete closed refund" if $self->closed =~ /^Y/i; - $self->SUPER::delete(@_); -} - =item replace OLD_RECORD -Currently unimplemented (accounting reasons). +You can, but probably shouldn't modify refunds... + +Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not +supplied, replaces this record. If there is an error, returns the error, +otherwise returns false. =cut sub replace { - return "Can't (yet?) modify cust_refund records!"; + my $self = shift; + return "Can't modify closed refund" if $self->closed =~ /^Y/i; + $self->SUPER::replace(@_); } =item check @@ -216,16 +292,24 @@ returns the error, otherwise returns false. Called by the insert method. sub check { my $self = shift; + $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum; + my $error = $self->ut_numbern('refundnum') || $self->ut_numbern('custnum') || $self->ut_money('refund') + || $self->ut_alphan('otaker') + || $self->ut_textn('reason') || $self->ut_numbern('_date') || $self->ut_textn('paybatch') || $self->ut_enum('closed', [ '', 'Y' ]) ; return $error if $error; + my $method = $ignore_empty_reasonnum ? 'ut_foreign_keyn' : 'ut_foreign_key'; + $error = $self->$method('reasonnum', 'reason', 'reasonnum'); + return $error if $error; + return "refund must be > 0 " if $self->refund <= 0; $self->_date(time) unless $self->_date; @@ -234,39 +318,206 @@ sub check { unless $self->crednum || qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); - $self->payby =~ /^(CARD|BILL|COMP)$/ or return "Illegal payby"; - $self->payby($1); - - #false laziness with cust_pay::check - if ( $self->payby eq 'CARD' ) { - my $payinfo = $self->payinfo; - $payinfo =~ s/\D//g; - $self->payinfo($payinfo); - if ( $self->payinfo ) { - $self->payinfo =~ /^(\d{13,16})$/ - or return "Illegal (mistyped?) credit card number (payinfo)"; - $self->payinfo($1); - validate($self->payinfo) or return "Illegal credit card number"; - return "Unknown card type" if cardtype($self->payinfo) eq "Unknown"; - } else { - $self->payinfo('N/A'); - } + $error = $self->payinfo_check; + return $error if $error; - } else { - $error = $self->ut_textn('payinfo'); - return $error if $error; - } + $self->SUPER::check; +} + +=item cust_credit_refund + +Returns all applications to credits (see L) for this +refund. + +=cut + +sub cust_credit_refund { + my $self = shift; + map { $_ } #return $self->num_cust_credit_refund unless wantarray; + sort { $a->_date <=> $b->_date } + qsearch( 'cust_credit_refund', { 'refundnum' => $self->refundnum } ) + ; +} + +=item cust_pay_refund + +Returns all applications to payments (see L) for this +refund. + +=cut + +sub cust_pay_refund { + my $self = shift; + map { $_ } #return $self->num_cust_pay_refund unless wantarray; + sort { $a->_date <=> $b->_date } + qsearch( 'cust_pay_refund', { 'refundnum' => $self->refundnum } ) + ; +} + +=item unapplied - $self->otaker(getotaker); +Returns the amount of this refund that is still unapplied; which is +amount minus all credit applications (see L) and +payment applications (see L). - ''; #no error +=cut + +sub unapplied { + my $self = shift; + my $amount = $self->refund; + $amount -= $_->amount foreach ( $self->cust_credit_refund ); + $amount -= $_->amount foreach ( $self->cust_pay_refund ); + sprintf("%.2f", $amount ); } =back -=head1 VERSION +=head1 CLASS METHODS + +=over 4 + +=item unapplied_sql + +Returns an SQL fragment to retreive the unapplied amount. + +=cut + +sub unapplied_sql { + my ($class, $start, $end) = @_; + my $credit_start = $start ? "AND cust_credit_refund._date <= $start" : ''; + my $credit_end = $end ? "AND cust_credit_refund._date > $end" : ''; + my $pay_start = $start ? "AND cust_pay_refund._date <= $start" : ''; + my $pay_end = $end ? "AND cust_pay_refund._date > $end" : ''; + + "refund + - COALESCE( + ( SELECT SUM(amount) FROM cust_credit_refund + WHERE cust_refund.refundnum = cust_credit_refund.refundnum + $credit_start $credit_end ) + ,0 + ) + - COALESCE( + ( SELECT SUM(amount) FROM cust_pay_refund + WHERE cust_refund.refundnum = cust_pay_refund.refundnum + $pay_start $pay_end ) + ,0 + ) + "; + +} + +=item reason + +Returns the text of the associated reason (see L) for this credit. + +=cut + +sub reason { + my ($self, $value, %options) = @_; + my $dbh = dbh; + my $reason; + my $typenum = $options{'reason_type'}; + + my $oldAutoCommit = $FS::UID::AutoCommit; # this should already be in + local $FS::UID::AutoCommit = 0; # a transaction if it matters + + if ( defined( $value ) ) { + my $hashref = { 'reason' => $value }; + $hashref->{'reason_type'} = $typenum if $typenum; + my $addl_from = "LEFT JOIN reason_type ON ( reason_type = typenum ) "; + my $extra_sql = " AND reason_type.class='F'"; + + $reason = qsearchs( { 'table' => 'reason', + 'hashref' => $hashref, + 'addl_from' => $addl_from, + 'extra_sql' => $extra_sql, + } ); + + if (!$reason && $typenum) { + $reason = new FS::reason( { 'reason_type' => $typenum, + 'reason' => $value, + 'disabled' => 'Y', + } ); + my $error = $reason->insert; + if ( $error ) { + warn "error inserting reason: $error\n"; + $reason = undef; + } + } + + $self->reasonnum($reason ? $reason->reasonnum : '') ; + warn "$me reason used in set mode with non-existant reason -- clearing" + unless $reason; + } + $reason = qsearchs( 'reason', { 'reasonnum' => $self->reasonnum } ); + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; -$Id: cust_refund.pm,v 1.17 2002-02-19 03:15:11 ivan Exp $ + ( $reason ? $reason->reason : '' ). + ( $self->addlinfo ? ' '.$self->addlinfo : '' ); +} + +# Used by FS::Upgrade to migrate to a new database. +sub _upgrade_data { # class method + my ($class, %opts) = @_; + + warn "$me upgrading $class\n" if $DEBUG; + + if (defined dbdef->table($class->table)->column('reason')) { + + warn "$me Checking for unmigrated reasons\n" if $DEBUG; + + my @cust_refunds = qsearch({ 'table' => $class->table, + 'hashref' => {}, + 'extra_sql' => 'WHERE reason IS NOT NULL', + }); + + if (scalar(grep { $_->getfield('reason') =~ /\S/ } @cust_refunds)) { + warn "$me Found unmigrated reasons\n" if $DEBUG; + my $hashref = { 'class' => 'F', 'type' => 'Legacy' }; + my $reason_type = qsearchs( 'reason_type', $hashref ); + unless ($reason_type) { + $reason_type = new FS::reason_type( $hashref ); + my $error = $reason_type->insert(); + die "$class had error inserting FS::reason_type into database: $error\n" + if $error; + } + + $hashref = { 'reason_type' => $reason_type->typenum, + 'reason' => '(none)' + }; + my $noreason = qsearchs( 'reason', $hashref ); + unless ($noreason) { + $hashref->{'disabled'} = 'Y'; + $noreason = new FS::reason( $hashref ); + my $error = $noreason->insert(); + die "can't insert legacy reason '(none)' into database: $error\n" + if $error; + } + + foreach my $cust_refund ( @cust_refunds ) { + my $reason = $cust_refund->getfield('reason'); + warn "Contemplating reason $reason\n" if $DEBUG > 1; + if ($reason =~ /\S/) { + $cust_refund->reason($reason, 'reason_type' => $reason_type->typenum) + or die "can't insert legacy reason $reason into database\n"; + }else{ + $cust_refund->reasonnum($noreason->reasonnum); + } + + $cust_refund->setfield('reason', ''); + my $error = $cust_refund->replace; + + warn "*** WARNING: error replacing reason in $class ". + $cust_refund->refundnum. ": $error ***\n" + if $error; + } + } + } + $class->_upgrade_otaker(%opts); +} + +=back =head1 BUGS