add MCRD payment type for manually processed ccards
[freeside.git] / FS / FS / cust_refund.pm
1 package FS::cust_refund;
2
3 use strict;
4 use vars qw( @ISA );
5 use Business::CreditCard;
6 use FS::Record qw( qsearch qsearchs dbh );
7 use FS::UID qw(getotaker);
8 use FS::cust_credit;
9 use FS::cust_credit_refund;
10 use FS::cust_pay_refund;
11 use FS::cust_main;
12
13 @ISA = qw( FS::Record );
14
15 =head1 NAME
16
17 FS::cust_refund - Object method for cust_refund objects
18
19 =head1 SYNOPSIS
20
21   use FS::cust_refund;
22
23   $record = new FS::cust_refund \%hash;
24   $record = new FS::cust_refund { 'column' => 'value' };
25
26   $error = $record->insert;
27
28   $error = $new_record->replace($old_record);
29
30   $error = $record->delete;
31
32   $error = $record->check;
33
34 =head1 DESCRIPTION
35
36 An FS::cust_refund represents a refund: the transfer of money to a customer;
37 equivalent to a negative payment (see L<FS::cust_pay>).  FS::cust_refund
38 inherits from FS::Record.  The following fields are currently supported:
39
40 =over 4
41
42 =item refundnum - primary key (assigned automatically for new refunds)
43
44 =item custnum - customer (see L<FS::cust_main>)
45
46 =item refund - Amount of the refund
47
48 =item reason - Reason for the refund
49
50 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
51 L<Time::Local> and L<Date::Parse> for conversion functions.
52
53 =item payby - `CARD' (credit cards), `CHEK' (electronic check/ACH),
54 `LECB' (Phone bill billing), `BILL' (billing), `CASH' (cash),
55 `WEST' (Western Union), `MCRD' (Manual credit card), or `COMP' (free)
56
57 =item payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username)
58
59 =item paybatch - text field for tracking card processing
60
61 =item otaker - order taker (assigned automatically, see L<FS::UID>)
62
63 =item closed - books closed flag, empty or `Y'
64
65 =back
66
67 =head1 METHODS
68
69 =over 4
70
71 =item new HASHREF
72
73 Creates a new refund.  To add the refund to the database, see L<"insert">.
74
75 =cut
76
77 sub table { 'cust_refund'; }
78
79 =item insert
80
81 Adds this refund to the database.
82
83 For backwards-compatibility and convenience, if the additional field crednum is
84 defined, an FS::cust_credit_refund record for the full amount of the refund
85 will be created.  Or (this time for convenience and consistancy), if the
86 additional field paynum is defined, an FS::cust_pay_refund record for the full
87 amount of the refund will be created.  In both cases, custnum is optional.
88
89 =cut
90
91 sub insert {
92   my $self = shift;
93
94   local $SIG{HUP} = 'IGNORE';
95   local $SIG{INT} = 'IGNORE';
96   local $SIG{QUIT} = 'IGNORE';
97   local $SIG{TERM} = 'IGNORE';
98   local $SIG{TSTP} = 'IGNORE';
99   local $SIG{PIPE} = 'IGNORE';
100
101   my $oldAutoCommit = $FS::UID::AutoCommit;
102   local $FS::UID::AutoCommit = 0;
103   my $dbh = dbh;
104
105   if ( $self->crednum ) {
106     my $cust_credit = qsearchs('cust_credit', { 'crednum' => $self->crednum } )
107       or do {
108         $dbh->rollback if $oldAutoCommit;
109         return "Unknown cust_credit.crednum: ". $self->crednum;
110       };
111     $self->custnum($cust_credit->custnum);
112   } elsif ( $self->paynum ) {
113     my $cust_pay = qsearchs('cust_pay', { 'paynum' => $self->paynum } )
114       or do {
115         $dbh->rollback if $oldAutoCommit;
116         return "Unknown cust_pay.paynum: ". $self->paynum;
117       };
118     $self->custnum($cust_pay->custnum);
119   }
120
121   my $error = $self->check;
122   return $error if $error;
123
124   $error = $self->SUPER::insert;
125   if ( $error ) {
126     $dbh->rollback if $oldAutoCommit;
127     return $error;
128   }
129
130   if ( $self->crednum ) {
131     my $cust_credit_refund = new FS::cust_credit_refund {
132       'crednum'   => $self->crednum,
133       'refundnum' => $self->refundnum,
134       'amount'    => $self->refund,
135       '_date'     => $self->_date,
136     };
137     $error = $cust_credit_refund->insert;
138     if ( $error ) {
139       $dbh->rollback if $oldAutoCommit;
140       return $error;
141     }
142     #$self->custnum($cust_credit_refund->cust_credit->custnum);
143   } elsif ( $self->paynum ) {
144     my $cust_pay_refund = new FS::cust_pay_refund {
145       'paynum'    => $self->paynum,
146       'refundnum' => $self->refundnum,
147       'amount'    => $self->refund,
148       '_date'     => $self->_date,
149     };
150     $error = $cust_pay_refund->insert;
151     if ( $error ) {
152       $dbh->rollback if $oldAutoCommit;
153       return $error;
154     }
155   }
156
157
158   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
159
160   '';
161
162 }
163
164 =item delete
165
166 Currently unimplemented (accounting reasons).
167
168 =cut
169
170 sub delete {
171   my $self = shift;
172   return "Can't delete closed refund" if $self->closed =~ /^Y/i;
173   $self->SUPER::delete(@_);
174 }
175
176 =item replace OLD_RECORD
177
178 Currently unimplemented (accounting reasons).
179
180 =cut
181
182 sub replace {
183    return "Can't (yet?) modify cust_refund records!";
184 }
185
186 =item check
187
188 Checks all fields to make sure this is a valid refund.  If there is an error,
189 returns the error, otherwise returns false.  Called by the insert method.
190
191 =cut
192
193 sub check {
194   my $self = shift;
195
196   my $error =
197     $self->ut_numbern('refundnum')
198     || $self->ut_numbern('custnum')
199     || $self->ut_money('refund')
200     || $self->ut_text('reason')
201     || $self->ut_numbern('_date')
202     || $self->ut_textn('paybatch')
203     || $self->ut_enum('closed', [ '', 'Y' ])
204   ;
205   return $error if $error;
206
207   return "refund must be > 0 " if $self->refund <= 0;
208
209   $self->_date(time) unless $self->_date;
210
211   return "unknown cust_main.custnum: ". $self->custnum
212     unless $self->crednum 
213            || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
214
215   $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP|CASH|WEST|MCRD)$/
216     or return "Illegal payby";
217   $self->payby($1);
218
219   #false laziness with cust_pay::check
220   if ( $self->payby eq 'CARD' ) {
221     my $payinfo = $self->payinfo;
222     $payinfo =~ s/\D//g;
223     $self->payinfo($payinfo);
224     if ( $self->payinfo ) {
225       $self->payinfo =~ /^(\d{13,16})$/
226         or return "Illegal (mistyped?) credit card number (payinfo)";
227       $self->payinfo($1);
228       validate($self->payinfo) or return "Illegal credit card number";
229       return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
230     } else {
231       $self->payinfo('N/A');
232     }
233
234   } else {
235     $error = $self->ut_textn('payinfo');
236     return $error if $error;
237   }
238
239   $self->otaker(getotaker);
240
241   $self->SUPER::check;
242 }
243
244 =item cust_credit_refund
245
246 Returns all applications to credits (see L<FS::cust_credit_refund>) for this
247 refund.
248
249 =cut
250
251 sub cust_credit_refund {
252   my $self = shift;
253   sort { $a->_date <=> $b->_date }
254     qsearch( 'cust_credit_refund', { 'refundnum' => $self->refundnum } )
255   ;
256 }
257
258 =item cust_pay_refund
259
260 Returns all applications to payments (see L<FS::cust_pay_refund>) for this
261 refund.
262
263 =cut
264
265 sub cust_pay_refund {
266   my $self = shift;
267   sort { $a->_date <=> $b->_date }
268     qsearch( 'cust_pay_refund', { 'refundnum' => $self->refundnum } )
269   ;
270 }
271
272 =item unapplied
273
274 Returns the amount of this refund that is still unapplied; which is
275 amount minus all credit applications (see L<FS::cust_credit_refund>) and
276 payment applications (see L<FS::cust_pay_refund>).
277
278 =cut
279
280 sub unapplied {
281   my $self = shift;
282   my $amount = $self->refund;
283   $amount -= $_->amount foreach ( $self->cust_credit_refund );
284   $amount -= $_->amount foreach ( $self->cust_pay_refund );
285   sprintf("%.2f", $amount );
286 }
287
288
289
290 =item payinfo_masked
291
292 Returns a "masked" payinfo field with all but the last four characters replaced
293 by 'x'es.  Useful for displaying credit cards.
294
295 =cut
296
297
298 sub payinfo_masked {
299   my $self = shift;
300   my $payinfo = $self->payinfo;
301   'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4));
302 }
303
304
305 =back
306
307 =head1 BUGS
308
309 Delete and replace methods.  payinfo_masked false laziness with cust_main.pm
310 and cust_pay.pm
311
312 =head1 SEE ALSO
313
314 L<FS::Record>, L<FS::cust_credit>, schema.html from the base documentation.
315
316 =cut
317
318 1;
319