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