+ my $cust_main = $self->cust_main;
+ my $old_balance = $cust_main->balance;
+
+ $error = $self->SUPER::insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error inserting cust_pay: $error";
+ }
+
+ if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
+ if ( my $months = $self->discount_term ) {
+ # XXX this should be moved out somewhere, but discount_term_values
+ # doesn't fit right
+ my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
+ return "can't accept prepayment for an unbilled customer" if !$cust_bill;
+
+ # %billing_pkgs contains this customer's active monthly packages.
+ # Recurring fees for those packages will be credited and then rebilled
+ # for the full discount term. Other packages on the last invoice
+ # (canceled, non-monthly recurring, or one-time charges) will be
+ # left as they are.
+ my %billing_pkgs = map { $_->pkgnum => $_ }
+ grep { $_->part_pkg->freq eq '1' }
+ $cust_main->billing_pkgs;
+ my $credit = 0; # sum of recurring charges from that invoice
+ my $last_bill_date = 0; # the real bill date
+ foreach my $item ( $cust_bill->cust_bill_pkg ) {
+ next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
+ $credit += $item->recur;
+ $last_bill_date = $item->cust_pkg->last_bill
+ if defined($item->cust_pkg)
+ and $item->cust_pkg->last_bill > $last_bill_date
+ }
+
+ my $cust_credit = new FS::cust_credit {
+ 'custnum' => $self->custnum,
+ 'amount' => sprintf('%.2f', $credit),
+ 'reason' => 'customer chose to prepay for discount',
+ };
+ $error = $cust_credit->insert('reason_type' => $credit_type);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error inserting prepayment credit: $error";
+ }
+ # don't apply it yet
+
+ # bill for the entire term
+ $_->bill($_->last_bill) foreach (values %billing_pkgs);
+ $error = $cust_main->bill(
+ # no recurring_only, we want unbilled packages with start dates to
+ # get billed
+ 'no_usage_reset' => 1,
+ 'time' => $last_bill_date, # not $cust_bill->_date
+ 'pkg_list' => [ values %billing_pkgs ],
+ 'freq_override' => $months,
+ );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error inserting cust_pay: $error";
+ }
+ $error = $cust_main->apply_payments_and_credits;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error inserting cust_pay: $error";
+ }
+ my $new_balance = $cust_main->balance;
+ if ($new_balance > 0) {
+ $dbh->rollback if $oldAutoCommit;
+ return "balance after prepay discount attempt: $new_balance";
+ }
+ # user friendly: override the "apply only to this invoice" mode
+ $self->invnum('');
+
+ }
+
+ }
+
+ if ( $self->invnum ) {
+ my $cust_bill_pay = new FS::cust_bill_pay {
+ 'invnum' => $self->invnum,
+ 'paynum' => $self->paynum,
+ 'amount' => $self->paid,
+ '_date' => $self->_date,
+ };
+ $error = $cust_bill_pay->insert(%options);
+ if ( $error ) {
+ if ( $ignore_noapply ) {
+ warn "warning: error inserting cust_bill_pay: $error ".
+ "(ignore_noapply flag set; inserting cust_pay record anyway)\n";
+ } else {
+ $dbh->rollback if $oldAutoCommit;
+ return "error inserting cust_bill_pay: $error";
+ }
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ #false laziness w/ cust_credit::insert
+ if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
+ my @errors = $cust_main->unsuspend;
+ #return
+ # side-fx with nested transactions? upstack rolls back?
+ warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
+ join(' / ', @errors)
+ if @errors;
+ }
+ #eslaf
+
+ #bill setup fees for voip_cdr bill_every_call packages
+ #some false laziness w/search in freeside-cdrd
+ my $addl_from =
+ 'LEFT JOIN part_pkg USING ( pkgpart ) '.
+ "LEFT JOIN part_pkg_option
+ ON ( cust_pkg.pkgpart = part_pkg_option.pkgpart
+ AND part_pkg_option.optionname = 'bill_every_call' )";
+
+ my $extra_sql = " AND plan = 'voip_cdr' AND optionvalue = '1' ".
+ " AND ( cust_pkg.setup IS NULL OR cust_pkg.setup = 0 ) ";
+
+ my @cust_pkg = qsearch({
+ 'table' => 'cust_pkg',
+ 'addl_from' => $addl_from,
+ 'hashref' => { 'custnum' => $self->custnum,
+ 'susp' => '',
+ 'cancel' => '',
+ },
+ 'extra_sql' => $extra_sql,
+ });
+
+ if ( @cust_pkg ) {
+ warn "voip_cdr bill_every_call packages found; billing customer\n";
+ my $bill_error = $self->cust_main->bill_and_collect( 'fatal' => 'return' );
+ if ( $bill_error ) {
+ warn "WARNING: Error billing customer: $bill_error\n";
+ }
+ }
+ #end of billing setup fees for voip_cdr bill_every_call packages
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ #payment receipt
+ my $trigger = $conf->config('payment_receipt-trigger',
+ $self->cust_main->agentnum) || 'cust_pay';
+ if ( $trigger eq 'cust_pay' ) {
+ my $error = $self->send_receipt(
+ 'manual' => $options{'manual'},
+ 'noemail' => $options{'noemail'},
+ 'cust_bill' => $cust_bill,
+ 'cust_main' => $cust_main,
+ );
+ warn "can't send payment receipt/statement: $error" if $error;
+ }
+
+ #run payment events immediately
+ my $due_cust_event = $self->cust_main->due_cust_event(
+ 'eventtable' => 'cust_pay',
+ 'objects' => [ $self ],
+ );
+ if ( !ref($due_cust_event) ) {
+ warn "Error searching for cust_pay billing events: $due_cust_event\n";
+ } else {
+ foreach my $cust_event (@$due_cust_event) {
+ next unless $cust_event->test_conditions;
+ if ( my $error = $cust_event->do_event() ) {
+ warn "Error running cust_pay billing event: $error\n";
+ }
+ }
+ }
+
+ '';
+
+}
+
+=item void [ REASON ]
+
+Voids this payment: deletes the payment and all associated applications and
+adds a record of the voided payment to the FS::cust_pay_void table.
+
+=cut
+
+sub void {
+ my $self = shift;
+ my $reason = shift;
+
+ unless (ref($reason) || !$reason) {
+ $reason = FS::reason->new_or_existing(
+ 'class' => 'X',
+ 'type' => 'Void payment',
+ 'reason' => $reason
+ );
+ }
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $cust_pay_void = new FS::cust_pay_void ( {
+ map { $_ => $self->get($_) } $self->fields
+ } );
+ $cust_pay_void->reasonnum($reason->reasonnum) if $reason;
+ my $error = $cust_pay_void->insert;
+
+ my $cust_pay_pending =
+ qsearchs('cust_pay_pending', { paynum => $self->paynum });
+ if ( $cust_pay_pending ) {
+ $cust_pay_pending->set('void_paynum', $self->paynum);
+ $cust_pay_pending->set('paynum', '');
+ $error ||= $cust_pay_pending->replace;
+ }
+
+ $error ||= $self->delete;
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ '';