refactor payment history slightly, add refund receipts, have "unapplied" refunds...
[freeside.git] / FS / FS / cust_refund.pm
1 package FS::cust_refund;
2
3 use strict;
4 use vars qw( @ISA @encrypted_fields );
5 use Business::CreditCard;
6 use FS::UID qw(getotaker);
7 use FS::Record qw( qsearch qsearchs dbh );
8 use FS::cust_main_Mixin;
9 use FS::payinfo_transaction_Mixin;
10 use FS::cust_credit;
11 use FS::cust_credit_refund;
12 use FS::cust_pay_refund;
13 use FS::cust_main;
14
15 @ISA = qw( FS::payinfo_transaction_Mixin FS::cust_main_Mixin FS::Record );
16
17 @encrypted_fields = ('payinfo');
18
19 =head1 NAME
20
21 FS::cust_refund - Object method for cust_refund objects
22
23 =head1 SYNOPSIS
24
25   use FS::cust_refund;
26
27   $record = new FS::cust_refund \%hash;
28   $record = new FS::cust_refund { 'column' => 'value' };
29
30   $error = $record->insert;
31
32   $error = $new_record->replace($old_record);
33
34   $error = $record->delete;
35
36   $error = $record->check;
37
38 =head1 DESCRIPTION
39
40 An FS::cust_refund represents a refund: the transfer of money to a customer;
41 equivalent to a negative payment (see L<FS::cust_pay>).  FS::cust_refund
42 inherits from FS::Record.  The following fields are currently supported:
43
44 =over 4
45
46 =item refundnum - primary key (assigned automatically for new refunds)
47
48 =item custnum - customer (see L<FS::cust_main>)
49
50 =item refund - Amount of the refund
51
52 =item reason - Reason for the refund
53
54 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
55 L<Time::Local> and L<Date::Parse> for conversion functions.
56
57 =item payby - Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
58
59 =item payinfo - Payment Information (See L<FS::payinfo_Mixin> for data format)
60
61 =item paymask - Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
62
63 =item paybatch - text field for tracking card processing
64
65 =item otaker - order taker (assigned automatically, see L<FS::UID>)
66
67 =item closed - books closed flag, empty or `Y'
68
69 =back
70
71 =head1 METHODS
72
73 =over 4
74
75 =item new HASHREF
76
77 Creates a new refund.  To add the refund to the database, see L<"insert">.
78
79 =cut
80
81 sub table { 'cust_refund'; }
82
83 =item insert
84
85 Adds this refund to the database.
86
87 For backwards-compatibility and convenience, if the additional field crednum is
88 defined, an FS::cust_credit_refund record for the full amount of the refund
89 will be created.  Or (this time for convenience and consistancy), if the
90 additional field paynum is defined, an FS::cust_pay_refund record for the full
91 amount of the refund will be created.  In both cases, custnum is optional.
92
93 =cut
94
95 sub insert {
96   my $self = shift;
97
98   local $SIG{HUP} = 'IGNORE';
99   local $SIG{INT} = 'IGNORE';
100   local $SIG{QUIT} = 'IGNORE';
101   local $SIG{TERM} = 'IGNORE';
102   local $SIG{TSTP} = 'IGNORE';
103   local $SIG{PIPE} = 'IGNORE';
104
105   my $oldAutoCommit = $FS::UID::AutoCommit;
106   local $FS::UID::AutoCommit = 0;
107   my $dbh = dbh;
108
109   if ( $self->crednum ) {
110     my $cust_credit = qsearchs('cust_credit', { 'crednum' => $self->crednum } )
111       or do {
112         $dbh->rollback if $oldAutoCommit;
113         return "Unknown cust_credit.crednum: ". $self->crednum;
114       };
115     $self->custnum($cust_credit->custnum);
116   } elsif ( $self->paynum ) {
117     my $cust_pay = qsearchs('cust_pay', { 'paynum' => $self->paynum } )
118       or do {
119         $dbh->rollback if $oldAutoCommit;
120         return "Unknown cust_pay.paynum: ". $self->paynum;
121       };
122     $self->custnum($cust_pay->custnum);
123   }
124
125   my $error = $self->check;
126   return $error if $error;
127
128   $error = $self->SUPER::insert;
129   if ( $error ) {
130     $dbh->rollback if $oldAutoCommit;
131     return $error;
132   }
133
134   if ( $self->crednum ) {
135     my $cust_credit_refund = new FS::cust_credit_refund {
136       'crednum'   => $self->crednum,
137       'refundnum' => $self->refundnum,
138       'amount'    => $self->refund,
139       '_date'     => $self->_date,
140     };
141     $error = $cust_credit_refund->insert;
142     if ( $error ) {
143       $dbh->rollback if $oldAutoCommit;
144       return $error;
145     }
146     #$self->custnum($cust_credit_refund->cust_credit->custnum);
147   } elsif ( $self->paynum ) {
148     my $cust_pay_refund = new FS::cust_pay_refund {
149       'paynum'    => $self->paynum,
150       'refundnum' => $self->refundnum,
151       'amount'    => $self->refund,
152       '_date'     => $self->_date,
153     };
154     $error = $cust_pay_refund->insert;
155     if ( $error ) {
156       $dbh->rollback if $oldAutoCommit;
157       return $error;
158     }
159   }
160
161
162   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
163
164   '';
165
166 }
167
168 =item delete
169
170 Unless the closed flag is set, deletes this refund and all associated
171 applications (see L<FS::cust_credit_refund> and L<FS::cust_pay_refund>).
172
173 =cut
174
175 sub delete {
176   my $self = shift;
177   return "Can't delete closed refund" if $self->closed =~ /^Y/i;
178
179   local $SIG{HUP} = 'IGNORE';
180   local $SIG{INT} = 'IGNORE';
181   local $SIG{QUIT} = 'IGNORE';
182   local $SIG{TERM} = 'IGNORE';
183   local $SIG{TSTP} = 'IGNORE';
184   local $SIG{PIPE} = 'IGNORE';
185
186   my $oldAutoCommit = $FS::UID::AutoCommit;
187   local $FS::UID::AutoCommit = 0;
188   my $dbh = dbh;
189
190   foreach my $cust_credit_refund ( $self->cust_credit_refund ) {
191     my $error = $cust_credit_refund->delete;
192     if ( $error ) {
193       $dbh->rollback if $oldAutoCommit;
194       return $error;
195     }
196   }
197
198   foreach my $cust_pay_refund ( $self->cust_pay_refund ) {
199     my $error = $cust_pay_refund->delete;
200     if ( $error ) {
201       $dbh->rollback if $oldAutoCommit;
202       return $error;
203     }
204   }
205
206   my $error = $self->SUPER::delete(@_);
207   if ( $error ) {
208     $dbh->rollback if $oldAutoCommit;
209     return $error;
210   }
211
212   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
213
214   '';
215
216 }
217
218 =item replace OLD_RECORD
219
220 Modifying a refund?  Well, don't say I didn't warn you.
221
222 =cut
223
224 sub replace {
225   my $self = shift;
226   $self->SUPER::replace(@_);
227 }
228
229 =item check
230
231 Checks all fields to make sure this is a valid refund.  If there is an error,
232 returns the error, otherwise returns false.  Called by the insert method.
233
234 =cut
235
236 sub check {
237   my $self = shift;
238
239   $self->otaker(getotaker) unless ($self->otaker);
240
241   my $error =
242     $self->ut_numbern('refundnum')
243     || $self->ut_numbern('custnum')
244     || $self->ut_money('refund')
245     || $self->ut_alpha('otaker')
246     || $self->ut_text('reason')
247     || $self->ut_numbern('_date')
248     || $self->ut_textn('paybatch')
249     || $self->ut_enum('closed', [ '', 'Y' ])
250   ;
251   return $error if $error;
252
253   return "refund must be > 0 " if $self->refund <= 0;
254
255   $self->_date(time) unless $self->_date;
256
257   return "unknown cust_main.custnum: ". $self->custnum
258     unless $self->crednum 
259            || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
260
261   $error = $self->payinfo_check;
262   return $error if $error;
263
264   $self->SUPER::check;
265 }
266
267 =item cust_credit_refund
268
269 Returns all applications to credits (see L<FS::cust_credit_refund>) for this
270 refund.
271
272 =cut
273
274 sub cust_credit_refund {
275   my $self = shift;
276   sort { $a->_date <=> $b->_date }
277     qsearch( 'cust_credit_refund', { 'refundnum' => $self->refundnum } )
278   ;
279 }
280
281 =item cust_pay_refund
282
283 Returns all applications to payments (see L<FS::cust_pay_refund>) for this
284 refund.
285
286 =cut
287
288 sub cust_pay_refund {
289   my $self = shift;
290   sort { $a->_date <=> $b->_date }
291     qsearch( 'cust_pay_refund', { 'refundnum' => $self->refundnum } )
292   ;
293 }
294
295 =item unapplied
296
297 Returns the amount of this refund that is still unapplied; which is
298 amount minus all credit applications (see L<FS::cust_credit_refund>) and
299 payment applications (see L<FS::cust_pay_refund>).
300
301 =cut
302
303 sub unapplied {
304   my $self = shift;
305   my $amount = $self->refund;
306   $amount -= $_->amount foreach ( $self->cust_credit_refund );
307   $amount -= $_->amount foreach ( $self->cust_pay_refund );
308   sprintf("%.2f", $amount );
309 }
310
311 =back
312
313 =head1 CLASS METHODS
314
315 =over 4
316
317 =item unapplied_sql
318
319 Returns an SQL fragment to retreive the unapplied amount.
320
321 =cut 
322
323 sub unapplied_sql {
324   #my $class = shift;
325
326   "refund
327     - COALESCE( 
328                 ( SELECT SUM(amount) FROM cust_credit_refund
329                     WHERE cust_refund.refundnum = cust_credit_refund.refundnum )
330                 ,0
331               )
332     - COALESCE(
333                 ( SELECT SUM(amount) FROM cust_pay_refund
334                     WHERE cust_refund.refundnum = cust_pay_refund.refundnum )
335                 ,0
336               )
337   ";
338
339 }
340
341 =back
342
343 =head1 BUGS
344
345 Delete and replace methods.
346
347 =head1 SEE ALSO
348
349 L<FS::Record>, L<FS::cust_credit>, schema.html from the base documentation.
350
351 =cut
352
353 1;
354